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
Коммит b5a8e92e45
32 изменённых файлов: 3812 добавлений и 0 удалений
+255
Просмотреть файл
@@ -0,0 +1,255 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Assembling received DMR packet data (SMS) while the radio is used as a radio.
*
* Wire format (confirmed against a working reference implementation):
*
* Data header (12 bytes)
* [0] bit7 G/I (0 = private), bit6 A (response requested), bits3..0 DPF
* DPF 2 = unconfirmed rate 1/2, DPF 3 = confirmed rate 1/2
* [1] bits7..4 SAP (4 = IP/UDP data), bits3..0 number of pad octets
* [2..4] destination id, big endian
* [5..7] source id, big endian
* [8] bit7 full message flag, bits6..0 blocks to follow
* [9] header class/type
* [10..11] header CRC-16
*
* Blocks (12 bytes each)
* confirmed: [0..1] DBSN + CRC-9, [2..11] ten payload bytes
* unconfirmed: all twelve bytes are payload
*
* Reassembled payload: IPv4 (20) + UDP (8) + TMS. The text sits right behind the
* TMS marker 04 0D 00 0A 00 and is UTF-16 LITTLE endian, terminated by 0x0000.
*
* 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 <string.h>
#include "functions/sms.h"
#include "functions/hotspot.h" // DT_* data type values
#include "functions/ticks.h"
#define SMS_BLOCK_LENGTH 12U
#define SMS_DPF_UNCONFIRMED 2U
#define SMS_DPF_CONFIRMED 3U
// Bursts arrive every 60 ms; this is generously above any realistic gap between them.
#define SMS_ASSEMBLY_TIMEOUT_MS 3000U
// Marks the start of the text inside the TMS payload.
static const uint8_t TMS_TEXT_MARKER[] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 };
typedef enum
{
SMS_RX_IDLE = 0,
SMS_RX_COLLECTING
} smsRxState_t;
// Filled from the interrupt, drained by smsTick().
static volatile uint8_t rxQueue[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH];
static volatile uint8_t rxQueueType[SMS_MAX_BLOCKS];
static volatile uint8_t rxQueueWrite = 0;
static volatile uint8_t rxQueueRead = 0;
static smsRxState_t rxState = SMS_RX_IDLE;
static uint8_t header[SMS_BLOCK_LENGTH];
static uint8_t blocks[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH];
static uint8_t blocksExpected = 0;
static uint8_t blocksCollected = 0;
static uint32_t assemblyStartedAt = 0;
static smsMessage_t latest;
static bool haveMessage = false;
static bool unread = false;
void smsInit(void)
{
rxQueueWrite = rxQueueRead = 0;
rxState = SMS_RX_IDLE;
blocksExpected = blocksCollected = 0;
haveMessage = false;
unread = false;
memset(&latest, 0, sizeof(latest));
}
void smsRxHandleBurst(const uint8_t *payload, uint8_t dataType)
{
uint8_t next = (uint8_t)((rxQueueWrite + 1) % SMS_MAX_BLOCKS);
if (next == rxQueueRead)
{
return;// queue full: drop, the partial message will time out
}
memcpy((uint8_t *)rxQueue[rxQueueWrite], payload, SMS_BLOCK_LENGTH);
rxQueueType[rxQueueWrite] = dataType;
rxQueueWrite = next;
}
static void abortAssembly(void)
{
rxState = SMS_RX_IDLE;
blocksExpected = blocksCollected = 0;
}
static void startAssembly(const uint8_t *hdr)
{
uint8_t dpf = (uint8_t)(hdr[0] & 0x0FU);
if ((dpf != SMS_DPF_CONFIRMED) && (dpf != SMS_DPF_UNCONFIRMED))
{
abortAssembly();// not a packet data message we can assemble
return;
}
memcpy(header, hdr, SMS_BLOCK_LENGTH);
blocksExpected = (uint8_t)(hdr[8] & 0x7FU);
blocksCollected = 0;
assemblyStartedAt = ticksGetMillis();
if ((blocksExpected == 0) || (blocksExpected > SMS_MAX_BLOCKS))
{
abortAssembly();
return;
}
rxState = SMS_RX_COLLECTING;
}
static void finishAssembly(void)
{
uint8_t data[SMS_MAX_BLOCKS * SMS_BLOCK_LENGTH];
uint16_t length = 0;
uint8_t dpf = (uint8_t)(header[0] & 0x0FU);
// Confirmed blocks carry two bytes of DBSN + CRC-9 before the payload.
uint8_t skip = (dpf == SMS_DPF_CONFIRMED) ? 2U : 0U;
int textStart = -1;
for (uint8_t i = 0; i < blocksCollected; i++)
{
memcpy(&data[length], &blocks[i][skip], (SMS_BLOCK_LENGTH - skip));
length = (uint16_t)(length + (SMS_BLOCK_LENGTH - skip));
}
// Locate the text rather than counting header bytes: the IP/UDP wrapper varies,
// the marker does not. Take the last match, so a marker-like byte pattern earlier
// in the wrapper cannot fool us.
for (int i = 0; (i + (int)sizeof(TMS_TEXT_MARKER)) <= (int)length; i++)
{
if (memcmp(&data[i], TMS_TEXT_MARKER, sizeof(TMS_TEXT_MARKER)) == 0)
{
textStart = i + (int)sizeof(TMS_TEXT_MARKER);
}
}
if (textStart < 0)
{
abortAssembly();// not a text message (could be GPS or another TMS payload)
return;
}
uint16_t t = 0;
for (int i = textStart; ((i + 1) < (int)length) && (t < SMS_MAX_TEXT_LENGTH); i += 2)
{
uint16_t ch = (uint16_t)(data[i] | (data[i + 1] << 8));// UTF-16 little endian
if (ch == 0)
{
break;
}
// Latin only for now: anything outside ASCII needs the Cyrillic font work.
latest.text[t++] = (ch < 0x80U) ? (char)ch : '?';
}
latest.text[t] = '\0';
latest.dstId = (uint32_t)((header[2] << 16) | (header[3] << 8) | header[4]);
latest.srcId = (uint32_t)((header[5] << 16) | (header[6] << 8) | header[7]);
latest.isPrivate = ((header[0] & 0x80U) == 0);
latest.unread = true;
haveMessage = (t > 0);
unread = haveMessage;
abortAssembly();
}
void smsTick(void)
{
while (rxQueueRead != rxQueueWrite)
{
uint8_t burst[SMS_BLOCK_LENGTH];
uint8_t type = rxQueueType[rxQueueRead];
memcpy(burst, (uint8_t *)rxQueue[rxQueueRead], SMS_BLOCK_LENGTH);
rxQueueRead = (uint8_t)((rxQueueRead + 1) % SMS_MAX_BLOCKS);
switch (type)
{
case DT_DATA_HEADER:
startAssembly(burst);
break;
case DT_RATE_12_DATA:
if (rxState == SMS_RX_COLLECTING)
{
if (blocksCollected < SMS_MAX_BLOCKS)
{
memcpy(blocks[blocksCollected], burst, SMS_BLOCK_LENGTH);
blocksCollected++;
}
if (blocksCollected >= blocksExpected)
{
finishAssembly();
}
}
break;
default:
break;// CSBK preambles and everything else
}
}
if ((rxState == SMS_RX_COLLECTING) &&
((ticksGetMillis() - assemblyStartedAt) > SMS_ASSEMBLY_TIMEOUT_MS))
{
abortAssembly();// sender stopped mid message, or bursts were lost
}
}
const smsMessage_t *smsGetLatest(void)
{
return (haveMessage ? &latest : NULL);
}
bool smsHasUnread(void)
{
return unread;
}
void smsMarkRead(void)
{
unread = false;
latest.unread = false;
}
+265
Просмотреть файл
@@ -0,0 +1,265 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Building an outgoing DMR text message: TMS payload, IP/UDP wrapper, message CRC,
* data header and the rate 1/2 blocks that carry it.
*
* This is a literal translation of a reference implementation that was verified on a PC
* against a known-good packet produced by the server side, byte for byte (including both
* IP/UDP checksums and the message CRC-32).
*
* 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 <string.h>
#include "functions/sms.h"
#define TMS_HEADER_OVERHEAD 8U // flag + 0x00 + seq + the five marker bytes
#define IP_HEADER_LENGTH 20U
#define UDP_HEADER_LENGTH 8U
#define TMS_PORT 0x0FA7U
static const uint8_t TMS_MARKER[5] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 };
/*
* ETSI / MOTOTRBO message CRC-32: bytes are swapped within each 16 bit pair, then an
* MSB-first CRC32 (poly 0x04C11DB7, init 0, no final xor) is run over the result.
*/
static uint32_t smsMessageCRC32(const uint8_t *data, uint16_t length)
{
uint32_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
// consume the bytes in swapped-pair order without copying the buffer
uint16_t idx = ((i & 1U) ? (i - 1U) : (i + 1U));
uint8_t b = ((idx < length) ? data[idx] : 0x00);
crc ^= ((uint32_t)b << 24);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x80000000U) ? ((crc << 1) ^ 0x04C11DB7U) : (crc << 1));
}
}
return crc;
}
static uint16_t onesComplementSum(const uint8_t *data, uint16_t length, uint32_t initial)
{
uint32_t sum = initial;
for (uint16_t i = 0; (i + 1U) < length; i += 2U)
{
sum += (uint32_t)((data[i] << 8) | data[i + 1U]);
}
if (length & 1U)
{
sum += (uint32_t)(data[length - 1U] << 8);
}
while (sum >> 16)
{
sum = (sum & 0xFFFFU) + (sum >> 16);
}
return (uint16_t)(~sum);
}
static uint16_t smsCRC16CCITT(const uint8_t *data, uint16_t length)
{
uint16_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
crc ^= ((uint16_t)data[i] << 8);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x8000U) ? ((crc << 1) ^ 0x1021U) : (crc << 1));
}
}
return crc;
}
uint16_t smsBuildPacket(uint8_t *out, uint16_t outSize, uint32_t srcId, uint32_t dstId,
bool isGroup, const char *text, uint8_t flag, uint8_t seq, uint8_t *padOut)
{
uint16_t textLen = (uint16_t)strlen(text);
uint16_t tmsLen = (uint16_t)(TMS_HEADER_OVERHEAD + (2U * textLen));
uint16_t udpLen = (uint16_t)(UDP_HEADER_LENGTH + 2U + tmsLen);// 2 = the TMS length field
uint16_t ipLen = (uint16_t)(IP_HEADER_LENGTH + udpLen);
uint32_t srcIp = (0x0CU << 24) | (srcId & 0x00FFFFFFU);
uint32_t dstIp = (((uint32_t)(isGroup ? 0xE1U : 0x0CU)) << 24) | (dstId & 0x00FFFFFFU);
uint16_t pos = 0;
if ((uint16_t)(ipLen + 12U + 4U) > outSize)
{
return 0;// would not fit
}
// ---- IP header (checksum filled in afterwards) ----
out[pos++] = 0x45;
out[pos++] = 0x00;
out[pos++] = (uint8_t)(ipLen >> 8);
out[pos++] = (uint8_t)(ipLen & 0xFF);
out[pos++] = 0x00;
out[pos++] = 0x01; // identification
out[pos++] = 0x00;
out[pos++] = 0x00;
out[pos++] = (isGroup ? 0x01 : 0x40); // TTL
out[pos++] = 0x11; // UDP
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(srcIp >> 24);
out[pos++] = (uint8_t)(srcIp >> 16);
out[pos++] = (uint8_t)(srcIp >> 8);
out[pos++] = (uint8_t)(srcIp);
out[pos++] = (uint8_t)(dstIp >> 24);
out[pos++] = (uint8_t)(dstIp >> 16);
out[pos++] = (uint8_t)(dstIp >> 8);
out[pos++] = (uint8_t)(dstIp);
uint16_t ipChecksum = onesComplementSum(out, IP_HEADER_LENGTH, 0);
out[10] = (uint8_t)(ipChecksum >> 8);
out[11] = (uint8_t)(ipChecksum & 0xFF);
// ---- UDP header + payload ----
uint16_t udpStart = pos;
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(udpLen >> 8);
out[pos++] = (uint8_t)(udpLen & 0xFF);
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(tmsLen >> 8);
out[pos++] = (uint8_t)(tmsLen & 0xFF);
out[pos++] = flag;
out[pos++] = 0x00;
out[pos++] = seq;
memcpy(&out[pos], TMS_MARKER, sizeof(TMS_MARKER));
pos += sizeof(TMS_MARKER);
for (uint16_t i = 0; i < textLen; i++)
{
out[pos++] = (uint8_t)text[i]; // UTF-16 little endian, ASCII only for now
out[pos++] = 0x00;
}
// UDP checksum covers a pseudo header plus the whole UDP segment
{
uint8_t pseudo[12];
pseudo[0] = (uint8_t)(srcIp >> 24); pseudo[1] = (uint8_t)(srcIp >> 16);
pseudo[2] = (uint8_t)(srcIp >> 8); pseudo[3] = (uint8_t)(srcIp);
pseudo[4] = (uint8_t)(dstIp >> 24); pseudo[5] = (uint8_t)(dstIp >> 16);
pseudo[6] = (uint8_t)(dstIp >> 8); pseudo[7] = (uint8_t)(dstIp);
pseudo[8] = 0x00; pseudo[9] = 0x11;
pseudo[10] = (uint8_t)(udpLen >> 8); pseudo[11] = (uint8_t)(udpLen & 0xFF);
uint32_t sum = 0;
for (uint8_t i = 0; i < sizeof(pseudo); i += 2U)
{
sum += (uint32_t)((pseudo[i] << 8) | pseudo[i + 1U]);
}
uint16_t udpChecksum = onesComplementSum(&out[udpStart], udpLen, sum);
out[udpStart + 6U] = (uint8_t)(udpChecksum >> 8);
out[udpStart + 7U] = (uint8_t)(udpChecksum & 0xFF);
}
// ---- pad to a multiple of 12, then append the message CRC as its own tail ----
uint8_t pad = (uint8_t)((12U - (pos % 12U)) % 12U);
for (uint8_t i = 0; i < pad; i++)
{
out[pos++] = 0x00;
}
uint32_t crc = smsMessageCRC32(out, pos);
out[pos++] = (uint8_t)(crc & 0xFF); // little endian
out[pos++] = (uint8_t)(crc >> 8);
out[pos++] = (uint8_t)(crc >> 16);
out[pos++] = (uint8_t)(crc >> 24);
if (padOut != NULL)
{
*padOut = pad;
}
return pos;
}
void smsBuildDataHeader(uint8_t *header, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t blocks, uint8_t pad)
{
memset(header, 0, 12);
header[0] = (uint8_t)((isGroup ? 0x80U : 0x00U) | 0x02U);// DPF = 2, unconfirmed
header[1] = (uint8_t)(0x40U | (pad & 0x0FU)); // SAP = 4 (IP based data)
header[2] = (uint8_t)(dstId >> 16);
header[3] = (uint8_t)(dstId >> 8);
header[4] = (uint8_t)(dstId);
header[5] = (uint8_t)(srcId >> 16);
header[6] = (uint8_t)(srcId >> 8);
header[7] = (uint8_t)(srcId);
header[8] = (uint8_t)(0x80U | (blocks & 0x7FU));
header[9] = 0x10;
uint16_t crc = (uint16_t)(smsCRC16CCITT(header, 10) ^ 0xCCCCU);
header[10] = (uint8_t)(crc >> 8);
header[11] = (uint8_t)(crc & 0xFF);
}
void smsBuildPreamble(uint8_t *preamble, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t countdown)
{
memset(preamble, 0, 12);
preamble[0] = 0xBD; // Preamble CSBK, LB set
preamble[1] = 0x00;
preamble[2] = (uint8_t)(isGroup ? 0x80U : 0x00U); // data content preamble
preamble[3] = countdown;
preamble[4] = (uint8_t)(dstId >> 16);
preamble[5] = (uint8_t)(dstId >> 8);
preamble[6] = (uint8_t)(dstId);
preamble[7] = (uint8_t)(srcId >> 16);
preamble[8] = (uint8_t)(srcId >> 8);
preamble[9] = (uint8_t)(srcId);
uint16_t crc = (uint16_t)(smsCRC16CCITT(preamble, 10) ^ 0xA5A5U);
preamble[10] = (uint8_t)(crc >> 8);
preamble[11] = (uint8_t)(crc & 0xFF);
}
+408
Просмотреть файл
@@ -0,0 +1,408 @@
/*
* 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;
}