DMR-данные (SMS) на TYT MD-UV380 под OpenGD77

Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки.

Что внутри:
- четыре новых модуля прошивки (приём, сборка исходящего, меню Messages);
- правки существующих файлов в виде скриптов, с порядком применения;
- инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета;
- NOTES.md: что проверено в эфире, что не доделано, где грабли.

Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота,
рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном
сервера побайтово, но в эфире не проверялась.

Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
2026-09-11 02:39:24 +09:00
Коммит b5a8e92e45
32 изменённых файлов: 3812 добавлений и 0 удалений
+255
Просмотреть файл
@@ -0,0 +1,255 @@
/*
* 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 <string.h>
#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;
}
+265
Просмотреть файл
@@ -0,0 +1,265 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Building an outgoing DMR text message: TMS payload, IP/UDP wrapper, message CRC,
* data header and the rate 1/2 blocks that carry it.
*
* This is a literal translation of a reference implementation that was verified on a PC
* against a known-good packet produced by the server side, byte for byte (including both
* IP/UDP checksums and the message CRC-32).
*
* 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 <string.h>
#include "functions/sms.h"
#define TMS_HEADER_OVERHEAD 8U // flag + 0x00 + seq + the five marker bytes
#define IP_HEADER_LENGTH 20U
#define UDP_HEADER_LENGTH 8U
#define TMS_PORT 0x0FA7U
static const uint8_t TMS_MARKER[5] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 };
/*
* ETSI / MOTOTRBO message CRC-32: bytes are swapped within each 16 bit pair, then an
* MSB-first CRC32 (poly 0x04C11DB7, init 0, no final xor) is run over the result.
*/
static uint32_t smsMessageCRC32(const uint8_t *data, uint16_t length)
{
uint32_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
// consume the bytes in swapped-pair order without copying the buffer
uint16_t idx = ((i & 1U) ? (i - 1U) : (i + 1U));
uint8_t b = ((idx < length) ? data[idx] : 0x00);
crc ^= ((uint32_t)b << 24);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x80000000U) ? ((crc << 1) ^ 0x04C11DB7U) : (crc << 1));
}
}
return crc;
}
static uint16_t onesComplementSum(const uint8_t *data, uint16_t length, uint32_t initial)
{
uint32_t sum = initial;
for (uint16_t i = 0; (i + 1U) < length; i += 2U)
{
sum += (uint32_t)((data[i] << 8) | data[i + 1U]);
}
if (length & 1U)
{
sum += (uint32_t)(data[length - 1U] << 8);
}
while (sum >> 16)
{
sum = (sum & 0xFFFFU) + (sum >> 16);
}
return (uint16_t)(~sum);
}
static uint16_t smsCRC16CCITT(const uint8_t *data, uint16_t length)
{
uint16_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
crc ^= ((uint16_t)data[i] << 8);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x8000U) ? ((crc << 1) ^ 0x1021U) : (crc << 1));
}
}
return crc;
}
uint16_t smsBuildPacket(uint8_t *out, uint16_t outSize, uint32_t srcId, uint32_t dstId,
bool isGroup, const char *text, uint8_t flag, uint8_t seq, uint8_t *padOut)
{
uint16_t textLen = (uint16_t)strlen(text);
uint16_t tmsLen = (uint16_t)(TMS_HEADER_OVERHEAD + (2U * textLen));
uint16_t udpLen = (uint16_t)(UDP_HEADER_LENGTH + 2U + tmsLen);// 2 = the TMS length field
uint16_t ipLen = (uint16_t)(IP_HEADER_LENGTH + udpLen);
uint32_t srcIp = (0x0CU << 24) | (srcId & 0x00FFFFFFU);
uint32_t dstIp = (((uint32_t)(isGroup ? 0xE1U : 0x0CU)) << 24) | (dstId & 0x00FFFFFFU);
uint16_t pos = 0;
if ((uint16_t)(ipLen + 12U + 4U) > outSize)
{
return 0;// would not fit
}
// ---- IP header (checksum filled in afterwards) ----
out[pos++] = 0x45;
out[pos++] = 0x00;
out[pos++] = (uint8_t)(ipLen >> 8);
out[pos++] = (uint8_t)(ipLen & 0xFF);
out[pos++] = 0x00;
out[pos++] = 0x01; // identification
out[pos++] = 0x00;
out[pos++] = 0x00;
out[pos++] = (isGroup ? 0x01 : 0x40); // TTL
out[pos++] = 0x11; // UDP
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(srcIp >> 24);
out[pos++] = (uint8_t)(srcIp >> 16);
out[pos++] = (uint8_t)(srcIp >> 8);
out[pos++] = (uint8_t)(srcIp);
out[pos++] = (uint8_t)(dstIp >> 24);
out[pos++] = (uint8_t)(dstIp >> 16);
out[pos++] = (uint8_t)(dstIp >> 8);
out[pos++] = (uint8_t)(dstIp);
uint16_t ipChecksum = onesComplementSum(out, IP_HEADER_LENGTH, 0);
out[10] = (uint8_t)(ipChecksum >> 8);
out[11] = (uint8_t)(ipChecksum & 0xFF);
// ---- UDP header + payload ----
uint16_t udpStart = pos;
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(udpLen >> 8);
out[pos++] = (uint8_t)(udpLen & 0xFF);
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(tmsLen >> 8);
out[pos++] = (uint8_t)(tmsLen & 0xFF);
out[pos++] = flag;
out[pos++] = 0x00;
out[pos++] = seq;
memcpy(&out[pos], TMS_MARKER, sizeof(TMS_MARKER));
pos += sizeof(TMS_MARKER);
for (uint16_t i = 0; i < textLen; i++)
{
out[pos++] = (uint8_t)text[i]; // UTF-16 little endian, ASCII only for now
out[pos++] = 0x00;
}
// UDP checksum covers a pseudo header plus the whole UDP segment
{
uint8_t pseudo[12];
pseudo[0] = (uint8_t)(srcIp >> 24); pseudo[1] = (uint8_t)(srcIp >> 16);
pseudo[2] = (uint8_t)(srcIp >> 8); pseudo[3] = (uint8_t)(srcIp);
pseudo[4] = (uint8_t)(dstIp >> 24); pseudo[5] = (uint8_t)(dstIp >> 16);
pseudo[6] = (uint8_t)(dstIp >> 8); pseudo[7] = (uint8_t)(dstIp);
pseudo[8] = 0x00; pseudo[9] = 0x11;
pseudo[10] = (uint8_t)(udpLen >> 8); pseudo[11] = (uint8_t)(udpLen & 0xFF);
uint32_t sum = 0;
for (uint8_t i = 0; i < sizeof(pseudo); i += 2U)
{
sum += (uint32_t)((pseudo[i] << 8) | pseudo[i + 1U]);
}
uint16_t udpChecksum = onesComplementSum(&out[udpStart], udpLen, sum);
out[udpStart + 6U] = (uint8_t)(udpChecksum >> 8);
out[udpStart + 7U] = (uint8_t)(udpChecksum & 0xFF);
}
// ---- pad to a multiple of 12, then append the message CRC as its own tail ----
uint8_t pad = (uint8_t)((12U - (pos % 12U)) % 12U);
for (uint8_t i = 0; i < pad; i++)
{
out[pos++] = 0x00;
}
uint32_t crc = smsMessageCRC32(out, pos);
out[pos++] = (uint8_t)(crc & 0xFF); // little endian
out[pos++] = (uint8_t)(crc >> 8);
out[pos++] = (uint8_t)(crc >> 16);
out[pos++] = (uint8_t)(crc >> 24);
if (padOut != NULL)
{
*padOut = pad;
}
return pos;
}
void smsBuildDataHeader(uint8_t *header, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t blocks, uint8_t pad)
{
memset(header, 0, 12);
header[0] = (uint8_t)((isGroup ? 0x80U : 0x00U) | 0x02U);// DPF = 2, unconfirmed
header[1] = (uint8_t)(0x40U | (pad & 0x0FU)); // SAP = 4 (IP based data)
header[2] = (uint8_t)(dstId >> 16);
header[3] = (uint8_t)(dstId >> 8);
header[4] = (uint8_t)(dstId);
header[5] = (uint8_t)(srcId >> 16);
header[6] = (uint8_t)(srcId >> 8);
header[7] = (uint8_t)(srcId);
header[8] = (uint8_t)(0x80U | (blocks & 0x7FU));
header[9] = 0x10;
uint16_t crc = (uint16_t)(smsCRC16CCITT(header, 10) ^ 0xCCCCU);
header[10] = (uint8_t)(crc >> 8);
header[11] = (uint8_t)(crc & 0xFF);
}
void smsBuildPreamble(uint8_t *preamble, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t countdown)
{
memset(preamble, 0, 12);
preamble[0] = 0xBD; // Preamble CSBK, LB set
preamble[1] = 0x00;
preamble[2] = (uint8_t)(isGroup ? 0x80U : 0x00U); // data content preamble
preamble[3] = countdown;
preamble[4] = (uint8_t)(dstId >> 16);
preamble[5] = (uint8_t)(dstId >> 8);
preamble[6] = (uint8_t)(dstId);
preamble[7] = (uint8_t)(srcId >> 16);
preamble[8] = (uint8_t)(srcId >> 8);
preamble[9] = (uint8_t)(srcId);
uint16_t crc = (uint16_t)(smsCRC16CCITT(preamble, 10) ^ 0xA5A5U);
preamble[10] = (uint8_t)(crc >> 8);
preamble[11] = (uint8_t)(crc & 0xFF);
}