Initial commit of Arduino libraries

This commit is contained in:
Sam
2025-05-23 10:47:41 +10:00
commit 5bfce5fc3e
2476 changed files with 1108481 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
import argparse
import numpy as np
from PIL import Image
from argparse import RawTextHelpFormatter
def main():
parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''
RadioLib image to array conversion tool.
Input is a PNG image to be transmitted via RadioLib SSTV.
The image must have correct size for the chose SSTV mode!
Output is a file (by default named "img.h") which can be included and transmitted.
The resulting array will be very large (typically 320 kB),
make sure your platform has sufficient Flash/RAM space.
''')
parser.add_argument('input',
type=str,
help='Input PNG file')
parser.add_argument('output',
type=str,
nargs='?',
default='img',
help='Output header file')
args = parser.parse_args()
outfile = f'{args.output}.h'
print(f'Converting "{args.input}" to "{outfile}"')
# open the image as numpy array
img = Image.open(args.input)
arr = np.array(img)
# open the output file
with open(outfile, 'w') as f:
print(f'const uint32_t img[{arr.shape[0]}][{arr.shape[1]}] = {{', file=f)
for row in arr:
print(' { ', end='', file=f)
for pix in row:
rgb = pix[0] << 16 | pix[1] << 8 | pix[2]
print(hex(rgb), end=', ', file=f)
print(' },', file=f)
print('};', file=f)
print('Done!')
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1,176 @@
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
import argparse
import serial
import sys
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from datetime import datetime
from argparse import RawTextHelpFormatter
# number of samples in each scanline
SCAN_WIDTH = 33
# scanline Serial start/end markers
SCAN_MARK_START = 'SCAN '
SCAN_MARK_FREQ = 'FREQ '
SCAN_MARK_END = ' END'
# output path
OUT_PATH = 'out'
# default settings
DEFAULT_BAUDRATE = 115200
DEFAULT_COLOR_MAP = 'viridis'
DEFAULT_SCAN_LEN = 200
DEFAULT_RSSI_OFFSET = -11
# Print iterations progress
# from https://stackoverflow.com/questions/3173320/text-progress-bar-in-terminal-with-block-characters
def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 50, fill = '', printEnd = "\r"):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + '-' * (length - filledLength)
print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd)
if iteration == total:
print()
def main():
parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''
RadioLib SX126x_Spectrum_Scan plotter script. Displays output from SX126x_Spectrum_Scan example
as grayscale and
Depends on pyserial and matplotlib, install by:
'python3 -m pip install pyserial matplotlib'
Step-by-step guide on how to use the script:
1. Upload the SX126x_Spectrum_Scan example to your Arduino board with SX1262 connected.
2. Run the script with appropriate arguments.
3. Once the scan is complete, output files will be saved to out/
''')
parser.add_argument('port',
type=str,
help='COM port to connect to the device')
parser.add_argument('--speed',
default=DEFAULT_BAUDRATE,
type=int,
help=f'COM port baudrate (defaults to {DEFAULT_BAUDRATE})')
parser.add_argument('--map',
default=DEFAULT_COLOR_MAP,
type=str,
help=f'Matplotlib color map to use for the output (defaults to "{DEFAULT_COLOR_MAP}")')
parser.add_argument('--len',
default=DEFAULT_SCAN_LEN,
type=int,
help=f'Number of scanlines to record (defaults to {DEFAULT_SCAN_LEN})')
parser.add_argument('--offset',
default=DEFAULT_RSSI_OFFSET,
type=int,
help=f'Default RSSI offset in dBm (defaults to {DEFAULT_RSSI_OFFSET})')
parser.add_argument('--freq',
default=-1,
type=float,
help=f'Default starting frequency in MHz')
args = parser.parse_args()
freq_mode = False
scan_len = args.len
if (args.freq != -1):
freq_mode = True
scan_len = 1000
# create the color map and the result array
arr = np.zeros((SCAN_WIDTH, scan_len))
# scanline counter
row = 0
# list of frequencies in frequency mode
freq_list = []
# open the COM port
with serial.Serial(args.port, args.speed, timeout=None) as com:
while(True):
# update the progress bar
if not freq_mode:
printProgressBar(row, scan_len)
# read a single line
try:
line = com.readline().decode('utf-8')
except:
continue
if SCAN_MARK_FREQ in line:
new_freq = float(line.split(' ')[1])
if (len(freq_list) > 1) and (new_freq < freq_list[-1]):
break
freq_list.append(new_freq)
print('{:.3f}'.format(new_freq), end = '\r')
continue
# check the markers
if (SCAN_MARK_START in line) and (SCAN_MARK_END in line):
# get the values
scanline = line[len(SCAN_MARK_START):-len(SCAN_MARK_END)].split(',')
for col in range(SCAN_WIDTH):
arr[col][row] = int(scanline[col])
# increment the row counter
row = row + 1
# check if we're done
if (not freq_mode) and (row >= scan_len):
break
# scale to the number of scans (sum of any given scanline)
num_samples = arr.sum(axis=0)[0]
arr *= (num_samples/arr.max())
if freq_mode:
scan_len = len(freq_list)
# create the figure
fig, ax = plt.subplots()
# display the result as heatmap
extent = [0, scan_len, -4*(SCAN_WIDTH + 1), args.offset]
if freq_mode:
extent[0] = freq_list[0]
extent[1] = freq_list[-1]
im = ax.imshow(arr[:,:scan_len], cmap=args.map, extent=extent)
fig.colorbar(im)
# set some properites and show
timestamp = datetime.now().strftime('%y-%m-%d %H-%M-%S')
title = f'RadioLib SX126x Spectral Scan {timestamp}'
if freq_mode:
plt.xlabel("Frequency [Hz]")
else:
plt.xlabel("Time [sample]")
plt.ylabel("RSSI [dBm]")
ax.set_aspect('auto')
fig.suptitle(title)
fig.canvas.manager.set_window_title(title)
plt.savefig(f'{OUT_PATH}/{title.replace(" ", "_")}.png', dpi=300)
plt.show()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,20 @@
#! /bin/bash
file=cppcheck.txt
cppcheck --version
cppcheck src --enable=all --force --inline-suppr --suppress=ConfigurationNotChecked --suppress=unusedFunction --quiet >> $file 2>&1
echo "Cppcheck finished with exit code $?"
error=$(grep ": error:" $file | wc -l)
warning=$(grep ": warning:" $file | wc -l)
style=$(grep ": style:" $file | wc -l)
performance=$(grep ": performance:" $file | wc -l)
echo "found $error erros, $warning warnings, $style style and $performance performance issues"
if [ $error -gt "0" ] || [ $warning -gt "0" ] || [ $style -gt "0" ] || [ $performance -gt "0" ]
then
cat $file
exitcode=1
fi
rm $file
exit $exitcode

