Skip to content

File syn_bacnet.c

File List > proto > syn_bacnet.c

Go to the documentation of this file

#include "syn_bacnet.h"

#include <string.h>

/* ── CRC Calculations (ISO 16484-5 Clause 9.3) ─────────────────────────── */

uint8_t syn_bacnet_crc8(const uint8_t *data, size_t len)
{
    uint8_t crc = 0xFFU;
    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x01U) {
                crc = (uint8_t)((crc >> 1) ^ 0x91U);
            } else {
                crc = (uint8_t)(crc >> 1);
            }
        }
    }
    return (uint8_t)(~crc);
}

uint16_t syn_bacnet_crc16(const uint8_t *data, size_t len)
{
    uint16_t crc = 0xFFFFU;
    for (size_t i = 0; i < len; i++) {
        crc ^= (uint16_t)data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001U) {
                crc = (uint16_t)((crc >> 1) ^ 0xA001U);
            } else {
                crc = (uint16_t)(crc >> 1);
            }
        }
    }
    return (uint16_t)(~crc);
}

/* ── MS/TP Framing ──────────────────────────────────────────────────────── */

size_t syn_bacnet_mstp_encode_frame(uint8_t frame_type, uint8_t dest_mac, uint8_t src_mac,
                                    const uint8_t *payload, uint16_t payload_len, uint8_t *out_buf)
{
    if (!out_buf)
        return 0;

    out_buf[0] = SYN_BACNET_MSTP_PREAMBLE_1;
    out_buf[1] = SYN_BACNET_MSTP_PREAMBLE_2;
    out_buf[2] = frame_type;
    out_buf[3] = dest_mac;
    out_buf[4] = src_mac;
    out_buf[5] = (uint8_t)((payload_len >> 8) & 0xFFU);
    out_buf[6] = (uint8_t)(payload_len & 0xFFU);
    out_buf[7] = syn_bacnet_crc8(&out_buf[2], 5);

    size_t idx = 8;
    if (payload && payload_len > 0) {
        memcpy(&out_buf[idx], payload, payload_len);
        idx += payload_len;

        uint16_t data_crc = syn_bacnet_crc16(payload, payload_len);
        out_buf[idx++] = (uint8_t)(data_crc & 0xFFU);
        out_buf[idx++] = (uint8_t)((data_crc >> 8) & 0xFFU);
    }

    return idx;
}

bool syn_bacnet_mstp_decode_frame(const uint8_t *buf, size_t len, SYN_BACnet_MSTP_Frame *frame)
{
    if (!buf || !frame || len < 8)
        return false;

    if (buf[0] != SYN_BACNET_MSTP_PREAMBLE_1 || buf[1] != SYN_BACNET_MSTP_PREAMBLE_2)
        return false;

    uint8_t header_crc = syn_bacnet_crc8(&buf[2], 5);
    if (header_crc != buf[7])
        return false;

    frame->frame_type = buf[2];
    frame->destination_mac = buf[3];
    frame->source_mac = buf[4];
    frame->data_len = ((uint16_t)buf[5] << 8) | (uint16_t)buf[6];

    if (frame->data_len > 501)
        return false;

    if (frame->data_len > 0) {
        if (len < (size_t)(8 + frame->data_len + 2))
            return false;

        memcpy(frame->payload, &buf[8], frame->data_len);

        uint16_t expected_crc = syn_bacnet_crc16(frame->payload, frame->data_len);
        uint16_t actual_crc =
            (uint16_t)buf[8 + frame->data_len] | ((uint16_t)buf[8 + frame->data_len + 1] << 8);

        if (expected_crc != actual_crc)
            return false;
    }

    return true;
}

/* ── Node & Object Management ───────────────────────────────────────────── */

