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