tests: subsys: lorawan: add frag_decoder unit-test

The unit test allows to test the frag decoder algorithms using random
binary data.

The coded fragments are created on the fly using the encoder algorithm
described by Semtech in the LoRaWAN TS004-1.0.0 document.

Signed-off-by: Martin Jäger <martin@libre.solar>
This commit is contained in:
Martin Jäger 2024-01-15 00:42:45 +01:00 committed by Alberto Escolar
commit 9e341b49c8
8 changed files with 372 additions and 0 deletions

View file

@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(lorawan_frag_decoder_test)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})

View file

@ -0,0 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
# Turn off log messages for failed communication with non-existing LoRa PHY
CONFIG_LORA_LOG_LEVEL_OFF=y

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 A Labs GmbH
*
* SPDX-License-Identifier: Apache-2.0
*
* This overlay defines a fake LoRa PHY node which is required to build the driver.
*/
#include <zephyr/dt-bindings/lora/sx126x.h>
/ {
chosen {
zephyr,code-partition = &slot0_partition;
};
aliases {
lora0 = &lora;
};
test {
#address-cells = <1>;
#size-cells = <1>;
test_spi: spi@33334444 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "vnd,spi";
reg = <0x33334444 0x1000>;
status = "okay";
clock-frequency = <2000000>;
cs-gpios = <&gpio0 1 GPIO_ACTIVE_LOW>;
lora: lora@0 {
compatible = "semtech,sx1262";
status = "okay";
reg = <0>;
reset-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
busy-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
tx-enable-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>;
rx-enable-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
dio1-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
dio2-tx-enable;
dio3-tcxo-voltage = <SX126X_DIO3_TCXO_3V3>;
tcxo-power-startup-delay-ms = <5>;
spi-max-frequency = <1000000>;
};
};
};
};

View file