View File

@@ -0,0 +1,22 @@
#include "<module_name>.h"
#if !defined(RADIOLIB_EXCLUDE_<module_name>)
<module_name>::<module_name>(Module* mod) {
/*
Constructor implementation MUST assign the provided "mod" pointer to the private "_mod" pointer.
*/
_mod = mod;
}
int16_t <module_name>::begin() {
/*
"begin" method implementation MUST call the "init" method with appropriate settings.
*/
_mod->init();
/*
"begin" method SHOULD implement some sort of mechanism to verify the connection between Arduino and the module.
For example, reading a version register
*/
}

View File

@@ -0,0 +1,99 @@
/*
RadioLib Module Template header file
Before opening pull request, please make sure that:
1. All files MUST be compiled without errors using default Arduino IDE settings.
2. All files SHOULD be compiled without warnings with compiler warnings set to "All".
3. Example sketches MUST be working correctly and MUST be stable enough to run for prolonged periods of time.
4. Writing style SHOULD be consistent.
5. Comments SHOULD be in place for the most important chunks of code and SHOULD be free of typos.
6. To indent, 2 spaces MUST be used.
If at any point you are unsure about the required style, please refer to the rest of the modules.
*/
#if !defined(_RADIOLIB_<module_name>_H) && !defined(RADIOLIB_EXCLUDE_<module_name>)
#if !defined(_RADIOLIB_<module_name>_H)
#define _RADIOLIB_<module_name>_H
/*
Header file for each module MUST include Module.h and TypeDef.h in the src folder.
The header file MAY include additional header files.
*/
#include "../../Module.h"
#include "../../TypeDef.h"
/*
Only use the following include if the module implements methods for OSI physical layer control.
This concerns only modules similar to SX127x/RF69/CC1101 etc.
In this case, your class MUST implement all virtual methods of PhysicalLayer class.
*/
//#include "../../protocols/PhysicalLayer/PhysicalLayer.h"
/*
Register map
Definition of SPI register map SHOULD be placed here. The register map SHOULD have two parts:
1 - Address map: only defines register names and addresses. Register names MUST match names in
official documentation (datasheets etc.).
2 - Variable map: defines variables inside register. This functions as a bit range map for a specific register.
Bit range (MSB and LSB) as well as short description for each variable MUST be provided in a comment.
See RF69 and SX127x header files for examples of register maps.
*/
// <module_name> register map | spaces up to this point
#define RADIOLIB_<module_name>_REG_<register_name> 0x00
// <module_name>_REG_<register_name> MSB LSB DESCRIPTION
#define RADIOLIB_<module_name>_<register_variable> 0b00000000 // 7 0 <description>
/*
Module class definition
The module class MAY inherit from the following classes:
1 - PhysicalLayer: In case the module implements methods for OSI physical layer control (e.g. SX127x).
2 - Common class: In case the module further specifies some more generic class (e.g. SX127x/SX1278)
*/
class <module_name> {
public:
/*
Constructor MUST have only one parameter "Module* mod".
The class MAY implement additional overloaded constructors.
*/
// constructor
<module_name>(Module* mod);
/*
The class MUST implement at least one basic method called "begin".
The "begin" method MUST initialize the module and return the status as int16_t type.
*/
// basic methods
int16_t begin();
/*
The class MAY implement additional methods.
All implemented methods SHOULD return the status as int16_t type.
*/
#if !defined(RADIOLIB_GODMODE)
private:
#endif
/*
The class MUST contain private member "Module* _mod"
*/
Module* _mod;
/*
The class MAY contain additional private variables and/or methods.
Private member variables MUST have a name prefixed with "_" (underscore, ASCII 0x5F)
Usually, these are variables for saving module configuration, or methods that do not have to be exposed to the end user.
*/
};
#endif
#endif

