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

21 KiB

Правки, сделанные командами (не отдельными файлами)

Извлечено из транскрипта сессии в порядке применения. Каждый блок - самостоятельная команда, менявшая исходники прошивки.

Правка 1 (строка транскрипта 966)

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)

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)

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)

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)

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)

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)

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)

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)

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