Skip to content

File syn_modbus.c

File List > proto > syn_modbus.c

Go to the documentation of this file

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

#if !defined(SYN_USE_MODBUS) || SYN_USE_MODBUS

#include "../util/syn_assert.h"
#include "syn_modbus.h"

#include <string.h>

/* ── Constants ──────────────────────────────────────────────────────────── */

/*
 * Inter-frame silence: 3.5 character times.
 * At 9600 baud: ~4ms. We use a conservative 5ms minimum.
 * At higher baud rates it's shorter, but we still need a minimum.
 */
#define MB_SILENCE_MS 5     
#define MB_MIN_FRAME_LEN 4  
#define MB_MAX_PDU_DATA 252 
#include "../util/syn_pack.h"

/* ── Helpers ────────────────────────────────────────────────────────────── */

static inline uint16_t read_u16(const uint8_t *buf)
{
    return syn_peek_u16(buf, 0);
}

static inline void write_u16(uint8_t *buf, uint16_t val)
{
    size_t pos = 0;
    syn_pack_u16(buf, &pos, val);
}

static bool check_crc(const uint8_t *buf, uint16_t len)
{
    if (len < 2)
        return false;
    uint16_t received = syn_peek_u16_le(buf, len - 2);
    uint16_t computed = syn_crc16_modbus(buf, len - 2);
    return received == computed;
}

static void append_crc(uint8_t *buf, uint16_t len)
{
    uint16_t crc = syn_crc16_modbus(buf, len);
    syn_poke_u16_le(crc, buf, len);
}

/* ── Send helpers ───────────────────────────────────────────────────────── */

static void send_response(SYN_Modbus *mb, uint16_t len)
{
    if (mb->buf[0] == 0) {
        return; /* Do not respond to broadcast */
    }
    append_crc(mb->buf, len);
    syn_port_uart_transmit(mb->cfg.uart, mb->buf, len + 2, 0);
    mb->frames_tx++;
}

static void send_exception(SYN_Modbus *mb, uint8_t func, uint8_t ex_code)
{
    if (mb->buf[0] == 0) {
        mb->errors++;
        return; /* Do not respond to broadcast exceptions */
    }
    mb->buf[0] = mb->cfg.slave_addr;
    mb->buf[1] = (uint8_t)(func | 0x80);
    mb->buf[2] = ex_code;
    send_response(mb, 3);
    mb->errors++;
}

/* ── Function code handlers ─────────────────────────────────────────────── */

static void handle_read_regs(SYN_Modbus *mb, uint16_t frame_len, const uint16_t *regs,
                             uint16_t reg_count)
{
    if (frame_len < 8) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t count = read_u16(&mb->buf[4]);

    if (count == 0 || count > 125) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if ((uint32_t)addr + count > reg_count) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    /* Build response */
    mb->buf[2] = (uint8_t)(count * 2); /* byte count */
    for (uint16_t i = 0; i < count; i++) {
        write_u16(&mb->buf[3 + i * 2], regs[addr + i]);
    }
    send_response(mb, (uint16_t)(3 + count * 2));
}

