Skip to content

File syn_ir.c

File List > proto > syn_ir.c

Go to the documentation of this file

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

#if !defined(SYN_USE_IR) || SYN_USE_IR

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

#include <string.h>

/* ── Protocol Descriptors ────────────────────────────────────────────────── */

typedef struct {
    SYN_IR_Protocol proto;
    const char *name;
    uint8_t carrier_khz;
    SYN_IR_EncodingType encoding;
    uint16_t leader_mark_us;
    uint16_t leader_space_us;
    uint16_t bit_mark_us;   /* PDM mark duration or PWM '0' mark */
    uint16_t zero_space_us; /* PDM '0' space or PWM fixed space */
    uint16_t one_space_us;  /* PDM '1' space */
    uint16_t one_mark_us;   /* PWM '1' mark */
    uint16_t half_bit_us;   /* Manchester half-bit duration */
    uint8_t total_bits;
    uint8_t addr_bits;
    uint8_t cmd_bits;
    uint16_t repeat_space_us;
    uint16_t tolerance_us;
} SYN_IR_ProtoDesc;

static const SYN_IR_ProtoDesc proto_table[SYN_IR_PROTO_COUNT] = {
    [SYN_IR_PROTO_UNKNOWN] = {.proto = SYN_IR_PROTO_UNKNOWN, .name = "Unknown"},
    [SYN_IR_PROTO_NEC] = {.proto = SYN_IR_PROTO_NEC,
                          .name = "NEC",
                          .carrier_khz = 38,
                          .encoding = SYN_IR_ENC_PDM,
                          .leader_mark_us = 9000,
                          .leader_space_us = 4500,
                          .bit_mark_us = 560,
                          .zero_space_us = 560,
                          .one_space_us = 1690,
                          .total_bits = 32,
                          .addr_bits = 8,
                          .cmd_bits = 8,
                          .repeat_space_us = 2250,
                          .tolerance_us = 200},
    [SYN_IR_PROTO_NEC_EXTENDED] = {.proto = SYN_IR_PROTO_NEC_EXTENDED,
                                   .name = "NEC-Extended",
                                   .carrier_khz = 38,
                                   .encoding = SYN_IR_ENC_PDM,
                                   .leader_mark_us = 9000,
                                   .leader_space_us = 4500,
                                   .bit_mark_us = 560,
                                   .zero_space_us = 560,
                                   .one_space_us = 1690,
                                   .total_bits = 32,
                                   .addr_bits = 16,
                                   .cmd_bits = 8,
                                   .repeat_space_us = 2250,
                                   .tolerance_us = 200},
    [SYN_IR_PROTO_SONY_12] = {.proto = SYN_IR_PROTO_SONY_12,
                              .name = "Sony-12",
                              .carrier_khz = 40,
                              .encoding = SYN_IR_ENC_PWM,
                              .leader_mark_us = 2400,
                              .leader_space_us = 600,
                              .bit_mark_us = 600,   /* '0' mark */
                              .one_mark_us = 1200,  /* '1' mark */
                              .zero_space_us = 600, /* fixed space */
                              .total_bits = 12,
                              .addr_bits = 5,
                              .cmd_bits = 7,
                              .tolerance_us = 150},
    [SYN_IR_PROTO_SONY_15] = {.proto = SYN_IR_PROTO_SONY_15,
                              .name = "Sony-15",
                              .carrier_khz = 40,
                              .encoding = SYN_IR_ENC_PWM,
                              .leader_mark_us = 2400,
                              .leader_space_us = 600,
                              .bit_mark_us = 600,
                              .one_mark_us = 1200,
                              .zero_space_us = 600,
                              .total_bits = 15,
                              .addr_bits = 8,
                              .cmd_bits = 7,
                              .tolerance_us = 150},
    [SYN_IR_PROTO_SONY_20] = {.proto = SYN_IR_PROTO_SONY_20,
                              .name = "Sony-20",
                              .carrier_khz = 40,
                              .encoding = SYN_IR_ENC_PWM,
                              .leader_mark_us = 2400,
                              .leader_space_us = 600,
                              .bit_mark_us = 600,
                              .one_mark_us = 1200,
                              .zero_space_us = 600,
                              .total_bits = 20,
                              .addr_bits = 5,
                              .cmd_bits = 7,
                              .tolerance_us = 150},
    [SYN_IR_PROTO_RC5] = {.proto = SYN_IR_PROTO_RC5,
                          .name = "Philips-RC5",
                          .carrier_khz = 36,
                          .encoding = SYN_IR_ENC_MANCHESTER,
                          .half_bit_us = 889,
                          .total_bits = 14,
                          .addr_bits = 5,
                          .cmd_bits = 6,
                          .tolerance_us = 200},
    [SYN_IR_PROTO_RC6] = {.proto = SYN_IR_PROTO_RC6,
                          .name = "Philips-RC6",
                          .carrier_khz = 36,
                          .encoding = SYN_IR_ENC_MANCHESTER,
                          .leader_mark_us = 2666,
                          .leader_space_us = 889,
                          .half_bit_us = 444,
                          .total_bits = 21,
                          .addr_bits = 8,
                          .cmd_bits = 8,
                          .tolerance_us = 150},
    [SYN_IR_PROTO_SAMSUNG] = {.proto = SYN_IR_PROTO_SAMSUNG,
                              .name = "Samsung",
                              .carrier_khz = 38,
                              .encoding = SYN_IR_ENC_PDM,
                              .leader_mark_us = 4500,
                              .leader_space_us = 4500,
                              .bit_mark_us = 590,
                              .zero_space_us = 590,
                              .one_space_us = 1690,
                              .total_bits = 32,
                              .addr_bits = 16,
                              .cmd_bits = 8,
                              .tolerance_us = 200},
    [SYN_IR_PROTO_KASEIKYO] = {.proto = SYN_IR_PROTO_KASEIKYO,
                               .name = "Kaseikyo/Panasonic",
                               .carrier_khz = 37,
                               .encoding = SYN_IR_ENC_PDM,
                               .leader_mark_us = 3456,
                               .leader_space_us = 1728,
                               .bit_mark_us = 432,
                               .zero_space_us = 432,
                               .one_space_us = 1296,
                               .total_bits = 48,
                               .addr_bits = 16,
                               .cmd_bits = 16,
                               .tolerance_us = 150},
    [SYN_IR_PROTO_DENON] = {.proto = SYN_IR_PROTO_DENON,
                            .name = "Denon/Sharp",
                            .carrier_khz = 38,
                            .encoding = SYN_IR_ENC_PDM,
                            .leader_mark_us = 0,
                            .leader_space_us = 0,
                            .bit_mark_us = 310,
                            .zero_space_us = 780,
                            .one_space_us = 1780,
                            .total_bits = 15,
                            .addr_bits = 5,
                            .cmd_bits = 8,
                            .tolerance_us = 150},
    [SYN_IR_PROTO_APPLE] = {.proto = SYN_IR_PROTO_APPLE,
                            .name = "Apple",
                            .carrier_khz = 38,
                            .encoding = SYN_IR_ENC_PDM,
                            .leader_mark_us = 9000,
                            .leader_space_us = 4500,
                            .bit_mark_us = 560,
                            .zero_space_us = 560,
                            .one_space_us = 1690,
                            .total_bits = 32,
                            .addr_bits = 16,
                            .cmd_bits = 8,
                            .repeat_space_us = 2250,
                            .tolerance_us = 200}};