View File

@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.18)
# create the project
project(rpi-sx1261)
# when using debuggers such as gdb, the following line can be used
#set(CMAKE_BUILD_TYPE Debug)
# if you did not build RadioLib as shared library (see README),
# you will have to add it as source directory
# the following is just an example, yours will likely be different
#add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../../RadioLib" "${CMAKE_CURRENT_BINARY_DIR}/RadioLib")
# add the executable
add_executable(${PROJECT_NAME} main.cpp)
# link both libraries
target_link_libraries(${PROJECT_NAME} RadioLib pigpio)
# you can also specify RadioLib compile-time flags here
#target_compile_definitions(${PROJECT_NAME} PUBLIC RADIOLIB_DEBUG RADIOLIB_VERBOSE)

View File

@@ -0,0 +1,151 @@
#ifndef PI_HAL_H
#define PI_HAL_H
// include RadioLib
#include <RadioLib/RadioLib.h>
// include the library for Raspberry GPIO pins
#include "pigpio.h"
// create a new Raspberry Pi hardware abstraction layer
// using the pigpio library
// the HAL must inherit from the base RadioLibHal class
// and implement all of its virtual methods
class PiHal : public RadioLibHal {
public:
// default constructor - initializes the base HAL and any needed private members
PiHal(uint8_t spiChannel, uint32_t spiSpeed = 2000000)
: RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, RISING_EDGE, FALLING_EDGE),
_spiChannel(spiChannel),
_spiSpeed(spiSpeed) {
}
void init() override {
// first initialise pigpio library
gpioInitialise();
// now the SPI
spiBegin();
// Waveshare LoRaWAN Hat also needs pin 18 to be pulled high to enable the radio
gpioSetMode(18, PI_OUTPUT);
gpioWrite(18, PI_HIGH);
}
void term() override {
// stop the SPI
spiEnd();
// pull the enable pin low
gpioSetMode(18, PI_OUTPUT);
gpioWrite(18, PI_LOW);
// finally, stop the pigpio library
gpioTerminate();
}
// GPIO-related methods (pinMode, digitalWrite etc.) should check
// RADIOLIB_NC as an alias for non-connected pins
void pinMode(uint32_t pin, uint32_t mode) override {
if(pin == RADIOLIB_NC) {
return;
}
gpioSetMode(pin, mode);
}
void digitalWrite(uint32_t pin, uint32_t value) override {
if(pin == RADIOLIB_NC) {
return;
}
gpioWrite(pin, value);
}
uint32_t digitalRead(uint32_t pin) override {
if(pin == RADIOLIB_NC) {
return(0);
}
return(gpioRead(pin));
}
void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override {
if(interruptNum == RADIOLIB_NC) {
return;
}
gpioSetISRFunc(interruptNum, mode, 0, (gpioISRFunc_t)interruptCb);
}
void detachInterrupt(uint32_t interruptNum) override {
if(interruptNum == RADIOLIB_NC) {
return;
}
gpioSetISRFunc(interruptNum, 0, 0, NULL);
}
void delay(RadioLibTime_t ms) override {
gpioDelay(ms * 1000);
}
void delayMicroseconds(RadioLibTime_t us) override {
gpioDelay(us);
}
RadioLibTime_t millis() override {
return(gpioTick() / 1000);
}
RadioLibTime_t micros() override {
return(gpioTick());
}
long pulseIn(uint32_t pin, uint32_t state, RadioLibTime_t timeout) override {
if(pin == RADIOLIB_NC) {
return(0);
}
this->pinMode(pin, PI_INPUT);
RadioLibTime_t start = this->micros();
RadioLibTime_t curtick = this->micros();
while(this->digitalRead(pin) == state) {
if((this->micros() - curtick) > timeout) {
return(0);
}
}
return(this->micros() - start);
}
void spiBegin() {
if(_spiHandle < 0) {
_spiHandle = spiOpen(_spiChannel, _spiSpeed, 0);
}
}
void spiBeginTransaction() {}
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) {
spiXfer(_spiHandle, (char*)out, (char*)in, len);
}
void spiEndTransaction() {}
void spiEnd() {
if(_spiHandle >= 0) {
spiClose(_spiHandle);
_spiHandle = -1;
}
}
private:
// the HAL can contain any additional private members
const unsigned int _spiSpeed;
const uint8_t _spiChannel;
int _spiHandle = -1;
};
#endif