static void handle_write_single(SYN_Modbus *mb, uint16_t frame_len)
{
    if (frame_len < 8) {
        send_exception(mb, SYN_MB_FC_WRITE_SINGLE, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t value = read_u16(&mb->buf[4]);

    if (addr >= mb->cfg.holding_count) {
        send_exception(mb, SYN_MB_FC_WRITE_SINGLE, SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    /* Check write callback */
    if (mb->cfg.on_write != NULL) {
        if (!mb->cfg.on_write(mb, addr, 1, mb->cfg.on_write_ctx)) {
            send_exception(mb, SYN_MB_FC_WRITE_SINGLE, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
    }

    mb->cfg.holding_regs[addr] = value;

    /* Echo request as response */
    send_response(mb, 6);
}

static void handle_write_multiple(SYN_Modbus *mb, uint16_t frame_len)
{
    if (frame_len < 9) {
        send_exception(mb, SYN_MB_FC_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t count = read_u16(&mb->buf[4]);
    uint8_t bytes = mb->buf[6];

    if (count == 0 || count > 123 || bytes != count * 2 || frame_len < (uint16_t)(9 + bytes)) {
        send_exception(mb, SYN_MB_FC_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if ((uint32_t)addr + count > mb->cfg.holding_count) {
        send_exception(mb, SYN_MB_FC_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    /* Check write callback */
    if (mb->cfg.on_write != NULL) {
        if (!mb->cfg.on_write(mb, addr, count, mb->cfg.on_write_ctx)) {
            send_exception(mb, SYN_MB_FC_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
    }

    /* Apply writes */
    for (uint16_t i = 0; i < count; i++) {
        mb->cfg.holding_regs[addr + i] = read_u16(&mb->buf[7 + i * 2]);
    }

    /* Response: addr, func, start addr, quantity */
    send_response(mb, 6);
}

static void handle_read_exception_status(SYN_Modbus *mb)
{
    mb->buf[2] = mb->cfg.exception_status;
    send_response(mb, 3);
}

static void handle_read_write_multiple(SYN_Modbus *mb)
{
    uint16_t read_addr = read_u16(&mb->buf[2]);
    uint16_t read_count = read_u16(&mb->buf[4]);
    uint16_t write_addr = read_u16(&mb->buf[6]);
    uint16_t write_count = read_u16(&mb->buf[8]);
    uint8_t write_bytes = mb->buf[10];

    if (read_count == 0 || read_count > 125 || write_count == 0 || write_count > 121 ||
        write_bytes != write_count * 2) {
        send_exception(mb, SYN_MB_FC_READ_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if ((uint32_t)read_addr + read_count > mb->cfg.holding_count ||
        (uint32_t)write_addr + write_count > mb->cfg.holding_count) {
        send_exception(mb, SYN_MB_FC_READ_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    if (mb->cfg.on_write != NULL) {
        if (!mb->cfg.on_write(mb, write_addr, write_count, mb->cfg.on_write_ctx)) {
            send_exception(mb, SYN_MB_FC_READ_WRITE_MULTIPLE, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
    }

    /* Write performed first according to spec */
    for (uint16_t i = 0; i < write_count; i++) {
        mb->cfg.holding_regs[write_addr + i] = read_u16(&mb->buf[11 + i * 2]);
    }

    /* Read performed second */
    mb->buf[2] = (uint8_t)(read_count * 2);
    for (uint16_t i = 0; i < read_count; i++) {
        write_u16(&mb->buf[3 + i * 2], mb->cfg.holding_regs[read_addr + i]);
    }
    send_response(mb, (uint16_t)(3 + read_count * 2));
}

static void handle_read_device_info(SYN_Modbus *mb)
{
    if (mb->buf[2] != SYN_MB_MEI_TYPE_READ_DEVICE_ID) {
        send_exception(mb, SYN_MB_FC_READ_DEVICE_INFO, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint8_t read_code = mb->buf[3];
    uint8_t object_id = mb->buf[4];

    if (read_code < 1 || read_code > 4) {
        send_exception(mb, SYN_MB_FC_READ_DEVICE_INFO, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    const SYN_Modbus_DeviceInfo *info = mb->cfg.device_info;

    mb->buf[2] = SYN_MB_MEI_TYPE_READ_DEVICE_ID;
    mb->buf[3] = read_code;
    mb->buf[4] = 0x01; /* Conformity level: Basic stream */
    mb->buf[5] = 0x00; /* More follows: 0 */
    mb->buf[6] = 0x00; /* Next object id */

    uint16_t pos = 8;
    uint8_t obj_count = 0;

    uint8_t start_obj = (read_code == 0x04) ? object_id : 0;
    uint8_t max_obj = (read_code == 0x01) ? 2 : 6;
    if (read_code == 0x04)
        max_obj = object_id;

    for (uint8_t id = start_obj; id <= max_obj; id++) {
        const char *str = NULL;
        if (info != NULL) {
            switch (id) {
            case 0x00:
                str = info->vendor_name;
                break;
            case 0x01:
                str = info->product_code;
                break;
            case 0x02:
                str = info->revision;
                break;
            case 0x03:
                str = info->vendor_url;
                break;
            case 0x04:
                str = info->product_name;
                break;
            case 0x05:
                str = info->model_name;
                break;
            case 0x06:
                str = info->user_app_name;
                break;
            }
        }
        if (str == NULL) {
            if (id == 0x00)
                str = "SyntropicOS";
            else if (id == 0x01)
                str = "SYN-MB";
            else if (id == 0x02)
                str = "1.0.0";
            else
                continue;
        }

        size_t slen = strlen(str);
        if (slen > 245)
            slen = 245;

        if ((uint32_t)pos + 2 + (uint32_t)slen > (uint32_t)mb->buf_size - 2)
            break;

        mb->buf[pos++] = id;
        mb->buf[pos++] = (uint8_t)slen;
        memcpy(&mb->buf[pos], str, slen);
        pos += slen;
        obj_count++;
    }

    mb->buf[7] = obj_count;
    send_response(mb, pos);
}

static void handle_read_file_record(SYN_Modbus *mb)
{
    uint8_t byte_count = mb->buf[2];
    if (byte_count < 7 || (byte_count % 7) != 0 || mb->cfg.on_read_file == NULL) {
        send_exception(mb, SYN_MB_FC_READ_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint8_t sub_req_count = byte_count / 7;
    uint8_t req_pos = 3;

    uint8_t resp_buf[256];
    uint16_t resp_pos = 3;

    for (uint8_t i = 0; i < sub_req_count; i++) {
        uint8_t ref_type = mb->buf[req_pos];
        uint16_t file_num = read_u16(&mb->buf[req_pos + 1]);
        uint16_t rec_num = read_u16(&mb->buf[req_pos + 3]);
        uint16_t rec_len = read_u16(&mb->buf[req_pos + 5]);
        req_pos += 7;

        if (ref_type != 0x06 || rec_len == 0 || rec_len > 120) {
            send_exception(mb, SYN_MB_FC_READ_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }

        uint16_t rec_data[120];
        if (!mb->cfg.on_read_file(mb, file_num, rec_num, rec_len, rec_data, mb->cfg.file_cb_ctx)) {
            send_exception(mb, SYN_MB_FC_READ_FILE_RECORD, SYN_MB_EX_ILLEGAL_ADDR);
            return;
        }

        uint8_t sub_resp_len = (uint8_t)(1 + rec_len * 2);
        if ((uint32_t)resp_pos + 2 + (uint32_t)rec_len * 2 > (uint32_t)mb->buf_size - 2) {
            send_exception(mb, SYN_MB_FC_READ_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }

        resp_buf[resp_pos++] = sub_resp_len;
        resp_buf[resp_pos++] = 0x06;
        for (uint16_t j = 0; j < rec_len; j++) {
            write_u16(&resp_buf[resp_pos], rec_data[j]);
            resp_pos += 2;
        }
    }

    resp_buf[0] = mb->cfg.slave_addr;
    resp_buf[1] = SYN_MB_FC_READ_FILE_RECORD;
    resp_buf[2] = (uint8_t)(resp_pos - 3);
    memcpy(mb->buf, resp_buf, resp_pos);
    send_response(mb, resp_pos);
}

static void handle_write_file_record(SYN_Modbus *mb)
{
    uint8_t data_len = mb->buf[2];
    if (data_len < 9 || mb->cfg.on_write_file == NULL) {
        send_exception(mb, SYN_MB_FC_WRITE_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t pos = 3;
    uint16_t end = 3 + data_len;

    while (pos < end) {
        if (pos + 7 > end) {
            send_exception(mb, SYN_MB_FC_WRITE_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
        uint8_t ref_type = mb->buf[pos];
        uint16_t file_num = read_u16(&mb->buf[pos + 1]);
        uint16_t rec_num = read_u16(&mb->buf[pos + 3]);
        uint16_t rec_len = read_u16(&mb->buf[pos + 5]);
        pos += 7;

        if (ref_type != 0x06 || rec_len == 0 || pos + rec_len * 2 > end) {
            send_exception(mb, SYN_MB_FC_WRITE_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }

        uint16_t rec_data[120];
        if (rec_len > 120) {
            send_exception(mb, SYN_MB_FC_WRITE_FILE_RECORD, SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
        for (uint16_t j = 0; j < rec_len; j++) {
            rec_data[j] = read_u16(&mb->buf[pos + j * 2]);
        }
        pos += rec_len * 2;

        if (!mb->cfg.on_write_file(mb, file_num, rec_num, rec_len, rec_data, mb->cfg.file_cb_ctx)) {
            send_exception(mb, SYN_MB_FC_WRITE_FILE_RECORD, SYN_MB_EX_ILLEGAL_ADDR);
            return;
        }
    }

    send_response(mb, (uint16_t)(3 + data_len));
}

/* ── API ────────────────────────────────────────────────────────────────── */

void syn_modbus_init(SYN_Modbus *mb, const SYN_Modbus_Config *cfg, uint8_t *buf, uint16_t buf_size)
{
    SYN_ASSERT(mb != NULL);
    SYN_ASSERT(cfg != NULL);
    SYN_ASSERT(buf != NULL);
    SYN_ASSERT(buf_size >= MB_MIN_FRAME_LEN);

    memset(mb, 0, sizeof(*mb));
    mb->cfg = *cfg;
    mb->buf = buf;
    mb->buf_size = buf_size;
}

void syn_modbus_feed(SYN_Modbus *mb, uint8_t byte)
{
    SYN_ASSERT(mb != NULL);

    uint32_t now = syn_port_get_tick_ms();
    uint32_t silence_gap = (mb->cfg.silence_ms > 0) ? mb->cfg.silence_ms : MB_SILENCE_MS;

    /* Detect inter-frame gap (3.5 char times) */
    if (mb->rx_len > 0 && (now - mb->last_byte_tick) >= silence_gap) {
        /* Previous frame timed out — start fresh */
        mb->frame_ready = (mb->rx_len >= MB_MIN_FRAME_LEN);
        if (!mb->frame_ready) {
            mb->rx_len = 0;
        }
        /* Don't accumulate into old frame — process first */
        return;
    }

    if (mb->rx_len < mb->buf_size) {
        mb->buf[mb->rx_len++] = byte;
    }
    mb->last_byte_tick = now;
}

static bool get_bit(const uint8_t *bits, uint16_t idx)
{
    return (bits[idx / 8U] & (uint8_t)(1U << (idx % 8U))) != 0U;
}

static void set_bit(uint8_t *bits, uint16_t idx, bool val)
{
    if (val) {
        bits[idx / 8U] |= (uint8_t)(1U << (idx % 8U));
    } else {
        bits[idx / 8U] &= (uint8_t)~(1U << (idx % 8U));
    }
}

static void handle_read_bits(SYN_Modbus *mb, uint16_t frame_len, const uint8_t *bits,
                             uint16_t total_bits)
{
    if (bits == NULL) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    if (frame_len < 8) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t count = read_u16(&mb->buf[4]);

    if (count == 0 || count > 2000) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if ((uint32_t)addr + count > total_bits) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    uint8_t num_bytes = (uint8_t)((count + 7U) / 8U);
    mb->buf[2] = num_bytes;
    (void)memset(&mb->buf[3], 0, num_bytes);

    for (uint16_t i = 0; i < count; i++) {
        if (get_bit(bits, (uint16_t)(addr + i))) {
            set_bit(&mb->buf[3], i, true);
        }
    }

    send_response(mb, (uint16_t)(3 + num_bytes));
    mb->comm_event_counter++;
}

static void handle_write_single_coil(SYN_Modbus *mb, uint16_t frame_len)
{
    if (mb->cfg.coils == NULL) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    if (frame_len < 8) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t val = read_u16(&mb->buf[4]);

    if (val != 0xFF00U && val != 0x0000U) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if (addr >= mb->cfg.coils_count) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    set_bit(mb->cfg.coils, addr, (val == 0xFF00U));
    send_response(mb, 6);
    mb->comm_event_counter++;
}

static void handle_write_multiple_coils(SYN_Modbus *mb, uint16_t frame_len)
{
    if (mb->cfg.coils == NULL) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    if (frame_len < 9) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t count = read_u16(&mb->buf[4]);
    uint8_t byte_count = mb->buf[6];

    uint8_t expected_bytes = (uint8_t)((count + 7U) / 8U);
    if (count == 0 || count > 1968 || byte_count != expected_bytes ||
        frame_len < (uint16_t)(9 + byte_count)) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
        return;
    }

    if ((uint32_t)addr + count > mb->cfg.coils_count) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    for (uint16_t i = 0; i < count; i++) {
        bool b = get_bit(&mb->buf[7], i);
        set_bit(mb->cfg.coils, (uint16_t)(addr + i), b);
    }

    send_response(mb, 6);
    mb->comm_event_counter++;
}

static void handle_diagnostics(SYN_Modbus *mb)
{
    uint16_t subfunc = read_u16(&mb->buf[2]);
    if (subfunc == 0x0000U) { /* Return Query Data (Echo) */
        send_response(mb, 6);
        mb->comm_event_counter++;
    } else {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
    }
}

static void handle_get_comm_event_cnt(SYN_Modbus *mb)
{
    mb->buf[2] = 0x00U;
    mb->buf[3] = 0x00U;
    write_u16(&mb->buf[4], mb->comm_event_counter);
    send_response(mb, 6);
}

static void handle_get_comm_event_log(SYN_Modbus *mb)
{
    mb->buf[2] = 0x08U; /* byte count */
    mb->buf[3] = 0x00U; /* status high */
    mb->buf[4] = 0x00U; /* status low */
    write_u16(&mb->buf[5], mb->comm_event_counter);
    write_u16(&mb->buf[7], mb->bus_message_count);
    mb->buf[9] = 0x00U;  /* event log byte 0 */
    mb->buf[10] = 0x00U; /* event log byte 1 */
    send_response(mb, 11);
}

static void handle_report_server_id(SYN_Modbus *mb)
{
    const uint8_t *id_bytes = mb->cfg.server_id;
    uint8_t id_len = mb->cfg.server_id_len;

    if (id_bytes == NULL) {
        id_bytes = (const uint8_t *)"SYN-MB";
        id_len = 6;
    }

    if (id_len > 240) {
        id_len = 240;
    }

    mb->buf[2] = (uint8_t)(id_len + 1); /* Byte count: ID bytes + Run Status indicator */
    memcpy(&mb->buf[3], id_bytes, id_len);
    mb->buf[3 + id_len] = 0xFFU; /* Run status: 0xFF = ON */

    send_response(mb, (uint16_t)(4 + id_len));
    mb->comm_event_counter++;
}

static void handle_mask_write_register(SYN_Modbus *mb)
{
    if (mb->cfg.holding_regs == NULL) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    uint16_t addr = read_u16(&mb->buf[2]);
    uint16_t and_mask = read_u16(&mb->buf[4]);
    uint16_t or_mask = read_u16(&mb->buf[6]);

    if (addr >= mb->cfg.holding_count) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    if (mb->cfg.on_write != NULL) {
        if (!mb->cfg.on_write(mb, addr, 1, mb->cfg.on_write_ctx)) {
            send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_VALUE);
            return;
        }
    }

    uint16_t cur = mb->cfg.holding_regs[addr];
    mb->cfg.holding_regs[addr] = (uint16_t)((cur & and_mask) | (or_mask & (uint16_t)~and_mask));
    send_response(mb, 8);
    mb->comm_event_counter++;
}

static void handle_read_fifo_queue(SYN_Modbus *mb)
{
    if (mb->cfg.fifo_queue == NULL) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_ILLEGAL_ADDR);
        return;
    }

    uint16_t fifo_count = mb->cfg.fifo_count;
    if (fifo_count > 31) {
        send_exception(mb, mb->buf[1], SYN_MB_EX_DEVICE_FAILURE);
        return;
    }

    uint16_t byte_count = (uint16_t)(2 + fifo_count * 2);
    write_u16(&mb->buf[2], byte_count);
    write_u16(&mb->buf[4], fifo_count);

    for (uint16_t i = 0; i < fifo_count; i++) {
        write_u16(&mb->buf[6 + i * 2], mb->cfg.fifo_queue[i]);
    }

    send_response(mb, (uint16_t)(6 + fifo_count * 2));
    mb->comm_event_counter++;
}

bool syn_modbus_process(SYN_Modbus *mb)
{
    SYN_ASSERT(mb != NULL);

    if (mb->rx_len < MB_MIN_FRAME_LEN) {
        mb->rx_len = 0;
        return false;
    }

    uint16_t len = mb->rx_len;
    mb->rx_len = 0;
    mb->frame_ready = false;

    /* Check CRC */
    if (!check_crc(mb->buf, len)) {
        mb->errors++;
        return false;
    }

    mb->frames_rx++;
    mb->bus_message_count++;

    /* Check slave address (0 = broadcast, we process but don't respond) */
    uint8_t addr = mb->buf[0];
    bool is_broadcast = (addr == 0);
    if (!is_broadcast && addr != mb->cfg.slave_addr) {
        return false; /* not for us */
    }

    uint8_t func = mb->buf[1];

    switch (func) {
    case SYN_MB_FC_READ_COILS:
        if (is_broadcast)
            break;
        handle_read_bits(mb, len, mb->cfg.coils, mb->cfg.coils_count);
        return true;

    case SYN_MB_FC_READ_DISCRETE_INPUTS:
        if (is_broadcast)
            break;
        handle_read_bits(mb, len, mb->cfg.discrete_inputs, mb->cfg.discrete_count);
        return true;

    case SYN_MB_FC_READ_HOLDING:
        if (is_broadcast)
            break;
        handle_read_regs(mb, len, mb->cfg.holding_regs, mb->cfg.holding_count);
        mb->comm_event_counter++;
        return true;

    case SYN_MB_FC_READ_INPUT:
        if (is_broadcast)
            break;
        handle_read_regs(mb, len, mb->cfg.input_regs, mb->cfg.input_count);
        mb->comm_event_counter++;
        return true;

    case SYN_MB_FC_WRITE_SINGLE_COIL:
        handle_write_single_coil(mb, len);
        return !is_broadcast;

    case SYN_MB_FC_WRITE_SINGLE:
        handle_write_single(mb, len);
        mb->comm_event_counter++;
        return !is_broadcast;

    case SYN_MB_FC_READ_EXCEPTION_STATUS:
        if (is_broadcast)
            break;
        handle_read_exception_status(mb);
        return true;

    case SYN_MB_FC_DIAGNOSTICS:
        if (is_broadcast)
            break;
        handle_diagnostics(mb);
        return true;

    case SYN_MB_FC_GET_COMM_EVENT_CNT:
        if (is_broadcast)
            break;
        handle_get_comm_event_cnt(mb);
        return true;

    case SYN_MB_FC_GET_COMM_EVENT_LOG:
        if (is_broadcast)
            break;
        handle_get_comm_event_log(mb);
        return true;

    case SYN_MB_FC_WRITE_MULTIPLE_COILS:
        handle_write_multiple_coils(mb, len);
        return !is_broadcast;

    case SYN_MB_FC_WRITE_MULTIPLE:
        handle_write_multiple(mb, len);
        mb->comm_event_counter++;
        return !is_broadcast;

    case SYN_MB_FC_REPORT_SERVER_ID:
        if (is_broadcast)
            break;
        handle_report_server_id(mb);
        return true;

    case SYN_MB_FC_READ_FILE_RECORD:
        if (is_broadcast)
            break;
        handle_read_file_record(mb);
        mb->comm_event_counter++;
        return true;

    case SYN_MB_FC_WRITE_FILE_RECORD:
        handle_write_file_record(mb);
        mb->comm_event_counter++;
        return !is_broadcast;

    case SYN_MB_FC_MASK_WRITE_REGISTER:
        handle_mask_write_register(mb);
        return !is_broadcast;

    case SYN_MB_FC_READ_WRITE_MULTIPLE:
        handle_read_write_multiple(mb);
        mb->comm_event_counter++;
        return !is_broadcast;

    case SYN_MB_FC_READ_FIFO_QUEUE:
        if (is_broadcast)
            break;
        handle_read_fifo_queue(mb);
        return true;

    case SYN_MB_FC_READ_DEVICE_INFO:
        if (is_broadcast)
            break;
        handle_read_device_info(mb);
        mb->comm_event_counter++;
        return true;

    default:
        if (!is_broadcast) {
            send_exception(mb, func, SYN_MB_EX_ILLEGAL_FUNC);
        }
        return false;
    }

    return false;
}

bool syn_modbus_poll(SYN_Modbus *mb)
{
    SYN_ASSERT(mb != NULL);

    /* Read available bytes */
    uint8_t byte;
    while (syn_port_uart_receive_byte(mb->cfg.uart, &byte, 0) == SYN_OK) {
        if (mb->rx_len < mb->buf_size) {
            mb->buf[mb->rx_len++] = byte;
        }
        mb->last_byte_tick = syn_port_get_tick_ms();
    }

    /* Check for inter-frame silence */
    if (mb->rx_len > 0) {
        uint32_t silence = syn_port_get_tick_ms() - mb->last_byte_tick;
        if (silence >= MB_SILENCE_MS) {
            return syn_modbus_process(mb);
        }
    }

    return false;
}

void syn_modbus_reset(SYN_Modbus *mb)
{
    SYN_ASSERT(mb != NULL);
    mb->rx_len = 0;
    mb->frame_ready = false;
}

#endif /* SYN_USE_MODBUS */