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 удалений
+493
Просмотреть файл
@@ -0,0 +1,493 @@
# Правки, сделанные командами (не отдельными файлами)
Извлечено из транскрипта сессии в порядке применения.
Каждый блок - самостоятельная команда, менявшая исходники прошивки.
## Правка 1 (строка транскрипта 966)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/include/functions/hotspot.h"
s = open(p).read()
anchor = "#define DMR_SYNC_DATA 0x40U\n"
if anchor not in s:
raise SystemExit("anchor not found in hotspot.h")
if "DT_RATE_12_DATA" in s:
raise SystemExit("already patched")
add = anchor + """
// DMR data burst types (ETSI TS 102 361-1, Slot Type "data type" field).
// The hotspot only ever handled the two voice-related ones; the packet data
// types below are needed to carry SMS/position data over the air.
#define DT_VOICE_PI_HEADER 0x00U
#define DT_CSBK 0x03U
#define DT_DATA_HEADER 0x06U
#define DT_RATE_12_DATA 0x07U
#define DT_RATE_34_DATA 0x08U
#define DT_IDLE 0x09U
#define DT_RATE_1_DATA 0x0AU
#define DT_MASK 0x0FU
// Marker used in hotspotTxDataType[] for "this buffered frame is voice, not data".
#define HOTSPOT_TX_TYPE_VOICE 0xFFU
"""
s = s.replace(anchor, add, 1)
# expose the per-buffer data-type markers to the HR-C6000 driver
tail = "extern bool hotspotMmdvmHostIsConnected;\n"
if tail not in s:
raise SystemExit("tail anchor not found")
s = s.replace(tail, tail + """
// For each frame queued in audioAndHotspotDataBuffer.hotspotBuffer[], this holds either
// HOTSPOT_TX_TYPE_VOICE or the DMR data type (DT_*) that the frame has to be sent as.
extern volatile uint8_t hotspotTxDataType[];
""", 1)
open(p, "w").write(s)
print("hotspot.h patched")
PYEOF' 2>&1 | tail -5
```
## Правка 2 (строка транскрипта 978)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataType" in s:
raise SystemExit("already patched")
# ---- 1. define the marker array next to the other hotspot state ----
anchor = "static uint8_t hotspotTxLC[9];\n"
assert anchor in s, "hotspotTxLC anchor missing"
s = s.replace(anchor, anchor + """
// Per-buffered-frame DMR data type. HOTSPOT_TX_TYPE_VOICE means \"ordinary voice frame\",
// anything else is a packet data burst (SMS etc) that must bypass the vocoder path.
volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];
""", 1)
# ---- 2. voice frames queued by storeNetFrame() must be marked as voice ----
old = """ memcpy((uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx], hotspotTxLC, 9);// copy the current LC into the data (mainly for use with the embedded data);
wavbuffer_count++;"""
assert old in s, "storeNetFrame body not found"
s = s.replace(old, """ memcpy((uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx], hotspotTxLC, 9);// copy the current LC into the data (mainly for use with the embedded data);
hotspotTxDataType[wavbuffer_write_idx] = HOTSPOT_TX_TYPE_VOICE;
wavbuffer_count++;""", 1)
# ---- 3. new function: queue a packet data burst coming from the host ----
anchor2 = "static uint8_t hotspotModeReceiveNetFrame(const uint8_t *comBuffer, uint8_t timeSlot)\n"
assert anchor2 in s, "hotspotModeReceiveNetFrame not found"
s = s.replace(anchor2, """// Queue a packet data burst (CSBK / data header / rate 1/2 payload) received from the
// MMDVM host, so that the HR-C6000 sends it as a real data burst rather than as audio.
//
// The host hands us a complete 33 byte DMR burst (BPTC(196,96) coded). The HR-C6000
// applies the FEC itself and only wants the 96 bit (12 byte) payload, which is exactly
// what BPTCdecode() gives us - the same route the radio already uses for LC headers.
static uint8_t storeNetDataFrame(volatile const uint8_t *comBuffer, uint8_t dataType)
{
if (wavbuffer_count >= HOTSPOT_BUFFER_COUNT)
{
return 5;// out of space: MMDVMHost treats a non-zero return as a NAK
}
BPTCglobalsInit();
taskENTER_CRITICAL();
BPTCdecode((uint8_t *)comBuffer + MMDVM_HEADER_LENGTH,
(uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx]);
hotspotTxDataType[wavbuffer_write_idx] = dataType;
wavbuffer_count++;
wavbuffer_write_idx = ((wavbuffer_write_idx + 1) % HOTSPOT_BUFFER_COUNT);
taskEXIT_CRITICAL();
// Data bursts are short and self-contained, so start transmitting straight away
// instead of waiting for the audio pre-buffering to fill up.
if ((hotspotState != HOTSPOT_STATE_TX_START_BUFFERING) && (hotspotState != HOTSPOT_STATE_TRANSMITTING))
{
timeoutCounter = TX_BUFFERING_TIMEOUT;
hotspotState = HOTSPOT_STATE_TX_START_BUFFERING;
}
return 0;
}
""" + anchor2, 1)
# ---- 4. dispatch data bursts before the voice-only decode ----
old2 = """ lc.srcId = 0;// zero these values as they are checked later in the function, but only updated if the data type is DT_VOICE_LC_HEADER
lc.dstId = 0;
"""
assert old2 in s, "dispatch anchor not found"
s = s.replace(old2, """ // MMDVM control byte: bit 0x40 marks a data sync burst, low nibble is the DMR data type.
// Voice LC header / terminator keep their existing handling below; the packet data types
// have no path at all in stock firmware, which is why SMS never reached the air.
if ((comBuffer[3] & DMR_SYNC_DATA) == DMR_SYNC_DATA)
{
uint8_t dataType = (comBuffer[3] & DT_MASK);
switch (dataType)
{
case DT_CSBK:
case DT_DATA_HEADER:
case DT_RATE_12_DATA:
return storeNetDataFrame(comBuffer, dataType);
default:
break;// fall through to the voice handling
}
}
""" + old2, 1)
open(p, "w").write(s)
print("hotspot.c patched")
PYEOF' 2>&1 | tail -5
```
## Правка 3 (строка транскрипта 1008)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "txDataType" in s:
raise SystemExit("already patched")
# ---- 1. state field: which DMR type the frame currently in deferredUpdateBuffer must be sent as
a = "\tvolatile bool hotspotDMRTxFrameBufferEmpty;\n"
assert a in s
s = s.replace(a, a + "\tvolatile uint8_t txDataType;// HOTSPOT_TX_TYPE_VOICE, or a DT_* packet data type\n", 1)
a = "\t\t.hotspotDMRTxFrameBufferEmpty = true,\n"
assert a in s
s = s.replace(a, a + "\t\t.txDataType = HOTSPOT_TX_TYPE_VOICE,\n", 1)
# ---- 2. remember the type whenever a frame is pulled out of the hotspot buffer
a = """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
// Note:"""
assert a in s, "first fetch site missing"
s = s.replace(a, """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
hrc.txDataType = hotspotTxDataType[wavbuffer_read_idx];
// Note:""", 1)
a = """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
wavbuffer_read_idx = ((wavbuffer_read_idx + 1) % HOTSPOT_BUFFER_COUNT);"""
assert a in s, "second fetch site missing"
s = s.replace(a, """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
hrc.txDataType = hotspotTxDataType[wavbuffer_read_idx];
wavbuffer_read_idx = ((wavbuffer_read_idx + 1) % HOTSPOT_BUFFER_COUNT);""", 1)
# ---- 3. TX_2: send a packet data burst instead of pushing the payload through the vocoder
a = """ if (settingsUsbMode == USB_MODE_HOTSPOT)
{
if (hrc.txSequence == 0)
{
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH); // put LC into hardware
}
"""
assert a in s, "TX_2 hotspot branch missing"
s = s.replace(a, """ if (settingsUsbMode == USB_MODE_HOTSPOT)
{
if (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)
{
// Packet data burst (SMS etc). The 96 bit payload goes into the same
// register page the radio already uses for LC data, and reg 0x50 below
// tells the HR-C6000 which kind of data burst to build around it.
// The vocoder path is left idle - this is what stock firmware lacked.
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH);
SPI1WritePageRegByteArray(0x03, 0x00, SILENCE_AUDIO, AMBE_AUDIO_LENGTH);
hrc.hotspotDMRTxFrameBufferEmpty = true;
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, (hrc.txDataType << 4)); // Data Type = DT_*, Data sync, LCSS = 0
hrc.txDataType = HOTSPOT_TX_TYPE_VOICE; // consumed
slotState = DMR_STATE_TX_1;
break;
}
if (hrc.txSequence == 0)
{
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH); // put LC into hardware
}
""", 1)
open(p, "w").write(s)
print("HR-C6000.c patched")
PYEOF' 2>&1 | tail -5
```
## Правка 4 (строка транскрипта 1052)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataPending" in s:
raise SystemExit("already patched")
# flag: there is at least one packet data burst waiting to go out
a = "volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];\n"
assert a in s
s = s.replace(a, a + "static volatile bool hotspotTxDataPending = false;// a packet data burst is queued\n", 1)
# set it when queuing a data burst
a = "\t\thotspotTxDataType[wavbuffer_write_idx] = dataType;\n"
assert a in s
s = s.replace(a, a + "\t\thotspotTxDataPending = true;\n", 1)
# Data bursts are only a few frames long, so the audio pre-buffering threshold
# (5 frames) would never be reached and the transmission would time out instead.
a = "\t\t\t\tif (wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION)\n"
assert a in s, "buffering condition not found"
s = s.replace(a, "\t\t\t\tif ((wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION) || hotspotTxDataPending)\n", 1)
# clear the flag once the buffer has drained
a = """ case HOTSPOT_STATE_TX_SHUTDOWN:
"""
assert a in s, "TX_SHUTDOWN case not found"
s = s.replace(a, """ case HOTSPOT_STATE_TX_SHUTDOWN:
if (wavbuffer_count == 0)
{
hotspotTxDataPending = false;
}
""", 1)
# and on (re)initialisation
a = """ case HOTSPOT_STATE_INITIALISE:
"""
assert a in s, "INITIALISE case not found"
s = s.replace(a, """ case HOTSPOT_STATE_INITIALISE:
hotspotTxDataPending = false;
""", 1)
open(p, "w").write(s)
print("patched: data bursts start transmitting immediately")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE " error |error:" | head -10
ls -l build_ogd77/OpenMDUV380.bin && arm-none-eabi-size build_ogd77/OpenMDUV380.elf | tail -1' 2>&1 | tail -8
```
## Правка 5 (строка транскрипта 1069)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataPending" in s:
raise SystemExit("already patched")
a = "volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];\n"
assert a in s, "array decl"
s = s.replace(a, a + "static volatile bool hotspotTxDataPending = false;// a packet data burst is queued\n", 1)
a = "\thotspotTxDataType[wavbuffer_write_idx] = dataType;\n"
assert a in s, "queue line"
s = s.replace(a, a + "\thotspotTxDataPending = true;\n", 1)
# Data bursts are only a few frames long, so the audio pre-buffering threshold
# would never be reached and the transmission would time out instead.
a = "\t\t\t\tif (wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION)\n"
assert a in s, "buffering condition"
s = s.replace(a, "\t\t\t\tif ((wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION) || hotspotTxDataPending)\n", 1)
a = "\t\tcase HOTSPOT_STATE_TX_SHUTDOWN:\n"
assert a in s, "TX_SHUTDOWN"
s = s.replace(a, a + "\t\t\tif (wavbuffer_count == 0)\n\t\t\t{\n\t\t\t\thotspotTxDataPending = false;\n\t\t\t}\n", 1)
a = "\t\tcase HOTSPOT_STATE_INITIALISE:\n"
assert a in s, "INITIALISE"
s = s.replace(a, a + "\t\t\thotspotTxDataPending = false;\n", 1)
open(p, "w").write(s)
print("patched OK")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE " error |error:" | head -10
echo "=== build result ==="
ls -l build_ogd77/OpenMDUV380.bin && arm-none-eabi-size build_ogd77/OpenMDUV380.elf | tail -1' 2>&1 | tail -8
```
## Правка 6 (строка транскрипта 1205)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- 1. new RX event type -------------------------------------------------
p = W + "/include/functions/hotspot.h"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" not in s:
a = "\tHOTSPOT_RX_STOP,\n"
assert a in s
s = s.replace(a, a + "\tHOTSPOT_RX_DATA_FRAME,// a packet data burst (SMS etc) was received off air\n", 1)
open(p,"w").write(s)
print("hotspot.h: RX event added")
# --- 2. bring the slot type encoder over from the MK22 branch -------------
src = open("/home/viktor/OpenGD77/firmware/source/hotspot/DMRSlotType.c").read()
start = src.index("static const uint16_t ENCODING_TABLE_2087[] =")
end = src.index(";", src.index("};", start))
table = src[start:src.index("};", start)+2]
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "ENCODING_TABLE_2087" in s:
raise SystemExit("hotspot.c already has the slot type encoder")
anchor = "static void BPTCencode(const uint8_t *inputData, uint8_t *outputData)\n{"
assert anchor in s, "BPTCencode definition not found"
block = """// Slot Type encoder, needed to build received data bursts for the host.
// Golay(20,8) table and logic taken from the project\\'s own DMRSlotType implementation.
""" + table + """
// Writes colour code + data type (with FEC) into the Slot Type half-bursts of a frame.
static void DMRSlotTypeEncode(uint32_t colorCode, uint32_t dataType, uint8_t *frame)
{
uint8_t slotType[3];
slotType[0] = (colorCode << 4) & 0xF0U;
slotType[0] |= (dataType << 0) & 0x0FU;
uint16_t cksum = ENCODING_TABLE_2087[slotType[0]];
slotType[1] = (cksum >> 0) & 0xFFU;
slotType[2] = (cksum >> 8) & 0xFFU;
frame[12] = (frame[12] & 0xC0U) | ((slotType[0] >> 2) & 0x3FU);
frame[13] = (frame[13] & 0x0FU) | ((slotType[0] << 6) & 0xC0U) | ((slotType[1] >> 2) & 0x30U);
frame[19] = (frame[19] & 0xF0U) | ((slotType[1] >> 2) & 0x0FU);
frame[20] = (frame[20] & 0x03U) | ((slotType[1] << 6) & 0xC0U) | ((slotType[2] >> 2) & 0x3CU);
}
""" + anchor
s = s.replace(anchor, block, 1)
open(p,"w").write(s)
print("hotspot.c: slot type encoder added (%d bytes of table)" % len(table))
PYEOF' 2>&1 | tail -6
```
## Правка 7 (строка транскрипта 3341)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
import glob, os
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# 1. new field at the end of the strings table
p = W + "/include/user_interface/languages/uiLanguage.h"
s = open(p).read()
if "messages[LANGUAGE_TEXTS_LENGTH]" in s:
print("uiLanguage.h: field already present")
else:
s = s.replace(" const char mute[LANGUAGE_TEXTS_LENGTH];\n} stringsTable_t;",
" const char mute[LANGUAGE_TEXTS_LENGTH];\n"
" const char messages[LANGUAGE_TEXTS_LENGTH];\n} stringsTable_t;", 1)
# the struct changed, so the language file version has to move on
s = s.replace("#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x05 }",
"#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x06 }", 1)
open(p, "w").write(s)
print("uiLanguage.h: added .messages, bumped tag version to 6")
# 2. give every language a value; designated initialisers mean a missing one would
# simply render as an empty menu entry, which looks broken.
done = 0
for f in sorted(glob.glob(W + "/include/user_interface/languages/*.h")):
if f.endswith("uiLanguage.h"):
continue
t = open(f, "r", encoding="latin-1").read()
if ".messages" in t:
continue
if ".mute" not in t:
print(" skipped %s (no .mute anchor)" % os.path.basename(f))
continue
idx = t.index(".mute")
eol = t.index("\n", idx)
t = t[:eol + 1] + ".messages\t\t\t\t= \"Messages\", // MaxLen: 16\n" + t[eol + 1:]
open(f, "w", encoding="latin-1").write(t)
done += 1
print("added .messages to %d language files" % done)
PYEOF' 2>&1 | tail -6
```
## Правка 8 (строка транскрипта 3357)
```bash
export SSHPASS='Adelina@2015'
sshpass -e scp -o StrictHostKeyChecking=no /tmp/claude-1000/-home-viktor-claude-uv390/44e0d3e2-595b-4c07-94e1-73e0e3f7531a/menuMessages.c viktor@10.0.0.200:'~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application/source/user_interface/menuMessages.c' >/dev/null 2>&1
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- declare the three screens and their menu ids -------------------------
p = W + "/include/user_interface/menuSystem.h"
s = open(p).read()
if "MENU_MESSAGES" not in s:
s = s.replace("\tMENU_LAST_HEARD,\n",
"\tMENU_LAST_HEARD,\n\tMENU_MESSAGES,\n\tMENU_MESSAGES_FOLDER,\n\tMENU_MESSAGES_LIST,\n", 1)
s = s.replace("menuStatus_t uiMessageBox(uiEvent_t *event, bool isFirstRun);",
"menuStatus_t uiMessageBox(uiEvent_t *event, bool isFirstRun);\n"
"menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun);\n"
"menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun);\n"
"menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun);", 1)
open(p, "w").write(s)
print("menuSystem.h: ids and prototypes added")
else:
print("menuSystem.h: already declared")
# --- register the screens in the dispatch table ---------------------------
p = W + "/source/user_interface/menuSystem.c"
s = open(p).read()
if "menuMessages," in s:
print("menuSystem.c: already registered")
else:
# the table order must match the enum order: our ids come right after MENU_LAST_HEARD
idx = s.index("static menuFunctionData_t menuFunctions[] =")
marker = "{ menuLastHeard,"
j = s.index(marker, idx)
eol = s.index("\n", j) + 1
s = s[:eol] + ("\t\t{ menuMessages, NULL, NULL, 0 },\n"
"\t\t{ menuMessagesFolder, NULL, NULL, 0 },\n"
"\t\t{ menuMessagesList, NULL, NULL, 0 },\n") + s[eol:]
# and a Messages entry in the main menu, using the new language string
s = s.replace("\t{ 7, MENU_LAST_HEARD },\n",
"\t{ 7, MENU_LAST_HEARD },\n\t{ 281, MENU_MESSAGES },\n", 1)
open(p, "w").write(s)
print("menuSystem.c: screens registered and main menu entry added")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE "error|undefined" | head -10
arm-none-eabi-size build_ogd77/OpenMDUV380.elf 2>/dev/null | tail -1' 2>&1 | tail -12
```
## Правка 9 (строка транскрипта 3364)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 '
W=~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application
sed -i "s/THEME_ITEM_FG_MENU_ITEM_SELECTED/THEME_ITEM_COLOUR_NONE/g" $W/source/user_interface/menuMessages.c
sed -i "s/, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM,/, 0, THEME_ITEM_FG_MENU_ITEM,/g" $W/source/user_interface/menuMessages.c
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE "error|undefined" | head -10
arm-none-eabi-size build_ogd77/OpenMDUV380.elf 2>/dev/null | tail -1' 2>&1 | tail -10
```
+105
Просмотреть файл
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Diagnostic build: make the radio show what its SMS assembler actually sees.
Three counters are surfaced on screen, so we can tell where reception stops:
RX - data bursts handed to the assembler by the driver
HDR - data headers parsed (with the announced block count)
ASM - completed assemblies (all blocks collected)
If RX stays at zero the bursts never reach the module and the problem is in the
driver-side capture conditions. If RX counts up but HDR does not, header parsing is
rejecting them. If HDR appears but ASM does not, blocks are being lost.
"""
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 "smsGetDiagCounters" not in s:
a = "bool smsHasUnread(void);\n"
assert a in s
s = s.replace(a, a + """
// Diagnostics: how many bursts reached the assembler, how many headers were parsed,
// and how many messages assembled completely.
void smsGetDiagCounters(uint16_t *bursts, uint16_t *headers, uint16_t *assembled, uint8_t *lastBlocks);
""", 1)
open(p, "w").write(s)
print("sms.h: diagnostics declared")
# ---------------------------------------------------------------- sms.c
p = W + "/source/functions/sms.c"
s = open(p).read()
if "diagBursts" in s:
print("sms.c: already instrumented")
else:
a = "static bool unread = false;\n"
assert a in s
s = s.replace(a, a + """
static volatile uint16_t diagBursts = 0;
static uint16_t diagHeaders = 0;
static uint16_t diagAssembled = 0;
static uint8_t diagLastBlocks = 0;
""", 1)
# count every burst the driver hands us
a = "\tmemcpy((uint8_t *)rxQueue[rxQueueWrite], payload, SMS_BLOCK_LENGTH);\n"
assert a in s
s = s.replace(a, "\tdiagBursts++;\n" + a, 1)
# count parsed headers, remembering the announced block count
a = "\tblocksExpected = (uint8_t)(hdr[8] & 0x7FU);\n"
assert a in s
s = s.replace(a, a + "\tdiagHeaders++;\n\tdiagLastBlocks = blocksExpected;\n", 1)
# count completed assemblies
a = "static void finishAssembly(void)\n{\n"
assert a in s
s = s.replace(a, a + "\tdiagAssembled++;\n", 1)
s += """
void smsGetDiagCounters(uint16_t *bursts, uint16_t *headers, uint16_t *assembled, uint8_t *lastBlocks)
{
*bursts = diagBursts;
*headers = diagHeaders;
*assembled = diagAssembled;
*lastBlocks = diagLastBlocks;
}
"""
open(p, "w").write(s)
print("sms.c: counters added")
# ------------------------------------------------------- applicationMain.c
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsGetDiagCounters" in s:
print("applicationMain.c: already showing diagnostics")
else:
a = "\t\tif (smsHasUnread())\n"
assert a in s, "unread check not found"
s = s.replace(a, """\t\t{
\t\t\t// Diagnostics: show the assembler counters whenever they change, so we can see
\t\t\t// how far a received message gets even when no text is produced.
\t\t\tstatic uint16_t lastShownBursts = 0;
\t\t\tstatic uint16_t lastShownHeaders = 0;
\t\t\tuint16_t dBursts, dHeaders, dAssembled;
\t\t\tuint8_t dBlocks;
\t\t\tsmsGetDiagCounters(&dBursts, &dHeaders, &dAssembled, &dBlocks);
\t\t\tif ((dBursts != lastShownBursts) || (dHeaders != lastShownHeaders))
\t\t\t{
\t\t\t\tchar diagLine[40];
\t\t\t\tlastShownBursts = dBursts;
\t\t\t\tlastShownHeaders = dHeaders;
\t\t\t\tsnprintf(diagLine, sizeof(diagLine), "RX%u HDR%u/%u ASM%u",
\t\t\t\t\t\t(unsigned int)dBursts, (unsigned int)dHeaders,
\t\t\t\t\t\t(unsigned int)dBlocks, (unsigned int)dAssembled);
\t\t\t\tuiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 8000, diagLine, true);
\t\t\t\tsoundSetMelody(MELODY_KEY_BEEP);
\t\t\t}
\t\t}
""" + a, 1)
open(p, "w").write(s)
print("applicationMain.c: diagnostics displayed")
+72
Просмотреть файл
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Expose the SMS diagnostics through a RAM window that can be read over USB.
Drawing counters on the screen turned out to be unreliable: the notification is
overwritten by the normal screen redraw, so a screenshot usually shows nothing even
when reception is happening. Instead the counters are mirrored into the tail of the
raw audio buffer, which the CPS protocol can read out at any moment (area 7).
Layout at rawBuffer[2376..2399]:
'S' 'M' 'S' 'D' magic, so a stale buffer cannot be mistaken for data
u16 bursts data bursts handed to the assembler
u16 headers data headers parsed
u16 assembled messages assembled completely
u16 lastBlocks block count announced by the last header
u16 collected blocks collected for the message in progress
u16 state 0 = idle, 1 = collecting
u16 textLen length of the last extracted text
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/sms.c"
s = open(p).read()
if "SMS_DIAG_OFFSET" in s:
print("already patched")
raise SystemExit(0)
s = s.replace('#include "functions/ticks.h"\n',
'#include "functions/ticks.h"\n#include "functions/sound.h"\n', 1)
anchor = "void smsInit(void)\n"
assert anchor in s
helper = """// Diagnostics mirror: the tail of the raw audio buffer is readable over USB (CPS area 7),
// which is far more dependable than trying to keep text on the screen.
#define SMS_DIAG_OFFSET 2376
static void smsDiagPublish(void)
{
volatile uint8_t *d = &audioAndHotspotDataBuffer.rawBuffer[SMS_DIAG_OFFSET];
uint16_t values[7];
values[0] = diagBursts;
values[1] = diagHeaders;
values[2] = diagAssembled;
values[3] = diagLastBlocks;
values[4] = blocksCollected;
values[5] = (uint16_t)rxState;
values[6] = diagTextLen;
d[0] = 'S'; d[1] = 'M'; d[2] = 'S'; d[3] = 'D';
for (uint8_t i = 0; i < 7; i++)
{
d[4 + (i * 2)] = (uint8_t)(values[i] & 0xFF);
d[5 + (i * 2)] = (uint8_t)(values[i] >> 8);
}
}
"""
s = s.replace(anchor, helper + anchor, 1)
# track the extracted text length too
s = s.replace("static uint8_t diagLastBlocks = 0;\n",
"static uint8_t diagLastBlocks = 0;\nstatic uint16_t diagTextLen = 0;\n", 1)
s = s.replace("\tlatest.text[t] = '\\0';\n",
"\tlatest.text[t] = '\\0';\n\tdiagTextLen = t;\n", 1)
# publish on every tick, so a read over USB always sees the current state
s = s.replace("void smsTick(void)\n{\n", "void smsTick(void)\n{\n\tsmsDiagPublish();\n\n", 1)
open(p, "w").write(s)
print("sms.c: diagnostics mirrored into the readable RAM window")
+448
Просмотреть файл
@@ -0,0 +1,448 @@
#!/usr/bin/env python3
"""Fix the Messages menus: wrong language string index, and hand-rolled navigation.
Two defects:
- the main menu entry pointed at string 281 while .messages is index 280, so the
entry rendered as "GD77LANG" (it picked up a neighbouring field);
- the screens tracked the selection in their own variables and drew entries at a
fixed offset, instead of using the menu system's list handling. That makes the
cursor behave wrongly (no scrolling, wrong highlight).
Both screens are rewritten to the pattern used by the stock list menus:
menuDataGlobal.currentItemIndex + menuGetMenuOffset() + menuSystemMenuIncrement().
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---- 1. correct the main menu string index ------------------------------
p = W + "/source/user_interface/menuSystem.c"
s = open(p).read()
if "{ 281, MENU_MESSAGES" in s:
s = s.replace("{ 281, MENU_MESSAGES },", "{ 280, MENU_MESSAGES },", 1)
open(p, "w").write(s)
print("menuSystem.c: string index corrected 281 -> 280")
else:
print("menuSystem.c: index already correct")
# ---- 2. rewrite the screens using the standard list handling ------------
p = W + "/source/user_interface/menuMessages.c"
src = open(p).read()
header_end = src.index("// Entries of the top level Messages menu.")
header = src[:header_end]
body = '''// Entries of the top level Messages menu.
enum
{
MESSAGES_ITEM_NEW = 0,
MESSAGES_ITEM_INBOX,
MESSAGES_ITEM_OUTBOX,
NUM_MESSAGES_ITEMS
};
// Entries of the Inbox / Outbox menus.
enum
{
FOLDER_ITEM_LIST = 0,
FOLDER_ITEM_DELETE_ALL,
NUM_FOLDER_ITEMS
};
static bool showingOutbox = false; // which folder the list screens act on
static bool viewingMessage = false;// a single message is open
static menuStatus_t exitCode = MENU_STATUS_SUCCESS;
static void messagesUpdateScreen(void);
static void messagesHandleEvent(uiEvent_t *ev);
static void folderUpdateScreen(void);
static void folderHandleEvent(uiEvent_t *ev);
static void listUpdateScreen(void);
static void listHandleEvent(uiEvent_t *ev);
// ---------------------------------------------------------------- Messages
menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = NUM_MESSAGES_ITEMS;
menuDataGlobal.currentItemIndex = 0;
messagesUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
messagesHandleEvent(ev);
}
return exitCode;
}
static void messagesUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
int mNum;
displayClearBuf();
menuDisplayTitle(currentLanguage->messages);
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(NUM_MESSAGES_ITEMS, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
switch (mNum)
{
case MESSAGES_ITEM_NEW:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "New msg");
break;
case MESSAGES_ITEM_INBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Inbox (%u)", (unsigned int)smsInboxCount());
break;
case MESSAGES_ITEM_OUTBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Outbox (%u)", (unsigned int)smsOutboxCount());
break;
default:
buf[0] = 0;
break;
}
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void messagesHandleEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, NUM_MESSAGES_ITEMS);
messagesUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, NUM_MESSAGES_ITEMS);
messagesUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
switch (menuDataGlobal.currentItemIndex)
{
case MESSAGES_ITEM_NEW:
// Sending is not implemented yet; say so rather than opening a dead screen.
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Not yet", true);
break;
case MESSAGES_ITEM_INBOX:
showingOutbox = false;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
case MESSAGES_ITEM_OUTBOX:
showingOutbox = true;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
}
}
// ------------------------------------------------------- Inbox / Outbox menu
menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = NUM_FOLDER_ITEMS;
menuDataGlobal.currentItemIndex = 0;
folderUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
folderHandleEvent(ev);
}
return exitCode;
}
static void folderUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
int mNum;
displayClearBuf();
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(NUM_FOLDER_ITEMS, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
if (mNum == FOLDER_ITEM_LIST)
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s (%u)", (showingOutbox ? "Outbox List" : "Inbox List"), (unsigned int)count);
}
else
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Delete All");
}
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void folderHandleEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, NUM_FOLDER_ITEMS);
folderUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, NUM_FOLDER_ITEMS);
folderUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
if (menuDataGlobal.currentItemIndex == FOLDER_ITEM_LIST)
{
viewingMessage = false;
menuSystemPushNewMenu(MENU_MESSAGES_LIST);
}
else
{
if (showingOutbox)
{
smsOutboxClear();
}
else
{
smsInboxClear();
}
soundSetMelody(MELODY_ACK_BEEP);
folderUpdateScreen();
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
}
}
// ------------------------------------------------------------- message list
menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = (showingOutbox ? smsOutboxCount() : smsInboxCount());
menuDataGlobal.currentItemIndex = 0;
viewingMessage = false;
listUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
listHandleEvent(ev);
}
return exitCode;
}
static void listUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
int mNum;
displayClearBuf();
if (count == 0)
{
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
displayPrintCentered(32, "Empty", FONT_SIZE_3);
displayRender();
return;
}
if (viewingMessage)
{
const smsMessage_t *msg = (showingOutbox
? smsOutboxGet((uint8_t)menuDataGlobal.currentItemIndex)
: smsInboxGet((uint8_t)menuDataGlobal.currentItemIndex));
if (msg != NULL)
{
const int charsPerLine = 20;
int len = (int)strlen(msg->text);
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%u", (unsigned int)(showingOutbox ? msg->dstId : msg->srcId));
menuDisplayTitle(buf);
// Wrap the text so long messages are readable instead of being cut off.
for (int line = 0; (line < 4) && ((line * charsPerLine) < len); line++)
{
int n = len - (line * charsPerLine);
if (n > charsPerLine)
{
n = charsPerLine;
}
memcpy(buf, &msg->text[line * charsPerLine], n);
buf[n] = 0;
displayPrintCore(0, (16 + (line * 12)), buf, FONT_SIZE_2, TEXT_ALIGN_LEFT, false);
}
}
displayRender();
return;
}
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(count, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
const smsMessage_t *m = (showingOutbox ? smsOutboxGet((uint8_t)mNum) : smsInboxGet((uint8_t)mNum));
if (m == NULL)
{
continue;
}
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s%u %.8s",
(m->unread ? "*" : ""),
(unsigned int)(showingOutbox ? m->dstId : m->srcId),
m->text);
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void listHandleEvent(uiEvent_t *ev)
{
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
if ((viewingMessage == false) && (count > 0))
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, count);
listUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, count);
listUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) && (count > 0))
{
if (viewingMessage == false)
{
viewingMessage = true;
if (showingOutbox == false)
{
smsMarkRead();
}
listUpdateScreen();
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
if (viewingMessage)
{
viewingMessage = false;
listUpdateScreen();
return;
}
menuSystemPopPreviousMenu();
}
}
'''
open(p, "w").write(header + body)
print("menuMessages.c: rewritten with standard list navigation")
+44
Просмотреть файл
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Stop the radio from dropping out of hotspot (modem) mode when the host goes quiet.
Stock behaviour: if the USB host does not talk to the radio for MMDVMHOST_TIMEOUT
(20 s), the firmware calls hotspotExit() and leaves modem mode by itself. That is a
safety net for a crashed host, but it makes the radio unusable as a permanently
attached modem - which is exactly how it is used here.
With the timeout set to 0 the inactivity check is disabled; leaving modem mode is
then only done deliberately (by the user from the radio, or by the host asking the
modem to go idle).
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "MMDVMHOST_TIMEOUT_DISABLED" in s:
print("already patched")
raise SystemExit(0)
old_const = "static const uint32_t MMDVMHOST_TIMEOUT = 20000; // 20s timeout (MMDVMHost mode only, there is no timeout for BlueDV)\n"
assert old_const in s, "timeout constant not found"
new_const = (
"// 0 disables the host-inactivity timeout, so the radio stays in modem mode until it is\n"
"// told to leave. Set this back to 20000 to restore the stock 20 second auto-exit.\n"
"#define MMDVMHOST_TIMEOUT_DISABLED 0\n"
"static const uint32_t MMDVMHOST_TIMEOUT = MMDVMHOST_TIMEOUT_DISABLED;\n"
)
s = s.replace(old_const, new_const, 1)
# Guard both inactivity checks so a zero timeout means "never time out".
old_check = """ if ((nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
new_check = """ if ((MMDVMHOST_TIMEOUT != 0) && (nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
count = s.count(old_check)
assert count == 2, "expected 2 inactivity checks, found %d" % count
s = s.replace(old_check, new_check)
open(p, "w").write(s)
print("hotspot.c: host-inactivity auto-exit disabled (%d checks guarded)" % count)
+62
Просмотреть файл
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Patch 1 of RX support: new RX event type + Slot Type encoder."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- 1. new RX event type -------------------------------------------------
p = W + "/include/functions/hotspot.h"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" not in s:
a = "\tHOTSPOT_RX_STOP,\n"
assert a in s, "RX enum anchor missing"
s = s.replace(a, a + "\tHOTSPOT_RX_DATA_FRAME,// a packet data burst (SMS etc) was received off air\n", 1)
open(p, "w").write(s)
print("hotspot.h: RX event added")
else:
print("hotspot.h: already has RX event")
# --- 2. bring the Slot Type encoder over from the older MK22 branch -------
src = open("/home/viktor/OpenGD77/firmware/source/hotspot/DMRSlotType.c").read()
start = src.index("static const uint16_t ENCODING_TABLE_2087[] =")
table = src[start:src.index("};", start) + 2]
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "ENCODING_TABLE_2087" in s:
print("hotspot.c: encoder already present")
raise SystemExit(0)
anchor = "static void BPTCencode(const uint8_t *inputData, uint8_t *outputData)\n{"
assert anchor in s, "BPTCencode definition not found"
header = (
"// Slot Type encoder, needed to build received data bursts for the host.\n"
"// Golay(20,8) table and logic come from the project's own DMRSlotType implementation\n"
"// (present in the MK22 branch, absent here because stock firmware never sent data).\n"
)
func = """
// Writes colour code + data type (FEC protected) into the Slot Type half-bursts of a frame.
static void DMRSlotTypeEncode(uint32_t colorCode, uint32_t dataType, uint8_t *frame)
{
\tuint8_t slotType[3];
\tslotType[0] = (colorCode << 4) & 0xF0U;
\tslotType[0] |= (dataType << 0) & 0x0FU;
\tuint16_t cksum = ENCODING_TABLE_2087[slotType[0]];
\tslotType[1] = (cksum >> 0) & 0xFFU;
\tslotType[2] = (cksum >> 8) & 0xFFU;
\tframe[12] = (frame[12] & 0xC0U) | ((slotType[0] >> 2) & 0x3FU);
\tframe[13] = (frame[13] & 0x0FU) | ((slotType[0] << 6) & 0xC0U) | ((slotType[1] >> 2) & 0x30U);
\tframe[19] = (frame[19] & 0xF0U) | ((slotType[1] >> 2) & 0x0FU);
\tframe[20] = (frame[20] & 0x03U) | ((slotType[1] << 6) & 0xC0U) | ((slotType[2] >> 2) & 0x3CU);
}
"""
s = s.replace(anchor, header + table + func + anchor, 1)
open(p, "w").write(s)
print("hotspot.c: slot type encoder added (table %d bytes)" % len(table))
+51
Просмотреть файл
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Patch 2 of RX support: hand received data bursts to the MMDVM host."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "sendDataFrameToHost" in s:
print("already patched")
raise SystemExit(0)
# --- 1. MS sourced data sync pattern (frames we originate towards the host) ---
a = "static const uint8_t MS_SOURCED_AUDIO_SYNC[] = { 0x07, 0xF7, 0xD5, 0xDD, 0x57, 0xDF, 0xD0 };\n"
assert a in s, "sync constant anchor missing"
s = s.replace(a, a + "static const uint8_t MS_SOURCED_DATA_SYNC[] = { 0x0D, 0x5D, 0x7F, 0x77, 0xFD, 0x75, 0x70 };\n", 1)
# --- 2. the function itself, placed right before the voice header builder ---
anchor = "static bool sendVoiceHeaderLC_Frame(volatile const uint8_t *receivedDMRDataAndAudio)\n{"
assert anchor in s, "sendVoiceHeaderLC_Frame not found"
func = '''// Rebuild a packet data burst that was received off air and pass it to the MMDVM host.
//
// The HR-C6000 hands us the decoded 96 bit payload (it strips the FEC itself), while the
// host expects a complete 33 byte DMR burst. So we run the payload back through BPTC,
// stamp the data sync pattern and the Slot Type (colour code + data type) into the middle
// of the burst, and send it as a normal DMR data frame.
static void sendDataFrameToHost(volatile const uint8_t *receivedData, uint8_t dataType)
{
\tuint8_t frameData[DMR_FRAME_LENGTH_BYTES + MMDVM_HEADER_LENGTH + 2] = {MMDVM_FRAME_START, (DMR_FRAME_LENGTH_BYTES + MMDVM_HEADER_LENGTH + 2), MMDVM_DMR_DATA2, DMR_SYNC_DATA | dataType};
\tBPTCglobalsInit();
\tBPTCencode((uint8_t *)receivedData, frameData + MMDVM_HEADER_LENGTH);
\t// data sync pattern goes into the middle of the burst
\tfor (uint8_t i = 0; i < 7; i++)
\t{
\t\tframeData[i + EMBEDDED_DATA_OFFSET + MMDVM_HEADER_LENGTH] =
\t\t\t\t(frameData[i + EMBEDDED_DATA_OFFSET + MMDVM_HEADER_LENGTH] & ~SYNC_MASK[i]) | MS_SOURCED_DATA_SYNC[i];
\t}
\tDMRSlotTypeEncode(trxGetDMRColourCode(), dataType, frameData + MMDVM_HEADER_LENGTH);
\tsetRSSIToFrame(frameData);
\tenqueueUSBData(frameData, frameData[1]);
}
'''
s = s.replace(anchor, func + anchor, 1)
open(p, "w").write(s)
print("hotspot.c: sendDataFrameToHost added")
+67
Просмотреть файл
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Patch 3 of RX support: dispatch in the hotspot state machine, and read the
payload out of the HR-C6000 when a data burst arrives off air."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---------------------------------------------------------------- hotspot.c
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "case HOTSPOT_RX_DATA_FRAME:" in s:
print("hotspot.c: dispatch already present")
else:
a = "\t\t\t\t\t\tcase HOTSPOT_RX_STOP:\n"
assert a in s, "RX_STOP case not found"
case = """\t\t\t\t\t\tcase HOTSPOT_RX_DATA_FRAME:
\t\t\t\t\t\t\t// Packet data burst (SMS, position report...) received off air.
\t\t\t\t\t\t\t// The data type the HR-C6000 reported is stored right after the frame.
\t\t\t\t\t\t\tsendDataFrameToHost(audioAndHotspotDataBuffer.hotspotBuffer[rfFrameBufReadIdx],
\t\t\t\t\t\t\t\t\taudioAndHotspotDataBuffer.hotspotBuffer[rfFrameBufReadIdx][AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1]);
\t\t\t\t\t\t\tlastRxState = HOTSPOT_RX_DATA_FRAME;
\t\t\t\t\t\t\trxFrameTime = ticksGetMillis();
\t\t\t\t\t\t\tbreak;
"""
s = s.replace(a, case + a, 1)
open(p, "w").write(s)
print("hotspot.c: RX_DATA_FRAME dispatch added")
# ------------------------------------------------------------- HR-C6000.c
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" in s:
print("HR-C6000.c: already patched")
raise SystemExit(0)
# Hook in right after the terminator handling, which is the existing example of
# "this is a data sync burst" handling in this interrupt.
a = """ if (hrc6000CrcIsValid() && (slotState != DMR_STATE_IDLE) && (hrc.skipCount > 0) && (rxSyncClass != SYNC_CLASS_DATA) && ((rxDataType & 0x07) == 0x01))
{
hrc.skipCount--;
}
"""
assert a in s, "skipCount anchor missing"
block = """ // Packet data bursts (SMS, position reports, CSBK). Stock firmware reads nothing off the
// chip for these, which is why data never reached the host. The HR-C6000 has already
// removed the FEC, so the 96 bit payload can be read from the same register page that
// carries LC data, and handed to the hotspot code to be rebuilt into a full burst.
if ((settingsUsbMode == USB_MODE_HOTSPOT) && hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
if (SPI0ReadPageRegByteArray(0x02, 0x00, (uint8_t *)DMR_frame_buffer, LC_DATA_LENGTH) == kStatus_Success)
{
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
return;
}
"""
s = s.replace(a, block + a, 1)
open(p, "w").write(s)
print("HR-C6000.c: data burst capture added")
+53
Просмотреть файл
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Show a received SMS on a proper message screen instead of a transient notification.
uiNotificationShow() draws over the current screen, but the normal screen redraw wipes
it again - which is why the text never appeared even though the message was received
and decoded correctly. The message box screen is a real menu: it stays until the user
dismisses it.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "MESSAGEBOX_TYPE_INFO" in s and "smsMarkRead" in s and "MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, \"%u:" in s:
print("already patched")
raise SystemExit(0)
# Replace the notification-based display with a message box screen.
old_start = "\t\tif (smsHasUnread())\n\t\t{\n"
assert old_start in s, "unread block not found"
start = s.index(old_start)
# find the end of that block (the closing brace of the if, at the same indent)
end_marker = "\n\t\t}\n"
end = s.index(end_marker, start) + len(end_marker)
old_block = s[start:end]
assert "smsMarkRead" in old_block, "unexpected block contents: %r" % old_block[:200]
new_block = """ if (smsHasUnread())
{
const smsMessage_t *msg = smsGetLatest();
// Only interrupt the user when they are on a normal operating screen.
int currentMenu = menuSystemGetCurrentMenuNumber();
if ((msg != NULL) && ((currentMenu == UI_VFO_MODE) || (currentMenu == UI_CHANNEL_MODE)))
{
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%u\\n%s",
(unsigned int)msg->srcId, msg->text);
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_OK;
uiDataGlobal.MessageBox.validatorCallback = NULL;
menuSystemPushNewMenu(UI_MESSAGE_BOX);
soundSetMelody(MELODY_ACK_BEEP);
smsMarkRead();
}
}
"""
s = s[:start] + new_block + s[end:]
open(p, "w").write(s)
print("applicationMain.c: incoming SMS now opens a message screen")
+60
Просмотреть файл
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""SMS step 1: capture data bursts in normal radio mode too, and route them to an
assembler module. In hotspot mode nothing changes - bursts still go to the host."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "smsRxHandleBurst" in s:
print("HR-C6000.c already routed to the SMS assembler")
else:
old = """ if ((settingsUsbMode == USB_MODE_HOTSPOT) && hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
if (SPI0ReadPageRegByteArray(0x02, 0x00, (uint8_t *)DMR_frame_buffer, LC_DATA_LENGTH) == kStatus_Success)
{
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
return;
}
"""
assert old in s, "existing capture block not found"
new = """ if (hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
uint8_t dataBurst[LC_DATA_LENGTH];
if (SPI0ReadPageRegByteArray(0x02, 0x00, dataBurst, LC_DATA_LENGTH) == kStatus_Success)
{
if (settingsUsbMode == USB_MODE_HOTSPOT)
{
// Acting as a modem: hand the burst to the host untouched.
memcpy((uint8_t *)DMR_frame_buffer, dataBurst, LC_DATA_LENGTH);
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
else
{
// Normal radio operation: collect the bursts into a message.
smsRxHandleBurst(dataBurst, rxDataType);
}
}
return;
}
"""
s = s.replace(old, new, 1)
# include the new module
anchor = '#include "functions/hotspot.h"\n'
assert anchor in s, "include anchor missing"
s = s.replace(anchor, anchor + '#include "functions/sms.h"\n', 1)
open(p, "w").write(s)
print("HR-C6000.c: data bursts now captured in normal mode too")
+35
Просмотреть файл
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""SMS step 2: run the assembler from the main loop and initialise it at startup."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsTick" in s:
print("already wired into the main loop")
raise SystemExit(0)
# include
anchor = None
for cand in ['#include "functions/voicePrompts.h"\n', '#include "functions/settings.h"\n', '#include "functions/ticks.h"\n']:
if cand in s:
anchor = cand
break
assert anchor, "no suitable include anchor in applicationMain.c"
s = s.replace(anchor, anchor + '#include "functions/sms.h"\n', 1)
# drain the received bursts once per main loop pass, next to the other periodic work
a = "\t\tvoxTick();\n"
assert a in s, "voxTick anchor missing"
s = s.replace(a, a + "\t\tsmsTick();// turn received data bursts into messages\n", 1)
# initialise alongside the other subsystems
init_anchor = None
for cand in ["\tbatteryUpdate();\n", "\tvoxInit();\n"]:
if cand in s:
init_anchor = cand
break
assert init_anchor, "no init anchor found"
s = s.replace(init_anchor, init_anchor + "\tsmsInit();\n", 1)
open(p, "w").write(s)
print("applicationMain.c: smsInit() + smsTick() wired in")
+34
Просмотреть файл
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""SMS step 3: show a received message on the screen and beep."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsGetLatest" in s:
print("already showing messages")
raise SystemExit(0)
old = "\t\tsmsTick();// turn received data bursts into messages\n"
assert old in s, "smsTick call not found"
new = """\t\tsmsTick();// turn received data bursts into messages
\t\tif (smsHasUnread())
\t\t{
\t\t\tconst smsMessage_t *msg = smsGetLatest();
\t\t\tif (msg != NULL)
\t\t\t{
\t\t\t\tchar smsLine[SMS_MAX_TEXT_LENGTH + 16];
\t\t\t\tsnprintf(smsLine, sizeof(smsLine), "%u: %s", (unsigned int)msg->srcId, msg->text);
\t\t\t\tuiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 10000, smsLine, true);
\t\t\t\tsoundSetMelody(MELODY_ACK_BEEP);
\t\t\t\tsmsMarkRead();
\t\t\t}
\t\t}
"""
s = s.replace(old, new, 1)
open(p, "w").write(s)
print("applicationMain.c: incoming SMS is shown and beeps")
+149
Просмотреть файл
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Add Inbox/Outbox storage to the SMS module."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ------------------------------------------------------------------ header
p = W + "/include/functions/sms.h"
s = open(p).read()
if "smsInboxCount" in s:
print("sms.h: already has storage API")
else:
s = s.replace(
"// Most recently received message, or NULL when nothing has been received yet.\n",
"""// How many messages each folder keeps. Older ones are dropped when full.
#define SMS_INBOX_SIZE 10
#define SMS_OUTBOX_SIZE 10
// Inbox: messages received off air. Newest first.
uint8_t smsInboxCount(void);
const smsMessage_t *smsInboxGet(uint8_t index);
void smsInboxClear(void);
uint8_t smsInboxUnreadCount(void);
// Outbox: messages we sent. Newest first.
uint8_t smsOutboxCount(void);
const smsMessage_t *smsOutboxGet(uint8_t index);
void smsOutboxClear(void);
void smsOutboxAdd(uint32_t dstId, bool isPrivate, const char *text);
// Most recently received message, or NULL when nothing has been received yet.
""", 1)
open(p, "w").write(s)
print("sms.h: storage API declared")
# ------------------------------------------------------------------ source
p = W + "/source/functions/sms.c"
s = open(p).read()
if "smsInboxCount" in s:
print("sms.c: already has storage")
raise SystemExit(0)
a = "static smsMessage_t latest;\n"
assert a in s
s = s.replace(a, a + """
// Message folders. Index 0 is always the newest; adding shifts the rest down.
static smsMessage_t inbox[SMS_INBOX_SIZE];
static uint8_t inboxCount = 0;
static smsMessage_t outbox[SMS_OUTBOX_SIZE];
static uint8_t outboxCount = 0;
""", 1)
# clear folders on init
s = s.replace("\tmemset(&latest, 0, sizeof(latest));\n",
"\tmemset(&latest, 0, sizeof(latest));\n"
"\tinboxCount = 0;\n"
"\toutboxCount = 0;\n", 1)
# store a completed message into the inbox
a = "\thaveMessage = (t > 0);\n"
assert a in s, "completion point not found"
s = s.replace(a, """ if (t > 0)
{
// Newest first: shift the existing entries down, dropping the oldest if full.
for (int8_t i = (int8_t)((inboxCount < SMS_INBOX_SIZE) ? inboxCount : (SMS_INBOX_SIZE - 1)); i > 0; i--)
{
inbox[i] = inbox[i - 1];
}
inbox[0] = latest;
if (inboxCount < SMS_INBOX_SIZE)
{
inboxCount++;
}
}
""" + a, 1)
s += """
uint8_t smsInboxCount(void)
{
return inboxCount;
}
const smsMessage_t *smsInboxGet(uint8_t index)
{
return ((index < inboxCount) ? &inbox[index] : NULL);
}
void smsInboxClear(void)
{
inboxCount = 0;
unread = false;
haveMessage = false;
}
uint8_t smsInboxUnreadCount(void)
{
uint8_t n = 0;
for (uint8_t i = 0; i < inboxCount; i++)
{
if (inbox[i].unread)
{
n++;
}
}
return n;
}
uint8_t smsOutboxCount(void)
{
return outboxCount;
}
const smsMessage_t *smsOutboxGet(uint8_t index)
{
return ((index < outboxCount) ? &outbox[index] : NULL);
}
void smsOutboxClear(void)
{
outboxCount = 0;
}
void smsOutboxAdd(uint32_t dstId, bool isPrivate, const char *text)
{
for (int8_t i = (int8_t)((outboxCount < SMS_OUTBOX_SIZE) ? outboxCount : (SMS_OUTBOX_SIZE - 1)); i > 0; i--)
{
outbox[i] = outbox[i - 1];
}
memset(&outbox[0], 0, sizeof(smsMessage_t));
outbox[0].dstId = dstId;
outbox[0].isPrivate = isPrivate;
outbox[0].unread = false;
strncpy(outbox[0].text, text, SMS_MAX_TEXT_LENGTH);
outbox[0].text[SMS_MAX_TEXT_LENGTH] = '\\0';
if (outboxCount < SMS_OUTBOX_SIZE)
{
outboxCount++;
}
}
"""
open(p, "w").write(s)
print("sms.c: inbox/outbox storage added")
+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")
+63
Просмотреть файл
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Stop emitting voice LC headers in front of a packet data transmission.
TX_START_1/3/5 always sent three Voice LC Headers built from the radio's own DMR ID,
which is correct for a voice call but pollutes the air before an SMS: the receiving
end logs them as "RF user <id> rejected for using TG 0". When the queued frame is a
data burst we send standard Idle data bursts instead - a legitimate filler that
receivers discard silently.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "DT_IDLE << 4" in s:
print("already patched")
raise SystemExit(0)
# The three start steps share the same two lines; patch each occurrence.
old_variants = [
# TX_START_1 (preceded by the LED write and header call)
(""" hrc6000SendPcOrTgLCHeader();
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" if (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)
{
// A data burst is queued: don't announce a voice call in front of it.
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, (DT_IDLE << 4)); // Idle data burst as filler
}
else
{
hrc6000SendPcOrTgLCHeader();
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
}
"""),
# TX_START_3 and TX_START_5 (no header call, just the two register writes)
(""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
SPI0WritePageRegByte(0x04, 0x50, ((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10));// Voice LC Header, or Idle filler ahead of data
"""),
(""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, ((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10));// Voice LC Header, or Idle filler ahead of data
"""),
]
count = 0
for old, new in old_variants:
if old in s:
s = s.replace(old, new, 1)
count += 1
if count == 0:
raise SystemExit("no TX_START patterns matched - aborting")
open(p, "w").write(s)
print("patched %d of 3 TX start steps" % count)
+74
Просмотреть файл
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Fix: a stale "this frame is packet data" marker could suppress the Voice LC Header
of an ordinary voice transmission.
hrc.txDataType is only ever set on the hotspot path, and was only cleared after a data
burst was actually sent. If a data transmission was cut short before that, the marker
stayed set - and the next PTT from the radio itself started without a Voice LC Header,
so receivers never recognised the call.
Two guards:
- the start-of-transmission checks now only apply while acting as a hotspot modem;
- the marker is cleared whenever a normal (non-hotspot) transmission begins.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "isHotspotDataTx" in s:
print("already patched")
raise SystemExit(0)
# 1. helper right after the struct instance, so all three call sites can use it
anchor = "\t\t.txDataType = HOTSPOT_TX_TYPE_VOICE,\n"
assert anchor in s, "hrc initialiser not found"
# find the end of the initialiser block to place the helper after it
marker = "static void hrc6000SendPcOrTgLCHeader(void);"
assert marker in s, "forward declaration anchor not found"
helper = """// True only when the frame about to be sent is a packet data burst queued by the host.
// Outside hotspot mode there is no such thing, so ordinary voice transmissions are never
// affected by a leftover marker.
static inline bool isHotspotDataTx(void)
{
return ((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE));
}
"""
s = s.replace(marker, marker + "\n" + helper, 1)
# 2. use it in the three start-of-transmission checks
a1 = "\t\t\tif (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)\n\t\t\t{\n\t\t\t\t// A data burst is queued: don't announce a voice call in front of it."
assert a1 in s, "TX_START_1 check not found"
s = s.replace(a1, "\t\t\tif (isHotspotDataTx())\n\t\t\t{\n\t\t\t\t// A data burst is queued: don't announce a voice call in front of it.", 1)
a2 = "((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10)"
n2 = s.count(a2)
assert n2 == 2, "expected 2 shared checks, found %d" % n2
s = s.replace(a2, "(isHotspotDataTx() ? (DT_IDLE << 4) : 0x10)")
# 3. and in the ongoing-TX branch, so a stale marker cannot divert voice frames either
a3 = "\t\t\t\t\tif (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)\n"
assert a3 in s, "TX_2 data branch not found"
s = s.replace(a3, "\t\t\t\t\tif (isHotspotDataTx())\n", 1)
# 4. belt and braces: clear the marker when a non-hotspot transmission starts
a4 = """ if (settingsUsbMode != USB_MODE_HOTSPOT)
{
codecInit(false);
}
else
{
hrc.hotspotPostponedFrameHandling = (HS_NUM_OF_SILENCE_SEQ_ON_STARTUP * 6);"""
assert a4 in s, "TX start branch not found"
s = s.replace(a4, """ if (settingsUsbMode != USB_MODE_HOTSPOT)
{
hrc.txDataType = HOTSPOT_TX_TYPE_VOICE;// plain voice call: never a data burst
codecInit(false);
}
else
{
hrc.hotspotPostponedFrameHandling = (HS_NUM_OF_SILENCE_SEQ_ON_STARTUP * 6);""", 1)
open(p, "w").write(s)
print("HR-C6000.c: voice transmissions can no longer be suppressed by a stale data marker")
+74
Просмотреть файл
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Hook sending into the interface: a Reply button on an incoming message, and a
working New msg entry.
For this first step the text is a fixed quick reply, so the whole transmit path can be
exercised on air before the keypad editor is written.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---- Reply button on the incoming message popup -------------------------
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsReplyValidator" in s:
print("applicationMain.c: reply already wired")
else:
# remember who to answer, and send the quick reply when the user confirms
anchor = "\t\tif (smsHasUnread())\n"
assert anchor in s, "unread block not found"
helper = """static uint32_t smsReplyToId = 0;
static bool smsReplyIsPrivate = true;
// Called when the user confirms the Reply prompt on an incoming message.
static bool smsReplyValidator(void)
{
smsSendText(smsReplyToId, (smsReplyIsPrivate == false), "OK");
return true;
}
"""
# place the helper just before the function that contains the unread block
fn_start = s.rindex("\nvoid ", 0, s.index(anchor))
s = s[:fn_start + 1] + helper + s[fn_start + 1:]
old_show = """ uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_OK;
uiDataGlobal.MessageBox.validatorCallback = NULL;"""
assert old_show in s, "message box setup not found"
s = s.replace(old_show, """ // Yes replies to the sender, No just dismisses the message.
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
uiDataGlobal.MessageBox.validatorCallback = smsReplyValidator;
smsReplyToId = msg->srcId;
smsReplyIsPrivate = msg->isPrivate;""", 1)
open(p, "w").write(s)
print("applicationMain.c: Reply wired into the incoming message popup")
# ---- New msg actually sends --------------------------------------------
p = W + "/source/user_interface/menuMessages.c"
s = open(p).read()
if "smsSendText" in s:
print("menuMessages.c: already sends")
else:
old = """ case MESSAGES_ITEM_NEW:
// Sending is not implemented yet; say so rather than opening a dead screen.
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Not yet", true);
break;"""
assert old in s, "New msg handler not found"
s = s.replace(old, """ case MESSAGES_ITEM_NEW:
// First step: send a fixed text to the current talkgroup, so the transmit
// path can be exercised on air. The keypad editor comes next.
if (smsSendText((trxTalkGroupOrPcId & 0x00FFFFFF), ((trxTalkGroupOrPcId >> 24) == TG_CALL_FLAG), "TEST FROM RADIO"))
{
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Sending...", true);
}
else
{
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Busy", true);
}
break;""", 1)
s = s.replace('#include "functions/sound.h"',
'#include "functions/sound.h"\n#include "functions/trx.h"\n#include "user_interface/uiUtilities.h"', 1)
open(p, "w").write(s)
print("menuMessages.c: New msg now transmits")
+56
Просмотреть файл
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Leave modem mode when the USB host goes away, instead of after a fixed timeout.
Stock firmware dropped out of hotspot mode if the host was quiet for 20 seconds, which
made the radio unusable as a permanently attached modem - so that timeout was disabled
on request. The side effect was that the radio then stayed a modem forever and had to be
power cycled by hand after every test.
This ties the exit to the actual USB connection state instead: while the host is present
the radio stays a modem no matter how quiet it is, and as soon as the cable is pulled (or
the host stops enumerating it) the radio returns to normal operation by itself.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "usbHostIsAttached" in s:
print("already patched")
raise SystemExit(0)
# a small helper next to the other statics
anchor = "static const uint32_t MMDVMHOST_TIMEOUT = MMDVMHOST_TIMEOUT_DISABLED;\n"
assert anchor in s, "timeout constant not found"
helper = anchor + """
// The USB device handle tells us whether a host is actually enumerating us. Losing that
// is what "the computer went away" really means; host silence alone is not.
extern USBD_HandleTypeDef hUsbDeviceFS;
static bool usbHostIsAttached(void)
{
return (hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED);
}
"""
s = s.replace(anchor, helper, 1)
# make sure the USB device header is included
if '#include "usb_device.h"' not in s:
inc_anchor = '#include "functions/ticks.h"\n'
assert inc_anchor in s, "include anchor missing"
s = s.replace(inc_anchor, inc_anchor + '#include "usb_device.h"\n', 1)
# both inactivity checks now also fire when the host has disconnected
old = """ if ((MMDVMHOST_TIMEOUT != 0) && (nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
new = """ if ((nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((usbHostIsAttached() == false) ||
((MMDVMHOST_TIMEOUT != 0) && ((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))))
"""
count = s.count(old)
assert count == 2, "expected 2 inactivity checks, found %d" % count
s = s.replace(old, new)
open(p, "w").write(s)
print("hotspot.c: modem mode now ends when the USB host disconnects (%d checks)" % count)