FreeTRX/MDUV380_firmware/application/source/user_interface/menuSMSCompose.c
Marcus Kida 31c31df861 DMR text messages (SMS): ETSI/Motorola-TMS messaging on the radio
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>
2026-07-13 16:52:54 +02:00

317 lines
8.3 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.
*
*/
#include <stdlib.h>
#include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h"
#include "user_interface/uiUtilities.h"
#include "io/keyboard.h"
#include "functions/smsCore.h"
#include "functions/smsStorage.h"
#define DEST_DIGITS_MAX 8 // 16777215 is the largest 24-bit DMR ID
#define TEXT_CHARS_PER_LINE (DISPLAY_SIZE_X / 8)
#define TEXT_VISIBLE_LINES 5
#define TEXT_AREA_Y (FONT_SIZE_3_HEIGHT * 2)
enum { FIELD_DESTINATION = 0, FIELD_TEXT };
static char composeText[SMS_MAX_TEXT_LENGTH + 1];
static char destDigits[DEST_DIGITS_MAX + 1];
static int textPos = 0;
static int focusedField = FIELD_TEXT;
static int quickTextEditSlot = -1;
// prefill is written straight into the compose buffers, consumed on first run
static bool prefillPending = false;
static int pendingQuickTextEditSlot = -1;
static menuStatus_t menuSMSComposeExitCode = MENU_STATUS_SUCCESS;
static void updateScreen(void);
static void handleEvent(uiEvent_t *ev);
void menuSMSComposePrefill(uint32_t dstId, const char *text)
{
destDigits[0] = 0;
composeText[0] = 0;
if (dstId != 0)
{
snprintf(destDigits, sizeof(destDigits), "%u", (unsigned int)dstId);
}
if (text != NULL)
{
strncpy(composeText, text, SMS_MAX_TEXT_LENGTH);
composeText[SMS_MAX_TEXT_LENGTH] = 0;
}
prefillPending = true;
}
void menuSMSComposeSetQuickTextEditTarget(int slotIndex)
{
pendingQuickTextEditSlot = slotIndex;
}
menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
quickTextEditSlot = pendingQuickTextEditSlot;
pendingQuickTextEditSlot = -1;
if (quickTextEditSlot >= 0)
{
const char *quickText = smsQuickTextGet(quickTextEditSlot);
composeText[0] = 0;
destDigits[0] = 0;
if (quickText != NULL)
{
strncpy(composeText, quickText, SMS_MAX_TEXT_LENGTH);
composeText[SMS_MAX_TEXT_LENGTH] = 0;
}
focusedField = FIELD_TEXT;
}
else
{
if (prefillPending == false)
{
composeText[0] = 0;
destDigits[0] = 0;
}
focusedField = ((destDigits[0] == 0) ? FIELD_DESTINATION : FIELD_TEXT);
}
textPos = strlen(composeText);
prefillPending = false;
updateScreen();
return (MENU_STATUS_INPUT_TYPE | MENU_STATUS_SUCCESS);
}
menuSMSComposeExitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
handleEvent(ev);
}
return menuSMSComposeExitCode;
}
static void updateScreen(void)
{
char lineBuffer[TEXT_CHARS_PER_LINE + 1];
displayClearBuf();
menuDisplayTitle((quickTextEditSlot >= 0) ? currentLanguage->quick_text : currentLanguage->new_message);
keypadAlphaEnable = (focusedField == FIELD_TEXT);
// destination line (hidden when editing a quick text template)
if (quickTextEditSlot < 0)
{
snprintf(lineBuffer, sizeof(lineBuffer), "%s:%s%s",
currentLanguage->send_to, destDigits,
((focusedField == FIELD_DESTINATION) ? "_" : ""));
displayPrintAt(0, FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
}
// text area with a scrolling window that follows the cursor
int textLength = strlen(composeText);
int cursorLine = textPos / TEXT_CHARS_PER_LINE;
int firstLine = ((cursorLine >= TEXT_VISIBLE_LINES) ? (cursorLine - TEXT_VISIBLE_LINES + 1) : 0);
for (int line = 0; line < TEXT_VISIBLE_LINES; line++)
{
int offset = (firstLine + 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, &composeText[offset], chunk);
lineBuffer[chunk] = 0;
displayPrintAt(0, TEXT_AREA_Y + (line * FONT_SIZE_3_HEIGHT), lineBuffer, FONT_SIZE_3);
}
if (focusedField == FIELD_TEXT)
{
int cursorX = (textPos % TEXT_CHARS_PER_LINE) * 8;
int cursorY = TEXT_AREA_Y + ((cursorLine - firstLine + 1) * FONT_SIZE_3_HEIGHT) - 2;
displayDrawFastHLine(cursorX, cursorY, 8, true);
}
// footer: remaining characters
snprintf(lineBuffer, sizeof(lineBuffer), "%d", SMS_MAX_TEXT_LENGTH - textLength);
displayPrintAt(DISPLAY_SIZE_X - (strlen(lineBuffer) * 8), DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
displayRender();
}
static void sendOrSave(void)
{
if (quickTextEditSlot >= 0)
{
if (composeText[0] != 0)
{
smsQuickTextSet(quickTextEditSlot, composeText);
keypadAlphaEnable = false;
menuSystemPopPreviousMenu();
}
else
{
menuSMSComposeExitCode |= MENU_STATUS_ERROR;
}
return;
}
uint32_t dstId = (uint32_t)strtoul(destDigits, NULL, 10);
if ((dstId == 0) || (composeText[0] == 0))
{
menuSMSComposeExitCode |= MENU_STATUS_ERROR;
return;
}
if (smsSendMessage(dstId, composeText))
{
keypadAlphaEnable = false;
menuSystemPopAllAndDisplayRootMenu(); // progress is reported via notifications
}
else
{
menuSMSComposeExitCode |= MENU_STATUS_ERROR; // busy or not in DMR mode
}
}
static void handleEvent(uiEvent_t *ev)
{
if (ev->events & KEY_EVENT)
{
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
keypadAlphaEnable = false;
menuSystemPopPreviousMenu();
return;
}
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
sendOrSave();
return;
}
else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0))
{
focusedField = FIELD_DESTINATION;
updateScreen();
return;
}
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
focusedField = FIELD_TEXT;
updateScreen();
return;
}
if (focusedField == FIELD_DESTINATION)
{
if (KEYCHECK_PRESS(ev->keys, KEY_LEFT))
{
int len = strlen(destDigits);
if (len > 0)
{
destDigits[len - 1] = 0;
}
updateScreen();
}
else
{
int keyval = menuGetKeypadKeyValue(ev, true);
if ((keyval != 99) && (strlen(destDigits) < DEST_DIGITS_MAX))
{
char digit[2] = { (char)('0' + keyval), 0 };
strcat(destDigits, digit);
updateScreen();
}
}
}
else // FIELD_TEXT
{
if (KEYCHECK_PRESS(ev->keys, KEY_LEFT))
{
moveCursorLeftInString(composeText, &textPos, BUTTONCHECK_DOWN(ev, BUTTON_SK2));
updateScreen();
}
else if (KEYCHECK_PRESS(ev->keys, KEY_RIGHT))
{
moveCursorRightInString(composeText, &textPos, SMS_MAX_TEXT_LENGTH, BUTTONCHECK_DOWN(ev, BUTTON_SK2));
updateScreen();
}
else if ((ev->keys.event & (KEY_MOD_PREVIEW | KEY_MOD_PRESS)) && (ev->keys.key >= 32) && (ev->keys.key <= 126))
{
if (textPos < SMS_MAX_TEXT_LENGTH)
{
bool atEnd = (composeText[textPos] == 0);
composeText[textPos] = ev->keys.key;
if (atEnd)
{
composeText[textPos + 1] = 0;
}
if ((ev->keys.event & KEY_MOD_PRESS) && (textPos < (SMS_MAX_TEXT_LENGTH - 1)))
{
textPos++;
}
updateScreen();
}
}
}
}
}