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>
323 lines
8.9 KiB
C
323 lines
8.9 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 <string.h>
|
|
#include "functions/smsStorage.h"
|
|
#include "functions/smsCore.h"
|
|
#include "functions/ticks.h"
|
|
#include "functions/trx.h"
|
|
#include "hardware/SPI_Flash.h"
|
|
#include "hardware/HR-C6000.h"
|
|
|
|
#define SMS_STORAGE_MAGIC 0x4D53444F // 'ODSM'
|
|
#define SMS_STORAGE_VERSION 2
|
|
#define SMS_STORAGE_DEBOUNCE_MS 1500
|
|
|
|
typedef struct __attribute__((packed))
|
|
{
|
|
uint32_t magic;
|
|
uint16_t version;
|
|
uint16_t imageSize;
|
|
uint8_t inboxCount;
|
|
uint8_t sentCount;
|
|
uint8_t quickTextCount;
|
|
uint8_t options;
|
|
uint32_t writeCounter;
|
|
uint32_t crc; // over everything after this field
|
|
smsRecord_t inbox[SMS_INBOX_SLOTS];
|
|
smsRecord_t sent[SMS_SENT_SLOTS];
|
|
char quickTexts[SMS_QUICKTEXT_SLOTS][SMS_MAX_TEXT_LENGTH + 1];
|
|
} smsStorageImage_t;
|
|
|
|
// SPI_Flash_write() can span at most two 4KB sectors, and dmrDataCRC32()
|
|
// consumes byte-swapped pairs, so the image must stay <= 8KB and even sized
|
|
typedef char assert_smsImageFitsTwoSectors[(sizeof(smsStorageImage_t) <= 8192) ? 1 : -1];
|
|
typedef char assert_smsImageSizeIsEven[((sizeof(smsStorageImage_t) % 2) == 0) ? 1 : -1];
|
|
|
|
static __attribute__((section(".ccmram"))) smsStorageImage_t storageImage;
|
|
|
|
static bool storageDirty = false;
|
|
static ticksTimer_t debounceTimer = { 0, 0 };
|
|
|
|
static const char *defaultQuickTexts[] =
|
|
{
|
|
"QRZ?",
|
|
"QSL 73",
|
|
"ON MY WAY",
|
|
"CALL ME",
|
|
"BACK IN 5 MIN"
|
|
};
|
|
|
|
static uint32_t computeImageCRC(void)
|
|
{
|
|
const uint8_t *afterCRC = ((const uint8_t *)&storageImage.crc) + sizeof(storageImage.crc);
|
|
uint16_t length = sizeof(smsStorageImage_t) - (afterCRC - (const uint8_t *)&storageImage);
|
|
|
|
return dmrDataCRC32(afterCRC, length);
|
|
}
|
|
|
|
static void formatStorage(void)
|
|
{
|
|
memset(&storageImage, 0, sizeof(storageImage));
|
|
storageImage.magic = SMS_STORAGE_MAGIC;
|
|
storageImage.version = SMS_STORAGE_VERSION;
|
|
storageImage.imageSize = sizeof(smsStorageImage_t);
|
|
storageImage.options = (SMS_OPTION_WAIT_FOR_ACK | SMS_OPTION_OWN_ID_ONLY);
|
|
|
|
storageImage.quickTextCount = (sizeof(defaultQuickTexts) / sizeof(defaultQuickTexts[0]));
|
|
for (uint8_t i = 0; i < storageImage.quickTextCount; i++)
|
|
{
|
|
strncpy(storageImage.quickTexts[i], defaultQuickTexts[i], SMS_MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
storageDirty = true;
|
|
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
|
}
|
|
|
|
void smsStorageInit(void)
|
|
{
|
|
SPI_Flash_read(SMS_STORAGE_FLASH_ADDRESS, (uint8_t *)&storageImage, sizeof(storageImage));
|
|
|
|
if ((storageImage.magic != SMS_STORAGE_MAGIC) ||
|
|
(storageImage.version != SMS_STORAGE_VERSION) ||
|
|
(storageImage.imageSize != sizeof(smsStorageImage_t)) ||
|
|
(storageImage.crc != computeImageCRC()))
|
|
{
|
|
formatStorage();
|
|
}
|
|
}
|
|
|
|
void smsStorageMarkDirty(void)
|
|
{
|
|
storageDirty = true;
|
|
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
|
}
|
|
|
|
void smsStorageTick(void)
|
|
{
|
|
if ((storageDirty == false) || (ticksTimerHasExpired(&debounceTimer) == false))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Writing takes ~100ms (2 sector erases + programming), so keep it away
|
|
// from live DMR traffic and in-flight SMS work
|
|
if (trxTransmissionEnabled || trxIsTransmitting ||
|
|
((trxGetMode() == RADIO_MODE_DIGITAL) && (slotState != DMR_STATE_IDLE)) ||
|
|
(smsCoreIsIdle() == false))
|
|
{
|
|
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
|
return;
|
|
}
|
|
|
|
storageImage.writeCounter++;
|
|
storageImage.crc = computeImageCRC();
|
|
|
|
if (SPI_Flash_write(SMS_STORAGE_FLASH_ADDRESS, (uint8_t *)&storageImage, sizeof(storageImage)))
|
|
{
|
|
storageDirty = false;
|
|
}
|
|
else
|
|
{
|
|
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS); // retry later
|
|
}
|
|
}
|
|
|
|
//
|
|
// Generic record-list helpers shared by inbox and sent box
|
|
//
|
|
static bool recordListAdd(smsRecord_t *records, uint8_t slots, uint8_t *count, uint32_t peerId, const char *text, uint8_t flags)
|
|
{
|
|
if ((text == NULL) || (text[0] == 0))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// shift down, newest lives at index 0
|
|
uint8_t used = *count;
|
|
|
|
if (used > (slots - 1))
|
|
{
|
|
used = slots - 1;
|
|
}
|
|
|
|
memmove(&records[1], &records[0], used * sizeof(smsRecord_t));
|
|
|
|
records[0].peerId = peerId;
|
|
records[0].flags = flags;
|
|
strncpy(records[0].text, text, SMS_MAX_TEXT_LENGTH);
|
|
records[0].text[SMS_MAX_TEXT_LENGTH] = 0;
|
|
|
|
if (*count < slots)
|
|
{
|
|
(*count)++;
|
|
}
|
|
|
|
smsStorageMarkDirty();
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool recordListDelete(smsRecord_t *records, uint8_t *count, uint8_t index)
|
|
{
|
|
if (index >= *count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
memmove(&records[index], &records[index + 1], (*count - index - 1) * sizeof(smsRecord_t));
|
|
(*count)--;
|
|
memset(&records[*count], 0, sizeof(smsRecord_t));
|
|
|
|
smsStorageMarkDirty();
|
|
|
|
return true;
|
|
}
|
|
|
|
uint8_t smsInboxCount(void)
|
|
{
|
|
return storageImage.inboxCount;
|
|
}
|
|
|
|
const smsRecord_t *smsInboxGet(uint8_t index)
|
|
{
|
|
return ((index < storageImage.inboxCount) ? &storageImage.inbox[index] : NULL);
|
|
}
|
|
|
|
bool smsInboxAdd(uint32_t srcId, const char *text)
|
|
{
|
|
return recordListAdd(storageImage.inbox, SMS_INBOX_SLOTS, &storageImage.inboxCount, srcId, text, SMS_RECORD_FLAG_UNREAD);
|
|
}
|
|
|
|
bool smsInboxDelete(uint8_t index)
|
|
{
|
|
return recordListDelete(storageImage.inbox, &storageImage.inboxCount, index);
|
|
}
|
|
|
|
void smsInboxMarkRead(uint8_t index)
|
|
{
|
|
if ((index < storageImage.inboxCount) && (storageImage.inbox[index].flags & SMS_RECORD_FLAG_UNREAD))
|
|
{
|
|
storageImage.inbox[index].flags &= ~SMS_RECORD_FLAG_UNREAD;
|
|
smsStorageMarkDirty();
|
|
}
|
|
}
|
|
|
|
uint8_t smsInboxUnreadCount(void)
|
|
{
|
|
uint8_t unread = 0;
|
|
|
|
for (uint8_t i = 0; i < storageImage.inboxCount; i++)
|
|
{
|
|
if (storageImage.inbox[i].flags & SMS_RECORD_FLAG_UNREAD)
|
|
{
|
|
unread++;
|
|
}
|
|
}
|
|
|
|
return unread;
|
|
}
|
|
|
|
uint8_t smsSentCount(void)
|
|
{
|
|
return storageImage.sentCount;
|
|
}
|
|
|
|
const smsRecord_t *smsSentGet(uint8_t index)
|
|
{
|
|
return ((index < storageImage.sentCount) ? &storageImage.sent[index] : NULL);
|
|
}
|
|
|
|
bool smsSentAdd(uint32_t dstId, const char *text)
|
|
{
|
|
return recordListAdd(storageImage.sent, SMS_SENT_SLOTS, &storageImage.sentCount, dstId, text, 0);
|
|
}
|
|
|
|
bool smsSentDelete(uint8_t index)
|
|
{
|
|
return recordListDelete(storageImage.sent, &storageImage.sentCount, index);
|
|
}
|
|
|
|
uint8_t smsQuickTextCount(void)
|
|
{
|
|
return storageImage.quickTextCount;
|
|
}
|
|
|
|
const char *smsQuickTextGet(uint8_t index)
|
|
{
|
|
return ((index < storageImage.quickTextCount) ? storageImage.quickTexts[index] : NULL);
|
|
}
|
|
|
|
bool smsQuickTextSet(uint8_t index, const char *text)
|
|
{
|
|
if ((text == NULL) || (text[0] == 0) || (index >= SMS_QUICKTEXT_SLOTS) || (index > storageImage.quickTextCount))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
strncpy(storageImage.quickTexts[index], text, SMS_MAX_TEXT_LENGTH);
|
|
storageImage.quickTexts[index][SMS_MAX_TEXT_LENGTH] = 0;
|
|
|
|
if (index == storageImage.quickTextCount)
|
|
{
|
|
storageImage.quickTextCount++;
|
|
}
|
|
|
|
smsStorageMarkDirty();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool smsQuickTextDelete(uint8_t index)
|
|
{
|
|
if (index >= storageImage.quickTextCount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
memmove(&storageImage.quickTexts[index], &storageImage.quickTexts[index + 1],
|
|
(storageImage.quickTextCount - index - 1) * sizeof(storageImage.quickTexts[0]));
|
|
storageImage.quickTextCount--;
|
|
memset(&storageImage.quickTexts[storageImage.quickTextCount], 0, sizeof(storageImage.quickTexts[0]));
|
|
|
|
smsStorageMarkDirty();
|
|
|
|
return true;
|
|
}
|
|
|
|
uint8_t smsStorageGetOptions(void)
|
|
{
|
|
return storageImage.options;
|
|
}
|
|
|
|
void smsStorageSetOptions(uint8_t options)
|
|
{
|
|
if (storageImage.options != options)
|
|
{
|
|
storageImage.options = options;
|
|
smsStorageMarkDirty();
|
|
}
|
|
}
|