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