Files
opengd77-uv380-sms/firmware/application/source/user_interface/menuMessages.c
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

409 строки
9.9 KiB
C

/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Messages menu: reading received texts, browsing sent ones, and starting a new one.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h"
#include "user_interface/uiUtilities.h"
#include "functions/sms.h"
#include "functions/settings.h"
#include "functions/sound.h"
// Entries of the top level Messages menu.
enum
{
MESSAGES_ITEM_NEW = 0,
MESSAGES_ITEM_INBOX,
MESSAGES_ITEM_OUTBOX,
MESSAGES_ITEM_COUNT
};
// Entries of the Inbox / Outbox menus.
enum
{
FOLDER_ITEM_LIST = 0,
FOLDER_ITEM_DELETE_ALL,
FOLDER_ITEM_COUNT
};
static int menuIndex = 0; // selection in the Messages menu
static int folderIndex = 0; // selection in an Inbox/Outbox menu
static int listIndex = 0; // selected message in a list
static bool showingOutbox = false; // which folder the list/delete screens act on
static bool viewingMessage = false;// a single message is open
static menuStatus_t exitCode = MENU_STATUS_SUCCESS;
static void updateMessagesScreen(void);
static void updateFolderScreen(void);
static void updateListScreen(void);
static bool handleMessagesEvent(uiEvent_t *ev);
static bool handleFolderEvent(uiEvent_t *ev);
static bool handleListEvent(uiEvent_t *ev);
// ---------------------------------------------------------------- Messages
static void updateMessagesScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
displayClearBuf();
menuDisplayTitle(currentLanguage->messages);
// New msg / Inbox (n) / Outbox (n)
for (int i = 0; i < MESSAGES_ITEM_COUNT; i++)
{
switch (i)
{
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;
}
menuDisplayEntry((i - menuIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
displayRender();
}
menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuIndex = 0;
updateMessagesScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleMessagesEvent(ev))
{
updateMessagesScreen();
}
}
return exitCode;
}
static bool handleMessagesEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuIndex = ((menuIndex + 1) % MESSAGES_ITEM_COUNT);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuIndex = ((menuIndex + MESSAGES_ITEM_COUNT - 1) % MESSAGES_ITEM_COUNT);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
switch (menuIndex)
{
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;
folderIndex = 0;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
case MESSAGES_ITEM_OUTBOX:
showingOutbox = true;
folderIndex = 0;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
return false;
}
return false;
}
// ------------------------------------------------------- Inbox / Outbox menu
static void updateFolderScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
displayClearBuf();
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = 0; i < FOLDER_ITEM_COUNT; i++)
{
if (i == 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 - folderIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
displayRender();
}
menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
updateFolderScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleFolderEvent(ev))
{
updateFolderScreen();
}
}
return exitCode;
}
static bool handleFolderEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
folderIndex = ((folderIndex + 1) % FOLDER_ITEM_COUNT);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
folderIndex = ((folderIndex + FOLDER_ITEM_COUNT - 1) % FOLDER_ITEM_COUNT);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
if (folderIndex == FOLDER_ITEM_LIST)
{
listIndex = 0;
viewingMessage = false;
menuSystemPushNewMenu(MENU_MESSAGES_LIST);
}
else
{
if (showingOutbox)
{
smsOutboxClear();
}
else
{
smsInboxClear();
}
soundSetMelody(MELODY_ACK_BEEP);
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
return false;
}
return false;
}
// ------------------------------------------------------------- message list
static void updateListScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
displayClearBuf();
if (count == 0)
{
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
displayPrintCentered(32, "Empty", FONT_SIZE_3);
displayRender();
return;
}
const smsMessage_t *msg = (showingOutbox ? smsOutboxGet((uint8_t)listIndex) : smsInboxGet((uint8_t)listIndex));
if (msg == NULL)
{
displayRender();
return;
}
if (viewingMessage)
{
// Full message: correspondent on the title line, text wrapped below.
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%u", (unsigned int)(showingOutbox ? msg->dstId : msg->srcId));
menuDisplayTitle(buf);
// Wrap the text over the available lines rather than letting it run off screen.
const int charsPerLine = 20;
int len = (int)strlen(msg->text);
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);
}
}
else
{
// List: one line per message, newest first.
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s %u/%u", (showingOutbox ? "Outbox" : "Inbox"),
(unsigned int)(listIndex + 1), (unsigned int)count);
menuDisplayTitle(buf);
for (int i = 0; i < count; i++)
{
const smsMessage_t *m = (showingOutbox ? smsOutboxGet((uint8_t)i) : smsInboxGet((uint8_t)i));
if (m == NULL)
{
continue;
}
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s%u %.10s",
((m->unread) ? "*" : " "),
(unsigned int)(showingOutbox ? m->dstId : m->srcId),
m->text);
menuDisplayEntry((i - listIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
}
displayRender();
}
menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
updateListScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleListEvent(ev))
{
updateListScreen();
}
}
return exitCode;
}
static bool handleListEvent(uiEvent_t *ev)
{
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN) && (count > 0) && (viewingMessage == false))
{
listIndex = ((listIndex + 1) % count);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (count > 0) && (viewingMessage == false))
{
listIndex = ((listIndex + count - 1) % count);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) && (count > 0))
{
if (viewingMessage == false)
{
viewingMessage = true;
// Opening a message marks it read.
if (showingOutbox == false)
{
smsMarkRead();
}
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
if (viewingMessage)
{
viewingMessage = false;
return true;
}
menuSystemPopPreviousMenu();
return false;
}
return false;
}