Files
viktor b5a8e92e45 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

266 строки
8.7 KiB
C

/*
* 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);
}