/* * Copyright (C) 2026 OpenGD77 contributors * * Assembling received DMR packet data (SMS) while the radio is used as a radio. * * Wire format (confirmed against a working reference implementation): * * Data header (12 bytes) * [0] bit7 G/I (0 = private), bit6 A (response requested), bits3..0 DPF * DPF 2 = unconfirmed rate 1/2, DPF 3 = confirmed rate 1/2 * [1] bits7..4 SAP (4 = IP/UDP data), bits3..0 number of pad octets * [2..4] destination id, big endian * [5..7] source id, big endian * [8] bit7 full message flag, bits6..0 blocks to follow * [9] header class/type * [10..11] header CRC-16 * * Blocks (12 bytes each) * confirmed: [0..1] DBSN + CRC-9, [2..11] ten payload bytes * unconfirmed: all twelve bytes are payload * * Reassembled payload: IPv4 (20) + UDP (8) + TMS. The text sits right behind the * TMS marker 04 0D 00 0A 00 and is UTF-16 LITTLE endian, terminated by 0x0000. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation, * incorporation in a commercial product or incorporation into a product or project which allows commercial use. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include "functions/sms.h" #include "functions/hotspot.h" // DT_* data type values #include "functions/ticks.h" #define SMS_BLOCK_LENGTH 12U #define SMS_DPF_UNCONFIRMED 2U #define SMS_DPF_CONFIRMED 3U // Bursts arrive every 60 ms; this is generously above any realistic gap between them. #define SMS_ASSEMBLY_TIMEOUT_MS 3000U // Marks the start of the text inside the TMS payload. static const uint8_t TMS_TEXT_MARKER[] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 }; typedef enum { SMS_RX_IDLE = 0, SMS_RX_COLLECTING } smsRxState_t; // Filled from the interrupt, drained by smsTick(). static volatile uint8_t rxQueue[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH]; static volatile uint8_t rxQueueType[SMS_MAX_BLOCKS]; static volatile uint8_t rxQueueWrite = 0; static volatile uint8_t rxQueueRead = 0; static smsRxState_t rxState = SMS_RX_IDLE; static uint8_t header[SMS_BLOCK_LENGTH]; static uint8_t blocks[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH]; static uint8_t blocksExpected = 0; static uint8_t blocksCollected = 0; static uint32_t assemblyStartedAt = 0; static smsMessage_t latest; static bool haveMessage = false; static bool unread = false; void smsInit(void) { rxQueueWrite = rxQueueRead = 0; rxState = SMS_RX_IDLE; blocksExpected = blocksCollected = 0; haveMessage = false; unread = false; memset(&latest, 0, sizeof(latest)); } void smsRxHandleBurst(const uint8_t *payload, uint8_t dataType) { uint8_t next = (uint8_t)((rxQueueWrite + 1) % SMS_MAX_BLOCKS); if (next == rxQueueRead) { return;// queue full: drop, the partial message will time out } memcpy((uint8_t *)rxQueue[rxQueueWrite], payload, SMS_BLOCK_LENGTH); rxQueueType[rxQueueWrite] = dataType; rxQueueWrite = next; } static void abortAssembly(void) { rxState = SMS_RX_IDLE; blocksExpected = blocksCollected = 0; } static void startAssembly(const uint8_t *hdr) { uint8_t dpf = (uint8_t)(hdr[0] & 0x0FU); if ((dpf != SMS_DPF_CONFIRMED) && (dpf != SMS_DPF_UNCONFIRMED)) { abortAssembly();// not a packet data message we can assemble return; } memcpy(header, hdr, SMS_BLOCK_LENGTH); blocksExpected = (uint8_t)(hdr[8] & 0x7FU); blocksCollected = 0; assemblyStartedAt = ticksGetMillis(); if ((blocksExpected == 0) || (blocksExpected > SMS_MAX_BLOCKS)) { abortAssembly(); return; } rxState = SMS_RX_COLLECTING; } static void finishAssembly(void) { uint8_t data[SMS_MAX_BLOCKS * SMS_BLOCK_LENGTH]; uint16_t length = 0; uint8_t dpf = (uint8_t)(header[0] & 0x0FU); // Confirmed blocks carry two bytes of DBSN + CRC-9 before the payload. uint8_t skip = (dpf == SMS_DPF_CONFIRMED) ? 2U : 0U; int textStart = -1; for (uint8_t i = 0; i < blocksCollected; i++) { memcpy(&data[length], &blocks[i][skip], (SMS_BLOCK_LENGTH - skip)); length = (uint16_t)(length + (SMS_BLOCK_LENGTH - skip)); } // Locate the text rather than counting header bytes: the IP/UDP wrapper varies, // the marker does not. Take the last match, so a marker-like byte pattern earlier // in the wrapper cannot fool us. for (int i = 0; (i + (int)sizeof(TMS_TEXT_MARKER)) <= (int)length; i++) { if (memcmp(&data[i], TMS_TEXT_MARKER, sizeof(TMS_TEXT_MARKER)) == 0) { textStart = i + (int)sizeof(TMS_TEXT_MARKER); } } if (textStart < 0) { abortAssembly();// not a text message (could be GPS or another TMS payload) return; } uint16_t t = 0; for (int i = textStart; ((i + 1) < (int)length) && (t < SMS_MAX_TEXT_LENGTH); i += 2) { uint16_t ch = (uint16_t)(data[i] | (data[i + 1] << 8));// UTF-16 little endian if (ch == 0) { break; } // Latin only for now: anything outside ASCII needs the Cyrillic font work. latest.text[t++] = (ch < 0x80U) ? (char)ch : '?'; } latest.text[t] = '\0'; latest.dstId = (uint32_t)((header[2] << 16) | (header[3] << 8) | header[4]); latest.srcId = (uint32_t)((header[5] << 16) | (header[6] << 8) | header[7]); latest.isPrivate = ((header[0] & 0x80U) == 0); latest.unread = true; haveMessage = (t > 0); unread = haveMessage; abortAssembly(); } void smsTick(void) { while (rxQueueRead != rxQueueWrite) { uint8_t burst[SMS_BLOCK_LENGTH]; uint8_t type = rxQueueType[rxQueueRead]; memcpy(burst, (uint8_t *)rxQueue[rxQueueRead], SMS_BLOCK_LENGTH); rxQueueRead = (uint8_t)((rxQueueRead + 1) % SMS_MAX_BLOCKS); switch (type) { case DT_DATA_HEADER: startAssembly(burst); break; case DT_RATE_12_DATA: if (rxState == SMS_RX_COLLECTING) { if (blocksCollected < SMS_MAX_BLOCKS) { memcpy(blocks[blocksCollected], burst, SMS_BLOCK_LENGTH); blocksCollected++; } if (blocksCollected >= blocksExpected) { finishAssembly(); } } break; default: break;// CSBK preambles and everything else } } if ((rxState == SMS_RX_COLLECTING) && ((ticksGetMillis() - assemblyStartedAt) > SMS_ASSEMBLY_TIMEOUT_MS)) { abortAssembly();// sender stopped mid message, or bursts were lost } } const smsMessage_t *smsGetLatest(void) { return (haveMessage ? &latest : NULL); } bool smsHasUnread(void) { return unread; } void smsMarkRead(void) { unread = false; latest.unread = false; }