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;
}