View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
mkdir -p build
cd build
cmake -G "CodeBlocks - Unix Makefiles" ..
make -j4
cd ..

View File

@@ -0,0 +1,3 @@
#!/bin/bash
rm -rf ./build

View File

@@ -0,0 +1,26 @@
// this is an autotest file for the SX126x
// runs on Raspberry Pi with Waveshare LoRaWAN hat
#include <RadioLib/RadioLib.h>
#include "PiHal.h"
#define RADIOLIB_TEST_ASSERT(STATEVAR) { if((STATEVAR) != RADIOLIB_ERR_NONE) { return(-1*(STATEVAR)); } }
PiHal* hal = new PiHal(1);
SX1261 radio = new Module(hal, 7, 17, 22, RADIOLIB_NC);
// the entry point for the program
int main(int argc, char** argv) {
int state = RADIOLIB_ERR_UNKNOWN;
state = radio.begin();
printf("[SX1261] Test:begin() = %d\n", state);
RADIOLIB_TEST_ASSERT(state);
state = radio.transmit("Hello World!");
printf("[SX1261] Test:transmit() = %d\n", state);
RADIOLIB_TEST_ASSERT(state);
hal->term();
return(0);
}

View File

@@ -0,0 +1,13 @@
#!/bin/bash
board=$1
sketch=$2
flags=$3
warnings="all"
arduino-cli compile \
--libraries ../../../../ \
--fqbn $board \
--build-property compiler.cpp.extra_flags="$flags" \
--warnings=$warnings \
$sketch --export-binaries

View File

