Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
195 строки
6.3 KiB
Python
195 строки
6.3 KiB
Python
#!/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")
|