lorawan: services: add Fragmented Data Block Transport

This service receives fragmented data (usually firmware images) and
stores them in the slot1_partition in the flash.

Also update CMakeLists.txt in loramac-node module to be able to use
FragDecoder.c

Signed-off-by: Martin Jäger <martin@libre.solar>
This commit is contained in:
Martin Jäger 2022-08-22 17:59:30 +02:00 committed by Alberto Escolar
parent e85074329e
commit f3e41c6a25
7 changed files with 622 additions and 0 deletions

View file

@ -373,6 +373,24 @@ int lorawan_clock_sync_get(uint32_t *gps_time);
#endif /* CONFIG_LORAWAN_APP_CLOCK_SYNC */
#ifdef CONFIG_LORAWAN_FRAG_TRANSPORT
/**
* @brief Run Fragmented Data Block Transport service
*
* This service receives fragmented data (usually firmware images) and
* stores them in the image-1 flash partition.
*
* After all fragments have been received, the provided callback is invoked.
*
* @param transport_finished_cb Callback for notification of finished data transfer.
*
* @return 0 if successful, negative errno otherwise.
*/
int lorawan_frag_transport_run(void (*transport_finished_cb)(void));
#endif /* CONFIG_LORAWAN_FRAG_TRANSPORT */
#ifdef __cplusplus
}
#endif

View file

