DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2026 OpenGD77 contributors
|
||||
*
|
||||
* Receiving DMR packet data (SMS) while the radio is used as a radio, rather than
|
||||
* as a hotspot modem.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#ifndef _OPENGD77_SMS_H_
|
||||
#define _OPENGD77_SMS_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// A single received message, as shown to the user.
|
||||
#define SMS_MAX_TEXT_LENGTH 160
|
||||
// Longest message we are willing to assemble. Each rate 1/2 block carries 10 bytes of
|
||||
// payload once the confirmed-mode overhead is removed, so this covers a full 160 char
|
||||
// message with room for the IP/UDP/TMS wrapper around it.
|
||||
#define SMS_MAX_BLOCKS 32
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t srcId;
|
||||
uint32_t dstId;
|
||||
bool isPrivate;
|
||||
bool unread;
|
||||
char text[SMS_MAX_TEXT_LENGTH + 1];
|
||||
} smsMessage_t;
|
||||
|
||||
/*
|
||||
* Called from the HR-C6000 interrupt for every received data burst while the radio is
|
||||
* NOT acting as a hotspot modem. Must stay short: it only copies the burst away and
|
||||
* flags it, all parsing happens later in smsTick().
|
||||
*
|
||||
* payload - the 12 byte (96 bit) burst payload as read from the chip
|
||||
* dataType - DMR data type of the burst (DT_CSBK / DT_DATA_HEADER / DT_RATE_12_DATA)
|
||||
*/
|
||||
void smsRxHandleBurst(const uint8_t *payload, uint8_t dataType);
|
||||
|
||||
/*
|
||||
* Runs from normal task context. Turns collected bursts into a message and, when one
|
||||
* completes, stores it and raises smsHasUnread().
|
||||
*/
|
||||
void smsTick(void);
|
||||
|
||||
void smsInit(void);
|
||||
|
||||
// Most recently received message, or NULL when nothing has been received yet.
|
||||
const smsMessage_t *smsGetLatest(void);
|
||||
|
||||
bool smsHasUnread(void);
|
||||
void smsMarkRead(void);
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Ссылка в новой задаче
Block a user