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 удалений
+194
Просмотреть файл
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Transmit an SMS from the radio itself.
The burst mechanism is the one already proven in hotspot mode: the 12 byte payload goes
to register page 0x02 and reg 0x50 selects the burst type, and the HR-C6000 builds the
FEC and sync around it. What is new here is driving it from normal radio operation,
where the state machine is otherwise built around voice.
A transmission is queued as a list of bursts (preambles, data header, then the rate 1/2
blocks). The timeslot interrupt pulls one burst per active slot until the queue empties,
then ends the transmission.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --------------------------------------------------------------- sms.h
p = W + "/include/functions/sms.h"
s = open(p).read()
if "smsSendText" in s:
print("sms.h: already declared")
else:
s = s.replace("uint8_t smsGetBlockCount(uint16_t packetLength);", """// Queue a text message for transmission. Returns false if a transmission is already
// in progress or the message does not fit.
bool smsSendText(uint32_t dstId, bool isGroup, const char *text);
// True while bursts are still waiting to go out.
bool smsTxIsActive(void);
// Called from the timeslot interrupt: fetch the next burst to send.
// Returns false when the transmission is finished.
bool smsTxGetNextBurst(uint8_t *payload, uint8_t *dataType);
uint8_t smsGetBlockCount(uint16_t packetLength);""", 1)
open(p, "w").write(s)
print("sms.h: transmit API declared")
# --------------------------------------------------------------- sms.c
p = W + "/source/functions/sms.c"
s = open(p).read()
if "smsSendText" in s:
print("sms.c: already has transmit")
else:
s = s.replace('#include "functions/ticks.h"\n',
'#include "functions/ticks.h"\n#include "functions/trx.h"\n#include "functions/settings.h"\n', 1)
s += """
// ---------------------------------------------------------------- transmit
#define SMS_TX_MAX_BURSTS 24U // 4 preambles + header + up to 19 blocks
#define SMS_PREAMBLE_COUNT 4U
static uint8_t txBursts[SMS_TX_MAX_BURSTS][SMS_BLOCK_LENGTH];
static uint8_t txTypes[SMS_TX_MAX_BURSTS];
static uint8_t txCount = 0;
static uint8_t txIndex = 0;
static uint8_t txSeq = 0x81;
bool smsSendText(uint32_t dstId, bool isGroup, const char *text)
{
uint8_t packet[(SMS_MAX_TEXT_LENGTH * 2) + 64];
uint8_t pad = 0;
uint16_t packetLength;
uint8_t blocks;
uint8_t n = 0;
if (txCount != 0)
{
return false;// a transmission is already queued
}
packetLength = smsBuildPacket(packet, sizeof(packet), trxDMRID, dstId, isGroup, text,
0xA0, txSeq, &pad);// 0xA0: we are not asking for a delivery report yet
if (packetLength == 0)
{
return false;
}
blocks = smsGetBlockCount(packetLength);
if ((uint8_t)(blocks + SMS_PREAMBLE_COUNT + 1U) > SMS_TX_MAX_BURSTS)
{
return false;// message too long for the queue
}
// Preambles first, counting down towards the header, as real radios send them.
for (uint8_t i = 0; i < SMS_PREAMBLE_COUNT; i++)
{
smsBuildPreamble(txBursts[n], trxDMRID, dstId, isGroup, (uint8_t)(SMS_PREAMBLE_COUNT - i));
txTypes[n] = DT_CSBK;
n++;
}
smsBuildDataHeader(txBursts[n], trxDMRID, dstId, isGroup, blocks, pad);
txTypes[n] = DT_DATA_HEADER;
n++;
for (uint8_t i = 0; i < blocks; i++)
{
smsGetBlock(packet, packetLength, i, txBursts[n]);
txTypes[n] = DT_RATE_12_DATA;
n++;
}
txIndex = 0;
txCount = n;
txSeq = (uint8_t)(0x80U | (((txSeq + 1U) & 0x7FU) ? ((txSeq + 1U) & 0x7FU) : 1U));
smsOutboxAdd(dstId, (isGroup == false), text);
// Key the transmitter; the timeslot interrupt will drain the queue.
trxTransmissionEnabled = true;
trxEnableTransmission();
return true;
}
bool smsTxIsActive(void)
{
return (txCount != 0);
}
bool smsTxGetNextBurst(uint8_t *payload, uint8_t *dataType)
{
if ((txCount == 0) || (txIndex >= txCount))
{
txCount = 0;
txIndex = 0;
return false;
}
memcpy(payload, txBursts[txIndex], SMS_BLOCK_LENGTH);
*dataType = txTypes[txIndex];
txIndex++;
return true;
}
"""
open(p, "w").write(s)
print("sms.c: transmit queue added")
# --------------------------------------------------------------- HR-C6000.c
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "smsTxGetNextBurst" in s:
print("HR-C6000.c: already patched")
else:
# In the ongoing-TX slot, send our data bursts instead of voice when a message is queued.
anchor = """ case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
if (hrc.transmissionEnabled)
{
"""
assert anchor in s, "TX_2 anchor not found"
s = s.replace(anchor, """ case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
if (hrc.transmissionEnabled)
{
// A text message queued by the radio itself takes over the slot: its
// payload goes to the LC page and reg 0x50 picks the burst type, exactly
// as in hotspot mode, while the vocoder path stays idle.
if ((settingsUsbMode != USB_MODE_HOTSPOT) && smsTxIsActive())
{
uint8_t smsPayload[LC_DATA_LENGTH];
uint8_t smsType;
if (smsTxGetNextBurst(smsPayload, &smsType))
{
SPI0WritePageRegByteArray(0x02, 0x00, smsPayload, LC_DATA_LENGTH);
SPI1WritePageRegByteArray(0x03, 0x00, SILENCE_AUDIO, AMBE_AUDIO_LENGTH);
SPI0WritePageRegByte(0x04, 0x41, 0x80);
SPI0WritePageRegByte(0x04, 0x50, (uint8_t)(smsType << 4));
}
else
{
// Queue drained: stop keying and let the normal shutdown run.
hrc.transmissionEnabled = false;
trxTransmissionEnabled = false;
}
slotState = DMR_STATE_TX_1;
break;
}
""", 1)
# And do not announce a voice call in front of the data when we are the sender.
s = s.replace("static inline bool isHotspotDataTx(void)\n{\n\treturn ((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE));\n}",
"static inline bool isHotspotDataTx(void)\n{\n"
"\t// Also true while the radio itself is sending a text message, so that the\n"
"\t// Voice LC Header is suppressed in front of the data bursts.\n"
"\treturn (((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE))\n"
"\t\t\t|| smsTxIsActive());\n}", 1)
open(p, "w").write(s)
print("HR-C6000.c: radio-originated data transmission wired in")