Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
449 строки
10 KiB
Python
449 строки
10 KiB
Python
#!/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")
|