// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Modem Host Software
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
 *  Copyright (C) 2024 Patrick McDonnell, W3AXL
 *  Copyright (C) 2026 Timothy Sawyer, WD6AWP
 *
 */
#include "Defines.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/DataHeader.h"
#include "common/p25/data/DataBlock.h"
#include "common/p25/data/LowSpeedData.h"
#include "common/p25/dfsi/LC.h"
#include "common/p25/dfsi/frames/Frames.h"
#include "common/p25/lc/LC.h"
#include "common/p25/lc/tdulc/TDULCFactory.h"
#include "common/p25/lc/tsbk/TSBKFactory.h"
#include "common/p25/NID.h"
#include "common/p25/P25Utils.h"
#include "common/p25/Sync.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "modem/ModemV24.h"

using namespace modem;
using namespace p25;
using namespace p25::defines;
using namespace p25::dfsi::defines;
using namespace p25::dfsi::frames;

#include <cassert>

// ---------------------------------------------------------------------------
//  Public Class Members
// ---------------------------------------------------------------------------

/* Initializes a new instance of the ModemV24 class. */

ModemV24::ModemV24(port::IModemPort* port, bool duplex, uint32_t p25QueueSize, uint32_t p25TxQueueSize,
    bool rtrt, uint16_t jitter, bool dumpModemStatus, bool displayDebugMessages, bool trace, bool debug) :
    Modem(port, duplex, false, false, false, false, false, 80U, 7U, 8U, 1U, p25QueueSize, 1U,
        false, false, dumpModemStatus, displayDebugMessages, trace, debug),
    m_rtrt(rtrt),
    m_superFrameCnt(0U),
    m_audio(),
    m_nid(nullptr),
    m_txP25Queue(p25TxQueueSize, "V.24 TX P25 Queue"),
    m_txImmP25Queue(p25TxQueueSize, "V.24 TX Immediate P25 Queue"),
    m_txCall(),
    m_rxCall(),
    m_txCallInProgress(false),
    m_rxCallInProgress(false),
    m_txLastFrameTime(0U),
    m_rxLastFrameTime(0U),
    m_callTimeout(200U),
    m_jitter(jitter),
    m_lastP25Tx(0U),
    m_rs(),
    m_useTIAFormat(false),
    m_txP25QueueLock()
{
    m_v24Connected = false; // defaulted to false for V.24 modems

    // Init m_call
    m_txCall = new DFSICallData();
    m_rxCall = new DFSICallData();
}

/* Finalizes a instance of the Modem class. */

ModemV24::~ModemV24()
{
    delete m_nid;
    delete m_txCall;
    delete m_rxCall;
}

/* Sets the call timeout. */

void ModemV24::setCallTimeout(uint16_t timeout)
{
    m_callTimeout = timeout;
}

/* Sets the P25 NAC. */

void ModemV24::setP25NAC(uint32_t nac)
{
    Modem::setP25NAC(nac);
    m_nid = new NID(nac);
}

/* Helper to set the TIA-102 format DFSI frame flag. */

void ModemV24::setTIAFormat(bool set)
{
    m_useTIAFormat = set;
}

/* Opens connection to the air interface modem. */

bool ModemV24::open()
{
    LogInfoEx(LOG_MODEM, "Initializing modem");
    m_gotModemStatus = false;

    bool ret = m_port->open();
    if (!ret)
        return false;

    ret = getFirmwareVersion();
    if (!ret) {
        m_port->close();
        return false;
    } else {
        // Stopping the inactivity timer here when a firmware version has been
        // successfuly read prevents the death spiral of "no reply from modem..."
        m_inactivityTimer.stop();
    }

    m_rspOffset = 0U;
    m_rspState = RESP_START;

    // do we have an open port handler?
    if (m_openPortHandler) {
        ret = m_openPortHandler(this);
        if (!ret)
            return false;

        m_error = false;
        return true;
    }

    m_statusTimer.start();

    m_error = false;

    if (m_useTIAFormat)
        LogInfoEx(LOG_MODEM, "Modem Ready [Direct Mode / TIA-102]");
    else
        LogInfoEx(LOG_MODEM, "Modem Ready [Direct Mode / V.24]");

    return true;
}

/* Updates the timer by the passed number of milliseconds. */

void ModemV24::clock(uint32_t ms)
{
    // poll the modem status
    m_statusTimer.clock(ms);
    if (m_statusTimer.hasExpired()) {
        getStatus();
        m_statusTimer.start();
    }

    m_inactivityTimer.clock(ms);
    if (m_inactivityTimer.hasExpired()) {
        LogError(LOG_MODEM, "No reply from the modem for some time, resetting it");
        reset();
    }

    uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    bool forceModemReset = false;
    RESP_TYPE_DVM type = getResponse();

    // do we have a custom response handler?
    if (m_rspHandler != nullptr) {
        // execute custom response handler
        if (m_rspHandler(this, ms, type, m_rspDoubleLength, m_buffer, m_length)) {
            // all logic handled by handler -- return
            return;
        }
    }

    if (type == RTM_TIMEOUT) {
        // Nothing to do
    }
    else if (type == RTM_ERROR) {
        // Nothing to do
    }
    else {
        // type == RTM_OK
        uint8_t cmdOffset = 2U;
        if (m_rspDoubleLength) {
            cmdOffset = 3U;
        }

        switch (m_buffer[cmdOffset]) {
        /** Project 25 */
        case CMD_P25_DATA:
        {
            if (m_p25Enabled) {
                std::lock_guard<std::mutex> lock(m_p25ReadLock);
            
                // convert data from V.24/DFSI formatting to TIA-102 air formatting
                if (m_useTIAFormat)
                    convertToAirTIA(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U));
                else
                    convertToAirV24(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U));
                if (m_trace)
                    Utils::dump(1U, "ModemV24::clock(), RX P25 Data", m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U));
            }
        }
        break;

        case CMD_P25_LOST:
        {
            if (m_p25Enabled) {
                std::lock_guard<std::mutex> lock(m_p25ReadLock);

                if (m_rspDoubleLength) {
                    LogError(LOG_MODEM, "CMD_P25_LOST double length?; len = %u", m_length);
                    break;
                }

                uint8_t data = 1U;
                m_rxP25Queue.addData(&data, 1U);

                data = TAG_LOST;
                m_rxP25Queue.addData(&data, 1U);
            }
        }
        break;

        /** General */
        case CMD_GET_STATUS:
        {
            m_isHotspot = (m_buffer[3U] & 0x01U) == 0x01U;

            // override hotspot flag if we're forcing hotspot
            if (m_forceHotspot) {
                m_isHotspot = m_forceHotspot;
            }

            bool dmrEnable = (m_buffer[3U] & 0x02U) == 0x02U;
            bool p25Enable = (m_buffer[3U] & 0x08U) == 0x08U;
            bool nxdnEnable = (m_buffer[3U] & 0x10U) == 0x10U;

            // flag indicating if free space is being reported in 16-byte blocks instead of LDUs
            bool spaceInBlocks = (m_buffer[3U] & 0x80U) == 0x80U;

            m_v24Connected = (m_buffer[3U] & 0x40U) == 0x40U;
            m_modemState = (DVM_STATE)m_buffer[4U];

            m_tx = (m_buffer[5U] & 0x01U) == 0x01U;

            bool adcOverflow = (m_buffer[5U] & 0x02U) == 0x02U;
            if (adcOverflow) {
                //LogError(LOG_MODEM, "ADC levels have overflowed");
                m_adcOverFlowCount++;

                if (m_adcOverFlowCount >= MAX_ADC_OVERFLOW / 2U) {
                    LogWarning(LOG_MODEM, "ADC overflow count > %u!", MAX_ADC_OVERFLOW / 2U);
                }

                if (!m_disableOFlowReset) {
                    if (m_adcOverFlowCount > MAX_ADC_OVERFLOW) {
                        LogError(LOG_MODEM, "ADC overflow count > %u, resetting modem", MAX_ADC_OVERFLOW);
                        forceModemReset = true;
                    }
                }
                else {
                    m_adcOverFlowCount = 0U;
                }
            }
            else {
                if (m_adcOverFlowCount != 0U) {
                    m_adcOverFlowCount--;
                }
            }

            bool rxOverflow = (m_buffer[5U] & 0x04U) == 0x04U;
            if (rxOverflow)
                LogError(LOG_MODEM, "RX buffer has overflowed");

            bool txOverflow = (m_buffer[5U] & 0x08U) == 0x08U;
            if (txOverflow)
                LogError(LOG_MODEM, "TX buffer has overflowed");

            m_lockout = (m_buffer[5U] & 0x10U) == 0x10U;

            bool dacOverflow = (m_buffer[5U] & 0x20U) == 0x20U;
            if (dacOverflow) {
                //LogError(LOG_MODEM, "DAC levels have overflowed");
                m_dacOverFlowCount++;

                if (m_dacOverFlowCount > MAX_DAC_OVERFLOW / 2U) {
                    LogWarning(LOG_MODEM, "DAC overflow count > %u!", MAX_DAC_OVERFLOW / 2U);
                }

                if (!m_disableOFlowReset) {
                    if (m_dacOverFlowCount > MAX_DAC_OVERFLOW) {
                        LogError(LOG_MODEM, "DAC overflow count > %u, resetting modem", MAX_DAC_OVERFLOW);
                        forceModemReset = true;
                    }
                }
                else {
                    m_dacOverFlowCount = 0U;
                }
            }
            else {
                if (m_dacOverFlowCount != 0U) {
                    m_dacOverFlowCount--;
                }
            }

            m_cd = (m_buffer[5U] & 0x40U) == 0x40U;

            // spaces from the modem are returned in "logical" frame count, or a block size, not raw byte size
            // DMR and NXDN space are always 0U since the board doesn't support them
            m_dmrSpace1 = 0U;
            m_dmrSpace2 = 0U;
            m_nxdnSpace = 0U;

            // P25 free space can be reported as 16-byte blocks or frames based on the flag above
            if (spaceInBlocks)
                m_p25Space = m_buffer[10U] * P25_BUFFER_BLOCK_SIZE;
            else
                m_p25Space = m_buffer[10U] * (P25DEF::P25_LDU_FRAME_LENGTH_BYTES);

            if (m_dumpModemStatus) {
                LogDebugEx(LOG_MODEM, "ModemV24::clock()", "CMD_GET_STATUS, isHotspot = %u, v24Connected = %u, dmr = %u / %u, p25 = %u / %u, nxdn = %u / %u, modemState = %u, tx = %u, adcOverflow = %u, rxOverflow = %u, txOverflow = %u, dacOverflow = %u, dmrSpace1 = %u, dmrSpace2 = %u, p25Space = %u, nxdnSpace = %u",
                    m_isHotspot, m_v24Connected, dmrEnable, m_dmrEnabled, p25Enable, m_p25Enabled, nxdnEnable, m_nxdnEnabled, m_modemState, m_tx, adcOverflow, rxOverflow, txOverflow, dacOverflow, m_dmrSpace1, m_dmrSpace2, m_p25Space, m_nxdnSpace);
                LogDebugEx(LOG_MODEM, "ModemV24::clock()", "CMD_GET_STATUS, rxDMRData1 size = %u, len = %u, free = %u; rxDMRData2 size = %u, len = %u, free = %u, rxP25Data size = %u, len = %u, free = %u, rxNXDNData size = %u, len = %u, free = %u",
                    m_rxDMRQueue1.length(), m_rxDMRQueue1.dataSize(), m_rxDMRQueue1.freeSpace(), m_rxDMRQueue2.length(), m_rxDMRQueue2.dataSize(), m_rxDMRQueue2.freeSpace(),
                    m_rxP25Queue.length(), m_rxP25Queue.dataSize(), m_rxP25Queue.freeSpace(), m_rxNXDNQueue.length(), m_rxNXDNQueue.dataSize(), m_rxNXDNQueue.freeSpace());
            }

            m_gotModemStatus = true;
            m_inactivityTimer.start();
        }
        break;

        case CMD_GET_VERSION:
        case CMD_ACK:
            break;

        case CMD_NAK:
        {
            LogWarning(LOG_MODEM, "NAK, command = 0x%02X (%s), reason = %u (%s)", m_buffer[3U], cmdToString(m_buffer[3U]).c_str(), m_buffer[4U], rsnToString(m_buffer[4U]).c_str());
            switch (m_buffer[4U]) {
                case RSN_RINGBUFF_FULL:
                {
                    switch (m_buffer[3U]) {
                        case CMD_DMR_DATA1:
                            LogWarning(LOG_MODEM, "NAK, %s, dmrSpace1 = %u", rsnToString(m_buffer[4U]).c_str(), m_dmrSpace1);
                            break;
                        case CMD_DMR_DATA2:
                            LogWarning(LOG_MODEM, "NAK, %s, dmrSpace2 = %u", rsnToString(m_buffer[4U]).c_str(), m_dmrSpace2);
                            break;

                        case CMD_P25_DATA:
                            LogWarning(LOG_MODEM, "NAK, %s, p25Space = %u", rsnToString(m_buffer[4U]).c_str(), m_p25Space);
                            break;

                        case CMD_NXDN_DATA:
                            LogWarning(LOG_MODEM, "NAK, %s, nxdnSpace = %u", rsnToString(m_buffer[4U]).c_str(), m_nxdnSpace);
                            break;
                        }
                }
                break;
            }
        }
        break;

        case CMD_DEBUG1:
        case CMD_DEBUG2:
        case CMD_DEBUG3:
        case CMD_DEBUG4:
        case CMD_DEBUG5:
        case CMD_DEBUG_DUMP:
            printDebug(m_buffer, m_length);
            break;

        default:
            LogWarning(LOG_MODEM, "Unknown message, type = %02X", m_buffer[2U]);
            Utils::dump("ModemV24::clock(), m_buffer", m_buffer, m_length);
            if (m_rspState != RESP_START)
                m_rspState = RESP_START;
            break;
        }
    }

    // force a modem reset because of a error condition
    if (forceModemReset) {
        forceModemReset = false;
        reset();
    }

    int len = 0;

    // write anything waiting to the serial port

    /*
    ** bryanb: Tim's fork changes the order of this, however, this is a terrible idea becuase it will ultimately break
    **  the intended purpose of this code which is to allow prioritized frames, reasonably speaking this code should
    **  not cause problems for timing -- but we should keep an eye on this (if its problematic for some reason,
    **  we can consider perhaps adding a flag that disables the immediate queue, forcing all packets in to the normal
    **  queue)
    */
    if (!m_txImmP25Queue.isEmpty())
        len = writeSerial(&m_txImmP25Queue);
    else
        len = writeSerial(&m_txP25Queue);
    if (m_debug && len > 0) {
        LogDebug(LOG_MODEM, "Wrote %u-byte message to the serial V24 device", len);
    } else if (len < 0) {
        LogError(LOG_MODEM, "Failed to write to serial port!");
    }

    // if both queues are empty -- unset the Tx flag since the modem won't be transmitting anything
    if (m_txImmP25Queue.isEmpty() && m_txP25Queue.isEmpty()) {
        m_tx = false;
    }

    // clear an RX call in progress flag if we're longer than our timeout value
    now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    if (m_rxCallInProgress && (now - m_rxLastFrameTime > m_callTimeout)) {
        m_rxCallInProgress = false;
        m_rxCall->resetCallData();
        LogWarning(LOG_MODEM, "No call data received from V24 for %u ms, resetting RX call", (now - m_rxLastFrameTime));
    }
}

/* Closes connection to the air interface modem. */

void ModemV24::close()
{
    LogInfoEx(LOG_MODEM, "Closing the modem");
    m_port->close();

    m_gotModemStatus = false;

    // do we have a close port handler?
    if (m_closePortHandler != nullptr) {
        m_closePortHandler(this);
    }
}

/* Helper to test if the P25 ring buffer has free space. */

bool ModemV24::hasP25Space(uint32_t length) const
{
    return Modem::hasP25Space(length);
}

/* Writes raw data to the air interface modem. */

int ModemV24::write(const uint8_t* data, uint32_t length, bool imm)
{
    assert(data != nullptr);

    uint8_t modemCommand = CMD_GET_VERSION;
    if (data[0U] == DVM_SHORT_FRAME_START) {
        modemCommand = data[2U];
    } else if (data[0U] == DVM_LONG_FRAME_START) {
        modemCommand = data[3U];
    }

    if (modemCommand == CMD_P25_DATA) {
        DECLARE_UINT8_ARRAY(buffer, length);
        ::memcpy(buffer, data + 2U, length);

        if (m_useTIAFormat)
            convertFromAirTIA(buffer, length, imm);
        else
            convertFromAirV24(buffer, length, imm);
        return length;
    } else {
        return Modem::write(data, length, imm);
    }
}