@@ -0,0 +1,41 @@
#!/bin/bash
#board=arduino:avr:mega
board="$1"
#skip="(STM32WL|LR11x0_Firmware_Update|NonArduino)"
skip="$2"
#options=""
options="$3"
# file for saving the compiled binary size reports
size_file="size_$board.txt"
rm -f $size_file
path="../../../examples"
for example in $(find $path -name '*.ino' | sort); do
# check whether to skip this sketch
if [ ! -z '$skip' ] && [[ ${example} =~ ${skip} ]]; then
# skip sketch
echo -e "\n\033[1;33mSkipped ${example##*/} (matched with $skip)\033[0m";
else
# apply special flags for LoRaWAN
if [[ ${example} =~ "LoRaWAN" ]]; then
flags="-DRADIOLIB_LORAWAN_DEV_ADDR=0 -DRADIOLIB_LORAWAN_FNWKSINT_KEY=0 -DRADIOLIB_LORAWAN_SNWKSINT_KEY=0 -DRADIOLIB_LORAWAN_NWKSENC_KEY=0 -DRADIOLIB_LORAWAN_APPS_KEY=0 -DRADIOLIB_LORAWAN_APP_KEY=0 -DRADIOLIB_LORAWAN_NWK_KEY=0 -DRADIOLIB_LORAWAN_DEV_EUI=0 -DARDUINO_TTGO_LORA32_V1"
fi
# build sketch
echo -e "\n\033[1;33mBuilding ${example##*/} ... \033[0m";
board_opts=$board$options
./build_arduino.sh $board_opts $example "$flags"
if [ $? -ne 0 ]; then
echo -e "\033[1;31m${example##*/} build FAILED\033[0m\n";
exit 1;
else
echo -e "\033[1;32m${example##*/} build PASSED\033[0m\n";
dir="$(dirname -- "$example")"
file="$(basename -- "$example")"
size="$(size $dir/build/*/$file.elf)"
echo $size >> $size_file
fi
fi
done

View File

@@ -0,0 +1,24 @@
#!/bin/bash
board=$1
hash=$(git rev-parse --short HEAD)
in_file="size_$board.txt"
out_file="size_${hash}_${board//:/-}.csv"
rm -f $out_file
# write the header
echo "text,data,bss,dec,hex,filename" > "$out_file"
# convert to CSV
awk 'NR > 1 {
split($12, path_parts, "/");
filename_with_ext = path_parts[length(path_parts)];
split(filename_with_ext, filename_parts, ".");
filename = filename_parts[1];
print $7 "," $8 "," $9 "," $10 "," $11 "," filename
}' "$in_file" >> "$out_file"
# remove input file
rm -f $in_file

View File

@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.13)
project(radiolib-unittest)
# add RadioLib sources
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../../RadioLib" "${CMAKE_CURRENT_BINARY_DIR}/RadioLib")
# add test sources
file(GLOB_RECURSE TEST_SOURCES
"tests/main.cpp"
"tests/TestModule.cpp"
)
# create the executable
add_executable(${PROJECT_NAME} ${TEST_SOURCES})
# include directories
target_include_directories(${PROJECT_NAME} PUBLIC include)
# link RadioLib
target_link_libraries(${PROJECT_NAME} RadioLib fmt)
# set target properties and options
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 20)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra)
# set RadioLib debug
#target_compile_definitions(RadioLib PUBLIC RADIOLIB_DEBUG_BASIC RADIOLIB_DEBUG_SPI)
#target_compile_definitions(RadioLib PUBLIC RADIOLIB_DEBUG_PORT=stdout)

View File

@@ -0,0 +1,71 @@
#ifndef HARDWARE_EMULATION_HPP
#define HARDWARE_EMULATION_HPP
#include <stdint.h>
// value that is returned by the emualted radio class when performing SPI transfer to it
#define EMULATED_RADIO_SPI_RETURN (0xFF)
// pin indexes
#define EMULATED_RADIO_NSS_PIN (1)
#define EMULATED_RADIO_IRQ_PIN (2)
#define EMULATED_RADIO_RST_PIN (3)
#define EMULATED_RADIO_GPIO_PIN (4)
enum PinFunction_t {
PIN_UNASSIGNED = 0,
PIN_CS,
PIN_IRQ,
PIN_RST,
PIN_GPIO,
};
// structure for emulating GPIO pins
struct EmulatedPin_t {
uint32_t mode;
uint32_t value;
bool event;
PinFunction_t func;
};
// structure for emulating SPI registers
struct EmulatedRegister_t {
uint8_t value;
uint8_t readOnlyBitFlags;
bool bufferAccess;
};
// base class for emulated radio modules (SX126x etc.)
class EmulatedRadio {
public:
void connect(EmulatedPin_t* csPin, EmulatedPin_t* irqPin, EmulatedPin_t* rstPin, EmulatedPin_t* gpioPin) {
this->cs = csPin;
this->cs->func = PIN_CS;
this->irq = irqPin;
this->irq->func = PIN_IRQ;
this->rst = rstPin;
this->rst->func = PIN_RST;
this->gpio = gpioPin;
this->gpio->func = PIN_GPIO;
}
virtual uint8_t HandleSPI(uint8_t b) {
(void)b;
// handle the SPI input and generate output here
return(EMULATED_RADIO_SPI_RETURN);
}
virtual void HandleGPIO() {
// handle discrete GPIO signals here (e.g. reset state machine on NSS falling edge)
}
protected:
// pointers to emulated GPIO pins
// this is done via pointers so that the same GPIO entity is shared, like with a real hardware
EmulatedPin_t* cs;
EmulatedPin_t* irq;
EmulatedPin_t* rst;
EmulatedPin_t* gpio;
};
#endif

