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