// ---------------------------------------------------------------------------
//  Private Class Members
// ---------------------------------------------------------------------------

/* Helper to write data from the P25 Tx queue to the serial interface. */

int ModemV24::writeSerial(RingBuffer<uint8_t>* queue)
{
    /*
     *  Serial TX ringbuffer format:
     * 
     *  | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | 0x0A | 0x0B | 0x0C | ... |
     *  |   Length    | Tag  |               int64_t timestamp in ms                 |   data     |
     */

    std::lock_guard<std::mutex> lock(m_txP25QueueLock);

    // check empty
    if (queue->isEmpty()) {
        m_tx = false;
        return 0U;
    }

    // get length
    uint8_t length[2U];
    ::memset(length, 0x00U, 2U);
    queue->peek(length, 2U);

    // convert length bytes to int
    uint16_t len = 0U;
    len = (length[0U] << 8) + length[1U];

    // this ensures we never get in a situation where we have length bytes stuck in the queue by themselves
    if (queue->dataSize() == 2U && len > queue->dataSize()) {
        queue->get(length, 2U); // ensure we pop bytes off
        m_tx = false;
        return 0U;
    }

    // get current timestamp
    int64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    int64_t ts = 0L;

    // peek the timestamp to see if we should wait
    if (queue->dataSize() >= 11U) {
        uint8_t lengthTagTs[11U];
        ::memset(lengthTagTs, 0x00U, 11U);
        queue->peek(lengthTagTs, 11U);

        // get the timestamp
        assert(sizeof ts == 8);
        ::memcpy(&ts, lengthTagTs + 3U, 8U);

        // if it's not time to send, return
        if (ts > now) {
            return 0U;
        }
    }

    // check if we have enough data to get everything - len + 2U (length bytes) + 1U (tag) + 8U (timestamp)
    if (queue->dataSize() >= len + 11U) {
        // peek the full entry so we can keep it queued if a write fails
        DECLARE_UINT8_ARRAY(entry, len + 11U);
        queue->peek(entry, len + 11U);

        // get the length, tag, timestamp and payload pointers from peeked data
        uint8_t* lengthTagTs = entry;
        uint8_t* buffer = entry + 11U;

        // sanity check on data tag
        uint8_t tag = lengthTagTs[2U];
        if (tag != TAG_DATA) {
            LogError(LOG_MODEM, "Got unexpected data tag from TX P25 ringbuffer! %02X", tag);

            // drop malformed entry so we can recover queue alignment
            DECLARE_UINT8_ARRAY(discard, len + 11U);
            queue->get(discard, len + 11U);
            return 0U;
        }

        // we already checked the timestamp above, so we just write it
        int ret = m_port->write(buffer, len);
        if (ret > 0) {
            // only remove an entry once it was written successfully
            DECLARE_UINT8_ARRAY(discard, len + 11U);
            queue->get(discard, len + 11U);

            // bryanb: because m_tx may not be set by the V.24 modem -- we will indicate "Tx" state here based on 
            // whether we're actually writing data to the modem or not
            m_tx = true;
        }

        return ret;
    }

    return 0U;
}

/* Helper to store converted Rx frames. */

void ModemV24::storeConvertedRx(const uint8_t* buffer, uint32_t length)
{
    // store converted frame into the Rx modem queue
    uint8_t storedLen[2U];
    if (length > 255U)
        storedLen[0U] = (length >> 8U) & 0xFFU;
    else
        storedLen[0U] = 0x00U;
    storedLen[1U] = length & 0xFFU;
    m_rxP25Queue.addData(storedLen, 2U);

    // Utils::dump("ModemV24::storeConvertedRx(), Storing Converted Rx Data", buffer, length);

    m_rxP25Queue.addData(buffer, length);
}

/* Internal helper to store converted PDU Rx frames. */

void ModemV24::storeConvertedRxPDU(data::DataHeader* dataHeader, uint8_t* pduUserData)
{
    assert(pduUserData != nullptr);

    uint32_t bitLength = ((dataHeader->getBlocksToFollow() + 1U) * P25_PDU_FEC_LENGTH_BITS) + P25_PREAMBLE_LENGTH_BITS;
    if (dataHeader->getPadLength() > 0U)
        bitLength += (dataHeader->getPadLength() * 8U);

    uint32_t offset = P25_PREAMBLE_LENGTH_BITS;

    DECLARE_UINT8_ARRAY(pdu, (bitLength / 8U) + 1U);

    uint8_t block[P25_PDU_FEC_LENGTH_BYTES];
    ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES);

    uint32_t blocksToFollow = dataHeader->getBlocksToFollow();

    // generate the PDU header and 1/2 rate Trellis
    dataHeader->encode(block);
    Utils::setBitRange(block, pdu, offset, P25_PDU_FEC_LENGTH_BITS);
    offset += P25_PDU_FEC_LENGTH_BITS;

    if (blocksToFollow > 0U) {
        uint32_t dataOffset = 0U;
        uint32_t packetLength = dataHeader->getPDULength();

        // generate the PDU data
        for (uint32_t i = 0U; i < blocksToFollow; i++) {
            data::DataBlock dataBlock = data::DataBlock();
            dataBlock.setFormat(*dataHeader);
            dataBlock.setSerialNo(i);
            dataBlock.setData(pduUserData + dataOffset);
            dataBlock.setLastBlock((i + 1U) == blocksToFollow);

            ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES);
            dataBlock.encode(block);
            Utils::setBitRange(block, pdu, offset, P25_PDU_FEC_LENGTH_BITS);

            offset += P25_PDU_FEC_LENGTH_BITS;
            dataOffset += (dataHeader->getFormat() == PDUFormatType::CONFIRMED) ? P25_PDU_CONFIRMED_DATA_LENGTH_BYTES : P25_PDU_UNCONFIRMED_LENGTH_BYTES;
        }
    }

    uint8_t data[P25_PDU_FRAME_LENGTH_BYTES + 2U];
    ::memset(data, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U);

    // add the data
    uint32_t newBitLength = P25Utils::encodeByLength(pdu, data + 2U, bitLength);
    uint32_t newByteLength = newBitLength / 8U;
    if ((newBitLength % 8U) > 0U)
        newByteLength++;

    // generate Sync
    Sync::addP25Sync(data + 2U);

    // generate NID
    m_nid->encode(data + 2U, DUID::PDU);

    // add status bits
    P25Utils::addStatusBits(data + 2U, newBitLength, true, true);
    P25Utils::setStatusBitsStartIdle(data + 2U);

    //Utils::dump("P25, Data::writeRF_PDU(), Raw PDU OSP", data, newByteLength + 2U);

    data[0U] = modem::TAG_DATA;
    data[1U] = 0x00U;
    storeConvertedRx(data, newByteLength + 2U);
}

/* Helper to generate a P25 TDU packet. */

void ModemV24::create_TDU(uint8_t* buffer)
{
    assert(buffer != nullptr);

    uint8_t data[P25_TDU_FRAME_LENGTH_BYTES + 2U];
    ::memset(data + 2U, 0x00U, P25_TDU_FRAME_LENGTH_BYTES);

    // generate Sync
    Sync::addP25Sync(data + 2U);

    // generate NID
    m_nid->encode(data + 2U, DUID::TDU);

    // add status bits
    P25Utils::addStatusBits(data + 2U, P25_TDU_FRAME_LENGTH_BITS, false, false);

    buffer[0U] = modem::TAG_EOT;
    buffer[1U] = 0x01U;
    ::memcpy(buffer, data, P25_TDU_FRAME_LENGTH_BYTES + 2U);
}

/* Internal helper to convert from V.24/DFSI to TIA-102 air interface. */

