blob: 24541e30de0edd4ef6db0cb0e9bdbd2c52268bad [file] [log] [blame]
/*
* Copyright (c) 2022-2024 Libre Solar Technologies GmbH
* Copyright (c) 2022-2024 tado GmbH
*
* Parts of this implementation were inspired by LmhpFragmentation.c from the
* LoRaMac-node firmware repository https://github.com/Lora-net/LoRaMac-node
* written by Miguel Luis (Semtech).
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "frag_flash.h"
#include "lorawan_services.h"
#include <LoRaMac.h>
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
#include <FragDecoder.h>
#elif defined(CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_LOWMEM)
#include "frag_decoder_lowmem.h"
#endif
#include <zephyr/lorawan/lorawan.h>
#include <zephyr/logging/log.h>
#include <zephyr/random/random.h>
#include <zephyr/sys/byteorder.h>
LOG_MODULE_REGISTER(lorawan_frag_transport, CONFIG_LORAWAN_SERVICES_LOG_LEVEL);
/**
* Version of LoRaWAN Fragmented Data Block Transport Specification
*
* This implementation only supports TS004-1.0.0.
*/
#define FRAG_TRANSPORT_VERSION 1
/**
* Maximum expected number of frag transport commands per packet
*
* The standard states "A message MAY carry more than one command". Even though this was not
* observed during testing, space for up to 3 packages is reserved.
*/
#define FRAG_TRANSPORT_MAX_CMDS_PER_PACKAGE 3
/* maximum length of frag_transport answers */
#define FRAG_TRANSPORT_MAX_ANS_LEN 5
enum frag_transport_commands {
FRAG_TRANSPORT_CMD_PKG_VERSION = 0x00,
FRAG_TRANSPORT_CMD_FRAG_STATUS = 0x01,
FRAG_TRANSPORT_CMD_FRAG_SESSION_SETUP = 0x02,
FRAG_TRANSPORT_CMD_FRAG_SESSION_DELETE = 0x03,
FRAG_TRANSPORT_CMD_DATA_FRAGMENT = 0x08,
};
struct frag_transport_context {
/** Stores if a session is active */
bool is_active;
union {
uint8_t frag_session;
struct {
/** Multicast groups allowed to input to this frag session */
uint8_t mc_group_bit_mask: 4;
/** Identifies this session */
uint8_t frag_index: 2;
};
};
/** Number of fragments of the data block for this session, max. 2^14-1 */
uint16_t nb_frag;
/** Number of fragments received in this session (including coded, uncoded and repeated) */
uint16_t nb_frag_received;
/** Size of each fragment in octets */
uint8_t frag_size;
union {
uint8_t control;
struct {
/** Random delay for some responses between 0 and 2^(BlockAckDelay + 4) */
uint8_t block_ack_delay: 3;
/** Used fragmentation algorithm (0 for forward error correction) */
uint8_t frag_algo: 3;
};
};
/** Padding in the last fragment if total size is not a multiple of frag_size */
uint8_t padding;
/** Application-specific descriptor for the data block, e.g. firmware version */
uint32_t descriptor;
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
/* variables required for FragDecoder.h */
FragDecoderCallbacks_t decoder_callbacks;
#elif defined(CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_LOWMEM)
struct frag_decoder decoder;
#endif
};
/*
* The frag decoder is a singleton, so we can only have one ongoing frag session at a time, even
* though the standard allows up to 4 sessions
*/
static struct frag_transport_context ctx;
/* Callback for notification of finished firmware transfer */
static void (*finished_cb)(void);
static void frag_transport_package_callback(uint8_t port, bool data_pending, int16_t rssi,
int8_t snr, uint8_t len, const uint8_t *rx_buf)
{
uint8_t tx_buf[FRAG_TRANSPORT_MAX_CMDS_PER_PACKAGE * FRAG_TRANSPORT_MAX_ANS_LEN];
uint8_t tx_pos = 0;
uint8_t rx_pos = 0;
int ans_delay = 0;
int decoder_process_status;
__ASSERT(port == LORAWAN_PORT_FRAG_TRANSPORT, "Wrong port %d", port);
while (rx_pos < len) {
uint8_t command_id = rx_buf[rx_pos++];
if (sizeof(tx_buf) - tx_pos < FRAG_TRANSPORT_MAX_ANS_LEN) {
LOG_ERR("insufficient tx_buf size, some requests discarded");
break;
}
switch (command_id) {
case FRAG_TRANSPORT_CMD_PKG_VERSION:
tx_buf[tx_pos++] = FRAG_TRANSPORT_CMD_PKG_VERSION;
tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_FRAG_TRANSPORT_BLOCK;
tx_buf[tx_pos++] = FRAG_TRANSPORT_VERSION;
break;
case FRAG_TRANSPORT_CMD_FRAG_STATUS: {
uint8_t frag_status = rx_buf[rx_pos++] & 0x07;
uint8_t participants = frag_status & 0x01;
uint8_t index = frag_status >> 1;
LOG_DBG("FragSessionStatusReq index %d, participants: %u", index,
participants);
uint8_t missing_frag = CLAMP(ctx.nb_frag - ctx.nb_frag_received, 0, 255);
uint8_t memory_error = 0;
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
FragDecoderStatus_t decoder_status = FragDecoderGetStatus();
memory_error = decoder_status.MatrixError;
#endif
if (participants == 1 || missing_frag > 0) {
tx_buf[tx_pos++] = FRAG_TRANSPORT_CMD_FRAG_STATUS;
tx_buf[tx_pos++] = ctx.nb_frag_received & 0xFF;
tx_buf[tx_pos++] =
(index << 6) | ((ctx.nb_frag_received >> 8) & 0x3F);
tx_buf[tx_pos++] = missing_frag;
tx_buf[tx_pos++] = memory_error & 0x01;
ans_delay = sys_rand32_get() % (1U << (ctx.block_ack_delay + 4));
LOG_DBG("FragSessionStatusAns index %d, NbFragReceived: %u, "
"MissingFrag: %u, MemoryError: %u, delay: %d",
index, ctx.nb_frag_received, missing_frag, memory_error,
ans_delay);
}
break;
}
case FRAG_TRANSPORT_CMD_FRAG_SESSION_SETUP: {
uint8_t frag_session = rx_buf[rx_pos++] & 0x3F;
uint8_t index = frag_session >> 4;
uint8_t status = index << 6;
if (!ctx.is_active || ctx.frag_index == index) {
ctx.frag_session = frag_session;
ctx.nb_frag_received = 0;
ctx.nb_frag = sys_get_le16(rx_buf + rx_pos);
rx_pos += sizeof(uint16_t);
ctx.frag_size = rx_buf[rx_pos++];
ctx.control = rx_buf[rx_pos++];
ctx.padding = rx_buf[rx_pos++];
ctx.descriptor = sys_get_le32(rx_buf + rx_pos);
rx_pos += sizeof(uint32_t);
LOG_INF("FragSessionSetupReq index %d, nb_frag: %u, frag_size: %u, "
"padding: %u, control: 0x%x, descriptor: 0x%.8x",
index, ctx.nb_frag, ctx.frag_size, ctx.padding, ctx.control,
ctx.descriptor);
} else {
/* FragIndex unsupported */
status |= BIT(2);
LOG_WRN("FragSessionSetupReq failed. Session %u still active",
ctx.frag_index);
}
if (ctx.frag_algo > 0) {
/* FragAlgo unsupported */
status |= BIT(0);
}
if (ctx.nb_frag > FRAG_MAX_NB || ctx.frag_size > FRAG_MAX_SIZE) {
/* Not enough memory */
status |= BIT(1);
}
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
if (ctx.nb_frag * ctx.frag_size > FragDecoderGetMaxFileSize()) {
/* Not enough memory */
status |= BIT(1);
}
#endif
/* Descriptor not used: Ignore Wrong Descriptor error */
if ((status & 0x1F) == 0) {
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
/*
* Assign callbacks after initialization to prevent the FragDecoder
* from writing byte-wise 0xFF to the entire flash. Instead, erase
* flash properly with own implementation.
*/
ctx.decoder_callbacks.FragDecoderWrite = NULL;
ctx.decoder_callbacks.FragDecoderRead = NULL;
FragDecoderInit(ctx.nb_frag, ctx.frag_size, &ctx.decoder_callbacks);
ctx.decoder_callbacks.FragDecoderWrite = frag_flash_write;
ctx.decoder_callbacks.FragDecoderRead = frag_flash_read;
#elif defined(CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_LOWMEM)
frag_dec_init(&ctx.decoder, ctx.nb_frag, ctx.frag_size);
#endif
frag_flash_init(ctx.frag_size);
ctx.is_active = true;
}
tx_buf[tx_pos++] = FRAG_TRANSPORT_CMD_FRAG_SESSION_SETUP;
tx_buf[tx_pos++] = status;
break;
}
case FRAG_TRANSPORT_CMD_FRAG_SESSION_DELETE: {
uint8_t index = rx_buf[rx_pos++] & 0x03;
uint8_t status = 0x00;
status |= index;
if (!ctx.is_active || ctx.frag_index != index) {
/* Session does not exist */
status |= BIT(2);
} else {
ctx.is_active = false;
}
tx_buf[tx_pos++] = FRAG_TRANSPORT_CMD_FRAG_SESSION_DELETE;
tx_buf[tx_pos++] = status;
break;
}
case FRAG_TRANSPORT_CMD_DATA_FRAGMENT: {
ctx.nb_frag_received++;
uint16_t frag_index_n = sys_get_le16(rx_buf + rx_pos);
rx_pos += 2;
uint16_t frag_counter = frag_index_n & 0x3FFF;
uint8_t index = (frag_index_n >> 14) & 0x03;
if (!ctx.is_active || index != ctx.frag_index) {
LOG_DBG("DataFragment received for inactive session %u", index);
break;
}
if (frag_counter > ctx.nb_frag) {
/* Additional fragments have to be cached in RAM for recovery
* algorithm.
*/
frag_flash_use_cache();
}
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_SEMTECH
decoder_process_status = FragDecoderProcess(
frag_counter, (uint8_t *)&rx_buf[rx_pos]);
#elif defined(CONFIG_LORAWAN_FRAG_TRANSPORT_DECODER_LOWMEM)
decoder_process_status = frag_dec(
&ctx.decoder, frag_counter, &rx_buf[rx_pos], ctx.frag_size);
#endif
LOG_INF("DataFragment %u of %u (%u lost), session: %u, decoder result: %d",
frag_counter, ctx.nb_frag, frag_counter - ctx.nb_frag_received,
index, decoder_process_status);
if (decoder_process_status >= 0) {
/* Positive status corresponds to number of lost (but recovered)
* fragments. Value >= 0 means the transport is done.
*/
frag_flash_finish();
LOG_INF("Frag Transport finished successfully");
if (finished_cb != NULL) {
finished_cb();
}
/* avoid processing further fragments */
ctx.is_active = false;
}
rx_pos += ctx.frag_size;
break;
}
default:
return;
}
}
if (tx_pos > 0) {
lorawan_services_schedule_uplink(LORAWAN_PORT_FRAG_TRANSPORT, tx_buf, tx_pos,
ans_delay);
}
}
static struct lorawan_downlink_cb downlink_cb = {
.port = (uint8_t)LORAWAN_PORT_FRAG_TRANSPORT,
.cb = frag_transport_package_callback,
};
int lorawan_frag_transport_run(void (*transport_finished_cb)(void))
{
finished_cb = transport_finished_cb;
lorawan_register_downlink_callback(&downlink_cb);
return 0;
}