/* ── Helper Functions ────────────────────────────────────────────────────── */

static bool timing_match(uint16_t actual, uint16_t expected, uint16_t tolerance)
{
    if (expected == 0)
        return false;
    uint16_t diff = (actual > expected) ? (actual - expected) : (expected - actual);
    return (diff <= tolerance);
}

const char *syn_ir_protocol_name(SYN_IR_Protocol proto)
{
    if (proto >= SYN_IR_PROTO_COUNT) {
        return "Unknown";
    }
    return proto_table[proto].name;
}

static void reset_decoder_state(SYN_IR_Decoder *decoder)
{
    bool have_last = decoder->have_last;
    SYN_IR_Frame last_f = decoder->last_frame;

    memset(decoder, 0, sizeof(SYN_IR_Decoder));
    decoder->have_last = have_last;
    decoder->last_frame = last_f;
    decoder->state = SYN_IR_STATE_IDLE;
    decoder->active_proto = SYN_IR_PROTO_UNKNOWN;
}

SYN_Status syn_ir_decoder_init(SYN_IR_Decoder *decoder)
{
    if (decoder == NULL) {
        return SYN_INVALID_PARAM;
    }
    memset(decoder, 0, sizeof(SYN_IR_Decoder));
    decoder->state = SYN_IR_STATE_IDLE;
    decoder->active_proto = SYN_IR_PROTO_UNKNOWN;
    return SYN_OK;
}

