Files
opengd77-uv380-sms/patches/00-inline-edits.md
T
viktor b5a8e92e45 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

494 строки
21 KiB
Markdown

# Правки, сделанные командами (не отдельными файлами)
Извлечено из транскрипта сессии в порядке применения.
Каждый блок - самостоятельная команда, менявшая исходники прошивки.
## Правка 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
```