void ModemV24::convertToAirV24(const uint8_t *data, uint32_t length)
{
    assert(data != nullptr);
    assert(length > 0U);

    uint8_t buffer[P25_PDU_FRAME_LENGTH_BYTES + 2U];
    ::memset(buffer, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U);

    // get the DFSI data (skip the 0x00 padded byte at the start)
    DECLARE_UINT8_ARRAY(dfsiData, length - 1U);
    ::memcpy(dfsiData, data + 1U, length - 1U);

    if (m_debug) {
        LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "msgType = $%02X", dfsiData[0U]);
        Utils::dump("ModemV24::convertToAirV24(), V.24 RX Data From Modem", dfsiData, length - 1U);
    }

    DFSIFrameType::E frameType = (DFSIFrameType::E)dfsiData[0U];
    m_rxLastFrameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

    // Switch based on DFSI frame type
    switch (frameType) {
        case DFSIFrameType::MOT_START_STOP:
        {
            MotStartOfStream start = MotStartOfStream(dfsiData);
            if (start.getParam1() == DSFI_MOT_ICW_PARM_PAYLOAD) {
                m_rxCall->resetCallData();
                m_rxCallInProgress = true;
                if (m_debug) {
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, ICW, opcode = $%02X, type = $%02X", start.getOpcode(), start.getArgument1());
                }
            } else {
                if (m_rxCallInProgress) {
                    m_rxCall->resetCallData();
                    m_rxCallInProgress = false;
                    if (m_debug) {
                        ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, ICW, opcode = $%02X, type = $%02X", start.getOpcode(), start.getArgument1());
                    }
                    // generate a TDU
                    create_TDU(buffer);
                    storeConvertedRx(buffer, P25_TDU_FRAME_LENGTH_BYTES + 2U);
                }
            }
        }
        break;

        case DFSIFrameType::MOT_VHDR_1:
        {
            MotStartOfStream start = MotStartOfStream(dfsiData);

            // copy to call data VHDR1
            ::memset(m_rxCall->VHDR1, 0x00U, DFSI_MOT_VHDR_1_LEN);
            ::memcpy(m_rxCall->VHDR1, dfsiData + DFSI_MOT_START_LEN, DFSI_MOT_VHDR_1_LEN - DFSI_MOT_START_LEN);

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirV24(), V.24 RX, VHDR1", m_rxCall->VHDR1, DFSI_MOT_VHDR_1_LEN);
        }
        break;
        case DFSIFrameType::MOT_VHDR_2:
        {
            MotStartOfStream start = MotStartOfStream(dfsiData);

            // copy to call data VHDR2
            ::memset(m_rxCall->VHDR2, 0x00U, DFSI_MOT_VHDR_2_LEN);
            ::memcpy(m_rxCall->VHDR2, dfsiData + 1U, DFSI_MOT_VHDR_2_LEN);

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirV24(), V.24 RX, VHDR2", m_rxCall->VHDR2, DFSI_MOT_VHDR_2_LEN);

            // buffer for raw VHDR data
            uint8_t raw[DFSI_VHDR_RAW_LEN];
            ::memset(raw, 0x00U, DFSI_VHDR_RAW_LEN);

            ::memcpy(raw + 0U,  m_rxCall->VHDR1,        8U);
            ::memcpy(raw + 8U,  m_rxCall->VHDR1 + 9U,   8U);
            ::memcpy(raw + 16U, m_rxCall->VHDR1 + 18U,  2U);

            ::memcpy(raw + 18U, m_rxCall->VHDR2,        8U);
            ::memcpy(raw + 26U, m_rxCall->VHDR2 + 9U,   8U);
            ::memcpy(raw + 34U, m_rxCall->VHDR2 + 18U,  2U);

            // Utils::dump("ModemV24::convertToAirV24(), V.24 RX VHDR, raw", raw, DFSI_VHDR_RAW_LEN);

            // buffer for decoded VHDR data
            uint8_t vhdr[DFSI_VHDR_LEN];

            uint32_t offset = 0U;
            for (uint32_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6)
                Utils::hex2Bin(raw[i], vhdr, offset);

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirV24() V.24 RX VHDR, VHDR Before FEC", vhdr, P25_HDU_LENGTH_BYTES);

            // try to decode the RS data
            try {
                bool ret = m_rs.decode362017(vhdr);
                if (!ret) {
                    LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode RS (36,20,17) FEC");
                } else {
                    if (m_debug && m_trace)
                        Utils::dump("ModemV24::convertToAirV24(), V.24 RX VHDR, VHDR After FEC", vhdr, P25_HDU_LENGTH_BYTES);

                    // late entry?
                    if (!m_rxCallInProgress) {
                        m_rxCallInProgress = true;
                        m_rxCall->resetCallData();
                        if (m_debug)
                            ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX VHDR late entry, resetting call data");
                    }

                    uint32_t dstId = GET_UINT16(vhdr, 13U);
                    if (m_rxCallInProgress && dstId == 0U) {
                        LogWarning(LOG_MODEM, "V.24/DFSI traffic sent voice header with no dstId while call is in progress?, ignoring header TGID");
                        break;
                    }

                    ::memcpy(m_rxCall->MI, vhdr, MI_LENGTH_BYTES);

                    m_rxCall->mfId = vhdr[9U];
                    m_rxCall->algoId = vhdr[10U];
                    m_rxCall->kId = GET_UINT16(vhdr, 11U);
                    m_rxCall->dstId = dstId;

                    if (m_debug) {
                        LogDebug(LOG_MODEM, "P25, VHDR algId = $%02X, kId = $%04X, dstId = $%04X", m_rxCall->algoId, m_rxCall->kId, m_rxCall->dstId);
                    }

                    // generate a HDU
                    lc::LC lc = lc::LC();
                    lc.setDstId(m_rxCall->dstId);
                    lc.setAlgId(m_rxCall->algoId);
                    lc.setKId(m_rxCall->kId);
                    lc.setMI(m_rxCall->MI);

                    // generate Sync
                    Sync::addP25Sync(buffer + 2U);

                    // generate NID
                    m_nid->encode(buffer + 2U, DUID::HDU);

                    // generate HDU
                    lc.encodeHDU(buffer + 2U);

                    // add status bits
                    P25Utils::addStatusBits(buffer + 2U, P25_HDU_FRAME_LENGTH_BITS, true, false);

                    buffer[0U] = modem::TAG_DATA;
                    buffer[1U] = 0x01U;
                    storeConvertedRx(buffer, P25_HDU_FRAME_LENGTH_BYTES + 2U);
                }
            }
            catch (...) {
                LogError(LOG_MODEM, "V.24/DFSI RX traffic got exception while trying to decode RS data for VHDR");
            }
        }
        break;

        // VOICE1/10 create a start voice frame
        case DFSIFrameType::LDU1_VOICE1:
        {
            MotStartVoiceFrame svf = MotStartVoiceFrame(dfsiData);
            ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
            ::memcpy(m_rxCall->netLDU1 + 10U, svf.fullRateVoice->imbeData, RAW_IMBE_LENGTH_BYTES);

            m_rxCall->errors = 0U;

            // process start of stream ICW for the voice call
            uint8_t* icw = svf.startOfStream->getICW();
            uint8_t payloadType = MotStreamPayload::VOICE;
            uint8_t rssi = 20U;

            if (icw != nullptr) {
                for (uint8_t i = 0U; i < 6U; i += 2U) {
                    uint8_t param = icw[i];
                    uint8_t value = icw[i + 1U];

                    switch (param) {
                    case DSFI_MOT_ICW_PARM_PAYLOAD:
                        payloadType = value;
                        break;
                    case DFSI_MOT_ICW_PARM_RSSI1:
                        rssi = value;
                        break;
                    case DFSI_MOT_ICW_PARM_RSSI2:
                        // don't do anything with this RSSI
                        break;
                    case DFSI_MOT_ICW_TX_ADDRESS:
                    case DFSI_MOT_ICW_RX_ADDRESS:
                        // don't do anything with this ICW
                        break;
                    default:
                        LogWarning(LOG_MODEM, "ModemV24::convertToAirV24() unknown ICW parameter $%02X with value %u", param, value);
                        break;
                    }
                }
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, Start of Voice LDU1, ICW opcode = $%02X, rssi = %u", svf.startOfStream->getOpcode(), rssi);
            }

            if (svf.fullRateVoice->getTotalErrors() > 0U) {
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", svf.fullRateVoice->getTotalErrors(), svf.fullRateVoice->getFrameType());
                m_rxCall->errors += svf.fullRateVoice->getTotalErrors();
            }

            m_rxCall->n++;
        }
        break;
        case DFSIFrameType::LDU2_VOICE10:
        {
            MotStartVoiceFrame svf = MotStartVoiceFrame(dfsiData);
            ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
            ::memcpy(m_rxCall->netLDU2 + 10U, svf.fullRateVoice->imbeData, RAW_IMBE_LENGTH_BYTES);

            m_rxCall->errors = 0U;

            // process start of stream ICW for the voice call
            uint8_t* icw = svf.startOfStream->getICW();
            uint8_t payloadType = MotStreamPayload::VOICE;
            uint8_t rssi = 20U;

            if (icw != nullptr) {
                for (uint8_t i = 0U; i < 6U; i += 2U) {
                    uint8_t param = icw[i];
                    uint8_t value = icw[i + 1U];

                    switch (param) {
                    case DSFI_MOT_ICW_PARM_PAYLOAD:
                        payloadType = value;
                        break;
                    case DFSI_MOT_ICW_PARM_RSSI1:
                        rssi = value;
                        break;
                    case DFSI_MOT_ICW_PARM_RSSI2:
                        // don't do anything with this RSSI
                        break;
                    case DFSI_MOT_ICW_TX_ADDRESS:
                    case DFSI_MOT_ICW_RX_ADDRESS:
                        // don't do anything with this ICW
                        break;
                    default:
                        LogWarning(LOG_MODEM, "ModemV24::convertToAirV24() unknown ICW parameter $%02X with value %u", param, value);
                        break;
                    }
                }
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, Start of Voice LDU2, ICW opcode = $%02X, rssi = %u", svf.startOfStream->getOpcode(), rssi);
            }

            if (svf.fullRateVoice->getTotalErrors() > 0U) {
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", svf.fullRateVoice->getTotalErrors(), svf.fullRateVoice->getFrameType());
                m_rxCall->errors += svf.fullRateVoice->getTotalErrors();
            }

            m_rxCall->n++;
        }
        break;

        case DFSIFrameType::MOT_TDULC:
        {
            MotTDULCFrame tf = MotTDULCFrame(dfsiData);
            lc::tdulc::LC_TDULC_RAW tdulc = lc::tdulc::LC_TDULC_RAW();
            if (!tdulc.decode(tf.tdulcData, true)) {
                LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode TDULC FEC");
            } else {
                if (m_debug) {
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, TDULC ISP, mfId = $%02X, lco = $%02X",
                        tdulc.getMFId(), tdulc.getLCO());
                    Utils::dump(1U, "V.24 RX, TDULC ISP", tf.tdulcData, P25_TDULC_FRAME_LENGTH_BYTES);
                }

                uint8_t buffer[P25_TDULC_FRAME_LENGTH_BYTES + 2U];
                ::memset(buffer, 0x00U, P25_TDULC_FRAME_LENGTH_BYTES + 2U);

                buffer[0U] = modem::TAG_DATA;
                buffer[1U] = 0x00U;

                // generate Sync
                Sync::addP25Sync(buffer + 2U);

                // generate NID
                m_nid->encode(buffer + 2U, DUID::TDULC);

                // regenerate TDULC Data
                tdulc.encode(buffer + 2U);

                // add status bits
                P25Utils::addStatusBits(buffer + 2U, P25_TDULC_FRAME_LENGTH_BYTES, false, true);
                P25Utils::addIdleStatusBits(buffer + 2U, P25_TDULC_FRAME_LENGTH_BYTES);
                P25Utils::setStatusBitsStartIdle(buffer + 2U);

                storeConvertedRx(buffer, P25_TDULC_FRAME_LENGTH_BYTES + 2U);
            }
        }
        break;

        case DFSIFrameType::MOT_PDU_SINGLE_UNCONF:
        {
            m_rxCall->resetCallData();

            uint8_t header[P25_PDU_HEADER_LENGTH_BYTES];
            ::memset(header, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);

            // skip the first 9 bytes (embedded start of stream)
            ::memcpy(header, dfsiData + 9U, P25_PDU_HEADER_LENGTH_BYTES);
            data::DataHeader* pduHeader = new data::DataHeader();
            bool ret = pduHeader->decode(header, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_PDU_STR ", unfixable RF 1/2 rate header data");
                Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES);
                break;
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, PDU ISP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, llId = %u",
                    pduHeader->getAckNeeded(), pduHeader->getOutbound(), pduHeader->getFormat(), pduHeader->getMFId(), pduHeader->getSAP(), pduHeader->getFullMessage(),
                    pduHeader->getBlocksToFollow(), pduHeader->getPadLength(), pduHeader->getPacketLength(), pduHeader->getSynchronize(), pduHeader->getNs(), pduHeader->getFSN(), pduHeader->getLastFragment(),
                    pduHeader->getHeaderOffset(), pduHeader->getLLId());
            }

            m_rxCall->dataCall = true;
            m_rxCall->dataHeader = pduHeader;

            int skipBytes = 1U; // skip padding between PDU blocks
            for (uint8_t i = 0U; i < pduHeader->getBlocksToFollow(); i++) {
                uint32_t blockOffset = 9U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_UNCONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Unconfirmed Data Block", dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                uint32_t offset = i * P25_PDU_UNCONFIRMED_LENGTH_BYTES;
                ::memcpy(m_rxCall->pduUserData + offset, dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                skipBytes++; // each block is separated by a padding byte
            }

            if (m_trace)
                Utils::dump(1U, "ModemV24::convertToAirV24(), Final PDU Unconfirmed Data", m_rxCall->pduUserData, (pduHeader->getBlocksToFollow() + 1U) * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

            storeConvertedRxPDU(pduHeader, m_rxCall->pduUserData);
        }
        break;

        case DFSIFrameType::MOT_PDU_UNCONF_HEADER:
        {
            m_rxCall->resetCallData();

            uint8_t header[P25_PDU_HEADER_LENGTH_BYTES];
            ::memset(header, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);

            // skip the first 9 bytes (embedded start of stream)
            ::memcpy(header, dfsiData + 9U, P25_PDU_HEADER_LENGTH_BYTES);
            data::DataHeader* pduHeader = new data::DataHeader();
            bool ret = pduHeader->decode(header, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_PDU_STR ", unfixable RF 1/2 rate header data");
                Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES);
                break;
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, PDU ISP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, llId = %u",
                    pduHeader->getAckNeeded(), pduHeader->getOutbound(), pduHeader->getFormat(), pduHeader->getMFId(), pduHeader->getSAP(), pduHeader->getFullMessage(),
                    pduHeader->getBlocksToFollow(), pduHeader->getPadLength(), pduHeader->getPacketLength(), pduHeader->getSynchronize(), pduHeader->getNs(), pduHeader->getFSN(), pduHeader->getLastFragment(),
                    pduHeader->getHeaderOffset(), pduHeader->getLLId());
            }

            // make sure we don't get a PDU with more blocks then we support
            if (pduHeader->getBlocksToFollow() >= P25_MAX_PDU_BLOCKS) {
                LogError(LOG_MODEM, P25_PDU_STR ", ISP, too many PDU blocks to process, %u > %u", pduHeader->getBlocksToFollow(), P25_MAX_PDU_BLOCKS);

                m_rxCall->resetCallData();
                break;
            }

            m_rxCall->dataCall = true;
            m_rxCall->dataHeader = pduHeader;

            // PDU_UNCONF_HEADER only contains 3 blocks
            int skipBytes = 1U; // skip padding between PDU blocks
            for (uint8_t i = 0U; i < 3U; i++) {
                uint32_t blockOffset = 9U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_UNCONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Unconfirmed Data Block", dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                ::memcpy(m_rxCall->pduUserData + m_rxCall->pduUserDataOffset, dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                m_rxCall->pduUserDataOffset += P25_PDU_UNCONFIRMED_LENGTH_BYTES;
                m_rxCall->pduTotalBlocks++;
                skipBytes++; // each block is separated by a padding byte
            }
        }
        break;
        case DFSIFrameType::MOT_PDU_UNCONF_BLOCK_1:
        case DFSIFrameType::MOT_PDU_UNCONF_BLOCK_2:
        case DFSIFrameType::MOT_PDU_UNCONF_BLOCK_3:
        case DFSIFrameType::MOT_PDU_UNCONF_BLOCK_4:
        case DFSIFrameType::MOT_PDU_UNCONF_END:
        {
            // only process blocks if we've received a header and started a data call
            if (!m_rxCall->dataCall)
                break;

            uint32_t blockCnt = DFSI_PDU_BLOCK_CNT;

            // PDU_UNCONF_END are variable length depending on the message
            if (frameType == DFSIFrameType::MOT_PDU_UNCONF_END) {
                blockCnt = m_rxCall->dataHeader->getBlocksToFollow() - m_rxCall->pduTotalBlocks;

                // bryanb: I wonder if there's a chance somehow the calculation will be less then zero...reasonably
                //  as far as I can tell that should never happen as PDU_UNCONF_BLOCK_X should *always* contain
                //  4 blocks of user data, and the PDU_UNCONF_END is always variable with at least the last data block
            }

            // PDU_UNCONF_BLOCK_X and PDU_UNCONF_END only contains 4 blocks each
            int skipBytes = 0U; // skip padding between PDU blocks
            for (uint8_t i = 0U; i < blockCnt; i++) {
                uint32_t blockOffset = 1U + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_UNCONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Unconfirmed Data Block", dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                ::memcpy(m_rxCall->pduUserData + m_rxCall->pduUserDataOffset, dataBlock, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                m_rxCall->pduUserDataOffset += P25_PDU_UNCONFIRMED_LENGTH_BYTES;
                m_rxCall->pduTotalBlocks++;
                skipBytes++; // each block is separated by a padding byte
            }

            if (frameType == DFSIFrameType::MOT_PDU_UNCONF_END) {
                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), Final PDU Unconfirmed Data", m_rxCall->pduUserData, m_rxCall->pduTotalBlocks * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                storeConvertedRxPDU(m_rxCall->dataHeader, m_rxCall->pduUserData);
            }
        }
        break;

        case DFSIFrameType::MOT_PDU_SINGLE_CONF:
        {
            m_rxCall->resetCallData();

            uint8_t header[P25_PDU_HEADER_LENGTH_BYTES];
            ::memset(header, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);

            // skip the first 9 bytes (embedded start of stream)
            ::memcpy(header, dfsiData + 9U, P25_PDU_HEADER_LENGTH_BYTES);
            data::DataHeader* pduHeader = new data::DataHeader();
            bool ret = pduHeader->decode(header, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_PDU_STR ", unfixable RF 1/2 rate header data");
                Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES);
                break;
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, PDU ISP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, llId = %u",
                    pduHeader->getAckNeeded(), pduHeader->getOutbound(), pduHeader->getFormat(), pduHeader->getMFId(), pduHeader->getSAP(), pduHeader->getFullMessage(),
                    pduHeader->getBlocksToFollow(), pduHeader->getPadLength(), pduHeader->getPacketLength(), pduHeader->getSynchronize(), pduHeader->getNs(), pduHeader->getFSN(), pduHeader->getLastFragment(),
                    pduHeader->getHeaderOffset(), pduHeader->getLLId());
            }

            m_rxCall->dataCall = true;
            m_rxCall->dataHeader = pduHeader;

            int skipBytes = 2U; // skip padding between PDU blocks -- and first block serial number
            for (uint8_t i = 0U; i < pduHeader->getBlocksToFollow(); i++) {
                uint32_t blockOffset = 9U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_CONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_CONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Confirmed Data Block", dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);

                uint32_t offset = i * P25_PDU_CONFIRMED_LENGTH_BYTES;
                ::memcpy(m_rxCall->pduUserData + offset, dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);
                skipBytes++; // each block is separated by a padding byte
            }

            if (m_trace)
                Utils::dump(1U, "ModemV24::convertToAirV24(), Final PDU Confirmed Data", m_rxCall->pduUserData, (pduHeader->getBlocksToFollow() + 1U) * P25_PDU_CONFIRMED_LENGTH_BYTES);

            storeConvertedRxPDU(pduHeader, m_rxCall->pduUserData);
        }
        break;

        case DFSIFrameType::MOT_PDU_CONF_HEADER:
        {
            m_rxCall->resetCallData();

            uint8_t header[P25_PDU_HEADER_LENGTH_BYTES];
            ::memset(header, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);

            // skip the first 9 bytes (embedded start of stream)
            ::memcpy(header, dfsiData + 9U, P25_PDU_HEADER_LENGTH_BYTES);
            data::DataHeader* pduHeader = new data::DataHeader();
            bool ret = pduHeader->decode(header, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_PDU_STR ", unfixable RF 1/2 rate header data");
                Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES);
                break;
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, PDU ISP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, llId = %u",
                    pduHeader->getAckNeeded(), pduHeader->getOutbound(), pduHeader->getFormat(), pduHeader->getMFId(), pduHeader->getSAP(), pduHeader->getFullMessage(),
                    pduHeader->getBlocksToFollow(), pduHeader->getPadLength(), pduHeader->getPacketLength(), pduHeader->getSynchronize(), pduHeader->getNs(), pduHeader->getFSN(), pduHeader->getLastFragment(),
                    pduHeader->getHeaderOffset(), pduHeader->getLLId());
            }

            // make sure we don't get a PDU with more blocks then we support
            if (pduHeader->getBlocksToFollow() >= P25_MAX_PDU_BLOCKS) {
                LogError(LOG_MODEM, P25_PDU_STR ", ISP, too many PDU blocks to process, %u > %u", pduHeader->getBlocksToFollow(), P25_MAX_PDU_BLOCKS);

                m_rxCall->resetCallData();
                break;
            }

            m_rxCall->dataCall = true;
            m_rxCall->dataHeader = pduHeader;

            // PDU_CONF_HEADER only contains 3 blocks
            int skipBytes = 2U; // skip padding between PDU blocks -- and first block serial number
            for (uint8_t i = 0U; i < 3U; i++) {
                uint32_t blockOffset = 9U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_CONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_CONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Confirmed Data Block", dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);

                ::memcpy(m_rxCall->pduUserData + m_rxCall->pduUserDataOffset, dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);
                m_rxCall->pduUserDataOffset += P25_PDU_CONFIRMED_LENGTH_BYTES;
                m_rxCall->pduTotalBlocks++;
                skipBytes++; // each block is separated by a padding byte
            }
        }
        break;
        case DFSIFrameType::MOT_PDU_CONF_BLOCK_1:
        case DFSIFrameType::MOT_PDU_CONF_BLOCK_2:
        case DFSIFrameType::MOT_PDU_CONF_BLOCK_3:
        case DFSIFrameType::MOT_PDU_CONF_BLOCK_4:
        case DFSIFrameType::MOT_PDU_CONF_END:
        {
            // only process blocks if we've received a header and started a data call
            if (!m_rxCall->dataCall)
                break;

            uint32_t blockCnt = DFSI_PDU_BLOCK_CNT;

            // PDU_CONF_END are variable length depending on the message
            if (frameType == DFSIFrameType::MOT_PDU_CONF_END) {
                blockCnt = m_rxCall->dataHeader->getBlocksToFollow() - m_rxCall->pduTotalBlocks;

                // bryanb: I wonder if there's a chance somehow the calculation will be less then zero...reasonably
                //  as far as I can tell that should never happen as PDU_CONF_BLOCK_X should *always* contain
                //  4 blocks of user data, and the PDU_CONF_END is always variable with at least the last data block
            }

            // PDU_CONF_BLOCK_X only contains 4 blocks each
            int skipBytes = 0U; // skip padding between PDU blocks
            for (uint8_t i = 0U; i < blockCnt; i++) {
                uint32_t blockOffset = 1U + (i * P25_PDU_CONFIRMED_LENGTH_BYTES) + skipBytes;

                uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES];
                ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES);
                ::memcpy(dataBlock, dfsiData + blockOffset, P25_PDU_CONFIRMED_LENGTH_BYTES);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), PDU Confirmed Data Block", dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);

                ::memcpy(m_rxCall->pduUserData + m_rxCall->pduUserDataOffset, dataBlock, P25_PDU_CONFIRMED_LENGTH_BYTES);
                m_rxCall->pduUserDataOffset += P25_PDU_CONFIRMED_LENGTH_BYTES;
                m_rxCall->pduTotalBlocks++;
                skipBytes++; // each block is separated by a padding byte
            }

            if (frameType == DFSIFrameType::MOT_PDU_CONF_END) {
                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertToAirV24(), Final PDU Confirmed Data", m_rxCall->pduUserData, m_rxCall->pduTotalBlocks * P25_PDU_CONFIRMED_LENGTH_BYTES);

                storeConvertedRxPDU(m_rxCall->dataHeader, m_rxCall->pduUserData);
            }
        }
        break;

        case DFSIFrameType::MOT_TSBK:
        {
            MotTSBKFrame tf = MotTSBKFrame(dfsiData);
            lc::tsbk::OSP_TSBK_RAW tsbk = lc::tsbk::OSP_TSBK_RAW();
            if (!tsbk.decode(tf.tsbkData, true)) {
                LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode TSBK FEC");
            } else {
                if (m_debug) {
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, TSBK ISP, mfId = $%02X, lco = $%02X",
                        tsbk.getMFId(), tsbk.getLCO());
                    Utils::dump(1U, "V.24 RX, TSBK ISP", tf.tsbkData, P25_TSBK_LENGTH_BYTES);
                }

                uint8_t buffer[P25_TSDU_FRAME_LENGTH_BYTES + 2U];
                ::memset(buffer, 0x00U, P25_TSDU_FRAME_LENGTH_BYTES + 2U);

                buffer[0U] = modem::TAG_DATA;
                buffer[1U] = 0x00U;

                // generate Sync
                Sync::addP25Sync(buffer + 2U);

                // generate NID
                m_nid->encode(buffer + 2U, DUID::TSDU);

                // regenerate TSDU Data
                tsbk.setLastBlock(true); // always set last block -- this a Single Block TSDU
                tsbk.encode(buffer + 2U);

                // add status bits
                P25Utils::addStatusBits(buffer + 2U, P25_TSDU_FRAME_LENGTH_BYTES, false, true);
                P25Utils::addIdleStatusBits(buffer + 2U, P25_TSDU_FRAME_LENGTH_BYTES);
                P25Utils::setStatusBitsStartIdle(buffer + 2U);

                storeConvertedRx(buffer, P25_TSDU_FRAME_LENGTH_BYTES + 2U);
            }
        }
        break;

        // The remaining LDUs all create full rate voice frames so we do that here
        default:
        {
            MotFullRateVoice voice = MotFullRateVoice(dfsiData);

            if (m_debug) {
                LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "Full Rate Voice, frameType = $%02X, errors = %u, busy = %u", voice.getFrameType(), voice.getTotalErrors(), voice.getBusy());
                Utils::dump(1U, "ModemV24::converToAirV24(), Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }

            if (voice.getTotalErrors() > 0U) {
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", voice.getTotalErrors(), voice.getFrameType());
                m_rxCall->errors += voice.getTotalErrors();
            }

            switch (frameType) {
                case DFSIFrameType::LDU1_VOICE2:
                {
                    ::memcpy(m_rxCall->netLDU1 + 26U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                }
                break;
                case DFSIFrameType::LDU1_VOICE3:
                {
                    ::memcpy(m_rxCall->netLDU1 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->lco = voice.additionalData[0U];
                        m_rxCall->mfId = voice.additionalData[1U];
                        m_rxCall->serviceOptions = voice.additionalData[2U];

                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[0U] = voice.additionalData[0U];
                        m_rxCall->LDULC[1U] = voice.additionalData[1U];
                        m_rxCall->LDULC[2U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC3 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE4:
                {
                    ::memcpy(m_rxCall->netLDU1 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->dstId = GET_UINT24(voice.additionalData, 0U);

                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[3U] = voice.additionalData[0U];
                        m_rxCall->LDULC[4U] = voice.additionalData[1U];
                        m_rxCall->LDULC[5U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC4 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE5:
                {
                    ::memcpy(m_rxCall->netLDU1 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->srcId = GET_UINT24(voice.additionalData, 0U);

                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[6U] = voice.additionalData[0U];
                        m_rxCall->LDULC[7U] = voice.additionalData[1U];
                        m_rxCall->LDULC[8U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC5 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE6:
                {
                    ::memcpy(m_rxCall->netLDU1 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[9U] = voice.additionalData[0U];
                        m_rxCall->LDULC[10U] = voice.additionalData[1U];
                        m_rxCall->LDULC[11U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC6 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE7:
                {
                    ::memcpy(m_rxCall->netLDU1 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[12U] = voice.additionalData[0U];
                        m_rxCall->LDULC[13U] = voice.additionalData[1U];
                        m_rxCall->LDULC[14U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC7 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE8:
                {
                    ::memcpy(m_rxCall->netLDU1 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        // copy LDU1 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[15U] = voice.additionalData[0U];
                        m_rxCall->LDULC[16U] = voice.additionalData[1U];
                        m_rxCall->LDULC[17U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC8 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU1_VOICE9:
                {
                    ::memcpy(m_rxCall->netLDU1 + 204U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->lsd1 = voice.additionalData[0U];
                        m_rxCall->lsd2 = voice.additionalData[1U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC9 traffic missing metadata");
                    }
                }
                break;

                case DFSIFrameType::LDU2_VOICE11:
                {
                    ::memcpy(m_rxCall->netLDU2 + 26U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                }
                break;
                case DFSIFrameType::LDU2_VOICE12:
                {
                    ::memcpy(m_rxCall->netLDU2 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        ::memcpy(m_rxCall->MI, voice.additionalData, 3U);

                        // copy LDU2 LC bytes into LDU LC buffer
                        ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
                        m_rxCall->LDULC[0U] = voice.additionalData[0U];
                        m_rxCall->LDULC[1U] = voice.additionalData[1U];
                        m_rxCall->LDULC[2U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC12 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE13:
                {
                    ::memcpy(m_rxCall->netLDU2 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        ::memcpy(m_rxCall->MI + 3U, voice.additionalData, 3U);

                        // copy LDU2 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[3U] = voice.additionalData[0U];
                        m_rxCall->LDULC[4U] = voice.additionalData[1U];
                        m_rxCall->LDULC[5U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC13 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE14:
                {
                    ::memcpy(m_rxCall->netLDU2 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        ::memcpy(m_rxCall->MI + 6U, voice.additionalData, 3U);

                        // copy LDU2 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[6U] = voice.additionalData[0U];
                        m_rxCall->LDULC[7U] = voice.additionalData[1U];
                        m_rxCall->LDULC[8U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC14 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE15:
                {
                    ::memcpy(m_rxCall->netLDU2 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->algoId = voice.additionalData[0U];
                        m_rxCall->kId = GET_UINT16(voice.additionalData, 1U);

                        // copy LDU2 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[9U] = voice.additionalData[0U];
                        m_rxCall->LDULC[10U] = voice.additionalData[1U];
                        m_rxCall->LDULC[11U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC15 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE16:
                {
                    ::memcpy(m_rxCall->netLDU2 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        // copy LDU2 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[12U] = voice.additionalData[0U];
                        m_rxCall->LDULC[13U] = voice.additionalData[1U];
                        m_rxCall->LDULC[14U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC16 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE17:
                {
                    ::memcpy(m_rxCall->netLDU2 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        // copy LDU2 LC bytes into LDU LC buffer
                        m_rxCall->LDULC[15U] = voice.additionalData[0U];
                        m_rxCall->LDULC[16U] = voice.additionalData[1U];
                        m_rxCall->LDULC[17U] = voice.additionalData[2U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC17 traffic missing metadata");
                    }
                }
                break;
                case DFSIFrameType::LDU2_VOICE18:
                {
                    ::memcpy(m_rxCall->netLDU2 + 204U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                    if (voice.additionalData != nullptr) {
                        m_rxCall->lsd1 = voice.additionalData[0U];
                        m_rxCall->lsd2 = voice.additionalData[1U];
                    } else {
                        LogWarning(LOG_MODEM, "V.24/DFSI VC18 traffic missing metadata");
                    }
                }
                break;

                default:
                    break;
            }

            // increment our voice frame counter
            m_rxCall->n++;
        }
        break;
    }

    // encode LDU1 if ready
    if (m_rxCall->n == 9U) {
        // decode RS (24,12,13) FEC
        // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now
        //  we'll just log the error
        try {
            bool ret = m_rs.decode241213(m_rxCall->LDULC);
            if (!ret) {
                LogError(LOG_MODEM, "V.24/DFSI LDU1, failed to decode RS (24,12,13) FEC");
            }
        }
        catch (...) {
            Utils::dump(2U, "Modem, V.24 LDU1 RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES);
        }

        if (m_rxCall->errors > 0U) {
            LogWarning(LOG_MODEM, P25_DFSI_LDU1_STR ", V.24, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F);
            m_rxCall->errors = 0U;
        }

        lc::LC lc = lc::LC();
        lc.setLCO(m_rxCall->lco);
        lc.setMFId(m_rxCall->mfId);

        if (lc.isStandardMFId()) {
            lc.setSrcId(m_rxCall->srcId);
            lc.setDstId(m_rxCall->dstId);
        } else {
            uint8_t rsBuffer[P25_LDU_LC_FEC_LENGTH_BYTES];
            ::memset(rsBuffer, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES);

            rsBuffer[0U] = m_rxCall->LDULC[0U];
            rsBuffer[1U] = m_rxCall->LDULC[1U];
            rsBuffer[2U] = m_rxCall->LDULC[2U];
            rsBuffer[3U] = m_rxCall->LDULC[3U];
            rsBuffer[4U] = m_rxCall->LDULC[4U];
            rsBuffer[5U] = m_rxCall->LDULC[5U];
            rsBuffer[6U] = m_rxCall->LDULC[6U];
            rsBuffer[7U] = m_rxCall->LDULC[7U];
            rsBuffer[8U] = m_rxCall->LDULC[8U];

            // combine bytes into ulong64_t (8 byte) value
            ulong64_t rsValue = 0U;
            rsValue = rsBuffer[1U];
            rsValue = (rsValue << 8) + rsBuffer[2U];
            rsValue = (rsValue << 8) + rsBuffer[3U];
            rsValue = (rsValue << 8) + rsBuffer[4U];
            rsValue = (rsValue << 8) + rsBuffer[5U];
            rsValue = (rsValue << 8) + rsBuffer[6U];
            rsValue = (rsValue << 8) + rsBuffer[7U];
            rsValue = (rsValue << 8) + rsBuffer[8U];

            lc.setRS(rsValue);
        }

        bool emergency = ((m_rxCall->serviceOptions & 0xFFU) & 0x80U) == 0x80U;    // Emergency Flag
        bool encryption = ((m_rxCall->serviceOptions & 0xFFU) & 0x40U) == 0x40U;   // Encryption Flag
        uint8_t priority = ((m_rxCall->serviceOptions & 0xFFU) & 0x07U);           // Priority
        lc.setEmergency(emergency);
        lc.setEncrypted(encryption);
        lc.setPriority(priority);

        data::LowSpeedData lsd = data::LowSpeedData();
        lsd.setLSD1(m_rxCall->lsd1);
        lsd.setLSD2(m_rxCall->lsd2);

        // generate Sync
        Sync::addP25Sync(buffer + 2U);

        // generate NID
        m_nid->encode(buffer + 2U, DUID::LDU1);

        // generate LDU1 Data
        lc.encodeLDU1(buffer + 2U);

        // generate Low Speed Data
        lsd.process(buffer + 2U);

        // generate audio
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 10U, 0U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 26U, 1U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 55U, 2U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 80U, 3U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 105U, 4U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 130U, 5U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 155U, 6U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 180U, 7U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 204U, 8U);

        // add status bits
        P25Utils::addStatusBits(buffer + 2U, P25_LDU_FRAME_LENGTH_BITS, true, false);

        buffer[0U] = modem::TAG_DATA;
        buffer[1U] = 0x01U;
        storeConvertedRx(buffer, P25_LDU_FRAME_LENGTH_BYTES + 2U);
    }
    
    // encode LDU2 if ready
    if (m_rxCall->n == 18U) {
        // decode RS (24,16,9) FEC
        // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now
        //  we'll just log the error
        try {
            bool ret = m_rs.decode24169(m_rxCall->LDULC);
            if (!ret) {
                LogError(LOG_MODEM, "V.24/DFSI LDU2, failed to decode RS (24,16,9) FEC");
            }
        }
        catch (...) {
            Utils::dump(2U, "Modem, V.24 LDU2 RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES);
        }

        if (m_rxCall->errors > 0U) {
            LogWarning(LOG_MODEM, P25_DFSI_LDU2_STR ", V.24, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F);
            m_rxCall->errors = 0U;
        }

        lc::LC lc = lc::LC();
        lc.setMI(m_rxCall->MI);
        lc.setAlgId(m_rxCall->algoId);
        lc.setKId(m_rxCall->kId);

        data::LowSpeedData lsd = data::LowSpeedData();
        lsd.setLSD1(m_rxCall->lsd1);
        lsd.setLSD2(m_rxCall->lsd2);

        // generate Sync
        Sync::addP25Sync(buffer + 2U);

        // generate NID
        m_nid->encode(buffer + 2U, DUID::LDU2);

        // generate LDU2 data
        lc.encodeLDU2(buffer + 2U);

        // generate Low Speed Data
        lsd.process(buffer + 2U);

        // generate audio
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 10U, 0U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 26U, 1U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 55U, 2U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 80U, 3U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 105U, 4U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 130U, 5U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 155U, 6U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 180U, 7U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 204U, 8U);

        // add status bits
        P25Utils::addStatusBits(buffer + 2U, P25_LDU_FRAME_LENGTH_BITS, true, false);

        buffer[0U] = modem::TAG_DATA;
        buffer[1U] = 0x01U;
        storeConvertedRx(buffer, P25_LDU_FRAME_LENGTH_BYTES + 2U);

        m_rxCall->n = 0;
    }
}

/* Internal helper to convert from TIA-102 DFSI to TIA-102 air interface. */

void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length)
{
    assert(data != nullptr);
    assert(length > 0U);

    uint8_t buffer[P25_PDU_FRAME_LENGTH_BYTES + 2U];
    ::memset(buffer, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U);

    // get the DFSI data (skip the 0x00 padded byte at the start)
    DECLARE_UINT8_ARRAY(dfsiData, length - 1U);
    ::memcpy(dfsiData, data + 1U, length - 1U);

    if (m_debug) {
        Utils::dump("ModemV24::convertToAirTIA(), DFSI RX Data From UDP", dfsiData, length - 1U);
    }

    ControlOctet ctrl = ControlOctet();
    ctrl.decode(dfsiData);

    uint8_t blockCnt = ctrl.getBlockHeaderCnt();

    if (m_debug)
        ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "blockCnt = %u", blockCnt);

    // iterate through blocks
    uint8_t hdrOffs = 1U, dataOffs = blockCnt + 1U;
    for (uint8_t i = 0U; i < blockCnt; i++) {
        BlockHeader hdr = BlockHeader();
        hdr.decode(dfsiData + hdrOffs);

        if (m_debug)
            ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "block = %u, blockType = $%02X", i, hdr.getBlockType());

        BlockType::E blockType = hdr.getBlockType();
        switch (blockType) {
        case BlockType::START_OF_STREAM:
        {
            StartOfStream start = StartOfStream();
            start.decode(dfsiData + dataOffs);

            uint16_t nac = ((start.getNID() & 0xFFFFU) >> 4) & 0xFFFU;
            if (m_debug)
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "Start of Stream, nac = $%03X, errs: %u", nac, start.getErrorCount());

            // bryanb: maybe compare the NACs?

            dataOffs += StartOfStream::LENGTH;

            // ack start of stream
            ackStartOfStreamTIA(); 
        }
        break;
        case BlockType::END_OF_STREAM:
        {
            dataOffs += 1U;

            // generate Sync
            Sync::addP25Sync(buffer + 2U);

            // generate NID
            m_nid->encode(buffer + 2U, DUID::TDU);

            // add status bits
            P25Utils::setStatusBitsAllIdle(buffer + 2U, P25_TDU_FRAME_LENGTH_BITS);

            buffer[0U] = modem::TAG_DATA;
            buffer[1U] = 0x01U;
            storeConvertedRx(buffer, P25_TDU_FRAME_LENGTH_BITS + 2U);
        }
        break;

        case BlockType::START_OF_STREAM_ACK:
        {
            if (m_debug)
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "Start of Stream ACK");

            // do nothing with the start of stream ack
        }
        break;

        case BlockType::VOICE_HEADER_P1:
        {
            // copy to call data VHDR1
            ::memset(m_rxCall->VHDR1, 0x00U, 18U);
            ::memcpy(m_rxCall->VHDR1, dfsiData + dataOffs + 1U, 18U);

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirTIA(), DFSI RX, VHDR1", m_rxCall->VHDR1, 18U);

            dataOffs += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker
        }
        break;
        case BlockType::VOICE_HEADER_P2:
        {
            // copy to call data VHDR2
            ::memset(m_rxCall->VHDR2, 0x00U, 18U);
            ::memcpy(m_rxCall->VHDR2, dfsiData + dataOffs + 1U, 18U);

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirTIA(), DFSI RX, VHDR2", m_rxCall->VHDR1, 18U);

            dataOffs += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker

            // buffer for raw VHDR data
            uint8_t raw[DFSI_VHDR_RAW_LEN];
            ::memset(raw, 0x00U, DFSI_VHDR_RAW_LEN);

            ::memcpy(raw, m_rxCall->VHDR1, 18U);
            ::memcpy(raw + 18U, m_rxCall->VHDR2, 18U);

            assert(raw != nullptr);

            // buffer for decoded VHDR data
            uint8_t vhdr[P25_HDU_LENGTH_BYTES];
            ::memset(vhdr, 0x00U, P25_HDU_LENGTH_BYTES);

            assert(vhdr != nullptr);

            uint32_t offset = 0U;
            for (uint32_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6) {
                Utils::hex2Bin(raw[i], vhdr, offset);
            }

            if (m_debug && m_trace)
                Utils::dump("ModemV24::convertToAirTIA(), DFSI RX VHDR, VHDR Before FEC", vhdr, P25_HDU_LENGTH_BYTES);

            // try to decode the RS data
            try {
                bool ret = m_rs.decode362017(vhdr);
                if (!ret) {
                    LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode RS (36,20,17) FEC");
                } else {
                    if (m_debug && m_trace)
                        Utils::dump("ModemV24::convertToAirTIA(), DFSI RX VHDR, VHDR After FEC", vhdr, P25_HDU_LENGTH_BYTES);

                    // late entry?
                    if (!m_rxCallInProgress) {
                        m_rxCallInProgress = true;
                        m_rxCall->resetCallData();
                        if (m_debug)
                            LogDebug(LOG_MODEM, "V24 RX VHDR late entry, resetting call data");
                    }

                    uint32_t dstId = GET_UINT16(vhdr, 13U);
                    if (m_rxCallInProgress && dstId == 0U) {
                        LogWarning(LOG_MODEM, "TIA/DFSI traffic sent voice header with no dstId while call is in progress?, ignoring header TGID");
                        break;
                    }

                    ::memcpy(m_rxCall->MI, vhdr, MI_LENGTH_BYTES);

                    m_rxCall->mfId = vhdr[9U];
                    m_rxCall->algoId = vhdr[10U];
                    m_rxCall->kId = GET_UINT16(vhdr, 11U);
                    m_rxCall->dstId = dstId;

                    if (m_debug) {
                        LogDebug(LOG_MODEM, "P25, VHDR algId = $%02X, kId = $%04X, dstId = $%04X", m_rxCall->algoId, m_rxCall->kId, m_rxCall->dstId);
                    }

                    // generate a HDU
                    lc::LC lc = lc::LC();
                    lc.setDstId(m_rxCall->dstId);
                    lc.setAlgId(m_rxCall->algoId);
                    lc.setKId(m_rxCall->kId);
                    lc.setMI(m_rxCall->MI);

                    // generate Sync
                    Sync::addP25Sync(buffer + 2U);

                    // generate NID
                    m_nid->encode(buffer + 2U, DUID::HDU);

                    // generate HDU
                    lc.encodeHDU(buffer + 2U);

                    // add status bits
                    P25Utils::addStatusBits(buffer + 2U, P25_HDU_FRAME_LENGTH_BITS, true, false);

                    buffer[0U] = modem::TAG_DATA;
                    buffer[1U] = 0x01U;
                    storeConvertedRx(buffer, P25_HDU_FRAME_LENGTH_BYTES + 2U);
                }
            }
            catch (...) {
                LogError(LOG_MODEM, "TIA/DFSI RX traffic got exception while trying to decode RS data for VHDR");
            }
        }
        break;
    
        case BlockType::FULL_RATE_VOICE:
        {
            FullRateVoice voice = FullRateVoice();
            //m_superFrameCnt = voice.getSuperframeCnt();
            voice.decode(dfsiData + dataOffs);

            if (m_debug) {
                LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "Full Rate Voice, frameType = $%02X, busy = %u, lostFrame = %u, muteFrame = %u, superFrameCnt = %u, errors = %u", voice.getFrameType(), voice.getBusy(), voice.getLostFrame(), voice.getMuteFrame(), voice.getSuperframeCnt(), voice.getTotalErrors());
                Utils::dump(1U, "ModemV24::convertToAirTIA(), Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }

            if (voice.getTotalErrors() > 0U) {
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "TIA/DFSI traffic has %u errors in frameType = $%02X", voice.getTotalErrors(), voice.getFrameType());
                m_rxCall->errors += voice.getTotalErrors();
            }

            dataOffs += voice.getLength();

            DFSIFrameType::E frameType = voice.getFrameType();
            switch (frameType) {
            case DFSIFrameType::LDU1_VOICE1:
            {
                ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
                ::memcpy(m_rxCall->netLDU1 + 10U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }
            break;
            case DFSIFrameType::LDU1_VOICE2:
            {
                ::memcpy(m_rxCall->netLDU1 + 26U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }
            break;
            case DFSIFrameType::LDU1_VOICE3:
            {
                ::memcpy(m_rxCall->netLDU1 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->lco = voice.additionalData[0U];
                    m_rxCall->mfId = voice.additionalData[1U];
                    m_rxCall->serviceOptions = voice.additionalData[2U];

                    // copy LDU1 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[0U] = voice.additionalData[0U];
                    m_rxCall->LDULC[1U] = voice.additionalData[1U];
                    m_rxCall->LDULC[2U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC3 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE4:
            {
                ::memcpy(m_rxCall->netLDU1 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->dstId = GET_UINT24(voice.additionalData, 0U);

                    // copy LDU1 LC bytes into LDU LC buffer
                    ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
                    m_rxCall->LDULC[3U] = voice.additionalData[0U];
                    m_rxCall->LDULC[4U] = voice.additionalData[1U];
                    m_rxCall->LDULC[5U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC4 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE5:
            {
                ::memcpy(m_rxCall->netLDU1 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->srcId = GET_UINT24(voice.additionalData, 0U);

                    // copy LDU1 LC bytes into LDU LC buffer
                    ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
                    m_rxCall->LDULC[6U] = voice.additionalData[0U];
                    m_rxCall->LDULC[7U] = voice.additionalData[1U];
                    m_rxCall->LDULC[8U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC5 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE6:
            {
                ::memcpy(m_rxCall->netLDU1 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    // copy LDU1 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[9U] = voice.additionalData[0U];
                    m_rxCall->LDULC[10U] = voice.additionalData[1U];
                    m_rxCall->LDULC[11U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC6 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE7:
            {
                ::memcpy(m_rxCall->netLDU1 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    // copy LDU1 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[12U] = voice.additionalData[0U];
                    m_rxCall->LDULC[13U] = voice.additionalData[1U];
                    m_rxCall->LDULC[14U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC7 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE8:
            {
                ::memcpy(m_rxCall->netLDU1 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    // copy LDU1 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[15U] = voice.additionalData[0U];
                    m_rxCall->LDULC[16U] = voice.additionalData[1U];
                    m_rxCall->LDULC[17U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC8 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU1_VOICE9:
            {
                ::memcpy(m_rxCall->netLDU1 + 204U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->lsd1 = voice.additionalData[0U];
                    m_rxCall->lsd2 = voice.additionalData[1U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC9 traffic missing metadata");
                }
            }
            break;

            case DFSIFrameType::LDU2_VOICE10:
            {
                ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES);
                ::memcpy(m_rxCall->netLDU2 + 10U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }
            break;
            case DFSIFrameType::LDU2_VOICE11:
            {
                ::memcpy(m_rxCall->netLDU2 + 26U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
            }
            break;
            case DFSIFrameType::LDU2_VOICE12:
            {
                ::memcpy(m_rxCall->netLDU2 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    ::memcpy(m_rxCall->MI, voice.additionalData, 3U);

                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[0U] = voice.additionalData[0U];
                    m_rxCall->LDULC[1U] = voice.additionalData[1U];
                    m_rxCall->LDULC[2U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC12 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE13:
            {
                ::memcpy(m_rxCall->netLDU2 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    ::memcpy(m_rxCall->MI + 3U, voice.additionalData, 3U);

                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[3U] = voice.additionalData[0U];
                    m_rxCall->LDULC[4U] = voice.additionalData[1U];
                    m_rxCall->LDULC[5U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC13 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE14:
            {
                ::memcpy(m_rxCall->netLDU2 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    ::memcpy(m_rxCall->MI + 6U, voice.additionalData, 3U);

                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[6U] = voice.additionalData[0U];
                    m_rxCall->LDULC[7U] = voice.additionalData[1U];
                    m_rxCall->LDULC[8U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC14 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE15:
            {
                ::memcpy(m_rxCall->netLDU2 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->algoId = voice.additionalData[0U];
                    m_rxCall->kId = GET_UINT16(voice.additionalData, 1U);

                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[9U] = voice.additionalData[0U];
                    m_rxCall->LDULC[10U] = voice.additionalData[1U];
                    m_rxCall->LDULC[11U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC15 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE16:
            {
                ::memcpy(m_rxCall->netLDU2 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[12U] = voice.additionalData[0U];
                    m_rxCall->LDULC[13U] = voice.additionalData[1U];
                    m_rxCall->LDULC[14U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC16 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE17:
            {
                ::memcpy(m_rxCall->netLDU2 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    // copy LDU2 LC bytes into LDU LC buffer
                    m_rxCall->LDULC[15U] = voice.additionalData[0U];
                    m_rxCall->LDULC[16U] = voice.additionalData[1U];
                    m_rxCall->LDULC[17U] = voice.additionalData[2U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC17 traffic missing metadata");
                }
            }
            break;
            case DFSIFrameType::LDU2_VOICE18:
            {
                ::memcpy(m_rxCall->netLDU2 + 204U, voice.imbeData, RAW_IMBE_LENGTH_BYTES);
                if (voice.additionalData != nullptr) {
                    m_rxCall->lsd1 = voice.additionalData[0U];
                    m_rxCall->lsd2 = voice.additionalData[1U];
                } else {
                    LogWarning(LOG_MODEM, "TIA/DFSI VC18 traffic missing metadata");
                }
            }
            break;

            default:
                break;
            }

            // increment our voice frame counter
            m_rxCall->n++;
        }
        break;
        
        default:
            break;
        }

        hdrOffs += BlockHeader::LENGTH;
    }

    m_rxLastFrameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

    // encode LDU1 if ready
    if (m_rxCall->n == 9U) {
        // decode RS (24,12,13) FEC
        // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now
        //  we'll just log the error
        try {
            bool ret = m_rs.decode241213(m_rxCall->LDULC);
            if (!ret) {
                LogError(LOG_MODEM, "TIA/DFSI LDU1, failed to decode RS (24,12,13) FEC");
            }
        }
        catch (...) {
            Utils::dump(2U, "Modem, TIA LDU1, RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES);
        }

        if (m_rxCall->errors > 0U) {
            LogWarning(LOG_MODEM, P25_DFSI_LDU1_STR ", TIA, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F);
            m_rxCall->errors = 0U;
        }

        lc::LC lc = lc::LC();
        lc.setLCO(m_rxCall->lco);
        lc.setMFId(m_rxCall->mfId);

        if (lc.isStandardMFId()) {
            lc.setSrcId(m_rxCall->srcId);
            lc.setDstId(m_rxCall->dstId);
        } else {
            uint8_t rsBuffer[P25_LDU_LC_FEC_LENGTH_BYTES];
            ::memset(rsBuffer, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES);

            rsBuffer[0U] = m_rxCall->LDULC[0U];
            rsBuffer[1U] = m_rxCall->LDULC[1U];
            rsBuffer[2U] = m_rxCall->LDULC[2U];
            rsBuffer[3U] = m_rxCall->LDULC[3U];
            rsBuffer[4U] = m_rxCall->LDULC[4U];
            rsBuffer[5U] = m_rxCall->LDULC[5U];
            rsBuffer[6U] = m_rxCall->LDULC[6U];
            rsBuffer[7U] = m_rxCall->LDULC[7U];
            rsBuffer[8U] = m_rxCall->LDULC[8U];

            // combine bytes into ulong64_t (8 byte) value
            ulong64_t rsValue = 0U;
            rsValue = rsBuffer[1U];
            rsValue = (rsValue << 8) + rsBuffer[2U];
            rsValue = (rsValue << 8) + rsBuffer[3U];
            rsValue = (rsValue << 8) + rsBuffer[4U];
            rsValue = (rsValue << 8) + rsBuffer[5U];
            rsValue = (rsValue << 8) + rsBuffer[6U];
            rsValue = (rsValue << 8) + rsBuffer[7U];
            rsValue = (rsValue << 8) + rsBuffer[8U];

            lc.setRS(rsValue);
        }

        bool emergency = ((m_rxCall->serviceOptions & 0xFFU) & 0x80U) == 0x80U;    // Emergency Flag
        bool encryption = ((m_rxCall->serviceOptions & 0xFFU) & 0x40U) == 0x40U;   // Encryption Flag
        uint8_t priority = ((m_rxCall->serviceOptions & 0xFFU) & 0x07U);           // Priority
        lc.setEmergency(emergency);
        lc.setEncrypted(encryption);
        lc.setPriority(priority);

        data::LowSpeedData lsd = data::LowSpeedData();
        lsd.setLSD1(m_rxCall->lsd1);
        lsd.setLSD2(m_rxCall->lsd2);

        // generate Sync
        Sync::addP25Sync(buffer + 2U);

        // generate NID
        m_nid->encode(buffer + 2U, DUID::LDU1);

        // generate LDU1 Data
        lc.encodeLDU1(buffer + 2U);

        // generate Low Speed Data
        lsd.process(buffer + 2U);

        // generate audio
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 10U, 0U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 26U, 1U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 55U, 2U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 80U, 3U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 105U, 4U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 130U, 5U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 155U, 6U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 180U, 7U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU1 + 204U, 8U);

        // add status bits
        P25Utils::addStatusBits(buffer + 2U, P25_LDU_FRAME_LENGTH_BITS, true, false);

        buffer[0U] = modem::TAG_DATA;
        buffer[1U] = 0x01U;
        storeConvertedRx(buffer, P25_LDU_FRAME_LENGTH_BYTES + 2U);
    }
    
    // encode LDU2 if ready
    if (m_rxCall->n == 18U) {
        // decode RS (24,16,9) FEC
        // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now
        //  we'll just log the error
        try {
            bool ret = m_rs.decode24169(m_rxCall->LDULC);
            if (!ret) {
                LogError(LOG_MODEM, "TIA/DFSI LDU2, failed to decode RS (24,16,9) FEC");
            }
        }
        catch (...) {
            Utils::dump(2U, "Modem, TIA LDU2, RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES);
        }

        if (m_rxCall->errors > 0U) {
            LogWarning(LOG_MODEM, P25_DFSI_LDU2_STR ", TIA, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F);
            m_rxCall->errors = 0U;
        }

        lc::LC lc = lc::LC();
        lc.setMI(m_rxCall->MI);
        lc.setAlgId(m_rxCall->algoId);
        lc.setKId(m_rxCall->kId);

        data::LowSpeedData lsd = data::LowSpeedData();
        lsd.setLSD1(m_rxCall->lsd1);
        lsd.setLSD2(m_rxCall->lsd2);

        // generate Sync
        Sync::addP25Sync(buffer + 2U);

        // generate NID
        m_nid->encode(buffer + 2U, DUID::LDU2);

        // generate LDU2 data
        lc.encodeLDU2(buffer + 2U);

        // generate Low Speed Data
        lsd.process(buffer + 2U);

        // generate audio
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 10U, 0U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 26U, 1U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 55U, 2U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 80U, 3U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 105U, 4U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 130U, 5U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 155U, 6U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 180U, 7U);
        m_audio.encode(buffer + 2U, m_rxCall->netLDU2 + 204U, 8U);

        // add status bits
        P25Utils::addStatusBits(buffer + 2U, P25_LDU_FRAME_LENGTH_BITS, true, false);

        buffer[0U] = modem::TAG_DATA;
        buffer[1U] = 0x01U;
        storeConvertedRx(buffer, P25_LDU_FRAME_LENGTH_BYTES + 2U);

        m_rxCall->n = 0;
    }
}

/* Helper to add a V.24 data frame to the P25 TX queue with the proper timestamp and formatting */

bool ModemV24::queueP25Frame(uint8_t* data, uint16_t len, SERIAL_TX_TYPE msgType, bool imm)
{
    assert(data != nullptr);
    assert(len > 0U);

    if (m_debug) {
        LogDebugEx(LOG_MODEM, "ModemV24::queueP25Frame()", "msgType = $%02X", msgType);
        Utils::dump("ModemV24::queueP25Frame(), V.24 TX Data To Modem", data, len);
    }

    // get current time in ms
    uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

    // timestamp for this message (in ms)
    uint64_t msgTime = 0U;

    // if this is our first message, timestamp is just now + the jitter buffer offset in ms
    if (m_lastP25Tx == 0U) {
        msgTime = now + m_jitter;

        // if the message type requests no jitter delay -- just set the message time to now
        if (msgType == STT_START_STOP_NO_JITTER)
            msgTime = now;
    }
    // if we had a message before this, calculate the new timestamp dynamically
    else {
        // if the last message occurred longer than our jitter buffer delay, we restart the sequence and calculate the same as above
        if ((int64_t)(now - m_lastP25Tx) > m_jitter) {
            msgTime = now + m_jitter;
        }
        // otherwise, we time out messages as required by the message type
        else {
            if (msgType == STT_DATA) {
                // data must go out at 20ms intervals
                msgTime = m_lastP25Tx + 20U;
            } else if (msgType == STT_DATA_FAST) {
                // fast data must go out at 10ms intervals
                msgTime = m_lastP25Tx + 10U;
            } else {
                // Otherwise we don't care, we use 5ms since that's the theoretical minimum time a 9600 baud message can take
                msgTime = m_lastP25Tx + 5U;
            }
        }
    }

    len += 4U;

    std::lock_guard<std::mutex> lock(m_txP25QueueLock);

    // check available ringbuffer space
    // keep one byte of headroom to avoid the ring buffer "full == empty"
    // ambiguity (iPtr == oPtr) when a write would consume exactly all free space
    uint32_t needed = len + 11U;
    if (imm) {
        uint32_t free = m_txImmP25Queue.freeSpace();
        if (free <= needed) {
            return false;
        }
    } else {
        uint32_t free = m_txP25Queue.freeSpace();
        if (free <= needed) {
            return false;
        }
    }

    // convert 16-bit length to 2 bytes
    uint8_t length[2U];
    if (len > 255U)
        length[0U] = (len >> 8U) & 0xFFU;
    else
        length[0U] = 0x00U;
    length[1U] = len & 0xFFU;

    if (imm)
        m_txImmP25Queue.addData(length, 2U);
    else
        m_txP25Queue.addData(length, 2U);

    // add the data tag
    uint8_t tag = TAG_DATA;
    if (imm)
        m_txImmP25Queue.addData(&tag, 1U);
    else
        m_txP25Queue.addData(&tag, 1U);

    // convert 64-bit timestamp to 8 bytes and add
    uint8_t tsBytes[8U];
    assert(sizeof msgTime == 8U);
    ::memcpy(tsBytes, &msgTime, 8U);
    if (imm)
        m_txImmP25Queue.addData(tsBytes, 8U);
    else
        m_txP25Queue.addData(tsBytes, 8U);

    // add the DVM start byte, length byte, CMD byte, and padding 0
    uint8_t header[4U];
    header[0U] = DVM_SHORT_FRAME_START;
    header[1U] = len & 0xFFU;
    header[2U] = CMD_P25_DATA;
    header[3U] = 0x00U;
    if (imm)
        m_txImmP25Queue.addData(header, 4U);
    else
        m_txP25Queue.addData(header, 4U);

    // add the data
    if (imm)
        m_txImmP25Queue.addData(data, len - 4U);
    else
        m_txP25Queue.addData(data, len - 4U);

    // update the last message time
    m_lastP25Tx = msgTime;

    return true;
}

/* Send a start of stream sequence (HDU, etc) to the connected serial V.24 device */

void ModemV24::startOfStreamV24(const p25::lc::LC& control)
{
    m_txCallInProgress = true;

    // start each Net->RF stream with a fresh scheduler epoch so a new call
    // doesn't inherit a future-biased timestamp from the previous call
    m_lastP25Tx = 0U;

    MotStartOfStream start = MotStartOfStream();
    start.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
    start.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
    start.setArgument1(MotStreamPayload::VOICE);

    // create buffer for bytes and encode
    uint8_t startBuf[DFSI_MOT_START_LEN];
    ::memset(startBuf, 0x00U, DFSI_MOT_START_LEN);
    start.encode(startBuf);

    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamV24(), StartOfStream", startBuf, DFSI_MOT_START_LEN);

    queueP25Frame(startBuf, DFSI_MOT_START_LEN, STT_START_STOP);

    uint8_t mi[MI_LENGTH_BYTES];
    ::memset(mi, 0x00U, MI_LENGTH_BYTES);
    control.getMI(mi);

    uint8_t vhdr[DFSI_VHDR_LEN];
    ::memset(vhdr, 0x00U, DFSI_VHDR_LEN);

    ::memcpy(vhdr, mi, MI_LENGTH_BYTES);

    vhdr[9U] = control.getMFId();
    vhdr[10U] = control.getAlgId();
    SET_UINT16(control.getKId(), vhdr, 11U);
    SET_UINT16(control.getDstId(), vhdr, 13U);

    // perform RS encoding
    m_rs.encode362017(vhdr);

    // convert the binary bytes to hex bytes
    uint8_t raw[DFSI_VHDR_RAW_LEN];
    uint32_t offset = 0;
    for (uint8_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6) {
        raw[i] = Utils::bin2Hex(vhdr, offset);
    }

    // prepare VHDR1
    uint8_t vhdr1Buf[DFSI_MOT_VHDR_1_LEN];
    ::memset(vhdr1Buf, 0x00U, DFSI_MOT_VHDR_1_LEN);
    start.encode(vhdr1Buf);

    ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 0U,  raw,       8U);
    ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 9U,  raw + 8U,  8U);
    ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 18U, raw + 16U, 2U);

    vhdr1Buf[0U] = DFSIFrameType::MOT_VHDR_1;
    vhdr1Buf[20U + DFSI_MOT_START_LEN] = DFSI_BUSY_BITS_INBOUND;

    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamV24(), VoiceHeader1", vhdr1Buf, DFSI_MOT_VHDR_1_LEN);

    queueP25Frame(vhdr1Buf, DFSI_MOT_VHDR_1_LEN, STT_START_STOP);

    // prepare VHDR2
    uint8_t vhdr2Buf[DFSI_MOT_VHDR_2_LEN];
    ::memset(vhdr2Buf, 0x00U, DFSI_MOT_VHDR_2_LEN);

    ::memcpy(vhdr2Buf + 1U,   raw + 18U, 8U);
    ::memcpy(vhdr2Buf + 10U,  raw + 26U, 8U);
    ::memcpy(vhdr2Buf + 19U,  raw + 34U, 2U);

    vhdr2Buf[0U] = DFSIFrameType::MOT_VHDR_2;
    vhdr2Buf[21U] = DFSI_BUSY_BITS_INBOUND;

    // send VHDR2
    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamV24(), VoiceHeader2", vhdr2Buf, DFSI_MOT_VHDR_2_LEN);

    queueP25Frame(vhdr2Buf, DFSI_MOT_VHDR_2_LEN, STT_START_STOP);
}

/* Send an end of stream sequence (TDU, etc) to the connected serial V.24 device */

void ModemV24::endOfStreamV24()
{
    MotStartOfStream end = MotStartOfStream();
    end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
    end.setParam1(DFSI_MOT_ICW_PARM_STOP);
    end.setArgument1(MotStreamPayload::VOICE);

    // create buffer and encode
    uint8_t endBuf[DFSI_MOT_START_LEN];
    ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN);
    end.encode(endBuf);

    if (m_trace)
        Utils::dump(1U, "ModemV24::endOfStreamV24(), StartOfStream", endBuf, DFSI_MOT_START_LEN);

    queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_START_STOP);

    m_txCallInProgress = false;
}

/* Helper to generate the NID value. */

uint16_t ModemV24::generateNID(DUID::E duid)
{
    uint8_t nid[2U];
    ::memset(nid, 0x00U, 2U);

    nid[0U] = (m_p25NAC >> 4) & 0xFFU;
    nid[1U] = (m_p25NAC << 4) & 0xF0U;
    nid[1U] |= duid;

    return GET_UINT16(nid, 0U);
}

/* Send a start of stream sequence (HDU, etc) to the connected UDP TIA-102 device. */

void ModemV24::startOfStreamTIA(const p25::lc::LC& control)
{
    m_txCallInProgress = true;
    m_superFrameCnt = 1U;

    // start each Net->RF stream with a fresh scheduler epoch so a new call
    // doesn't inherit a future-biased timestamp from the previous call
    m_lastP25Tx = 0U;

    p25::lc::LC lc = p25::lc::LC(control);
    
    uint16_t length = 0U;
    uint8_t buffer[P25_HDU_LENGTH_BYTES];
    ::memset(buffer, 0x00U, P25_HDU_LENGTH_BYTES);

    // generate control octet
    ControlOctet ctrl = ControlOctet();
    ctrl.setBlockHeaderCnt(1U);
    ctrl.encode(buffer);
    length += ControlOctet::LENGTH;

    // generate block header
    BlockHeader hdr = BlockHeader();
    hdr.setBlockType(BlockType::START_OF_STREAM);
    hdr.encode(buffer + 1U);
    length += BlockHeader::LENGTH;

    // generate start of stream
    StartOfStream start = StartOfStream();
    start.setNID(generateNID(DUID::HDU));
    start.encode(buffer + 2U);
    length += StartOfStream::LENGTH;

    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamTIA(), StartOfStream", buffer, length);

    queueP25Frame(buffer, length, STT_START_STOP);

    uint8_t mi[MI_LENGTH_BYTES];
    ::memset(mi, 0x00U, MI_LENGTH_BYTES);
    control.getMI(mi);

    uint8_t vhdr[P25_HDU_LENGTH_BYTES];
    ::memset(vhdr, 0x00U, P25_HDU_LENGTH_BYTES);

    ::memcpy(vhdr, mi, MI_LENGTH_BYTES);

    vhdr[9U] = control.getMFId();
    vhdr[10U] = control.getAlgId();
    SET_UINT16(control.getKId(), vhdr, 11U);
    SET_UINT16(control.getDstId(), vhdr, 13U);

    // perform RS encoding
    m_rs.encode362017(vhdr);

    // convert the binary bytes to hex bytes
    uint8_t raw[DFSI_VHDR_RAW_LEN];
    uint32_t offset = 0U;
    for (uint8_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6) {
        raw[i] = Utils::bin2Hex(vhdr, offset);
    }

    ::memset(buffer, 0x00U, P25_HDU_LENGTH_BYTES);
    length = 0U;

    // generate control octet
    ctrl.setBlockHeaderCnt(2U);
    ctrl.encode(buffer);
    length += ControlOctet::LENGTH;

    // generate block header 1
    hdr.setBlockType(BlockType::VOICE_HEADER_P1);
    hdr.encode(buffer + 1U);
    length += BlockHeader::LENGTH;
    hdr.setBlockType(BlockType::START_OF_STREAM);
    hdr.encode(buffer + 2U);
    length += BlockHeader::LENGTH;

    // generate voice header 1
    uint8_t hdu[P25_HDU_LENGTH_BYTES];
    ::memset(hdu, 0x00U, P25_HDU_LENGTH_BYTES);
    lc.encodeHDU(hdu, true);

    // prepare VHDR1
    buffer[3U] = DFSIFrameType::MOT_VHDR_1;
    ::memcpy(buffer + 4U, raw, 18U);
    length += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker

    start.encode(buffer + length);
    length += StartOfStream::LENGTH;

    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamTIA(), VoiceHeader1", buffer, length);

    queueP25Frame(buffer, length, STT_START_STOP);

    ::memset(buffer, 0x00U, P25_HDU_LENGTH_BYTES);
    length = 0U;

    // generate control octet
    ctrl.setBlockHeaderCnt(2U);
    ctrl.encode(buffer);
    length += ControlOctet::LENGTH;

    // generate block header 1
    hdr.setBlockType(BlockType::VOICE_HEADER_P2);
    hdr.encode(buffer + 1U);
    length += BlockHeader::LENGTH;
    hdr.setBlockType(BlockType::START_OF_STREAM);
    hdr.encode(buffer + 2U);
    length += BlockHeader::LENGTH;

    // prepare VHDR2
    buffer[3U] = DFSIFrameType::MOT_VHDR_2;
    ::memcpy(buffer + 4U, raw + 18U, 18U);
    length += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker

    start.encode(buffer + length);
    length += StartOfStream::LENGTH;

    if (m_trace)
        Utils::dump(1U, "ModemV24::startOfStreamTIA(), VoiceHeader2", buffer, length);

    queueP25Frame(buffer, length, STT_START_STOP);
}

/* Send an end of stream sequence (TDU, etc) to the connected UDP TIA-102 device. */

void ModemV24::endOfStreamTIA()
{
    m_superFrameCnt = 1U;

    uint16_t length = 0U;
    uint8_t buffer[2U];
    ::memset(buffer, 0x00U, 2U);

    // generate control octet
    ControlOctet ctrl = ControlOctet();
    ctrl.setBlockHeaderCnt(1U);
    ctrl.encode(buffer);
    length += ControlOctet::LENGTH;

    // generate block header
    BlockHeader hdr = BlockHeader();
    hdr.setBlockType(BlockType::END_OF_STREAM);
    hdr.encode(buffer + 1U);
    length += BlockHeader::LENGTH;

    if (m_trace)
        Utils::dump(1U, "ModemV24::endOfStreamTIA(), EndOfStream", buffer, length);

    queueP25Frame(buffer, length, STT_START_STOP);

    m_txCallInProgress = false;
}

/* Send a start of stream ACK. */

void ModemV24::ackStartOfStreamTIA()
{
    uint16_t length = 0U;
    uint8_t buffer[P25_HDU_LENGTH_BYTES];
    ::memset(buffer, 0x00U, P25_HDU_LENGTH_BYTES);

    // generate control octet
    ControlOctet ctrl = ControlOctet();
    ctrl.setBlockHeaderCnt(1U);
    ctrl.encode(buffer);
    length += ControlOctet::LENGTH;

    // generate block header
    BlockHeader hdr = BlockHeader();
    hdr.setBlockType(BlockType::START_OF_STREAM_ACK);
    hdr.encode(buffer + 1U);
    length += BlockHeader::LENGTH;

    if (m_trace)
        Utils::dump(1U, "ModemV24::ackStartOfStreamTIA(), Ack StartOfStream", buffer, length);

    queueP25Frame(buffer, length, STT_START_STOP);
}

/* Internal helper to convert from TIA-102 air interface to V.24/DFSI. */

void ModemV24::convertFromAirV24(uint8_t* data, uint32_t length, bool imm)
{
    assert(data != nullptr);
    assert(length > 0U);

    if (m_trace)
        Utils::dump(1U, "ModemV24::convertFromAirV24(), data", data, length);

    uint8_t ldu[9U * 25U];
    ::memset(ldu, 0x00U, 9 * 25U);

    // decode the NID
    bool valid = m_nid->decode(data + 2U);
    if (!valid)
        return;

    DUID::E duid = m_nid->getDUID();

    // handle individual DUIDs
    lc::LC lc = lc::LC();
    data::LowSpeedData lsd = data::LowSpeedData();
    switch (duid) {
        case DUID::HDU:
        {
            bool ret = lc.decodeHDU(data + 2U);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_HDU_STR ", undecodable LC");
            }

            startOfStreamV24(lc);
        }
        break;
        case DUID::LDU1:
        {
            bool ret = lc.decodeLDU1(data + 2U, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_LDU1_STR ", undecodable LC");
                return;
            }

            lsd.process(data + 2U);

            // late entry?
            if (!m_txCallInProgress) {
                startOfStreamV24(lc);
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirV24()", "V.24 TX VHDR late entry, resetting TX call data");
            }

            // generate audio
            m_audio.decode(data + 2U, ldu + 10U, 0U);
            m_audio.decode(data + 2U, ldu + 26U, 1U);
            m_audio.decode(data + 2U, ldu + 55U, 2U);
            m_audio.decode(data + 2U, ldu + 80U, 3U);
            m_audio.decode(data + 2U, ldu + 105U, 4U);
            m_audio.decode(data + 2U, ldu + 130U, 5U);
            m_audio.decode(data + 2U, ldu + 155U, 6U);
            m_audio.decode(data + 2U, ldu + 180U, 7U);
            m_audio.decode(data + 2U, ldu + 204U, 8U);
        }
        break;
        case DUID::LDU2:
        {
            bool ret = lc.decodeLDU2(data + 2U);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_LDU2_STR ", undecodable LC");
                return;
            }

            lsd.process(data + 2U);

            // generate audio
            m_audio.decode(data + 2U, ldu + 10U, 0U);
            m_audio.decode(data + 2U, ldu + 26U, 1U);
            m_audio.decode(data + 2U, ldu + 55U, 2U);
            m_audio.decode(data + 2U, ldu + 80U, 3U);
            m_audio.decode(data + 2U, ldu + 105U, 4U);
            m_audio.decode(data + 2U, ldu + 130U, 5U);
            m_audio.decode(data + 2U, ldu + 155U, 6U);
            m_audio.decode(data + 2U, ldu + 180U, 7U);
            m_audio.decode(data + 2U, ldu + 204U, 8U);
        }
        break;

        case DUID::TDU:
            if (m_txCallInProgress)
                endOfStreamV24();
            break;

        case DUID::TDULC:
        {
            if (m_txCallInProgress)
                endOfStreamV24();

            lc::tdulc::LC_TDULC_RAW tdulc = lc::tdulc::LC_TDULC_RAW();
            if (!tdulc.decode(data + 2U)) {
                LogWarning(LOG_MODEM, P25_TDULC_STR ", undecodable LC");
                return;
            }

            MotTDULCFrame tf = MotTDULCFrame();
            tf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
            tf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
            tf.startOfStream->setArgument1(MotStreamPayload::TERM_LC);
            delete[] tf.tdulcData;

            tf.tdulcData = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U];
            ::memset(tf.tdulcData, 0x00U, P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U);
            ::memcpy(tf.tdulcData, tdulc.getDecodedRaw(), P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U);

            // create buffer and encode
            uint8_t tdulcBuf[DFSI_MOT_TDULC_LEN];
            ::memset(tdulcBuf, 0x00U, DFSI_MOT_TDULC_LEN);
            tf.encode(tdulcBuf);

            if (m_trace)
                Utils::dump(1U, "ModemV24::convertFromAirV24(), MotTDULCFrame", tdulcBuf, DFSI_MOT_TDULC_LEN);

            queueP25Frame(tdulcBuf, DFSI_MOT_TDULC_LEN, STT_DATA);
        }
        break;

        case DUID::PDU:
        {
            uint8_t buffer[P25_PDU_FRAME_LENGTH_BYTES];
            ::memset(buffer, 0x00U, P25_PDU_FRAME_LENGTH_BYTES);

            uint32_t bits = P25Utils::decode(data + 2U, buffer, 0U, P25_PDU_FRAME_LENGTH_BITS);

            uint8_t rawPDU[P25_PDU_FRAME_LENGTH_BYTES];
            ::memset(rawPDU, 0x00U, P25_PDU_FRAME_LENGTH_BYTES);
            uint32_t pduBitCnt = Utils::getBits(buffer, rawPDU, 0U, bits);

            uint32_t offset = P25_PREAMBLE_LENGTH_BITS + P25_PDU_FEC_LENGTH_BITS;
            ::memset(buffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES);
            Utils::getBitRange(rawPDU, buffer, P25_PREAMBLE_LENGTH_BITS, P25_PDU_FEC_LENGTH_BITS);
            data::DataHeader dataHeader = data::DataHeader();
            bool ret = dataHeader.decode(buffer);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_PDU_STR ", unfixable RF 1/2 rate header data");
                Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES);
                return;
            }

            if (m_debug) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirV24()", "PDU OSP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, llId = %u",
                    dataHeader.getAckNeeded(), dataHeader.getOutbound(), dataHeader.getFormat(), dataHeader.getMFId(), dataHeader.getSAP(), dataHeader.getFullMessage(),
                    dataHeader.getBlocksToFollow(), dataHeader.getPadLength(), dataHeader.getPacketLength(), dataHeader.getSynchronize(), dataHeader.getNs(), dataHeader.getFSN(), dataHeader.getLastFragment(),
                    dataHeader.getHeaderOffset(), dataHeader.getLLId());
            }

            // make sure we don't get a PDU with more blocks then we support
            if (dataHeader.getBlocksToFollow() >= P25_MAX_PDU_BLOCKS) {
                ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirV24()", "PDU OSP, too many PDU blocks to process, %u > %u", dataHeader.getBlocksToFollow(), P25_MAX_PDU_BLOCKS);
                return;
            }

            uint32_t blocksToFollow = dataHeader.getBlocksToFollow();
            uint32_t bitLength = ((blocksToFollow + 1U) * P25_PDU_FEC_LENGTH_BITS) + P25_PREAMBLE_LENGTH_BITS;
            if (pduBitCnt >= bitLength) {
                // process all blocks in the data stream
                // if the primary header has a header offset ensure data if offset by that amount 
                if (dataHeader.getHeaderOffset() > 0U) {
                    offset += dataHeader.getHeaderOffset() * 8;
                }

                data::DataBlock* dataBlocks = new data::DataBlock[P25_MAX_PDU_BLOCKS];

                // decode data blocks
                for (uint32_t i = 0U; i < blocksToFollow; i++) {
                    ::memset(buffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES);
                    Utils::getBitRange(rawPDU, buffer, offset, P25_PDU_FEC_LENGTH_BITS);
                    bool ret = dataBlocks[i].decode(buffer, dataHeader);
                    if (ret) {
                        // if we are getting unconfirmed or confirmed blocks, and if we've reached the total number of blocks
                        // set this block as the last block for full packet CRC
                        if ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) || (dataHeader.getFormat() == PDUFormatType::UNCONFIRMED)) {
                            if ((i + 1U) == blocksToFollow) {
                                dataBlocks[i].setLastBlock(true);
                            }
                        }

                        if (m_debug) {
                            ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirV24()", "PDU OSP, block %u, fmt = $%02X, lastBlock = %u",
                                (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? dataBlocks[i].getSerialNo() : i, dataBlocks[i].getFormat(),
                                dataBlocks[i].getLastBlock());
                        }
                    }

                    offset += P25_PDU_FEC_LENGTH_BITS;
                }

                MotStartOfStream start = MotStartOfStream();
                start.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                start.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
                start.setArgument1(MotStreamPayload::DATA);

                // create buffer for bytes and encode
                uint8_t startBuf[DFSI_MOT_START_LEN];
                ::memset(startBuf, 0x00U, DFSI_MOT_START_LEN);
                start.encode(startBuf);

                if (m_trace)
                    Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU StartOfStream", startBuf, DFSI_MOT_START_LEN);

                queueP25Frame(startBuf, DFSI_MOT_START_LEN, STT_START_STOP);

                // assemble V.24 PDU frames
                /*
                ** If there are 3 or fewer blocks, we can send them all in a single PDU frame.
                */
                if (blocksToFollow <= 3U) {
                    uint32_t pduLen = P25_PDU_HEADER_LENGTH_BYTES + (blocksToFollow * ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? (P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 2U) : P25_PDU_UNCONFIRMED_LENGTH_BYTES)) + 9U;
                    DECLARE_UINT8_ARRAY(pduBuf, pduLen);

                    // header block contains the embedded start of stream
                    MotStartOfStream embedded = MotStartOfStream();
                    embedded.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                    embedded.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
                    embedded.setArgument1((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? MotStreamPayload::DATA_18 : MotStreamPayload::DATA_12);
                    embedded.encode(pduBuf);

                    // set opcode properly
                    pduBuf[0U] = (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_SINGLE_CONF : DFSIFrameType::MOT_PDU_SINGLE_UNCONF;

                    dataHeader.encode(pduBuf + 9U, true);
                    for (uint32_t i = 0U; i < blocksToFollow; i++) {
                        if (dataHeader.getFormat() == PDUFormatType::CONFIRMED) {
                            uint32_t blockOffset = 10U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                            pduBuf[blockOffset] = dataBlocks[i].getSerialNo();

                            uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES + 1U];
                            ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES + 1U);
                            dataBlocks[i].encode(dataBlock, true);

                            ::memcpy(pduBuf + blockOffset + 1U, dataBlock + 2U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);

                            if (m_trace)
                                Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Confirmed Data Block", pduBuf + blockOffset, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);
                        } else {
                            uint32_t blockOffset = 10U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                            dataBlocks[i].encode(pduBuf + blockOffset, true);

                            if (m_trace)
                                Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Unconfirmed Data Block", pduBuf + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                        }
                    }

                    if (m_trace)
                        Utils::dump(1U, "ModemV24::convertFromAirV24(), MotPDUFrame", pduBuf, pduLen);

                    queueP25Frame(pduBuf, pduLen, STT_DATA);

                    MotStartOfStream end = MotStartOfStream();
                    end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                    end.setParam1(DFSI_MOT_ICW_PARM_STOP);
                    end.setArgument1(MotStreamPayload::DATA);

                    // create buffer and encode
                    uint8_t endBuf[DFSI_MOT_START_LEN];
                    ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN);
                    end.encode(endBuf);

                    queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_START_STOP);
                    queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_START_STOP);
                }
                /*
                ** More than 3 blocks, need to segment into multiple PDU frames. 
                */
                else {
                    uint32_t remainderBlocks = (blocksToFollow - 3U) % DFSI_PDU_BLOCK_CNT;
                    uint32_t baseBlockCnt = (blocksToFollow - 3U) / DFSI_PDU_BLOCK_CNT;
                    uint32_t currentBlock = 0U;

                    uint32_t pduLen = P25_PDU_HEADER_LENGTH_BYTES + (DFSI_PDU_BLOCK_CNT * ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? (P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 2U) : P25_PDU_UNCONFIRMED_LENGTH_BYTES)) + 9U;
                    DECLARE_UINT8_ARRAY(pduBuf, pduLen);

                    // assemble the first frame
                    // header block contains the embedded start of stream
                    MotStartOfStream embedded = MotStartOfStream();
                    embedded.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                    embedded.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
                    embedded.setArgument1((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? MotStreamPayload::DATA_18 : MotStreamPayload::DATA_12);
                    embedded.encode(pduBuf);

                    // set opcode properly
                    pduBuf[0U] = (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_CONF_HEADER : DFSIFrameType::MOT_PDU_UNCONF_HEADER;

                    dataHeader.encode(pduBuf + 9U, true);
                    for (uint32_t i = 0U; i < DFSI_PDU_BLOCK_CNT - 1U; i++) {
                        if (dataHeader.getFormat() == PDUFormatType::CONFIRMED) {
                            uint32_t blockOffset = 10U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                            pduBuf[blockOffset] = dataBlocks[i].getSerialNo();

                            uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES + 1U];
                            ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES + 1U);
                            dataBlocks[currentBlock].encode(dataBlock, true);

                            ::memcpy(pduBuf + blockOffset + 1U, dataBlock + 2U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);

                            if (m_trace)
                                Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Confirmed Header Data Block", pduBuf + blockOffset, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);
                        } else {
                            uint32_t blockOffset = 10U + P25_PDU_HEADER_LENGTH_BYTES + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                            dataBlocks[currentBlock].encode(pduBuf + blockOffset, true);

                            if (m_trace)
                                Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Unconfirmed Header Data Block", pduBuf + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                        }

                        currentBlock++;
                    }

                    if (m_trace)
                        Utils::dump(1U, "ModemV24::convertFromAirV24(), MotPDUFrame", pduBuf, pduLen);

                    queueP25Frame(pduBuf, pduLen, STT_DATA);

                    // iterate through the count of full 4 block buffers and send
                    uint8_t currentOpcode = (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_CONF_BLOCK_1 : DFSIFrameType::MOT_PDU_UNCONF_BLOCK_1;
                    for (uint32_t i = 1U; i < baseBlockCnt; i++) {
                        // reset buffer and set data
                        ::memset(pduBuf, 0x00U, pduLen);
                        pduBuf[0U] = currentOpcode;

                        pduLen = 1U;
                        for (uint32_t i = 0U; i < DFSI_PDU_BLOCK_CNT - 1U; i++) {
                            if (dataHeader.getFormat() == PDUFormatType::CONFIRMED) {
                                uint32_t blockOffset = 1U + (i * P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                                pduBuf[blockOffset] = dataBlocks[i].getSerialNo();

                                uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES];
                                ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES);
                                dataBlocks[currentBlock].encode(dataBlock, true);

                                ::memcpy(pduBuf + blockOffset + 1U, dataBlock + 2U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                                if (m_trace)
                                    Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Confirmed Data Block", pduBuf + blockOffset, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);
                            } else {
                                uint32_t blockOffset = 1U + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                                dataBlocks[currentBlock].encode(pduBuf + blockOffset, true);

                                if (m_trace)
                                    Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Unconfirmed Data Block", pduBuf + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                            }

                            pduLen += 1U + (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? (P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U) : P25_PDU_UNCONFIRMED_LENGTH_BYTES;
                            currentBlock++;
                        }

                        currentOpcode++;
                        if (currentOpcode > ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_CONF_BLOCK_4 : DFSIFrameType::MOT_PDU_UNCONF_BLOCK_4))
                            currentOpcode = (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_CONF_BLOCK_1 : DFSIFrameType::MOT_PDU_UNCONF_BLOCK_1;

                        if (m_trace)
                            Utils::dump(1U, "ModemV24::convertFromAirV24(), MotPDUFrame", pduBuf, pduLen);

                        queueP25Frame(pduBuf, pduLen, STT_DATA);
                    }

                    // do we have any remaining blocks?
                    if (remainderBlocks > 0) {
                        // reset buffer and set data
                        ::memset(pduBuf, 0x00U, pduLen);
                        pduBuf[0U] = (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? DFSIFrameType::MOT_PDU_CONF_END : DFSIFrameType::MOT_PDU_UNCONF_END;

                        pduLen = 1U;
                        for (uint32_t i = 0U; i < remainderBlocks; i++) {
                            if (dataHeader.getFormat() == PDUFormatType::CONFIRMED) {
                                uint32_t blockOffset = 1U + (i * P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                                pduBuf[blockOffset] = dataBlocks[i].getSerialNo();

                                uint8_t dataBlock[P25_PDU_CONFIRMED_LENGTH_BYTES];
                                ::memset(dataBlock, 0x00U, P25_PDU_CONFIRMED_LENGTH_BYTES);
                                dataBlocks[currentBlock].encode(dataBlock, true);

                                ::memcpy(pduBuf + blockOffset + 1U, dataBlock + 2U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);

                                if (m_trace)
                                    Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Confirmed End Data Block", pduBuf + blockOffset, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U);
                            } else {
                                uint32_t blockOffset = 1U + (i * P25_PDU_UNCONFIRMED_LENGTH_BYTES);

                                dataBlocks[currentBlock].encode(pduBuf + blockOffset, true);

                                if (m_trace)
                                    Utils::dump(1U, "ModemV24::convertFromAirV24(), PDU Tx Unconfirmed End Data Block", pduBuf + blockOffset, P25_PDU_UNCONFIRMED_LENGTH_BYTES);
                            }

                            pduLen += 1U + (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? (P25_PDU_CONFIRMED_DATA_LENGTH_BYTES + 1U) : P25_PDU_UNCONFIRMED_LENGTH_BYTES;
                            currentBlock++;
                        }

                        if (m_trace)
                            Utils::dump(1U, "ModemV24::convertFromAirV24(), MotPDUFrame", pduBuf, pduLen);

                        queueP25Frame(pduBuf, pduLen, STT_DATA);
                    }

                    MotStartOfStream end = MotStartOfStream();
                    end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                    end.setParam1(DFSI_MOT_ICW_PARM_STOP);
                    end.setArgument1(MotStreamPayload::DATA);

                    // create buffer and encode
                    uint8_t endBuf[DFSI_MOT_START_LEN];
                    ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN);
                    end.encode(endBuf);

                    queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_START_STOP);
                    queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_START_STOP);
                }

                delete[] dataBlocks;
            }
        }
        break;

        case DUID::TSDU:
        {
            lc::tsbk::OSP_TSBK_RAW tsbk = lc::tsbk::OSP_TSBK_RAW();
            if (!tsbk.decode(data + 2U)) {
                LogWarning(LOG_MODEM, P25_TSDU_STR ", undecodable LC");
                return;
            }

            MotTSBKFrame tf = MotTSBKFrame();
            tf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
            tf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
            tf.startOfStream->setArgument1(MotStreamPayload::TSBK);
            delete[] tf.tsbkData;

            tf.tsbkData = new uint8_t[P25_TSBK_LENGTH_BYTES];
            ::memset(tf.tsbkData, 0x00U, P25_TSBK_LENGTH_BYTES);
            ::memcpy(tf.tsbkData, tsbk.getDecodedRaw(), P25_TSBK_LENGTH_BYTES);

            // create buffer and encode
            uint8_t tsbkBuf[DFSI_MOT_TSBK_LEN];
            ::memset(tsbkBuf, 0x00U, DFSI_MOT_TSBK_LEN);
            tf.encode(tsbkBuf);

            if (m_trace)
                Utils::dump(1U, "ModemV24::convertFromAirV24(), MotTSBKFrame", tsbkBuf, DFSI_MOT_TSBK_LEN);

            queueP25Frame(tsbkBuf, DFSI_MOT_TSBK_LEN, STT_DATA_FAST, imm);
        }
        break;

        default:
            break;
    }

    if (duid == DUID::LDU1 || duid == DUID::LDU2) {
        uint8_t rs[P25_LDU_LC_FEC_LENGTH_BYTES];
        ::memset(rs, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES);

        if (duid == DUID::LDU1) {
            rs[0U] = lc.getLCO();                                                   // LCO

            // split ulong64_t (8 byte) value into bytes
            rs[1U] = (uint8_t)((lc.getRS() >> 56) & 0xFFU);
            rs[2U] = (uint8_t)((lc.getRS() >> 48) & 0xFFU);
            rs[3U] = (uint8_t)((lc.getRS() >> 40) & 0xFFU);
            rs[4U] = (uint8_t)((lc.getRS() >> 32) & 0xFFU);
            rs[5U] = (uint8_t)((lc.getRS() >> 24) & 0xFFU);
            rs[6U] = (uint8_t)((lc.getRS() >> 16) & 0xFFU);
            rs[7U] = (uint8_t)((lc.getRS() >> 8) & 0xFFU);
            rs[8U] = (uint8_t)((lc.getRS() >> 0) & 0xFFU);

            // encode RS (24,12,13) FEC
            m_rs.encode241213(rs);
        } else {
            // generate MI data
            uint8_t mi[MI_LENGTH_BYTES];
            ::memset(mi, 0x00U, MI_LENGTH_BYTES);
            lc.getMI(mi);

            for (uint32_t i = 0; i < MI_LENGTH_BYTES; i++)
                rs[i] = mi[i];                                                      // Message Indicator

            rs[9U] = lc.getAlgId();                                                 // Algorithm ID
            rs[10U] = (lc.getKId() >> 8) & 0xFFU;                                   // Key ID
            rs[11U] = (lc.getKId() >> 0) & 0xFFU;                                   // ...

            // encode RS (24,16,9) FEC
            m_rs.encode24169(rs);
        }

        for (int n = 0; n < 9; n++) {
            uint8_t* buffer = nullptr;
            uint16_t bufferSize = 0;
            MotFullRateVoice voice = MotFullRateVoice();
            voice.setBusy(DFSI_BUSY_BITS_INBOUND);

            switch (n) {
                case 0: // VOICE1/10
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE1 : DFSIFrameType::LDU2_VOICE10);

                    MotStartVoiceFrame svf = MotStartVoiceFrame();
                    svf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE);
                    svf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD);
                    svf.startOfStream->setArgument1(MotStreamPayload::VOICE);
                    svf.fullRateVoice->setFrameType(voice.getFrameType());

                    ::memcpy(svf.fullRateVoice->imbeData, ldu + 10U, RAW_IMBE_LENGTH_BYTES);

                    buffer = new uint8_t[MotStartVoiceFrame::LENGTH];
                    ::memset(buffer, 0x00U, MotStartVoiceFrame::LENGTH);
                    svf.encode(buffer);
                    bufferSize = MotStartVoiceFrame::LENGTH;
                }
                break;
                case 1: // VOICE2/11
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE2 : DFSIFrameType::LDU2_VOICE11);
                    ::memcpy(voice.imbeData, ldu + 26U, RAW_IMBE_LENGTH_BYTES);
                }
                break;
                case 2: // VOICE3/12
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE3 : DFSIFrameType::LDU2_VOICE12);
                    ::memcpy(voice.imbeData, ldu + 55U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    // copy additional data
                    voice.additionalData[0U] = rs[0U];
                    voice.additionalData[1U] = rs[1U];
                    voice.additionalData[2U] = rs[2U];
                }
                break;
                case 3: // VOICE4/13
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE4 : DFSIFrameType::LDU2_VOICE13);
                    ::memcpy(voice.imbeData, ldu + 80U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    // copy additional data
                    voice.additionalData[0U] = rs[3U];
                    voice.additionalData[1U] = rs[4U];
                    voice.additionalData[2U] = rs[5U];
                }
                break;
                case 4: // VOICE5/14
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE5 : DFSIFrameType::LDU2_VOICE14);
                    ::memcpy(voice.imbeData, ldu + 105U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[6U];
                    voice.additionalData[1U] = rs[7U];
                    voice.additionalData[2U] = rs[8U];
                }
                break;
                case 5: // VOICE6/15
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE6 : DFSIFrameType::LDU2_VOICE15);
                    ::memcpy(voice.imbeData, ldu + 130U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[9U];
                    voice.additionalData[1U] = rs[10U];
                    voice.additionalData[2U] = rs[11U];
                }
                break;
                case 6: // VOICE7/16
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE7 : DFSIFrameType::LDU2_VOICE16);
                    ::memcpy(voice.imbeData, ldu + 155U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[12U];
                    voice.additionalData[1U] = rs[13U];
                    voice.additionalData[2U] = rs[14U];
                }
                break;
                case 7: // VOICE8/17
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE8 : DFSIFrameType::LDU2_VOICE17);
                    ::memcpy(voice.imbeData, ldu + 180U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[15U];
                    voice.additionalData[1U] = rs[16U];
                    voice.additionalData[2U] = rs[17U];
                }
                break;
                case 8: // VOICE9/18
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE9 : DFSIFrameType::LDU2_VOICE18);
                    ::memcpy(voice.imbeData, ldu + 204U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = lsd.getLSD1();
                    voice.additionalData[1U] = lsd.getLSD2();
                }
                break;
            }

            // For n=0 (VHDR1/10) case we create the buffer in the switch, for all other frame types we do that here
            if (n != 0) {
                buffer = new uint8_t[voice.size()];
                ::memset(buffer, 0x00U, voice.size());
                voice.encode(buffer);
                bufferSize = voice.size();
            }

            if (buffer != nullptr) {
                if (m_trace) {
                    Utils::dump("ModemV24::convertFromAirV24(), Encoded V.24 Voice Frame Data", buffer, bufferSize);
                }

                queueP25Frame(buffer, bufferSize, STT_DATA);
                delete[] buffer;
            }
        }
    }
}

/* Internal helper to convert from TIA-102 air interface to TIA-102 DFSI. */

void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length, bool imm)
{
    assert(data != nullptr);
    assert(length > 0U);

    if (m_trace)
        Utils::dump(1U, "ModemV24::convertFromAirTIA(), data", data, length);

    uint8_t ldu[9U * 25U];
    ::memset(ldu, 0x00U, 9 * 25U);

    // decode the NID
    bool valid = m_nid->decode(data + 2U);
    if (!valid)
        return;

    DUID::E duid = m_nid->getDUID();

    // handle individual DUIDs
    lc::LC lc = lc::LC();
    data::LowSpeedData lsd = data::LowSpeedData();
    switch (duid) {
        case DUID::HDU:
        {
            bool ret = lc.decodeHDU(data + 2U);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_HDU_STR ", undecodable LC");
            }

            startOfStreamTIA(lc);
        }
        break;
        case DUID::LDU1:
        {
            bool ret = lc.decodeLDU1(data + 2U, true);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_LDU1_STR ", undecodable LC");
                return;
            }

            lsd.process(data + 2U);

            // late entry?
            if (!m_txCallInProgress) {
                startOfStreamTIA(lc);
                if (m_debug)
                    ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirTIA()", "DFSI TX VHDR late entry, resetting TX call data");
            }

            // generate audio
            m_audio.decode(data + 2U, ldu + 10U, 0U);
            m_audio.decode(data + 2U, ldu + 26U, 1U);
            m_audio.decode(data + 2U, ldu + 55U, 2U);
            m_audio.decode(data + 2U, ldu + 80U, 3U);
            m_audio.decode(data + 2U, ldu + 105U, 4U);
            m_audio.decode(data + 2U, ldu + 130U, 5U);
            m_audio.decode(data + 2U, ldu + 155U, 6U);
            m_audio.decode(data + 2U, ldu + 180U, 7U);
            m_audio.decode(data + 2U, ldu + 204U, 8U);
        }
        break;
        case DUID::LDU2:
        {
            bool ret = lc.decodeLDU2(data + 2U);
            if (!ret) {
                LogWarning(LOG_MODEM, P25_LDU2_STR ", undecodable LC");
                return;
            }

            lsd.process(data + 2U);

            // generate audio
            m_audio.decode(data + 2U, ldu + 10U, 0U);
            m_audio.decode(data + 2U, ldu + 26U, 1U);
            m_audio.decode(data + 2U, ldu + 55U, 2U);
            m_audio.decode(data + 2U, ldu + 80U, 3U);
            m_audio.decode(data + 2U, ldu + 105U, 4U);
            m_audio.decode(data + 2U, ldu + 130U, 5U);
            m_audio.decode(data + 2U, ldu + 155U, 6U);
            m_audio.decode(data + 2U, ldu + 180U, 7U);
            m_audio.decode(data + 2U, ldu + 204U, 8U);
        }
        break;

        case DUID::TDU:
        case DUID::TDULC:
            if (m_txCallInProgress)
                endOfStreamTIA();
            break;

        case DUID::PDU:
            break;

        case DUID::TSDU:
            break;

        default:
            break;
    }

    if (duid == DUID::LDU1 || duid == DUID::LDU2) {
        uint8_t rs[P25_LDU_LC_FEC_LENGTH_BYTES];
        ::memset(rs, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES);

        if (duid == DUID::LDU1) {
            rs[0U] = lc.getLCO();                                               // LCO

            // split ulong64_t (8 byte) value into bytes
            rs[1U] = (uint8_t)((lc.getRS() >> 56) & 0xFFU);
            rs[2U] = (uint8_t)((lc.getRS() >> 48) & 0xFFU);
            rs[3U] = (uint8_t)((lc.getRS() >> 40) & 0xFFU);
            rs[4U] = (uint8_t)((lc.getRS() >> 32) & 0xFFU);
            rs[5U] = (uint8_t)((lc.getRS() >> 24) & 0xFFU);
            rs[6U] = (uint8_t)((lc.getRS() >> 16) & 0xFFU);
            rs[7U] = (uint8_t)((lc.getRS() >> 8) & 0xFFU);
            rs[8U] = (uint8_t)((lc.getRS() >> 0) & 0xFFU);

            // encode RS (24,12,13) FEC
            m_rs.encode241213(rs);
        } else {
            // generate MI data
            uint8_t mi[MI_LENGTH_BYTES];
            ::memset(mi, 0x00U, MI_LENGTH_BYTES);
            lc.getMI(mi);

            for (uint32_t i = 0; i < MI_LENGTH_BYTES; i++)
                rs[i] = mi[i];                                                      // Message Indicator

            rs[9U] = lc.getAlgId();                                                 // Algorithm ID
            rs[10U] = (lc.getKId() >> 8) & 0xFFU;                                   // Key ID
            rs[11U] = (lc.getKId() >> 0) & 0xFFU;                                   // ...

            // encode RS (24,16,9) FEC
            m_rs.encode24169(rs);
        }

        for (int n = 0; n < 9; n++) {
            uint8_t* buffer = nullptr;
            uint16_t bufferSize = 0;
            FullRateVoice voice = FullRateVoice();
            voice.setBusy(DFSI_BUSY_BITS_BUSY);
            voice.setSuperframeCnt(m_superFrameCnt);

            switch (n) {
                case 0: // VOICE1/10
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE1 : DFSIFrameType::LDU2_VOICE10);
                    ::memcpy(voice.imbeData, ldu + 10U, RAW_IMBE_LENGTH_BYTES);
                }
                break;
                case 1: // VOICE2/11
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE2 : DFSIFrameType::LDU2_VOICE11);
                    ::memcpy(voice.imbeData, ldu + 26U, RAW_IMBE_LENGTH_BYTES);
                }
                break;
                case 2: // VOICE3/12
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE3 : DFSIFrameType::LDU2_VOICE12);
                    ::memcpy(voice.imbeData, ldu + 55U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    // copy additional data
                    voice.additionalData[0U] = rs[0U];
                    voice.additionalData[1U] = rs[1U];
                    voice.additionalData[2U] = rs[2U];
                }
                break;
                case 3: // VOICE4/13
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE4 : DFSIFrameType::LDU2_VOICE13);
                    ::memcpy(voice.imbeData, ldu + 80U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    // copy additional data
                    voice.additionalData[0U] = rs[3U];
                    voice.additionalData[1U] = rs[4U];
                    voice.additionalData[2U] = rs[5U];
                }
                break;
                case 4: // VOICE5/14
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE5 : DFSIFrameType::LDU2_VOICE14);
                    ::memcpy(voice.imbeData, ldu + 105U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[6U];
                    voice.additionalData[1U] = rs[7U];
                    voice.additionalData[2U] = rs[8U];
                }
                break;
                case 5: // VOICE6/15
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE6 : DFSIFrameType::LDU2_VOICE15);
                    ::memcpy(voice.imbeData, ldu + 130U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[9U];
                    voice.additionalData[1U] = rs[10U];
                    voice.additionalData[2U] = rs[11U];
                }
                break;
                case 6: // VOICE7/16
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE7 : DFSIFrameType::LDU2_VOICE16);
                    ::memcpy(voice.imbeData, ldu + 155U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[12U];
                    voice.additionalData[1U] = rs[13U];
                    voice.additionalData[2U] = rs[14U];
                }
                break;
                case 7: // VOICE8/17
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE8 : DFSIFrameType::LDU2_VOICE17);
                    ::memcpy(voice.imbeData, ldu + 180U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = rs[15U];
                    voice.additionalData[1U] = rs[16U];
                    voice.additionalData[2U] = rs[17U];
                }
                break;
                case 8: // VOICE9/18
                {
                    voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE9 : DFSIFrameType::LDU2_VOICE18);
                    ::memcpy(voice.imbeData, ldu + 204U, RAW_IMBE_LENGTH_BYTES);

                    voice.additionalData = new uint8_t[voice.ADDITIONAL_LENGTH];
                    ::memset(voice.additionalData, 0x00U, voice.ADDITIONAL_LENGTH);

                    voice.additionalData[0U] = lsd.getLSD1();
                    voice.additionalData[1U] = lsd.getLSD2();
                }
                break;
            }

            buffer = new uint8_t[P25_PDU_FRAME_LENGTH_BYTES];
            ::memset(buffer, 0x00U, P25_PDU_FRAME_LENGTH_BYTES);

            // generate control octet
            ControlOctet ctrl = ControlOctet();
            ctrl.setBlockHeaderCnt(2U);
            ctrl.encode(buffer);
            bufferSize += ControlOctet::LENGTH;

            // generate block header
            BlockHeader hdr = BlockHeader();
            hdr.setBlockType(BlockType::FULL_RATE_VOICE);
            hdr.encode(buffer + 1U);
            bufferSize += BlockHeader::LENGTH;
            hdr.setBlockType(BlockType::START_OF_STREAM);
            hdr.encode(buffer + 2U);
            bufferSize += BlockHeader::LENGTH;

            // encode voice frame
            voice.encode(buffer + bufferSize);
            bufferSize += voice.getLength(); // 18, 17 or 14 depending on voice frame type

            // generate start of stream and encode
            StartOfStream start = StartOfStream();
            start.setNID(generateNID(duid));
            start.encode(buffer + bufferSize);
            bufferSize += StartOfStream::LENGTH;

            if (buffer != nullptr) {
                if (m_trace) {
                    Utils::dump("ModemV24::convertFromAirTIA(), Encoded V.24 Voice Frame Data", buffer, bufferSize);
                }

                queueP25Frame(buffer, bufferSize, STT_DATA);
                delete[] buffer;
            }
        }

        // bryanb: this is a naive way of incrementing the superframe counter, we basically just increment it after
        // processing and LDU2
        if (duid == DUID::LDU2) {
            if (m_superFrameCnt == 3U)
                m_superFrameCnt = 0U;
            else
                m_superFrameCnt++;
        }
    }
}
