From f3e41c6a25bfa36a57f55e30ae7841635d9ccde4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20J=C3=A4ger?= Date: Mon, 22 Aug 2022 17:59:30 +0200 Subject: [PATCH] lorawan: services: add Fragmented Data Block Transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- include/zephyr/lorawan/lorawan.h | 18 ++ modules/loramac-node/CMakeLists.txt | 6 + subsys/lorawan/services/CMakeLists.txt | 6 + subsys/lorawan/services/Kconfig | 84 ++++++ subsys/lorawan/services/frag_flash.c | 129 ++++++++++ subsys/lorawan/services/frag_flash.h | 69 +++++ subsys/lorawan/services/frag_transport.c | 310 +++++++++++++++++++++++ 7 files changed, 622 insertions(+) create mode 100644 subsys/lorawan/services/frag_flash.c create mode 100644 subsys/lorawan/services/frag_flash.h create mode 100644 subsys/lorawan/services/frag_transport.c diff --git a/include/zephyr/lorawan/lorawan.h b/include/zephyr/lorawan/lorawan.h index 1bad5130730..4ce1605237a 100644 --- a/include/zephyr/lorawan/lorawan.h +++ b/include/zephyr/lorawan/lorawan.h @@ -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 diff --git a/modules/loramac-node/CMakeLists.txt b/modules/loramac-node/CMakeLists.txt index 1a84a5f6ac4..9e0bdbc9877 100644 --- a/modules/loramac-node/CMakeLists.txt +++ b/modules/loramac-node/CMakeLists.txt @@ -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 diff --git a/subsys/lorawan/services/CMakeLists.txt b/subsys/lorawan/services/CMakeLists.txt index b634efd69b3..2f3aeeeaca2 100644 --- a/subsys/lorawan/services/CMakeLists.txt +++ b/subsys/lorawan/services/CMakeLists.txt @@ -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 diff --git a/subsys/lorawan/services/Kconfig b/subsys/lorawan/services/Kconfig index 6ab6d4e55af..95905a7cbff 100644 --- a/subsys/lorawan/services/Kconfig +++ b/subsys/lorawan/services/Kconfig @@ -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 diff --git a/subsys/lorawan/services/frag_flash.c b/subsys/lorawan/services/frag_flash.c new file mode 100644 index 00000000000..2d5db55a7a8 --- /dev/null +++ b/subsys/lorawan/services/frag_flash.c @@ -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 + +#include +#include +#include +#include + +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); + } +} diff --git a/subsys/lorawan/services/frag_flash.h b/subsys/lorawan/services/frag_flash.h new file mode 100644 index 00000000000..cecb91f3acc --- /dev/null +++ b/subsys/lorawan/services/frag_flash.h @@ -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 + +/** + * 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_ */ diff --git a/subsys/lorawan/services/frag_transport.c b/subsys/lorawan/services/frag_transport.c new file mode 100644 index 00000000000..2db28bb0366 --- /dev/null +++ b/subsys/lorawan/services/frag_transport.c @@ -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 +#include +#include +#include +#include +#include + +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; +}