Full messaging support, implemented fresh in three layers: - dmrDataProtocol: pure short-data codec (CSBK preamble, data/response headers, IP/UDP/TMS payload, ETSI CRCs) validated against on-air captures by a host-side test suite. Transmits Motorola TMS (UDP 4007), receives TMS and Anytone/"DMR standard" (UDP 5016). - smsCore/smsStorage: TX queue with wait-for-channel and delivery confirmation (response PDU / ACK, resend prompt on timeout), ISR-side block assembly with layout auto-detection (confirmed/unconfirmed, rate-1/2 and full 18-byte rate-3/4 blocks), delivery reports to senders that request them, and a checksummed message store in SPI flash at 0xC00000 (inbox + sent, 8 each; 8 quick texts). - UI: Messages hub (main menu or long-press GREEN), keypad compose with multi-tap (160 chars), inbox/sent/view with reply-resend-delete, quick-text picker/editor, options (Wait for ACK, My ID only), and an incoming-message popup with chime. Strings in all 20 languages. The HR-C6000 does the BPTC/FEC; the driver streams 12-byte logical bursts through the existing TX state machine (repeater wake included), locks out PTT during a send, and leaves hotspot mode untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
366 lines
9.4 KiB
C
366 lines
9.4 KiB
C
/*
|
|
* Copyright (C) 2026-today Marcus Kida, DK1DA
|
|
*
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Message list (inbox and sent box) and single-message view.
|
|
* One implementation serves MENU_SMS_INBOX, MENU_SMS_SENT and MENU_SMS_VIEW.
|
|
*/
|
|
#include "user_interface/uiGlobals.h"
|
|
#include "user_interface/menuSystem.h"
|
|
#include "user_interface/uiLocalisation.h"
|
|
#include "user_interface/uiUtilities.h"
|
|
#include "functions/smsCore.h"
|
|
#include "functions/smsStorage.h"
|
|
|
|
#define TEXT_CHARS_PER_LINE (DISPLAY_SIZE_X / 8)
|
|
#define VIEW_TEXT_LINES ((DISPLAY_SIZE_Y / FONT_SIZE_3_HEIGHT) - 2) // minus title and sender line
|
|
|
|
static bool viewIsSentBox = false;
|
|
static uint8_t viewIndex = 0;
|
|
static int viewScrollLine = 0;
|
|
|
|
static menuStatus_t menuSMSListExitCode = MENU_STATUS_SUCCESS;
|
|
|
|
static void updateListScreen(bool isSentBox);
|
|
static void updateViewScreen(void);
|
|
static void handleListEvent(uiEvent_t *ev, bool isSentBox);
|
|
static void handleViewEvent(uiEvent_t *ev);
|
|
static bool deleteMessageCallback(void);
|
|
|
|
void menuSMSListSetViewTarget(bool sentBox, uint8_t recordIndex)
|
|
{
|
|
viewIsSentBox = sentBox;
|
|
viewIndex = recordIndex;
|
|
viewScrollLine = 0;
|
|
}
|
|
|
|
static const smsRecord_t *getRecord(bool isSentBox, uint8_t index)
|
|
{
|
|
return (isSentBox ? smsSentGet(index) : smsInboxGet(index));
|
|
}
|
|
|
|
static uint8_t getRecordCount(bool isSentBox)
|
|
{
|
|
return (isSentBox ? smsSentCount() : smsInboxCount());
|
|
}
|
|
|
|
static void peerNameLookup(uint32_t peerId, char *buffer, size_t bufferLen)
|
|
{
|
|
char nameBuf[SCREEN_LINE_BUFFER_SIZE];
|
|
|
|
if (contactIDLookup(peerId, CONTACT_CALLTYPE_PC, nameBuf) == false)
|
|
{
|
|
dmrIdDataStruct_t record;
|
|
|
|
if (dmrIDLookup(peerId, &record))
|
|
{
|
|
strncpy(nameBuf, record.text, sizeof(nameBuf) - 1);
|
|
nameBuf[sizeof(nameBuf) - 1] = 0;
|
|
}
|
|
else
|
|
{
|
|
snprintf(nameBuf, sizeof(nameBuf), "%u", (unsigned int)peerId);
|
|
}
|
|
}
|
|
|
|
// only keep the callsign / first word for list display
|
|
char *space = strchr(nameBuf, ' ');
|
|
|
|
if (space != NULL)
|
|
{
|
|
*space = 0;
|
|
}
|
|
|
|
strncpy(buffer, nameBuf, bufferLen - 1);
|
|
buffer[bufferLen - 1] = 0;
|
|
}
|
|
|
|
menuStatus_t menuSMSList(uiEvent_t *ev, bool isFirstRun)
|
|
{
|
|
int currentMenu = menuSystemGetCurrentMenuNumber();
|
|
bool isView = (currentMenu == MENU_SMS_VIEW);
|
|
bool isSentBox = ((currentMenu == MENU_SMS_SENT) || (isView && viewIsSentBox));
|
|
|
|
if (isFirstRun)
|
|
{
|
|
if (isView)
|
|
{
|
|
if (viewIndex >= getRecordCount(viewIsSentBox))
|
|
{
|
|
menuSystemPopPreviousMenu();
|
|
return MENU_STATUS_SUCCESS;
|
|
}
|
|
|
|
if (viewIsSentBox == false)
|
|
{
|
|
smsInboxMarkRead(viewIndex);
|
|
}
|
|
|
|
updateViewScreen();
|
|
}
|
|
else
|
|
{
|
|
menuDataGlobal.numItems = getRecordCount(isSentBox);
|
|
menuDataGlobal.currentItemIndex = 0;
|
|
updateListScreen(isSentBox);
|
|
}
|
|
|
|
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
|
|
}
|
|
|
|
menuSMSListExitCode = MENU_STATUS_SUCCESS;
|
|
|
|
if (isView)
|
|
{
|
|
// the record can vanish underneath us (deleted via the confirm box)
|
|
if (viewIndex >= getRecordCount(viewIsSentBox))
|
|
{
|
|
menuSystemPopPreviousMenu();
|
|
return MENU_STATUS_SUCCESS;
|
|
}
|
|
|
|
if (ev->hasEvent)
|
|
{
|
|
handleViewEvent(ev);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ev->hasEvent)
|
|
{
|
|
handleListEvent(ev, isSentBox);
|
|
}
|
|
}
|
|
|
|
return menuSMSListExitCode;
|
|
}
|
|
|
|
static void updateListScreen(bool isSentBox)
|
|
{
|
|
char entryBuffer[SCREEN_LINE_BUFFER_SIZE * 2];
|
|
char nameBuffer[SCREEN_LINE_BUFFER_SIZE];
|
|
int mNum;
|
|
|
|
displayClearBuf();
|
|
menuDisplayTitle(isSentBox ? currentLanguage->sent_box : currentLanguage->inbox);
|
|
|
|
if (menuDataGlobal.numItems == 0)
|
|
{
|
|
displayPrintCentered(DISPLAY_SIZE_Y / 2, currentLanguage->none, FONT_SIZE_3);
|
|
}
|
|
|
|
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
|
|
{
|
|
if (menuDataGlobal.numItems == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
mNum = menuGetMenuOffset(menuDataGlobal.numItems, i);
|
|
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
|
|
{
|
|
continue;
|
|
}
|
|
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const smsRecord_t *record = getRecord(isSentBox, (uint8_t)mNum);
|
|
|
|
if (record == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
peerNameLookup(record->peerId, nameBuffer, sizeof(nameBuffer));
|
|
snprintf(entryBuffer, sizeof(entryBuffer), "%s%s:%s",
|
|
((record->flags & SMS_RECORD_FLAG_UNREAD) ? "*" : ""), nameBuffer, record->text);
|
|
|
|
menuDisplayEntry(i, mNum, entryBuffer, 0, THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
|
}
|
|
|
|
displayRender();
|
|
}
|
|
|
|
static void updateViewScreen(void)
|
|
{
|
|
char lineBuffer[TEXT_CHARS_PER_LINE + 1];
|
|
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
|
|
|
if (record == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
displayClearBuf();
|
|
|
|
peerNameLookup(record->peerId, lineBuffer, sizeof(lineBuffer));
|
|
menuDisplayTitle(lineBuffer);
|
|
|
|
snprintf(lineBuffer, sizeof(lineBuffer), "%s%u", (viewIsSentBox ? ">" : "<"), (unsigned int)record->peerId);
|
|
displayPrintAt(0, FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
|
|
|
|
int textLength = strlen(record->text);
|
|
int totalLines = (textLength + TEXT_CHARS_PER_LINE - 1) / TEXT_CHARS_PER_LINE;
|
|
|
|
if (viewScrollLine > (totalLines - VIEW_TEXT_LINES))
|
|
{
|
|
viewScrollLine = totalLines - VIEW_TEXT_LINES;
|
|
}
|
|
|
|
if (viewScrollLine < 0)
|
|
{
|
|
viewScrollLine = 0;
|
|
}
|
|
|
|
for (int line = 0; line < VIEW_TEXT_LINES; line++)
|
|
{
|
|
int offset = (viewScrollLine + line) * TEXT_CHARS_PER_LINE;
|
|
|
|
if (offset >= textLength)
|
|
{
|
|
break;
|
|
}
|
|
|
|
int chunk = textLength - offset;
|
|
|
|
if (chunk > TEXT_CHARS_PER_LINE)
|
|
{
|
|
chunk = TEXT_CHARS_PER_LINE;
|
|
}
|
|
|
|
memcpy(lineBuffer, &record->text[offset], chunk);
|
|
lineBuffer[chunk] = 0;
|
|
displayPrintAt(0, (FONT_SIZE_3_HEIGHT * 2) + (line * FONT_SIZE_3_HEIGHT), lineBuffer, FONT_SIZE_3);
|
|
}
|
|
|
|
displayRender();
|
|
}
|
|
|
|
static void handleListEvent(uiEvent_t *ev, bool isSentBox)
|
|
{
|
|
if ((ev->events & KEY_EVENT) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
|
{
|
|
menuSystemPopPreviousMenu();
|
|
return;
|
|
}
|
|
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
|
{
|
|
if (menuDataGlobal.numItems > 0)
|
|
{
|
|
menuSMSListSetViewTarget(isSentBox, (uint8_t)menuDataGlobal.currentItemIndex);
|
|
menuSystemPushNewMenu(MENU_SMS_VIEW);
|
|
}
|
|
return;
|
|
}
|
|
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
|
{
|
|
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
|
updateListScreen(isSentBox);
|
|
}
|
|
else if (KEYCHECK_PRESS(ev->keys, KEY_UP))
|
|
{
|
|
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
|
updateListScreen(isSentBox);
|
|
}
|
|
}
|
|
|
|
static bool deleteMessageCallback(void)
|
|
{
|
|
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
|
{
|
|
if (viewIsSentBox)
|
|
{
|
|
smsSentDelete(viewIndex);
|
|
}
|
|
else
|
|
{
|
|
smsInboxDelete(viewIndex);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void handleViewEvent(uiEvent_t *ev)
|
|
{
|
|
if ((ev->events & KEY_EVENT) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
|
{
|
|
if (BUTTONCHECK_DOWN(ev, BUTTON_SK2))
|
|
{
|
|
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
|
|
|
if (record != NULL)
|
|
{
|
|
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s", currentLanguage->delete_message);
|
|
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
|
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
|
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
|
uiDataGlobal.MessageBox.validatorCallback = deleteMessageCallback;
|
|
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
menuSystemPopPreviousMenu();
|
|
}
|
|
return;
|
|
}
|
|
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
|
{
|
|
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
|
|
|
if (record != NULL)
|
|
{
|
|
// inbox: reply to the sender; sent box: re-edit and resend
|
|
menuSMSComposePrefill(record->peerId, (viewIsSentBox ? record->text : NULL));
|
|
menuSystemPushNewMenu(MENU_SMS_COMPOSE);
|
|
}
|
|
return;
|
|
}
|
|
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
|
{
|
|
viewScrollLine++;
|
|
updateViewScreen();
|
|
}
|
|
else if (KEYCHECK_PRESS(ev->keys, KEY_UP))
|
|
{
|
|
viewScrollLine--;
|
|
updateViewScreen();
|
|
}
|
|
}
|