static bool unpack_frame(const SYN_IR_Decoder *decoder, SYN_IR_Frame *frame_out)
{
    SYN_IR_Protocol proto = decoder->active_proto;
    if (proto >= SYN_IR_PROTO_COUNT) {
        return false;
    }
    const SYN_IR_ProtoDesc *desc = &proto_table[proto];

    frame_out->protocol = proto;
    frame_out->carrier_khz = desc->carrier_khz;
    frame_out->flags = SYN_IR_FLAG_NONE;
    frame_out->bit_count = decoder->bit_idx;

    uint64_t bits = decoder->bits;

    switch (proto) {
    case SYN_IR_PROTO_NEC:
    case SYN_IR_PROTO_NEC_EXTENDED:
    case SYN_IR_PROTO_APPLE: {
        uint8_t addr = (uint8_t)(bits & 0xFFU);
        uint8_t addr_inv = (uint8_t)((bits >> 8) & 0xFFU);
        uint8_t cmd = (uint8_t)((bits >> 16) & 0xFFU);
        uint8_t cmd_inv = (uint8_t)((bits >> 24) & 0xFFU);

        if ((bits & 0xFFFFU) == 0xEE87U) {
            frame_out->protocol = SYN_IR_PROTO_APPLE;
            frame_out->address = (uint16_t)(bits & 0xFFFFU);
            frame_out->command = cmd;
        } else if ((cmd ^ cmd_inv) == 0xFFU) {
            if ((addr ^ addr_inv) == 0xFFU) {
                frame_out->protocol = SYN_IR_PROTO_NEC;
                frame_out->address = addr;
            } else {
                frame_out->protocol = SYN_IR_PROTO_NEC_EXTENDED;
                frame_out->address = (uint16_t)(bits & 0xFFFFU);
            }
            frame_out->command = cmd;
        } else {
            return false;
        }
        break;
    }

    case SYN_IR_PROTO_SONY_12:
    case SYN_IR_PROTO_SONY_15:
    case SYN_IR_PROTO_SONY_20: {
        if (decoder->bit_idx == 12) {
            frame_out->protocol = SYN_IR_PROTO_SONY_12;
            frame_out->command = (uint32_t)(bits & 0x7FU);
            frame_out->address = (uint32_t)((bits >> 7) & 0x1FU);
        } else if (decoder->bit_idx == 15) {
            frame_out->protocol = SYN_IR_PROTO_SONY_15;
            frame_out->command = (uint32_t)(bits & 0x7FU);
            frame_out->address = (uint32_t)((bits >> 7) & 0xFFU);
        } else if (decoder->bit_idx == 20) {
            frame_out->protocol = SYN_IR_PROTO_SONY_20;
            frame_out->command = (uint32_t)(bits & 0x7FU);
            frame_out->address =
                (uint32_t)((bits >> 7) & 0x1FU) | (((uint32_t)((bits >> 12) & 0xFFU)) << 5);
        } else {
            return false;
        }
        break;
    }

    case SYN_IR_PROTO_RC5: {
        /* 14 bits: S1(1), S2(1), Toggle(1), Address(5), Command(6) */
        uint8_t toggle = (uint8_t)((bits >> 11) & 0x01U);
        uint8_t address = (uint8_t)((bits >> 6) & 0x1FU);
        uint8_t command = (uint8_t)(bits & 0x3FU);

        if (toggle) {
            frame_out->flags |= SYN_IR_FLAG_TOGGLE;
        }
        frame_out->address = address;
        frame_out->command = command;
        break;
    }

    case SYN_IR_PROTO_RC6: {
        /* 21 bits: S(1), Mode(3), Toggle(1), Address(8), Command(8) */
        uint8_t toggle = (uint8_t)((bits >> 16) & 0x01U);
        uint8_t address = (uint8_t)((bits >> 8) & 0xFFU);
        uint8_t command = (uint8_t)(bits & 0xFFU);

        if (toggle) {
            frame_out->flags |= SYN_IR_FLAG_TOGGLE;
        }
        frame_out->address = address;
        frame_out->command = command;
        break;
    }

    case SYN_IR_PROTO_SAMSUNG: {
        uint16_t addr = (uint16_t)(bits & 0xFFFFU);
        uint8_t cmd = (uint8_t)((bits >> 16) & 0xFFU);
        uint8_t cmd_inv = (uint8_t)((bits >> 24) & 0xFFU);

        if ((cmd ^ cmd_inv) != 0xFFU) {
            return false;
        }
        frame_out->address = addr;
        frame_out->command = cmd;
        break;
    }

    case SYN_IR_PROTO_KASEIKYO: {
        uint16_t addr = (uint16_t)(bits & 0xFFFFU);
        uint32_t cmd = (uint32_t)((bits >> 16) & 0xFFFFFFFFULL);
        frame_out->address = addr;
        frame_out->command = cmd;
        break;
    }

    case SYN_IR_PROTO_DENON: {
        /* 15 bits: 5-bit address, 8-bit command, 2-bit checksum */
        frame_out->address = (uint32_t)((bits >> 10) & 0x1FU);
        frame_out->command = (uint32_t)((bits >> 2) & 0xFFU);
        break;
    }

    default:
        return false;
    }

    return true;
}