View File

@@ -0,0 +1,237 @@
#ifndef TEST_HAL_HPP
#define TEST_HAL_HPP
#include <chrono>
#include <thread>
#include <fmt/format.h>
#include <RadioLib.h>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#if defined(TEST_HAL_LOG)
#define HAL_LOG(...) BOOST_TEST_MESSAGE(__VA_ARGS__)
#else
#define HAL_LOG(...) {}
#endif
#include "HardwareEmulation.hpp"
#define TEST_HAL_INPUT (0)
#define TEST_HAL_OUTPUT (1)
#define TEST_HAL_LOW (0)
#define TEST_HAL_HIGH (1)
#define TEST_HAL_RISING (0)
#define TEST_HAL_FALLING (1)
// number of emulated GPIO pins
#define TEST_HAL_NUM_GPIO_PINS (32)
#define TEST_HAL_SPI_LOG_LENGTH (512)
class TestHal : public RadioLibHal {
public:
TestHal() : RadioLibHal(TEST_HAL_INPUT, TEST_HAL_OUTPUT, TEST_HAL_LOW, TEST_HAL_HIGH, TEST_HAL_RISING, TEST_HAL_FALLING) { }
void init() override {
HAL_LOG("TestHal::init()");
// save program start timestamp
start = std::chrono::high_resolution_clock::now();
// init emulated GPIO
for(int i = 0; i < TEST_HAL_NUM_GPIO_PINS; i++) {
this->gpio[i].mode = 0;
this->gpio[i].value = 0;
this->gpio[i].event = false;
this->gpio[i].func = PIN_UNASSIGNED;
}
}
void term() override {
HAL_LOG("TestHal::term()");
}
void pinMode(uint32_t pin, uint32_t mode) override {
HAL_LOG("TestHal::pinMode(pin=" << pin << ", mode=" << mode << " [" << ((mode == TEST_HAL_INPUT) ? "INPUT" : "OUTPUT") << "])");
// check the range
BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range");
// check known modes
BOOST_ASSERT_MSG(((mode == TEST_HAL_INPUT) || (mode == TEST_HAL_OUTPUT)), "Invalid pin mode");
// set mode
this->gpio[pin].mode = mode;
}
void digitalWrite(uint32_t pin, uint32_t value) override {
HAL_LOG("TestHal::digitalWrite(pin=" << pin << ", value=" << value << " [" << ((value == TEST_HAL_LOW) ? "LOW" : "HIGH") << "])");
// check the range
BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range");
// check it is output
BOOST_ASSERT_MSG(this->gpio[pin].mode == TEST_HAL_OUTPUT, "GPIO is not output!");
// check known values
BOOST_ASSERT_MSG(((value == TEST_HAL_LOW) || (value == TEST_HAL_HIGH)), "Invalid output value");
// set value
this->gpio[pin].value = value;
this->gpio[pin].event = true;
if(radio) {
this->radio->HandleGPIO();
}
this->gpio[pin].event = false;
}
uint32_t digitalRead(uint32_t pin) override {
HAL_LOG("TestHal::digitalRead(pin=" << pin << ")");
// check the range
BOOST_ASSERT_MSG(pin < TEST_HAL_NUM_GPIO_PINS, "Pin number out of range");
// check it is input
BOOST_ASSERT_MSG(this->gpio[pin].mode == TEST_HAL_INPUT, "GPIO is not input");
// read the value
uint32_t value = this->gpio[pin].value;
HAL_LOG("TestHal::digitalRead(pin=" << pin << ")=" << value << " [" << ((value == TEST_HAL_LOW) ? "LOW" : "HIGH") << "]");
return(value);
}
void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override {
HAL_LOG("TestHal::attachInterrupt(interruptNum=" << interruptNum << ", interruptCb=" << interruptCb << ", mode=" << mode << ")");
}
void detachInterrupt(uint32_t interruptNum) override {
HAL_LOG("TestHal::detachInterrupt(interruptNum=" << interruptNum << ")");
}
void delay(unsigned long ms) override {
HAL_LOG("TestHal::delay(ms=" << ms << ")");
const auto start = std::chrono::high_resolution_clock::now();
// sleep_for is sufficient for ms-precision sleep
std::this_thread::sleep_for(std::chrono::duration<unsigned long, std::milli>(ms));
// measure and print
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double, std::milli> elapsed = end - start;
HAL_LOG("TestHal::delay(ms=" << ms << ")=" << elapsed.count() << "ms");
}
void delayMicroseconds(unsigned long us) override {
HAL_LOG("TestHal::delayMicroseconds(us=" << us << ")");
const auto start = std::chrono::high_resolution_clock::now();
// busy wait is needed for microseconds precision
const auto len = std::chrono::microseconds(us);
while(std::chrono::high_resolution_clock::now() - start < len);
// measure and print
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double, std::micro> elapsed = end - start;
HAL_LOG("TestHal::delayMicroseconds(us=" << us << ")=" << elapsed.count() << "us");
}
void yield() override {
HAL_LOG("TestHal::yield()");
}
unsigned long millis() override {
HAL_LOG("TestHal::millis()");
std::chrono::time_point now = std::chrono::high_resolution_clock::now();
auto res = std::chrono::duration_cast<std::chrono::milliseconds>(now - this->start);
HAL_LOG("TestHal::millis()=" << res.count());
return(res.count());
}
unsigned long micros() override {
HAL_LOG("TestHal::micros()");
std::chrono::time_point now = std::chrono::high_resolution_clock::now();
auto res = std::chrono::duration_cast<std::chrono::microseconds>(now - this->start);
HAL_LOG("TestHal::micros()=" << res.count());
return(res.count());
}
long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override {
HAL_LOG("TestHal::pulseIn(pin=" << pin << ", state=" << state << ", timeout=" << timeout << ")");
return(0);
}
void spiBegin() {
HAL_LOG("TestHal::spiBegin()");
}
void spiBeginTransaction() {
HAL_LOG("TestHal::spiBeginTransaction()");
// wipe history log
memset(this->spiLog, 0x00, TEST_HAL_SPI_LOG_LENGTH);
this->spiLogPtr = this->spiLog;
}
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) {
HAL_LOG("TestHal::spiTransfer(len=" << len << ")");
for(size_t i = 0; i < len; i++) {
// append to log
(*this->spiLogPtr++) = out[i];
// process the SPI byte
in[i] = this->radio->HandleSPI(out[i]);
// outpu debug
HAL_LOG(fmt::format("out={:#02x}, in={:#02x}", out[i], in[i]));
}
}
void spiEndTransaction() {
HAL_LOG("TestHal::spiEndTransaction()");
}
void spiEnd() {
HAL_LOG("TestHal::spiEnd()");
}
void tone(uint32_t pin, unsigned int frequency, unsigned long duration = 0) {
HAL_LOG("TestHal::tone(pin=" << pin << ", frequency=" << frequency << ", duration=" << duration << ")");
}
void noTone(uint32_t pin) {
HAL_LOG("TestHal::noTone(pin=" << pin << ")");
}
// method to compare buffer to the internal SPI log, for verifying SPI transactions
int spiLogMemcmp(const void* in, size_t n) {
return(memcmp(this->spiLog, in, n));
}
// method that "connects" the emualted radio hardware to this HAL
void connectRadio(EmulatedRadio* r) {
this->radio = r;
this->radio->connect(&this->gpio[EMULATED_RADIO_NSS_PIN],
&this->gpio[EMULATED_RADIO_IRQ_PIN],
&this->gpio[EMULATED_RADIO_RST_PIN],
&this->gpio[EMULATED_RADIO_GPIO_PIN]);
}
private:
// array of emulated GPIO pins
EmulatedPin_t gpio[TEST_HAL_NUM_GPIO_PINS];
// start time point
std::chrono::time_point<std::chrono::high_resolution_clock> start;
// emulated radio hardware
EmulatedRadio* radio;
// SPI history log
uint8_t spiLog[TEST_HAL_SPI_LOG_LENGTH];
uint8_t* spiLogPtr;
};
#endif

