Skip to content

File syn_ymodem.c

File List > proto > syn_ymodem.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_YMODEM) || SYN_USE_YMODEM

#include "../util/syn_assert.h"
#include "../util/syn_crc.h"
#include "syn_ymodem.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SYN_YMODEM_RECV_TIMEOUT_MS 1000U

static void ymodem_send_byte(SYN_YMODEM_Receiver *rx, uint8_t b)
{
    if (rx != NULL && rx->putchar_fn != NULL) {
        rx->putchar_fn(b, rx->ctx);
    }
}

static int ymodem_recv_byte(SYN_YMODEM_Receiver *rx, uint32_t timeout_ms)
{
    return rx->getchar_fn(timeout_ms, rx->ctx);
}

static void ymodem_parse_block0(SYN_YMODEM_Receiver *rx, const uint8_t *payload, size_t payload_len)
{
    rx->filename[0] = '\0';
    rx->filesize = 0;

    /* Extract NULL-terminated filename */
    size_t fn_len = 0;
    while (fn_len < payload_len && payload[fn_len] != '\0' &&
           fn_len < (SYN_YMODEM_MAX_FILENAME - 1U)) {
        rx->filename[fn_len] = (char)payload[fn_len];
        fn_len++;
    }
    rx->filename[fn_len] = '\0';

    /* Extract file size (ASCII space-delimited decimal after filename) */
    size_t size_pos = fn_len + 1U;
    if (size_pos < payload_len && payload[size_pos] != '\0') {
        rx->filesize = (uint32_t)strtoul((const char *)&payload[size_pos], NULL, 10);
    }
}

static int ymodem_read_packet(SYN_YMODEM_Receiver *rx, uint8_t header_byte, size_t *out_payload_len)
{
    size_t payload_len = (header_byte == SYN_YMODEM_STX) ? 1024U : 128U;
    size_t total_len = payload_len + 5U; /* header + seq + ~seq + payload + 2B CRC */

    rx->pkt_buf[0] = header_byte;

    for (size_t i = 1U; i < total_len; i++) {
        int b = ymodem_recv_byte(rx, SYN_YMODEM_RECV_TIMEOUT_MS);
        if (b < 0) {
            return SYN_YMODEM_ERR_TIMEOUT;
        }
        rx->pkt_buf[i] = (uint8_t)b;
    }

    uint8_t seq = rx->pkt_buf[1];
    uint8_t seq_inv = rx->pkt_buf[2];

    if ((uint8_t)(seq + seq_inv) != 0xFFU) {
        return SYN_YMODEM_ERR_SEQUENCE;
    }

    /* Compute CRC-16/XMODEM (poly 0x1021, init 0x0000) over payload */
    uint16_t computed_crc = syn_crc16_ccitt_update(0x0000U, &rx->pkt_buf[3], payload_len);
    uint16_t rx_crc =
        (uint16_t)(((uint16_t)rx->pkt_buf[3U + payload_len] << 8) | rx->pkt_buf[4U + payload_len]);

    if (computed_crc != rx_crc) {
        return SYN_YMODEM_ERR_CRC;
    }

    *out_payload_len = payload_len;
    return (int)seq;
}

void syn_ymodem_receiver_init(SYN_YMODEM_Receiver *rx, SYN_YMODEM_PutChar putchar_fn,
                              SYN_YMODEM_GetChar getchar_fn, SYN_YMODEM_EventCallback event_fn,
                              void *ctx)
{
    if (rx == NULL) {
        return;
    }
    SYN_ASSERT(rx != NULL);

    memset(rx, 0, sizeof(SYN_YMODEM_Receiver));
    rx->putchar_fn = putchar_fn;
    rx->getchar_fn = getchar_fn;
    rx->event_fn = event_fn;
    rx->ctx = ctx;
}