/* ── Decoder FSM ─────────────────────────────────────────────────────────── */

bool syn_ir_decode_pulse(SYN_IR_Decoder *decoder, uint16_t duration_us, bool is_mark,
                         SYN_IR_Frame *frame_out)
{
    if (decoder == NULL || frame_out == NULL) {
        return false;
    }

    /* Manchester bit phase tracking */
    if (decoder->state == SYN_IR_STATE_DATA) {
        SYN_IR_Protocol proto = decoder->active_proto;
        const SYN_IR_ProtoDesc *desc = &proto_table[proto];
        if (desc->encoding == SYN_IR_ENC_MANCHESTER) {
            bool is_full = timing_match(duration_us, desc->half_bit_us * 2, desc->tolerance_us);
            bool is_half = timing_match(duration_us, desc->half_bit_us, desc->tolerance_us);
            if (is_half || is_full) {
                if (is_full) {
                    decoder->manchester_phase = !decoder->manchester_phase;
                }
                decoder->manchester_phase = !decoder->manchester_phase;
                if (decoder->manchester_phase) {
                    uint8_t bit_val = is_mark ? 1 : 0;
                    decoder->bits = (decoder->bits << 1) | bit_val;
                    decoder->bit_idx++;

                    if (decoder->bit_idx >= decoder->expected_bits) {
                        if (unpack_frame(decoder, frame_out)) {
                            decoder->last_frame = *frame_out;
                            decoder->have_last = true;
                            reset_decoder_state(decoder);
                            return true;
                        }
                        reset_decoder_state(decoder);
                        return false;
                    }
                }
            }
        }
    }

    if (is_mark) {
        decoder->last_mark_us = duration_us;
        return false;
    }

    /* Process space pulse paired with previous mark */
    uint16_t mark_us = decoder->last_mark_us;
    uint16_t space_us = duration_us;
    decoder->last_space_us = space_us;

    if (decoder->state == SYN_IR_STATE_IDLE) {
        /* Check leader pulses against protocol table */
        for (int p = 1; p < SYN_IR_PROTO_COUNT; p++) {
            /* Skip duplicate variants in leader lookup */
            if (p == SYN_IR_PROTO_SONY_12 || p == SYN_IR_PROTO_SONY_15 ||
                p == SYN_IR_PROTO_NEC_EXTENDED) {
                continue;
            }

            const SYN_IR_ProtoDesc *desc = &proto_table[p];

            /* Check NEC repeat code first */
            if ((p == SYN_IR_PROTO_NEC || p == SYN_IR_PROTO_APPLE) &&
                timing_match(mark_us, desc->leader_mark_us, desc->tolerance_us) &&
                timing_match(space_us, desc->repeat_space_us, desc->tolerance_us) &&
                decoder->have_last) {
                *frame_out = decoder->last_frame;
                frame_out->flags |= SYN_IR_FLAG_REPEAT;
                return true;
            }

            /* Standard leader pulse match */
            if (desc->leader_mark_us > 0 &&
                timing_match(mark_us, desc->leader_mark_us, desc->tolerance_us) &&
                timing_match(space_us, desc->leader_space_us, desc->tolerance_us)) {
                decoder->active_proto = (SYN_IR_Protocol)p;
                decoder->expected_bits = desc->total_bits;
                decoder->state = SYN_IR_STATE_DATA;
                decoder->bits = 0;
                decoder->bit_idx = 0;
                return false;
            }
        }

        /* Check Denon leaderless start */
        const SYN_IR_ProtoDesc *denon = &proto_table[SYN_IR_PROTO_DENON];
        if (timing_match(mark_us, denon->bit_mark_us, denon->tolerance_us) &&
            (timing_match(space_us, denon->zero_space_us, denon->tolerance_us) ||
             timing_match(space_us, denon->one_space_us, denon->tolerance_us))) {
            decoder->active_proto = SYN_IR_PROTO_DENON;
            decoder->expected_bits = denon->total_bits;
            decoder->state = SYN_IR_STATE_DATA;
            decoder->bits = 0;
            decoder->bit_idx = 0;
            /* Process first bit */
            uint8_t bit = timing_match(space_us, denon->one_space_us, denon->tolerance_us) ? 1 : 0;
            decoder->bits |= (((uint64_t)bit) << decoder->bit_idx);
            decoder->bit_idx++;
            return false;
        }

        /* Check RC5 / Manchester start bit */
        const SYN_IR_ProtoDesc *rc5 = &proto_table[SYN_IR_PROTO_RC5];
        if (timing_match(mark_us, rc5->half_bit_us, rc5->tolerance_us) ||
            timing_match(mark_us, rc5->half_bit_us * 2, rc5->tolerance_us)) {
            decoder->active_proto = SYN_IR_PROTO_RC5;
            decoder->expected_bits = rc5->total_bits;
            decoder->state = SYN_IR_STATE_DATA;
            decoder->bits = 1; /* First start bit = 1 */
            decoder->bit_idx = 1;
            decoder->manchester_phase = false;
            return false;
        }

        return false;
    }

    /* DATA State */
    if (decoder->state == SYN_IR_STATE_DATA) {
        SYN_IR_Protocol proto = decoder->active_proto;
        const SYN_IR_ProtoDesc *desc = &proto_table[proto];

        if (desc->encoding == SYN_IR_ENC_PDM || desc->encoding == SYN_IR_ENC_PPM) {
            if (!timing_match(mark_us, desc->bit_mark_us, desc->tolerance_us)) {
                reset_decoder_state(decoder);
                return false;
            }

            uint8_t bit_val = 0;
            if (timing_match(space_us, desc->one_space_us, desc->tolerance_us)) {
                bit_val = 1;
            } else if (timing_match(space_us, desc->zero_space_us, desc->tolerance_us)) {
                bit_val = 0;
            } else {
                reset_decoder_state(decoder);
                return false;
            }

            if (decoder->bit_idx < 64) {
                decoder->bits |= (((uint64_t)bit_val) << decoder->bit_idx);
            }
            decoder->bit_idx++;

            if (decoder->bit_idx >= decoder->expected_bits) {
                if (unpack_frame(decoder, frame_out)) {
                    decoder->last_frame = *frame_out;
                    decoder->have_last = true;
                    reset_decoder_state(decoder);
                    return true;
                }
                reset_decoder_state(decoder);
                return false;
            }
        } else if (desc->encoding == SYN_IR_ENC_PWM) {
            /* Sony PWM: space is fixed, mark duration indicates bit 0 (600us) vs bit 1 (1200us) */
            uint8_t bit_val = 0;
            if (timing_match(mark_us, desc->one_mark_us, desc->tolerance_us)) {
                bit_val = 1;
            } else if (timing_match(mark_us, desc->bit_mark_us, desc->tolerance_us)) {
                bit_val = 0;
            } else {
                reset_decoder_state(decoder);
                return false;
            }

            if (decoder->bit_idx < 64) {
                decoder->bits |= (((uint64_t)bit_val) << decoder->bit_idx);
            }
            decoder->bit_idx++;

            if (decoder->bit_idx >= decoder->expected_bits || decoder->bit_idx == 12 ||
                decoder->bit_idx == 15) {
                if (unpack_frame(decoder, frame_out)) {
                    decoder->last_frame = *frame_out;
                    decoder->have_last = true;
                    if (decoder->bit_idx >= decoder->expected_bits) {
                        reset_decoder_state(decoder);
                    }
                    return true;
                }
            }
        }
    }

    return false;
}