SYN_Status syn_bacnet_node_init(SYN_BACnet_Node *node, uint8_t mac_address, uint32_t device_id)
{
    if (!node)
        return SYN_INVALID_PARAM;

    memset(node, 0, sizeof(SYN_BACnet_Node));
    node->mac_address = mac_address;
    node->device_id = device_id;
    node->max_master = 127;

    /* Automatically register Device Object */
    syn_bacnet_add_object(node, SYN_BACNET_OBJ_DEVICE, device_id, 0.0f,
                          "SyntropicOS-BACnet-Device");

    return SYN_OK;
}

SYN_Status syn_bacnet_add_object(SYN_BACnet_Node *node, uint8_t object_type, uint32_t instance_id,
                                 float init_value, const char *name)
{
    if (!node || node->object_count >= SYN_BACNET_MAX_OBJECTS)
        return SYN_ERROR;

    SYN_BACnet_Object *obj = &node->objects[node->object_count++];
    obj->object_type = object_type;
    obj->instance_id = instance_id;
    obj->present_value = init_value;
    obj->name = name;

    return SYN_OK;
}

/* ── Process Incoming APDU Requests ─────────────────────────────────────── */

SYN_Status syn_bacnet_node_process(SYN_BACnet_Node *node, const SYN_BACnet_MSTP_Frame *rx_frame,
                                   SYN_BACnet_MSTP_Frame *tx_frame, bool *has_tx)
{
    if (!node || !rx_frame || !tx_frame || !has_tx)
        return SYN_INVALID_PARAM;

    *has_tx = false;

    /* Ignore frames not addressed to local MAC or Broadcast */
    if (rx_frame->destination_mac != node->mac_address &&
        rx_frame->destination_mac != SYN_BACNET_BROADCAST_MAC) {
        return SYN_OK;
    }

    /* Handle Token / Poll For Master MS/TP control frames */
    if (rx_frame->frame_type == SYN_BACNET_MSTP_FRAME_TOKEN) {
        return SYN_OK;
    }

    if (rx_frame->frame_type == SYN_BACNET_MSTP_FRAME_POLL_FOR_MASTER) {
        if (rx_frame->destination_mac == node->mac_address) {
            tx_frame->frame_type = SYN_BACNET_MSTP_FRAME_REPLY_TO_POLL_FOR_MASTER;
            tx_frame->destination_mac = rx_frame->source_mac;
            tx_frame->source_mac = node->mac_address;
            tx_frame->data_len = 0;
            *has_tx = true;
        }
        return SYN_OK;
    }

    /* Process APDU Payload */
    if (rx_frame->data_len < 2) {
        return SYN_OK;
    }

    uint8_t pdu_type = rx_frame->payload[0] & 0xF0U;

    /* 1. Unconfirmed Request (PDU Type 0x10): Who-Is -> Respond with I-Am */
    if (pdu_type == 0x10U) {
        uint8_t service_choice = rx_frame->payload[1];
        if (service_choice == SYN_BACNET_SERVICE_UNCONFIRMED_WHO_IS) {
            uint8_t payload[16];
            payload[0] = 0x10;                                /* Unconfirmed APDU */
            payload[1] = SYN_BACNET_SERVICE_UNCONFIRMED_I_AM; /* Service Choice I-Am */

            /* Device Object Identifier (4 bytes) */
            payload[2] = 0xC4;
            payload[3] = (uint8_t)((node->device_id >> 16) & 0xFFU);
            payload[4] = (uint8_t)((node->device_id >> 8) & 0xFFU);
            payload[5] = (uint8_t)(node->device_id & 0xFFU);

            /* Vendor ID (2 bytes) */
            payload[6] = 0x21;
            payload[7] = 0x05;

            tx_frame->frame_type = SYN_BACNET_MSTP_FRAME_DATA_NOT_EXPECTING_REPLY;
            tx_frame->destination_mac = SYN_BACNET_BROADCAST_MAC;
            tx_frame->source_mac = node->mac_address;
            tx_frame->data_len = 8;
            memcpy(tx_frame->payload, payload, 8);
            *has_tx = true;
            return SYN_OK;
        }
    }

    /* 2. Confirmed Request (PDU Type 0x00) */
    if (pdu_type == 0x00U && rx_frame->data_len >= 4) {
        uint8_t invoke_id = rx_frame->payload[2];
        uint8_t service_choice = rx_frame->payload[3];

        if (service_choice == SYN_BACNET_SERVICE_CONFIRMED_READ_PROPERTY) {
            /* Decode requested instance_id if payload contains Context Tag 0 */
            uint32_t req_instance = 0;
            if (rx_frame->data_len >= 9 && rx_frame->payload[4] == 0x0C) {
                uint32_t obj_id = ((uint32_t)rx_frame->payload[5] << 24) |
                                  ((uint32_t)rx_frame->payload[6] << 16) |
                                  ((uint32_t)rx_frame->payload[7] << 8) |
                                  (uint32_t)rx_frame->payload[8];
                req_instance = obj_id & 0x003FFFFFU;
            }

            /* Look up requested object by instance_id */
            float val = 0.0f;
            bool found = false;

            for (size_t i = 0; i < node->object_count; i++) {
                if (node->objects[i].instance_id == req_instance) {
                    val = node->objects[i].present_value;
                    found = true;
                    break;
                }
            }

            if (found) {
                uint8_t payload[32];
                payload[0] = 0x30; /* Complex-ACK APDU */
                payload[1] = invoke_id;
                payload[2] = SYN_BACNET_SERVICE_CONFIRMED_READ_PROPERTY;

                /* Encoded Present Value Float */
                payload[3] = 0x44; /* Real tag */
                memcpy(&payload[4], &val, sizeof(float));

                tx_frame->frame_type = SYN_BACNET_MSTP_FRAME_DATA_NOT_EXPECTING_REPLY;
                tx_frame->destination_mac = rx_frame->source_mac;
                tx_frame->source_mac = node->mac_address;
                tx_frame->data_len = 8;
                memcpy(tx_frame->payload, payload, 8);
                *has_tx = true;
            }
            return SYN_OK;
        }

        if (service_choice == SYN_BACNET_SERVICE_CONFIRMED_WRITE_PROPERTY) {
            /* WriteProperty APDU: Object ID at payload[4..8], Property Value float at
             * payload[10..13] */
            uint32_t req_instance = 0;
            if (rx_frame->data_len >= 9 && rx_frame->payload[4] == 0x0C) {
                uint32_t obj_id = ((uint32_t)rx_frame->payload[5] << 24) |
                                  ((uint32_t)rx_frame->payload[6] << 16) |
                                  ((uint32_t)rx_frame->payload[7] << 8) |
                                  (uint32_t)rx_frame->payload[8];
                req_instance = obj_id & 0x003FFFFFU;
            }

            float new_val = 0.0f;
            if (rx_frame->data_len >= 14 && rx_frame->payload[9] == 0x44) {
                memcpy(&new_val, &rx_frame->payload[10], sizeof(float));
            }

            /* Update object value */
            for (size_t i = 0; i < node->object_count; i++) {
                if (node->objects[i].instance_id == req_instance) {
                    node->objects[i].present_value = new_val;
                    break;
                }
            }

            /* Return WriteProperty Simple-ACK APDU (PDU Type 0x20) */
            uint8_t payload[4];
            payload[0] = 0x20; /* Simple-ACK APDU */
            payload[1] = invoke_id;
            payload[2] = SYN_BACNET_SERVICE_CONFIRMED_WRITE_PROPERTY;

            tx_frame->frame_type = SYN_BACNET_MSTP_FRAME_DATA_NOT_EXPECTING_REPLY;
            tx_frame->destination_mac = rx_frame->source_mac;
            tx_frame->source_mac = node->mac_address;
            tx_frame->data_len = 3;
            memcpy(tx_frame->payload, payload, 3);
            *has_tx = true;
            return SYN_OK;
        }
    }

    return SYN_OK;
}