SYN_YMODEM_Status syn_ymodem_receive(SYN_YMODEM_Receiver *rx)
{
    if (rx == NULL || rx->putchar_fn == NULL || rx->getchar_fn == NULL) {
        return SYN_YMODEM_ERR_PARAM;
    }

    bool session_active = true;

    while (session_active) {
        /* Initiate transfer by requesting CRC-16 mode */
        uint8_t retries = 0;
        int first_byte = -1;

        while (retries < SYN_YMODEM_MAX_RETRIES) {
            ymodem_send_byte(rx, SYN_YMODEM_CRC);
            first_byte = ymodem_recv_byte(rx, SYN_YMODEM_RECV_TIMEOUT_MS);
            if (first_byte >= 0) {
                break;
            }
            retries++;
        }

        if (first_byte < 0) {
            return SYN_YMODEM_ERR_TIMEOUT;
        }

        /* Check for initial Cancel request */
        if (first_byte == SYN_YMODEM_CAN) {
            return SYN_YMODEM_ERR_CANCEL;
        }

        /* Expect Block 0 (SOH or STX) */
        if (first_byte != SYN_YMODEM_SOH && first_byte != SYN_YMODEM_STX) {
            return SYN_YMODEM_ERR_SEQUENCE;
        }

        size_t payload_len = 0;
        int seq_or_err = ymodem_read_packet(rx, (uint8_t)first_byte, &payload_len);
        if (seq_or_err < 0) {
            ymodem_send_byte(rx, SYN_YMODEM_CAN);
            return (SYN_YMODEM_Status)seq_or_err;
        }

        if (seq_or_err != 0) {
            ymodem_send_byte(rx, SYN_YMODEM_CAN);
            return SYN_YMODEM_ERR_SEQUENCE;
        }

        /* Parse Block 0 header */
        if (rx->pkt_buf[3] == '\0') {
            /* Empty Block 0 -> End of session */
            ymodem_send_byte(rx, SYN_YMODEM_ACK);
            if (rx->event_fn != NULL) {
                rx->event_fn(SYN_YMODEM_EVENT_SESSION_END, NULL, 0, rx->ctx);
            }
            break;
        }

        ymodem_parse_block0(rx, &rx->pkt_buf[3], payload_len);

        /* Valid Block 0 -> Fire FILE_START and send ACK + 'C' */
        if (rx->event_fn != NULL) {
            int cb_res = rx->event_fn(SYN_YMODEM_EVENT_FILE_START, (const uint8_t *)rx->filename,
                                      strlen(rx->filename), rx->ctx);
            if (cb_res != 0) {
                ymodem_send_byte(rx, SYN_YMODEM_CAN);
                return SYN_YMODEM_ERR_CANCEL;
            }
        }

        ymodem_send_byte(rx, SYN_YMODEM_ACK);
        ymodem_send_byte(rx, SYN_YMODEM_CRC);

        /* Receive data blocks */
        rx->expected_seq = 1U;
        rx->bytes_received = 0U;
        bool file_active = true;

        while (file_active) {
            int header_byte = ymodem_recv_byte(rx, SYN_YMODEM_RECV_TIMEOUT_MS);
            if (header_byte < 0) {
                return SYN_YMODEM_ERR_TIMEOUT;
            }

            if (header_byte == SYN_YMODEM_CAN) {
                int second_can = ymodem_recv_byte(rx, 100U);
                if (second_can == SYN_YMODEM_CAN) {
                    return SYN_YMODEM_ERR_CANCEL;
                }
            }

            if (header_byte == SYN_YMODEM_EOT) {
                /* End of transmission sequence: send NAK, wait for 2nd EOT, then ACK */
                ymodem_send_byte(rx, SYN_YMODEM_NAK);
                int second_eot = ymodem_recv_byte(rx, SYN_YMODEM_RECV_TIMEOUT_MS);
                if (second_eot == SYN_YMODEM_EOT) {
                    ymodem_send_byte(rx, SYN_YMODEM_ACK);
                } else {
                    ymodem_send_byte(rx, SYN_YMODEM_ACK);
                }

                if (rx->event_fn != NULL) {
                    rx->event_fn(SYN_YMODEM_EVENT_FILE_END, NULL, 0, rx->ctx);
                }
                break;
            }

            if (header_byte != SYN_YMODEM_SOH && header_byte != SYN_YMODEM_STX) {
                ymodem_send_byte(rx, SYN_YMODEM_NAK);
                continue;
            }

            size_t data_payload_len = 0;
            int block_seq = ymodem_read_packet(rx, (uint8_t)header_byte, &data_payload_len);

            if (block_seq < 0) {
                ymodem_send_byte(rx, SYN_YMODEM_NAK);
                continue;
            }

            if ((uint8_t)block_seq == (uint8_t)(rx->expected_seq - 1U)) {
                /* Duplicate packet — ACK and discard */
                ymodem_send_byte(rx, SYN_YMODEM_ACK);
                continue;
            }

            if ((uint8_t)block_seq != rx->expected_seq) {
                ymodem_send_byte(rx, SYN_YMODEM_CAN);
                return SYN_YMODEM_ERR_SEQUENCE;
            }

            /* Calculate chunk size based on remaining filesize */
            size_t chunk_len = data_payload_len;
            if (rx->filesize > 0U) {
                uint32_t remaining =
                    (rx->bytes_received < rx->filesize) ? (rx->filesize - rx->bytes_received) : 0U;
                if (remaining < chunk_len) {
                    chunk_len = (size_t)remaining;
                }
            }

            if (rx->event_fn != NULL && chunk_len > 0U) {
                int cb_res =
                    rx->event_fn(SYN_YMODEM_EVENT_DATA, &rx->pkt_buf[3], chunk_len, rx->ctx);
                if (cb_res != 0) {
                    ymodem_send_byte(rx, SYN_YMODEM_CAN);
                    return SYN_YMODEM_ERR_CANCEL;
                }
            }

            rx->bytes_received += (uint32_t)chunk_len;
            rx->expected_seq++;
            ymodem_send_byte(rx, SYN_YMODEM_ACK);
        }
    }

    return SYN_YMODEM_OK;
}

#endif /* SYN_USE_YMODEM */