bool syn_ir_decode_timeout(SYN_IR_Decoder *decoder, SYN_IR_Frame *frame_out)
{
    if (decoder == NULL || frame_out == NULL) {
        return false;
    }

    if (decoder->state == SYN_IR_STATE_DATA && decoder->bit_idx > 0) {
        if (unpack_frame(decoder, frame_out)) {
            decoder->last_frame = *frame_out;
            decoder->have_last = true;
            reset_decoder_state(decoder);
            return true;
        }
    }

    reset_decoder_state(decoder);
    return false;
}

/* ── Encoder ─────────────────────────────────────────────────────────────── */

SYN_Status syn_ir_encode_frame(const SYN_IR_Frame *frame, SYN_IR_Pulse *pulse_buf, size_t buf_len,
                               size_t *count_out)
{
    if (frame == NULL || pulse_buf == NULL || count_out == NULL) {
        return SYN_INVALID_PARAM;
    }

    if ((int)frame->protocol <= 0 || frame->protocol >= SYN_IR_PROTO_COUNT) {
        return SYN_INVALID_PARAM;
    }

    const SYN_IR_ProtoDesc *desc = &proto_table[frame->protocol];
    size_t required_pulses = 0;

    if (desc->leader_mark_us > 0) {
        required_pulses += 2; /* Leader mark + space */
    }
    required_pulses += ((size_t)desc->total_bits) * 2; /* Data mark + space */
    required_pulses += 1;                              /* Trailer stop bit mark */

    if (buf_len < required_pulses) {
        return SYN_ERROR;
    }

    size_t idx = 0;

    /* Leader Pulse */
    if (desc->leader_mark_us > 0) {
        pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = desc->leader_mark_us, .is_mark = true};
        pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = desc->leader_space_us, .is_mark = false};
    }

    /* Build raw bit stream integer depending on protocol */
    uint64_t raw_bits = 0;

    switch (frame->protocol) {
    case SYN_IR_PROTO_NEC: {
        uint8_t addr = (uint8_t)(frame->address & 0xFFU);
        uint8_t cmd = (uint8_t)(frame->command & 0xFFU);
        raw_bits = ((uint64_t)addr) | (((uint64_t)(~addr & 0xFFU)) << 8) | (((uint64_t)cmd) << 16) |
                   (((uint64_t)(~cmd & 0xFFU)) << 24);
        break;
    }

    case SYN_IR_PROTO_NEC_EXTENDED: {
        uint16_t addr = (uint16_t)(frame->address & 0xFFFFU);
        uint8_t cmd = (uint8_t)(frame->command & 0xFFU);
        raw_bits = ((uint64_t)addr) | (((uint64_t)cmd) << 16) | (((uint64_t)(~cmd & 0xFFU)) << 24);
        break;
    }

    case SYN_IR_PROTO_APPLE: {
        uint16_t addr = (uint16_t)(frame->address & 0xFFFFU);
        uint8_t cmd = (uint8_t)(frame->command & 0xFFU);
        raw_bits = ((uint64_t)addr) | (((uint64_t)cmd) << 16) | (((uint64_t)cmd) << 24);
        break;
    }

    case SYN_IR_PROTO_SONY_12:
        raw_bits = (frame->command & 0x7FU) | ((frame->address & 0x1FU) << 7);
        break;

    case SYN_IR_PROTO_SONY_15:
        raw_bits = (frame->command & 0x7FU) | ((frame->address & 0xFFU) << 7);
        break;

    case SYN_IR_PROTO_SONY_20:
        raw_bits = (frame->command & 0x7FU) | ((frame->address & 0x1FU) << 7) |
                   (((frame->address >> 5) & 0xFFU) << 12);
        break;

    case SYN_IR_PROTO_RC5: {
        uint8_t toggle = (frame->flags & SYN_IR_FLAG_TOGGLE) ? 1 : 0;
        /* 14 bits MSB to LSB: S1(1), S2(1), Toggle(1), Address(5), Command(6) */
        raw_bits = (1ULL << 13) | (1ULL << 12) | (((uint64_t)toggle) << 11) |
                   (((uint64_t)(frame->address & 0x1FU)) << 6) |
                   ((uint64_t)(frame->command & 0x3FU));
        break;
    }

    case SYN_IR_PROTO_RC6: {
        uint8_t toggle = (frame->flags & SYN_IR_FLAG_TOGGLE) ? 1 : 0;
        /* 21 bits MSB to LSB: Start(1), Mode(3), Toggle(1), Address(8), Command(8) */
        raw_bits = (1ULL << 20) | (((uint64_t)toggle) << 16) |
                   (((uint64_t)(frame->address & 0xFFU)) << 8) |
                   ((uint64_t)(frame->command & 0xFFU));
        break;
    }

    case SYN_IR_PROTO_SAMSUNG: {
        uint16_t addr = (uint16_t)(frame->address & 0xFFFFU);
        uint8_t cmd = (uint8_t)(frame->command & 0xFFU);
        raw_bits = ((uint64_t)addr) | (((uint64_t)cmd) << 16) | (((uint64_t)(~cmd & 0xFFU)) << 24);
        break;
    }

    case SYN_IR_PROTO_KASEIKYO:
        raw_bits = (frame->address & 0xFFFFU) | (((uint64_t)frame->command) << 16);
        break;

    case SYN_IR_PROTO_DENON:
        raw_bits = ((frame->address & 0x1FU) << 10) | ((frame->command & 0xFFU) << 2);
        break;

    default:
        return SYN_INVALID_PARAM;
    }

    /* Encode Data Bits */
    if (desc->encoding == SYN_IR_ENC_MANCHESTER) {
        for (int b = desc->total_bits - 1; b >= 0; b--) {
            uint8_t bit_val = (uint8_t)((raw_bits >> b) & 0x01U);
            if (frame->protocol == SYN_IR_PROTO_RC6) {
                bit_val = !bit_val;
            }
            uint16_t half = desc->half_bit_us;
            if (bit_val == 1) {
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = half, .is_mark = false};
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = half, .is_mark = true};
            } else {
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = half, .is_mark = true};
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = half, .is_mark = false};
            }
        }
    } else {
        for (uint8_t b = 0; b < desc->total_bits; b++) {
            uint8_t bit_val = (uint8_t)((raw_bits >> b) & 0x01U);

            if (desc->encoding == SYN_IR_ENC_PDM || desc->encoding == SYN_IR_ENC_PPM) {
                uint16_t space = (bit_val == 1) ? desc->one_space_us : desc->zero_space_us;
                pulse_buf[idx++] =
                    (SYN_IR_Pulse){.duration_us = desc->bit_mark_us, .is_mark = true};
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = space, .is_mark = false};
            } else if (desc->encoding == SYN_IR_ENC_PWM) {
                uint16_t mark = (bit_val == 1) ? desc->one_mark_us : desc->bit_mark_us;
                pulse_buf[idx++] = (SYN_IR_Pulse){.duration_us = mark, .is_mark = true};
                pulse_buf[idx++] =
                    (SYN_IR_Pulse){.duration_us = desc->zero_space_us, .is_mark = false};
            }
        }
    }

    /* Trailer Stop Bit Mark */
    pulse_buf[idx++] = (SYN_IR_Pulse){
        .duration_us = desc->bit_mark_us > 0 ? desc->bit_mark_us : 500, .is_mark = true};

    *count_out = idx;
    return SYN_OK;
}

#endif /* SYN_USE_IR */