DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
@@ -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;
|
||||
}
|
||||
Ссылка в новой задаче
Block a user