@ -30,6 +30,8 @@ if(${CONFIG_HAS_SEMTECH_LORAMAC})
zephyr_library_include_directories(
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/mac
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/mac/region
# required for FUOTA FragDecoder.h
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/apps/LoRaMac/common/LmHandler/packages
)
endif()
@ -53,6 +55,10 @@ zephyr_library_sources_ifdef(CONFIG_HAS_SEMTECH_LORAMAC
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/mac/LoRaMacSerializer.c
)
zephyr_library_sources_ifdef(CONFIG_LORAWAN_FRAG_TRANSPORT
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/apps/LoRaMac/common/LmHandler/packages/FragDecoder.c
)
zephyr_library_sources_ifdef(CONFIG_HAS_SEMTECH_LORAMAC
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/mac/region/Region.c
${ZEPHYR_LORAMAC_NODE_MODULE_DIR}/src/mac/region/RegionCommon.c

View file

@ -9,6 +9,12 @@ if(CONFIG_LORAWAN_SERVICES)
clock_sync.c
)
zephyr_library_sources_ifdef(
CONFIG_LORAWAN_FRAG_TRANSPORT
frag_flash.c
frag_transport.c
)
zephyr_library_sources_ifdef(
CONFIG_LORAWAN_REMOTE_MULTICAST
multicast.c

View file

@ -54,6 +54,90 @@ config LORAWAN_APP_CLOCK_SYNC_PERIODICITY
Default setting: 24h.
config LORAWAN_FRAG_TRANSPORT
bool "Fragmented Data Block Transport"
select FLASH_MAP
select FLASH_PAGE_LAYOUT
select IMG_MANAGER
help
Enables the LoRaWAN Fragmented Data Block Transport service
according to TS004-1.0.0 as published by the LoRa Alliance.
The used default port for this service is 201.
DT_CHOSEN_Z_CODE_PARTITION := zephyr,code-partition
config LORAWAN_FRAG_TRANSPORT_IMAGE_SIZE
int "Total size of firmware image"
depends on LORAWAN_FRAG_TRANSPORT
default $(dt_chosen_reg_size_int,$(DT_CHOSEN_Z_CODE_PARTITION))
help
Size of the flash partition for the application firmware image
in bytes.
The minimum number of fragments to be transferred is calculated from
this value divided by the fragment size.
This setting has significant influence on RAM usage.
config LORAWAN_FRAG_TRANSPORT_MAX_FRAG_SIZE
int "Maximum size of transported fragments"
depends on LORAWAN_FRAG_TRANSPORT
range 1 239
default 232
help
Maximum size of one fragment transferred during the fragmented data
block transport session of the FUOTA process. It is chosen on the
server side.
The fragment has to fit into the LoRaWAN payload, which can be up to
242 bytes depending on the region and frequency settings. 3 bytes of
the payload are consumed for protocol information.
For some MCUs like the STM32WL the fragment size has to be a multiple
of 8 (see flash driver for further information).
This setting has significant influence on RAM usage. If the exact
fragment size is known, use that value for MIN and MAX config to
reduce memory consumption.
config LORAWAN_FRAG_TRANSPORT_MIN_FRAG_SIZE
int "Minimum size of transported fragments"
depends on LORAWAN_FRAG_TRANSPORT
range 1 239
default 48
help
Minimum size of one fragment transferred during the fragmented data
block transport session of the FUOTA process. It is chosen on the
server side.
The fragment has to fit into the LoRaWAN payload, which can be up to
242 bytes depending on the region and frequency settings. 3 bytes of
the payload are consumed for protocol information.
For some MCUs like the STM32WL the fragment size has to be a multiple
of 8 (see flash driver for further information).
This setting has significant influence on RAM usage. If the exact
fragment size is known, use that value for MIN and MAX config to
reduce memory consumption.
config LORAWAN_FRAG_TRANSPORT_MAX_REDUNDANCY
int "Percentage of redundant fragments"
depends on LORAWAN_FRAG_TRANSPORT
range 1 100
default 20
help
The built-in forward error correction (FEC) mechanism allows to
reconstruct missed packages from additional redundant packages
sent by the server after all packages have been sent.
This parameter specifies the maximum amount of packet loss (in
percent) for which it should be possible to reconstruct the full
firmware image.
This setting has significant influence on RAM usage.
config LORAWAN_REMOTE_MULTICAST
bool "Remote Multicast Setup"
depends on LORAWAN_APP_CLOCK_SYNC

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2022-2024 Libre Solar Technologies GmbH
* Copyright (c) 2022-2024 tado GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "frag_flash.h"
#include <FragDecoder.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/storage/flash_map.h>
LOG_MODULE_REGISTER(lorawan_frag_flash, CONFIG_LORAWAN_SERVICES_LOG_LEVEL);
#define TARGET_IMAGE_AREA FIXED_PARTITION_ID(slot1_partition)
struct frag_cache_entry {
uint32_t addr;
uint8_t data[FRAG_MAX_SIZE];
};
static struct frag_cache_entry frag_cache[FRAG_MAX_REDUNDANCY];
static uint32_t frag_size;
static int cached_frags;
static bool use_cache;
static const struct flash_area *fa;
int frag_flash_init(uint32_t fragment_size)
{
int err;
if (fragment_size > FRAG_MAX_SIZE) {
return -ENOSPC;
}
frag_size = fragment_size;
cached_frags = 0;
use_cache = false;
err = flash_area_open(TARGET_IMAGE_AREA, &fa);
if (err) {
return err;
}
LOG_DBG("Starting to erase flash area");
err = flash_area_erase(fa, 0, fa->fa_size);
LOG_DBG("Finished erasing flash area");
return err;
}
int8_t frag_flash_write(uint32_t addr, uint8_t *data, uint32_t size)
{
int8_t err = 0;
if (!use_cache) {
LOG_DBG("Writing %u bytes to addr 0x%X", size, addr);
err = flash_area_write(fa, addr, data, size);
} else {
LOG_DBG("Caching %u bytes for addr 0x%X", size, addr);
if (size != frag_size) {
LOG_ERR("Invalid fragment size %d", size);
return -EINVAL;
}
/* overwrite fragment in cache if existing */
for (int i = 0; i < cached_frags; i++) {
if (frag_cache[i].addr == addr) {
memcpy(frag_cache[i].data, data, size);
return 0;
}
}
/* otherwise create new cache entry */
if (cached_frags < ARRAY_SIZE(frag_cache)) {
frag_cache[cached_frags].addr = addr;
memcpy(frag_cache[cached_frags].data, data, size);
cached_frags++;
} else {
LOG_ERR("Fragment cache too small");
err = -ENOSPC;
}
}
return err == 0 ? 0 : -1;
}
int8_t frag_flash_read(uint32_t addr, uint8_t *data, uint32_t size)
{
for (int i = 0; i < cached_frags; i++) {
if (frag_cache[i].addr == addr) {
memcpy(data, frag_cache[i].data, size);
return 0;
}
}
return flash_area_read(fa, addr, data, size) == 0 ? 0 : -1;
}
void frag_flash_use_cache(void)
{
use_cache = true;
}
void frag_flash_finish(void)
{
int err;
for (int i = 0; i < cached_frags; i++) {
LOG_DBG("Writing %u bytes to addr 0x%x", frag_size, frag_cache[i].addr);
flash_area_write(fa, frag_cache[i].addr, frag_cache[i].data, frag_size);
}
flash_area_close(fa);
err = boot_request_upgrade(BOOT_UPGRADE_TEST);
if (err) {
LOG_ERR("Failed to request upgrade (err %d)", err);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022-2024 Libre Solar Technologies GmbH
* Copyright (c) 2022-2024 tado GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_SUBSYS_LORAWAN_SERVICES_FRAG_FLASH_H_
#define ZEPHYR_SUBSYS_LORAWAN_SERVICES_FRAG_FLASH_H_
#include <stdint.h>
/**
* Initialize flash driver and prepare partition for new firmware image.
*
* This function mass-erases the flash partition and may take a while to return.
*
* @param frag_size Fragment size used for this session
*
* @returns 0 for success, otherwise negative error code
*/
int frag_flash_init(uint32_t frag_size);
/**
* Write received data fragment to flash.
*
* This function is called by FragDecoder from LoRaMAC-node stack.
*
* @param addr Flash address relative to start of slot
* @param data Data buffer
* @param size Number of bytes in the buffer
*
* @returns 0 for success, otherwise negative error code
*/
int8_t frag_flash_write(uint32_t addr, uint8_t *data, uint32_t size);
/**
* Read back data from flash.
*
* This function is called by FragDecoder from LoRaMAC-node stack.
*
* @param addr Flash address relative to start of slot
* @param data Data buffer
* @param size Number of bytes in the buffer
*
* @returns 0 for success, otherwise negative error code
*/
int8_t frag_flash_read(uint32_t addr, uint8_t *data, uint32_t size);
/**
* Start caching fragments in RAM.
*
* Coded/redundant fragments may be overwritten with future fragments,
* so we have to cache them in RAM instead of flash.
*
* This function must be called once all uncoded fragments have been received.
*/
void frag_flash_use_cache(void);
/**
* Finalize flashing after sufficient fragments have been received.
*
* This call will also write cached fragments to flash.
*
* After this call the new firmware is ready to be checked and booted.
*/
void frag_flash_finish(void);
#endif /* ZEPHYR_SUBSYS_LORAWAN_SERVICES_FRAG_FLASH_H_ */

View file

@ -0,0 +1,310 @@
/*
* 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>
#include <FragDecoder.h>
#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;
/* variables required for FragDecoder.h */
FragDecoderCallbacks_t decoder_callbacks;
int32_t decoder_process_status;
};
/*
* 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;
__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);
FragDecoderStatus_t decoder_status = FragDecoderGetStatus();
uint8_t memory_error = decoder_status.MatrixError;
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 ||
ctx.nb_frag * ctx.frag_size > FragDecoderGetMaxFileSize()) {
/* Not enough memory */
status |= BIT(1);
}
/* Descriptor not used: Ignore Wrong Descriptor error */
if ((status & 0x1F) == 0) {
/*
* 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;
frag_flash_init(ctx.frag_size);
ctx.is_active = true;
ctx.decoder_process_status = FRAG_SESSION_ONGOING;
}
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 (ctx.decoder_process_status == FRAG_SESSION_ONGOING) {
if (frag_counter > ctx.nb_frag) {
/* Additional fragments have to be cached in RAM for
* recovery algorithm.
*/
frag_flash_use_cache();
}
ctx.decoder_process_status = FragDecoderProcess(
frag_counter, (uint8_t *)&rx_buf[rx_pos]);
}
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, ctx.decoder_process_status);
if (ctx.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();
}
ctx.is_active = false;
/* avoid processing further fragments */
ctx.decoder_process_status = FRAG_SESSION_NOT_STARTED;
}
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;
/* initialize non-zero static variables */
ctx.decoder_process_status = FRAG_SESSION_NOT_STARTED;
lorawan_register_downlink_callback(&downlink_cb);
return 0;
}