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>
719 lines
19 KiB
C
719 lines
19 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/smsCore.h"
|
|
#include "functions/smsStorage.h"
|
|
#include "functions/ticks.h"
|
|
#include "functions/trx.h"
|
|
#include "functions/settings.h"
|
|
#include "hardware/HR-C6000.h"
|
|
|
|
#define SMS_WAIT_CHANNEL_TIMEOUT_MS 4000
|
|
#define SMS_WAIT_ACK_TIMEOUT_MS 6000
|
|
#define SMS_ACK_RESPONSE_DELAY_MS 1500
|
|
#define SMS_RX_ASSEMBLY_STALL_MS 2000
|
|
#define SMS_RX_DEDUPE_WINDOW_MS 30000
|
|
|
|
#define SMS_EVENT_QUEUE_SIZE 8
|
|
|
|
//
|
|
// Outgoing transmission
|
|
//
|
|
static struct
|
|
{
|
|
volatile smsTxState_t state;
|
|
volatile bool jobPending;
|
|
bool jobIsAckResponse;
|
|
uint32_t peerId;
|
|
char text[SMS_MAX_TEXT_LENGTH + 1]; // kept for the sent box and resend
|
|
bool haveLastMessage;
|
|
ticksTimer_t channelTimer;
|
|
ticksTimer_t ackTimer;
|
|
uint16_t ipSequence;
|
|
} tx;
|
|
|
|
static smsAirJob_t txAirJob;
|
|
|
|
// set from ISR context, consumed in smsCoreTick()
|
|
static volatile bool txAirDoneFlag = false;
|
|
static volatile bool txRejectedFlag = false;
|
|
static volatile bool txDeliveredFlag = false;
|
|
|
|
//
|
|
// Incoming assembly (written from ISR context only).
|
|
//
|
|
// The raw bursts are kept as-received: senders differ in whether the blocks
|
|
// carry a confirmed-data prefix, and it is not guaranteed how many of a
|
|
// rate-3/4 block's 18 bytes the chip exposes, so the extraction layout is
|
|
// determined at decode time by trying the possible models against the IP
|
|
// header checksum.
|
|
//
|
|
#define SMS_RX_MAX_BLOCKS 32
|
|
#define SMS_RX_BLOCK_STRIDE DMR_DATA_RATE34_BLOCK_LENGTH
|
|
|
|
static volatile struct
|
|
{
|
|
bool active;
|
|
uint8_t expectedBlocks;
|
|
uint8_t receivedBlocks;
|
|
uint8_t framesSeen; // progress marker for the stall watchdog
|
|
uint32_t srcId;
|
|
bool responseRequested;
|
|
bool confirmedData; // header DPF was "confirmed"
|
|
bool rate34; // payload arrived as rate-3/4 blocks
|
|
} rxAssembly;
|
|
|
|
// Shared between assembly (ISR) and decoding (main task): while
|
|
// rxDecode.pending is set the buffers belong to the main task and the ISR
|
|
// won't open a new assembly into them.
|
|
static uint8_t rxRawBlocks[SMS_RX_MAX_BLOCKS * SMS_RX_BLOCK_STRIDE];
|
|
static __attribute__((section(".ccmram"))) uint8_t rxAssembledPayload[SMS_RX_MAX_BLOCKS * (SMS_RX_BLOCK_STRIDE - DMR_DATA_CONFIRMED_PREFIX_LENGTH)];
|
|
|
|
// completed assembly handed over for decoding in the main task
|
|
static volatile struct
|
|
{
|
|
bool pending;
|
|
uint8_t blockCount;
|
|
bool confirmedData;
|
|
bool rate34;
|
|
uint32_t srcId;
|
|
bool responseRequested;
|
|
} rxDecode;
|
|
|
|
//
|
|
// Delivery report we owe to a sender
|
|
//
|
|
static struct
|
|
{
|
|
bool pending;
|
|
uint32_t dstId;
|
|
ticksTimer_t delayTimer;
|
|
} ackResponse;
|
|
|
|
// UI event queue and unseen-message flag (main task context only)
|
|
static uint8_t eventQueue[SMS_EVENT_QUEUE_SIZE];
|
|
static uint8_t eventQueueHead = 0;
|
|
static uint8_t eventQueueCount = 0;
|
|
|
|
static bool unseenRxMessage = false;
|
|
static uint32_t lastRxStoreTime = 0;
|
|
|
|
static void pushEvent(smsEvent_t event)
|
|
{
|
|
if (eventQueueCount < SMS_EVENT_QUEUE_SIZE)
|
|
{
|
|
eventQueue[(eventQueueHead + eventQueueCount) % SMS_EVENT_QUEUE_SIZE] = (uint8_t)event;
|
|
eventQueueCount++;
|
|
}
|
|
}
|
|
|
|
smsEvent_t smsPopEvent(void)
|
|
{
|
|
if (eventQueueCount == 0)
|
|
{
|
|
return SMS_EVENT_NONE;
|
|
}
|
|
|
|
smsEvent_t event = (smsEvent_t)eventQueue[eventQueueHead];
|
|
eventQueueHead = (eventQueueHead + 1) % SMS_EVENT_QUEUE_SIZE;
|
|
eventQueueCount--;
|
|
|
|
return event;
|
|
}
|
|
|
|
void smsCoreInit(void)
|
|
{
|
|
memset(&tx, 0, sizeof(tx));
|
|
memset((void *)&rxAssembly, 0, sizeof(rxAssembly));
|
|
memset((void *)&rxDecode, 0, sizeof(rxDecode));
|
|
memset(&ackResponse, 0, sizeof(ackResponse));
|
|
eventQueueHead = 0;
|
|
eventQueueCount = 0;
|
|
unseenRxMessage = false;
|
|
txAirDoneFlag = false;
|
|
txRejectedFlag = false;
|
|
txDeliveredFlag = false;
|
|
}
|
|
|
|
bool smsCoreIsIdle(void)
|
|
{
|
|
return ((tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false) &&
|
|
(rxAssembly.active == false) && (rxDecode.pending == false) &&
|
|
(ackResponse.pending == false));
|
|
}
|
|
|
|
smsTxState_t smsGetTxState(void)
|
|
{
|
|
return tx.state;
|
|
}
|
|
|
|
//
|
|
// Air job construction. The payload blocks are built in place: the rows of
|
|
// txAirJob.frames are contiguous, so the encoder writes the whole payload
|
|
// directly into the rows following the preambles and the header.
|
|
//
|
|
static void buildAirJobFrames(uint32_t dstId, uint32_t srcId, uint8_t preambleCount,
|
|
const uint8_t *headerFrame, uint8_t blockCount)
|
|
{
|
|
uint8_t frameIndex = 0;
|
|
|
|
for (uint8_t i = 0; i < preambleCount; i++)
|
|
{
|
|
// remaining frames after this preamble: the other preambles, the
|
|
// header, and the payload blocks
|
|
uint8_t blocksToFollow = (preambleCount - 1 - i) + 1 + blockCount;
|
|
|
|
dmrDataBuildPreambleCSBK(txAirJob.frames[frameIndex], dstId, srcId, blocksToFollow);
|
|
frameIndex++;
|
|
}
|
|
|
|
memcpy(txAirJob.frames[frameIndex], headerFrame, DMR_DATA_BURST_LENGTH);
|
|
frameIndex++;
|
|
|
|
// the payload block contents are already in place (message jobs), or absent (ACK jobs)
|
|
txAirJob.preambleCount = preambleCount;
|
|
txAirJob.frameCount = frameIndex + blockCount;
|
|
}
|
|
|
|
static bool queueMessageTransmission(uint32_t dstId, const char *text)
|
|
{
|
|
uint16_t payloadLength = 0;
|
|
uint8_t padOctets = 0;
|
|
uint8_t *payloadArea = txAirJob.frames[SMS_TX_PREAMBLE_COUNT + 1]; // rows after preambles + header
|
|
|
|
tx.ipSequence++;
|
|
|
|
if (smsPayloadBuildMotorola(payloadArea, &payloadLength, &padOctets,
|
|
dstId, trxDMRID, text, tx.ipSequence) != SMS_ENCODE_OK)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint8_t blockCount = payloadLength / DMR_DATA_BURST_LENGTH;
|
|
uint8_t headerFrame[DMR_DATA_BURST_LENGTH];
|
|
|
|
dmrDataBuildUnconfirmedHeader(headerFrame, dstId, trxDMRID, blockCount, padOctets, smsOptionWaitForAck());
|
|
buildAirJobFrames(dstId, trxDMRID, SMS_TX_PREAMBLE_COUNT, headerFrame, blockCount);
|
|
|
|
tx.peerId = dstId;
|
|
strncpy(tx.text, text, SMS_MAX_TEXT_LENGTH);
|
|
tx.text[SMS_MAX_TEXT_LENGTH] = 0;
|
|
tx.haveLastMessage = true;
|
|
tx.jobIsAckResponse = false;
|
|
tx.jobPending = true;
|
|
tx.state = SMS_TX_STATE_WAIT_CHANNEL;
|
|
ticksTimerStart(&tx.channelTimer, SMS_WAIT_CHANNEL_TIMEOUT_MS);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void queueAckResponseTransmission(uint32_t dstId)
|
|
{
|
|
uint8_t headerFrame[DMR_DATA_BURST_LENGTH];
|
|
// repeaters/hotspots expect the SAP + response-type variant of the
|
|
// delivery report, OEM radios on simplex the plain one
|
|
bool repeaterProfile = (currentChannelData->rxFreq != currentChannelData->txFreq);
|
|
|
|
dmrDataBuildResponseHeader(headerFrame, dstId, trxDMRID, repeaterProfile);
|
|
buildAirJobFrames(dstId, trxDMRID, SMS_TX_ACK_PREAMBLE_COUNT, headerFrame, 0);
|
|
|
|
tx.jobIsAckResponse = true;
|
|
tx.jobPending = true;
|
|
tx.state = SMS_TX_STATE_WAIT_CHANNEL;
|
|
ticksTimerStart(&tx.channelTimer, SMS_WAIT_CHANNEL_TIMEOUT_MS);
|
|
}
|
|
|
|
static bool smsTransmissionIsAllowed(void)
|
|
{
|
|
return ((trxGetMode() == RADIO_MODE_DIGITAL) &&
|
|
(settingsUsbMode != USB_MODE_HOTSPOT) &&
|
|
(settingsIsOptionBitSet(BIT_TX_INHIBIT) == false) &&
|
|
(trxTransmissionEnabled == false) &&
|
|
(trxIsTransmitting == false));
|
|
}
|
|
|
|
static bool tryStartAirTransmission(void)
|
|
{
|
|
return HRC6000StartSmsTransmission();
|
|
}
|
|
|
|
bool smsSendMessage(uint32_t dstId, const char *text)
|
|
{
|
|
if ((tx.state != SMS_TX_STATE_IDLE) || tx.jobPending ||
|
|
(dstId == 0) || (dstId > 0xFFFFFF) || (smsTransmissionIsAllowed() == false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return queueMessageTransmission(dstId, text);
|
|
}
|
|
|
|
bool smsCanResendLast(void)
|
|
{
|
|
return (tx.haveLastMessage && (tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false));
|
|
}
|
|
|
|
bool smsResendLast(void)
|
|
{
|
|
if (smsCanResendLast() == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return smsSendMessage(tx.peerId, tx.text);
|
|
}
|
|
|
|
//
|
|
// RX notification for the popup screen
|
|
//
|
|
bool smsHasUnseenRxMessage(void)
|
|
{
|
|
return unseenRxMessage;
|
|
}
|
|
|
|
void smsAcknowledgeRxMessage(void)
|
|
{
|
|
unseenRxMessage = false;
|
|
}
|
|
|
|
//
|
|
// Options (persisted with the message store)
|
|
//
|
|
bool smsOptionWaitForAck(void)
|
|
{
|
|
return ((smsStorageGetOptions() & SMS_OPTION_WAIT_FOR_ACK) != 0);
|
|
}
|
|
|
|
void smsOptionSetWaitForAck(bool waitForAck)
|
|
{
|
|
uint8_t options = smsStorageGetOptions();
|
|
|
|
smsStorageSetOptions(waitForAck ? (options | SMS_OPTION_WAIT_FOR_ACK) : (options & ~SMS_OPTION_WAIT_FOR_ACK));
|
|
}
|
|
|
|
bool smsOptionOwnIdOnly(void)
|
|
{
|
|
return ((smsStorageGetOptions() & SMS_OPTION_OWN_ID_ONLY) != 0);
|
|
}
|
|
|
|
void smsOptionSetOwnIdOnly(bool ownIdOnly)
|
|
{
|
|
uint8_t options = smsStorageGetOptions();
|
|
|
|
smsStorageSetOptions(ownIdOnly ? (options | SMS_OPTION_OWN_ID_ONLY) : (options & ~SMS_OPTION_OWN_ID_ONLY));
|
|
}
|
|
|
|
//
|
|
// ISR-side interface
|
|
//
|
|
const smsAirJob_t *smsGetPendingAirJob(void)
|
|
{
|
|
return (tx.jobPending ? &txAirJob : NULL);
|
|
}
|
|
|
|
void smsAirJobStarted(void)
|
|
{
|
|
tx.jobPending = false;
|
|
}
|
|
|
|
void smsTxNotifyAirDone(void)
|
|
{
|
|
txAirDoneFlag = true;
|
|
}
|
|
|
|
void smsTxNotifyRejected(void)
|
|
{
|
|
txRejectedFlag = true;
|
|
}
|
|
|
|
void smsTxNotifyHwAck(void)
|
|
{
|
|
if (tx.state == SMS_TX_STATE_WAIT_ACK)
|
|
{
|
|
txDeliveredFlag = true;
|
|
}
|
|
}
|
|
|
|
static void rxAssemblyReset(void)
|
|
{
|
|
rxAssembly.active = false;
|
|
rxAssembly.expectedBlocks = 0;
|
|
rxAssembly.receivedBlocks = 0;
|
|
}
|
|
|
|
static void rxAssemblyComplete(void)
|
|
{
|
|
rxDecode.blockCount = ((rxAssembly.receivedBlocks < SMS_RX_MAX_BLOCKS) ? rxAssembly.receivedBlocks : SMS_RX_MAX_BLOCKS);
|
|
rxDecode.confirmedData = rxAssembly.confirmedData;
|
|
rxDecode.rate34 = rxAssembly.rate34;
|
|
rxDecode.srcId = rxAssembly.srcId;
|
|
rxDecode.responseRequested = rxAssembly.responseRequested;
|
|
rxDecode.pending = true; // buffers now belong to the main task
|
|
|
|
rxAssemblyReset();
|
|
}
|
|
|
|
bool smsRxProcessDataFrame(uint8_t dataType, const uint8_t *frame)
|
|
{
|
|
rxAssembly.framesSeen++;
|
|
|
|
if (dataType == DMR_DATA_TYPE_DATA_HEADER)
|
|
{
|
|
dmrDataHeader_t header;
|
|
|
|
if (dmrDataParseHeader(frame, &header) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// delivery report for our outstanding message? (it can arrive while
|
|
// the state machine is still finishing up the transmission)
|
|
if (header.isResponsePdu)
|
|
{
|
|
if (((tx.state == SMS_TX_STATE_WAIT_ACK) || (tx.state == SMS_TX_STATE_SENDING)) &&
|
|
(header.srcId == tx.peerId))
|
|
{
|
|
txDeliveredFlag = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Longer messages than the raw-block store still assemble; the text
|
|
// beyond the stored blocks is truncated at decode time
|
|
if ((header.sap != 0x04) || header.isGroup ||
|
|
(header.blockCount == 0) || (header.blockCount > SMS_MAX_DATA_BLOCKS))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (smsOptionOwnIdOnly() && (header.dstId != trxDMRID))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (rxDecode.pending) // buffers still owned by the decoder, drop this message
|
|
{
|
|
return false;
|
|
}
|
|
|
|
rxAssembly.expectedBlocks = header.blockCount;
|
|
rxAssembly.receivedBlocks = 0;
|
|
rxAssembly.srcId = header.srcId;
|
|
rxAssembly.responseRequested = header.responseRequested;
|
|
rxAssembly.confirmedData = (header.dpf == 0x03);
|
|
rxAssembly.rate34 = false;
|
|
rxAssembly.active = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
if (rxAssembly.active == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((dataType != DMR_DATA_TYPE_RATE_12_DATA) && (dataType != DMR_DATA_TYPE_RATE_34_DATA))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (rxAssembly.receivedBlocks < SMS_RX_MAX_BLOCKS)
|
|
{
|
|
// keep the burst raw; the extraction layout is decided at decode time
|
|
memcpy(&rxRawBlocks[rxAssembly.receivedBlocks * SMS_RX_BLOCK_STRIDE], frame,
|
|
((dataType == DMR_DATA_TYPE_RATE_34_DATA) ? DMR_DATA_RATE34_BLOCK_LENGTH : DMR_DATA_BURST_LENGTH));
|
|
|
|
if (rxAssembly.receivedBlocks == 0)
|
|
{
|
|
rxAssembly.rate34 = (dataType == DMR_DATA_TYPE_RATE_34_DATA);
|
|
}
|
|
}
|
|
|
|
rxAssembly.receivedBlocks++;
|
|
|
|
if (rxAssembly.receivedBlocks >= rxAssembly.expectedBlocks)
|
|
{
|
|
rxAssemblyComplete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// Main task processing
|
|
//
|
|
|
|
// Compact the raw bursts into a contiguous payload, taking `take` bytes
|
|
// starting at `skip` from each block. Returns the assembled length.
|
|
static uint16_t rxExtractPayload(uint8_t blockCount, uint8_t skip, uint8_t take)
|
|
{
|
|
for (uint8_t i = 0; i < blockCount; i++)
|
|
{
|
|
memcpy(&rxAssembledPayload[i * take], &rxRawBlocks[(i * SMS_RX_BLOCK_STRIDE) + skip], take);
|
|
}
|
|
|
|
return (uint16_t)blockCount * take;
|
|
}
|
|
|
|
static void processCompletedRxMessage(void)
|
|
{
|
|
char text[SMS_MAX_TEXT_LENGTH + 1];
|
|
uint32_t srcId = rxDecode.srcId;
|
|
bool responseRequested = rxDecode.responseRequested;
|
|
|
|
// Candidate block layouts, likeliest first (based on the header's
|
|
// confirmed/unconfirmed DPF). For rate-3/4, the 12-byte layouts cover
|
|
// the case where the chip exposes only part of the 18-byte block.
|
|
const uint8_t prefix = DMR_DATA_CONFIRMED_PREFIX_LENGTH;
|
|
uint8_t blockLengths[4];
|
|
uint8_t skips[4];
|
|
uint8_t modelCount;
|
|
|
|
if (rxDecode.rate34)
|
|
{
|
|
blockLengths[0] = DMR_DATA_RATE34_BLOCK_LENGTH;
|
|
blockLengths[1] = DMR_DATA_BURST_LENGTH;
|
|
blockLengths[2] = DMR_DATA_RATE34_BLOCK_LENGTH;
|
|
blockLengths[3] = DMR_DATA_BURST_LENGTH;
|
|
skips[0] = skips[1] = (rxDecode.confirmedData ? prefix : 0);
|
|
skips[2] = skips[3] = (rxDecode.confirmedData ? 0 : prefix);
|
|
modelCount = 4;
|
|
}
|
|
else
|
|
{
|
|
blockLengths[0] = blockLengths[1] = DMR_DATA_BURST_LENGTH;
|
|
skips[0] = (rxDecode.confirmedData ? prefix : 0);
|
|
skips[1] = (rxDecode.confirmedData ? 0 : prefix);
|
|
modelCount = 2;
|
|
}
|
|
|
|
bool decoded = false;
|
|
uint16_t length = 0;
|
|
|
|
for (uint8_t m = 0; (m < modelCount) && (decoded == false); m++)
|
|
{
|
|
length = rxExtractPayload(rxDecode.blockCount, skips[m], blockLengths[m] - skips[m]);
|
|
|
|
decoded = (smsPayloadParseMotorola(rxAssembledPayload, length, text, sizeof(text)) ||
|
|
smsPayloadParseStandard(rxAssembledPayload, length, text, sizeof(text)));
|
|
}
|
|
|
|
if (decoded == false)
|
|
{
|
|
// No layout produced a structurally valid packet: salvage readable
|
|
// text from every layout and keep the longest result
|
|
char candidate[SMS_MAX_TEXT_LENGTH + 1];
|
|
|
|
text[0] = 0;
|
|
|
|
for (uint8_t m = 0; m < modelCount; m++)
|
|
{
|
|
length = rxExtractPayload(rxDecode.blockCount, skips[m], blockLengths[m] - skips[m]);
|
|
|
|
if (smsPayloadScanText(rxAssembledPayload, length, candidate, sizeof(candidate)) &&
|
|
(strlen(candidate) > strlen(text)))
|
|
{
|
|
memcpy(text, candidate, sizeof(text));
|
|
}
|
|
}
|
|
|
|
decoded = (text[0] != 0);
|
|
}
|
|
|
|
rxDecode.pending = false; // buffers released back to the ISR assembler
|
|
|
|
if (decoded)
|
|
{
|
|
// OEM radios repeat a message when they miss our delivery report;
|
|
// don't store consecutive identical copies arriving close together
|
|
const smsRecord_t *newest = smsInboxGet(0);
|
|
bool isDuplicate = ((newest != NULL) && (newest->peerId == srcId) &&
|
|
(strcmp(newest->text, text) == 0) &&
|
|
((ticksGetMillis() - lastRxStoreTime) < SMS_RX_DEDUPE_WINDOW_MS));
|
|
|
|
if (isDuplicate == false)
|
|
{
|
|
smsInboxAdd(srcId, text);
|
|
lastRxStoreTime = ticksGetMillis();
|
|
unseenRxMessage = true;
|
|
pushEvent(SMS_EVENT_RX_MESSAGE);
|
|
}
|
|
}
|
|
|
|
// acknowledge receipt at the data-link level even when the payload
|
|
// didn't decode to text - all blocks did arrive
|
|
if (responseRequested && (srcId != 0) && (srcId != trxDMRID))
|
|
{
|
|
ackResponse.pending = true;
|
|
ackResponse.dstId = srcId;
|
|
ticksTimerStart(&ackResponse.delayTimer, SMS_ACK_RESPONSE_DELAY_MS);
|
|
}
|
|
}
|
|
|
|
static void watchRxAssemblyStall(void)
|
|
{
|
|
static uint8_t lastFramesSeen = 0;
|
|
static ticksTimer_t stallTimer = { 0, 0 };
|
|
static bool watching = false;
|
|
|
|
if (rxAssembly.active)
|
|
{
|
|
if ((watching == false) || (rxAssembly.framesSeen != lastFramesSeen))
|
|
{
|
|
lastFramesSeen = rxAssembly.framesSeen;
|
|
ticksTimerStart(&stallTimer, SMS_RX_ASSEMBLY_STALL_MS);
|
|
watching = true;
|
|
}
|
|
else if (ticksTimerHasExpired(&stallTimer))
|
|
{
|
|
rxAssemblyReset(); // transmission broke off mid-message
|
|
watching = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
watching = false;
|
|
}
|
|
}
|
|
|
|
static void finishTransmission(smsEvent_t event)
|
|
{
|
|
bool wasAckResponse = tx.jobIsAckResponse;
|
|
|
|
tx.state = SMS_TX_STATE_IDLE;
|
|
tx.jobPending = false;
|
|
tx.jobIsAckResponse = false;
|
|
|
|
if ((wasAckResponse == false) && (event != SMS_EVENT_NONE))
|
|
{
|
|
pushEvent(event);
|
|
}
|
|
}
|
|
|
|
void smsCoreTick(void)
|
|
{
|
|
if (rxDecode.pending)
|
|
{
|
|
processCompletedRxMessage();
|
|
}
|
|
|
|
watchRxAssemblyStall();
|
|
|
|
// pending delivery report: transmit once the delay elapsed and we're free
|
|
if (ackResponse.pending && ticksTimerHasExpired(&ackResponse.delayTimer) &&
|
|
(tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false))
|
|
{
|
|
if (smsTransmissionIsAllowed())
|
|
{
|
|
queueAckResponseTransmission(ackResponse.dstId);
|
|
}
|
|
|
|
ackResponse.pending = false; // single attempt, dropped when not possible
|
|
}
|
|
|
|
switch (tx.state)
|
|
{
|
|
case SMS_TX_STATE_WAIT_CHANNEL:
|
|
if (tryStartAirTransmission())
|
|
{
|
|
tx.state = SMS_TX_STATE_SENDING;
|
|
|
|
if (tx.jobIsAckResponse == false)
|
|
{
|
|
pushEvent(SMS_EVENT_TX_SENDING);
|
|
}
|
|
}
|
|
else if (ticksTimerHasExpired(&tx.channelTimer))
|
|
{
|
|
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
|
}
|
|
break;
|
|
|
|
case SMS_TX_STATE_SENDING:
|
|
if (HRC6000GetIsWakingState() == WAKING_MODE_FAILED)
|
|
{
|
|
// The repeater never woke up; tear the transmission down
|
|
HRC6000CancelSmsTransmission();
|
|
trxTransmissionEnabled = false;
|
|
HRC6000ClearIsWakingState();
|
|
HRC6000TerminateDigital();
|
|
trxDisableTransmission();
|
|
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
|
}
|
|
else if (txRejectedFlag)
|
|
{
|
|
txRejectedFlag = false;
|
|
trxDisableTransmission();
|
|
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
|
}
|
|
else if (txAirDoneFlag)
|
|
{
|
|
txAirDoneFlag = false;
|
|
|
|
if (tx.jobIsAckResponse)
|
|
{
|
|
finishTransmission(SMS_EVENT_NONE);
|
|
}
|
|
else
|
|
{
|
|
smsSentAdd(tx.peerId, tx.text);
|
|
pushEvent(SMS_EVENT_TX_SENT);
|
|
|
|
if (smsOptionWaitForAck())
|
|
{
|
|
tx.state = SMS_TX_STATE_WAIT_ACK;
|
|
ticksTimerStart(&tx.ackTimer, SMS_WAIT_ACK_TIMEOUT_MS);
|
|
}
|
|
else
|
|
{
|
|
finishTransmission(SMS_EVENT_NONE);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SMS_TX_STATE_WAIT_ACK:
|
|
if (txDeliveredFlag)
|
|
{
|
|
txDeliveredFlag = false;
|
|
finishTransmission(SMS_EVENT_TX_DELIVERED);
|
|
}
|
|
else if (ticksTimerHasExpired(&tx.ackTimer))
|
|
{
|
|
finishTransmission(SMS_EVENT_TX_TIMEOUT);
|
|
}
|
|
break;
|
|
|
|
case SMS_TX_STATE_IDLE:
|
|
default:
|
|
txAirDoneFlag = false;
|
|
txRejectedFlag = false;
|
|
txDeliveredFlag = false;
|
|
break;
|
|
}
|
|
}
|