@ -0,0 +1,33 @@
# SPDX-License-Identifier: Apache-2.0
CONFIG_ZTEST=y
# General Zephyr settings
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_THREAD_NAME=y
CONFIG_LOG=y
# LoRa PHY and required peripherals
CONFIG_LORA=y
CONFIG_SPI=y
CONFIG_GPIO=y
# Random number generator required for several LoRaWAN services
CONFIG_ENTROPY_GENERATOR=y
# LoRaWAN application layer
CONFIG_LORAWAN=y
CONFIG_LORAWAN_EMUL=y
CONFIG_LORAMAC_REGION_EU868=y
# LoRaWAN services required for this test
CONFIG_LORAWAN_SERVICES=y
CONFIG_LORAWAN_FRAG_TRANSPORT=y
# Flash driver to store firmware image
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_STREAM_FLASH=y
CONFIG_IMG_MANAGER=y

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2024 A Labs GmbH
* Copyright (c) 2024 tado GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Implementation of the fragment encoding algorithm described in the LoRaWAN TS004-1.0.0.
* https://lora-alliance.org/wp-content/uploads/2020/11/fragmented_data_block_transport_v1.0.0.pdf
*
* Note: This algorithm is not compatible with TS004-2.0.0, which has some subtle differences
* in the parity matrix generation.
*
* Variable naming according to LoRaWAN specification:
*
* M: Number of uncoded fragments (original data)
* N: Number of coded fragments (including the original data at the beginning)
* CR: Coding ratio M/N
*/
#include "frag_encoder.h"
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lorawan_frag_enc, CONFIG_LORAWAN_SERVICES_LOG_LEVEL);
/**
* Generate a 23bit Pseudorandom Binary Sequence (PRBS)
*
* @param seed Seed input value
*
* @returns Pseudorandom output value
*/
static int32_t prbs23(int32_t seed)
{
int32_t b0 = seed & 1;
int32_t b1 = (seed & 32) / 32;
return (seed / 2) + ((b0 ^ b1) << 22);
}
/**
* Generate vector for coded fragment n of the MxN parity matrix
*
* @param m Total number of uncoded fragments (M)
* @param n Coded fragment number (starting at 1 and not 0)
* @param vec Output vector (buffer size must be greater than m)
*/
void lorawan_fec_parity_matrix_vector(int m, int n, uint8_t *vec)
{
int mm, x, r;
memset(vec, 0, m);
/*
* Powers of 2 must be treated differently to make sure matrix content is close
* to random. Powers of 2 tend to generate patterns.
*/
if (is_power_of_two(m)) {
mm = m + 1;
} else {
mm = m;
}
x = 1 + (1001 * n);
for (int nb_coeff = 0; nb_coeff < (m / 2); nb_coeff++) {
r = (1 << 16);
while (r >= m) {
x = prbs23(x);
r = x % mm;
}
vec[r] = 1;
}
}
int lorawan_frag_encoder(const uint8_t *uncoded, size_t uncoded_len, uint8_t *coded,
size_t coded_size, size_t frag_size, unsigned int redundant_frags)
{
int uncoded_frags = DIV_ROUND_UP(uncoded_len, frag_size);
int coded_frags = uncoded_frags + redundant_frags;
uint8_t parity_vec[frag_size];
memset(parity_vec, 0, sizeof(parity_vec));
if (coded_size < coded_frags * frag_size) {
LOG_ERR("output buffer not large enough");
return -EINVAL;
}
/* copy uncoded frags to the beginning of coded fragments and pad with zeros */
memcpy(coded, uncoded, uncoded_len);
memset(coded + uncoded_len, 0, uncoded_frags * frag_size - uncoded_len);
/* generate remaining coded (redundant) frags */
for (int i = 1; i <= redundant_frags; i++) {
lorawan_fec_parity_matrix_vector(uncoded_frags, i, parity_vec);
uint8_t *out = coded + (uncoded_frags + i - 1) * frag_size;
for (int j = 0; j < uncoded_frags; j++) {
if (parity_vec[j] == 1) {
for (int m = 0; m < frag_size; m++) {
out[m] ^= coded[j * frag_size + m];
}
}
}
}
return 0;
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 A Labs GmbH
* Copyright (c) 2024 tado GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef TEST_SUBSYS_LORAWAN_FRAG_DECODER_SRC_FRAG_ENCODER_H_
#define TEST_SUBSYS_LORAWAN_FRAG_DECODER_SRC_FRAG_ENCODER_H_
#include <stdint.h>
#include <string.h>
/**
* Generate coded binary data according to LoRaWAN TS004-1.0.0
*
* @param uncoded Pointer to uncoded data buffer (e.g. firmware binary)
* @param uncoded_len Length of uncoded data in bytes
* @param coded Pointer to buffer for resulting coded data
* @param coded_size Size of the buffer for coded data
* @param frag_size Fragment size to be used
* @param redundant_frags Absolute number of redundant fragments to be generated
*
* @returns 0 for success or negative error code otherwise.
*/
int lorawan_frag_encoder(const uint8_t *uncoded, size_t uncoded_len, uint8_t *coded,
size_t coded_size, size_t frag_size, unsigned int redundant_frags);
#endif /* TEST_SUBSYS_LORAWAN_FRAG_DECODER_SRC_FRAG_ENCODER_H_ */

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2024 A Labs GmbH
* Copyright (c) 2024 tado GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/lorawan/lorawan.h>
#include <zephyr/lorawan/emul.h>
#include <zephyr/random/random.h>
#include <zephyr/sys/util.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/ztest.h>
#include "frag_encoder.h"
#define FRAG_SIZE CONFIG_LORAWAN_FRAG_TRANSPORT_MAX_FRAG_SIZE
#define FIRMWARE_SIZE (FRAG_SIZE * 100 + 1) /* not divisible by frag size to test padding */
#define UNCODED_FRAGS (DIV_ROUND_UP(FIRMWARE_SIZE, FRAG_SIZE))
#define REDUNDANT_FRAGS \
(DIV_ROUND_UP(UNCODED_FRAGS * CONFIG_LORAWAN_FRAG_TRANSPORT_MAX_REDUNDANCY, 100))
#define PADDING (UNCODED_FRAGS * FRAG_SIZE - FIRMWARE_SIZE)
#define CMD_FRAG_SESSION_SETUP (0x02)
#define CMD_DATA_FRAGMENT (0x08)
#define FRAG_TRANSPORT_PORT (201)
#define FRAG_SESSION_INDEX (1)
#define TARGET_IMAGE_AREA FIXED_PARTITION_ID(slot1_partition)
/* below array would normally hold the actual firmware binary */
static uint8_t fw_uncoded[FIRMWARE_SIZE];
/* enough space for redundancy of up to 100% */
static uint8_t fw_coded[(UNCODED_FRAGS + REDUNDANT_FRAGS) * FRAG_SIZE];
static const struct flash_area *fa;
static struct k_sem fuota_finished_sem;
static void fuota_finished(void)
{
k_sem_give(&fuota_finished_sem);
}
ZTEST(frag_decoder, test_frag_transport)
{
uint8_t buf[256]; /* maximum size of one LoRaWAN message */
uint8_t frag_session_setup_req[] = {
CMD_FRAG_SESSION_SETUP,
0x1f,
UNCODED_FRAGS & 0xFF,
(UNCODED_FRAGS >> 8) & 0xFF,
FRAG_SIZE,
0x01,
PADDING,
0x00,
0x00,
0x00,
0x00,
};
int ret;
k_sem_reset(&fuota_finished_sem);
lorawan_emul_send_downlink(FRAG_TRANSPORT_PORT, false, 0, 0, sizeof(frag_session_setup_req),
frag_session_setup_req);
for (int i = 0; i < sizeof(fw_coded) / FRAG_SIZE; i++) {
if (i % 10 == 9) {
/* loose every 10th packet */
continue;
}
buf[0] = CMD_DATA_FRAGMENT;
buf[1] = (i + 1) & 0xFF;
buf[2] = (FRAG_SESSION_INDEX << 6) | ((i + 1) >> 8);
memcpy(buf + 3, fw_coded + i * FRAG_SIZE, FRAG_SIZE);
lorawan_emul_send_downlink(FRAG_TRANSPORT_PORT, false, 0, 0, FRAG_SIZE + 3, buf);
}
for (int i = 0; i < UNCODED_FRAGS; i++) {
size_t num_bytes = (i == UNCODED_FRAGS - 1) ? (FRAG_SIZE - PADDING) : FRAG_SIZE;
flash_area_read(fa, i * FRAG_SIZE, buf, num_bytes);
zassert_mem_equal(buf, fw_coded + i * FRAG_SIZE, num_bytes, "fragment %d invalid",
i + 1);
}
ret = k_sem_take(&fuota_finished_sem, K_MSEC(100));
zassert_equal(ret, 0, "FUOTA finish timed out");
}
static void *frag_decoder_setup(void)
{
const struct device *lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
struct lorawan_join_config join_cfg = {0};
int ret;
/* populate firmware image with random data */
sys_rand_get(fw_uncoded, sizeof(fw_uncoded));
/* create coded data (including redundant fragments) from firmware image */
ret = lorawan_frag_encoder(fw_uncoded, sizeof(fw_uncoded), fw_coded, sizeof(fw_coded),
FRAG_SIZE, REDUNDANT_FRAGS);
zassert_equal(ret, 0, "creating coded data failed: %d", ret);
k_sem_init(&fuota_finished_sem, 0, 1);
ret = flash_area_open(TARGET_IMAGE_AREA, &fa);
zassert_equal(ret, 0, "opening flash area failed: %d", ret);
zassert_true(device_is_ready(lora_dev), "LoRa device not ready");
ret = lorawan_start();
zassert_equal(ret, 0, "lorawan_start failed: %d", ret);
ret = lorawan_join(&join_cfg);
zassert_equal(ret, 0, "lorawan_join failed: %d", ret);
lorawan_frag_transport_run(fuota_finished);
return NULL;
}
ZTEST_SUITE(frag_decoder, NULL, frag_decoder_setup, NULL, NULL, NULL);

View file

@ -0,0 +1,7 @@
common:
tags:
- lorawan
tests:
lorawan.frag_decoder:
platform_allow:
- native_sim