View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
# build the test binary
mkdir -p build
cd build
cmake -G "CodeBlocks - Unix Makefiles" ..
make -j4
# run it
cd ..
./build/radiolib-unittest --log_level=message

View File

@@ -0,0 +1,103 @@
// boost test header
#include <boost/test/unit_test.hpp>
// mock HAL
#include "TestHal.hpp"
// testing fixture
struct ModuleFixture {
TestHal* hal = nullptr;
Module* mod = nullptr;
EmulatedRadio* radioHardware = nullptr;
ModuleFixture() {
BOOST_TEST_MESSAGE("--- Module fixture setup ---");
hal = new TestHal();
radioHardware = new EmulatedRadio();
hal->connectRadio(radioHardware);
mod = new Module(hal, EMULATED_RADIO_NSS_PIN, EMULATED_RADIO_IRQ_PIN, EMULATED_RADIO_RST_PIN, EMULATED_RADIO_GPIO_PIN);
mod->init();
}
~ModuleFixture() {
BOOST_TEST_MESSAGE("--- Module fixture teardown ---");
mod->term();
delete[] mod;
delete[] hal;
}
};
BOOST_FIXTURE_TEST_SUITE(suite_Module, ModuleFixture)
BOOST_FIXTURE_TEST_CASE(Module_SPIgetRegValue_reg, ModuleFixture)
{
BOOST_TEST_MESSAGE("--- Test Module::SPIgetRegValue register access ---");
int16_t ret;
// basic register read with default config
const uint8_t address = 0x12;
const uint8_t spiTxn[] = { address, 0x00 };
ret = mod->SPIgetRegValue(address);
// check return code, value and history log
BOOST_TEST(ret >= RADIOLIB_ERR_NONE);
BOOST_TEST(ret == EMULATED_RADIO_SPI_RETURN);
BOOST_TEST(hal->spiLogMemcmp(spiTxn, sizeof(spiTxn)) == 0);
// register read masking test
const uint8_t msb = 5;
const uint8_t lsb = 1;
ret = mod->SPIgetRegValue(address, msb, lsb);
BOOST_TEST(ret == 0x3E);
// invalid mask tests (swapped MSB and LSB, out of range bit masks)
ret = mod->SPIgetRegValue(address, lsb, msb);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
ret = mod->SPIgetRegValue(address, 10, lsb);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
ret = mod->SPIgetRegValue(address, msb, 10);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
}
BOOST_FIXTURE_TEST_CASE(Module_SPIgetRegValue_stream, ModuleFixture)
{
BOOST_TEST_MESSAGE("--- Test Module::SPIgetRegValue stream access ---");
int16_t ret;
// change settings to stream type
mod->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_ADDR] = Module::BITS_16;
mod->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_CMD] = Module::BITS_8;
mod->spiConfig.statusPos = 1;
mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_READ] = RADIOLIB_SX126X_CMD_READ_REGISTER;
mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_WRITE] = RADIOLIB_SX126X_CMD_WRITE_REGISTER;
mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_NOP] = RADIOLIB_SX126X_CMD_NOP;
mod->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_STATUS] = RADIOLIB_SX126X_CMD_GET_STATUS;
mod->spiConfig.stream = true;
// basic register read
const uint8_t address = 0x12;
const uint8_t spiTxn[] = { RADIOLIB_SX126X_CMD_READ_REGISTER, 0x00, address, 0x00, 0x00 };
ret = mod->SPIgetRegValue(address);
// check return code, value and history log
BOOST_TEST(ret >= RADIOLIB_ERR_NONE);
BOOST_TEST(ret == EMULATED_RADIO_SPI_RETURN);
BOOST_TEST(hal->spiLogMemcmp(spiTxn, sizeof(spiTxn)) == 0);
// register read masking test
const uint8_t msb = 5;
const uint8_t lsb = 1;
ret = mod->SPIgetRegValue(address, msb, lsb);
BOOST_TEST(ret == 0x3E);
// invalid mask tests (swapped MSB and LSB, out of range bit masks)
ret = mod->SPIgetRegValue(address, lsb, msb);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
ret = mod->SPIgetRegValue(address, 10, lsb);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
ret = mod->SPIgetRegValue(address, msb, 10);
BOOST_TEST(ret == RADIOLIB_ERR_INVALID_BIT_RANGE);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@@ -0,0 +1,5 @@
#define BOOST_TEST_MODULE "RadioLib Unit test"
#include <boost/test/included/unit_test.hpp>
// intentionally left blank, boost.test creates its own entrypoint