micropython-lib/micropython/lora/tests/test_time_on_air.py

311 wiersze
12 KiB
Python

# MicroPython LoRa modem driver time on air tests
# MIT license; Copyright (c) 2023 Angus Gratton
#
# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
#
# ## What is this?
#
# Host tests for the BaseModem.get_time_on_air_us() function. Theses against
# dummy test values produced by the Semtech "SX1261 LoRa Calculator" software,
# as downloaded from
# https://lora-developers.semtech.com/documentation/product-documents/
#
# The app notes for SX1276 (AN1200.3) suggest a similar calculator exists for that
# modem, but it doesn't appear to be available for download any more. I couldn't find
# an accurate calculator for SX1276, so manually calculated the SF5 & SF6 test cases below
# (other values should be the same as SX1262).
#
# ## Instructions
#
# These tests are intended to be run on a host PC via micropython unix port:
#
# cd /path/to/micropython-lib/micropython/lora
# micropython -m tests.test_time_on_air
#
# Note: Using the working directory shown above is easiest way to ensure 'lora' files are imported.
#
from lora import SX1262, SX1276
# Allow time calculations to deviate by up to this much as a ratio
# of the expected value (due to floating point, etc.)
TIME_ERROR_RATIO = 0.00001 # 0.001%
def main():
sx1262 = SX1262(spi=DummySPI(), cs=DummyPin(), busy=DummyPin())
sx1276 = SX1276(spi=DummySPI(0x12), cs=DummyPin())
# Test case format is based on the layout of the Semtech Calculator UI:
#
# (modem_instance,
# (modem settings),
# [
# ((packet config), (output values)),
# ...
# ],
# ),
#
# where each set of modem settings maps to zero or more packet config / output pairs
#
# - modem instance is sx1262 or sx1276 (SF5 & SF6 are different between these modems)
# - (modem settings) is (sf, bw (in khz), coding_rate, low_datarate_optimize)
# - (packet config) is (preamble_len, payload_len, explicit_header, crc_en)
# - (output values) is (total_symbols_excl, symbol_time in ms, time_on_air in ms)
#
# NOTE: total_symbols_excl is the value shown in the calculator output,
# which doesn't include 8 symbols of constant overhead between preamble and
# header+payload+crc. I think this is a bug in the Semtech calculator(!).
# These 8 symbols are included when the calculator derives the total time on
# air.
#
# NOTE ALSO: The "symbol_time" only depends on the modem settings so is
# repeated each group of test cases, and the "time_on_air" is the previous
# two output values multiplied (after accounting for the 8 symbols noted
# above). This repetition is deliberate to make the cases easier to read
# line-by-line when comparing to the calculator window.
CASES = [
(
sx1262,
(12, 500, 5, False), # Calculator defaults when launching calculator
[
((8, 1, True, True), (17.25, 8.192, 206.848)), # Calculator defaults
((12, 64, True, True), (71.25, 8.192, 649.216)),
((8, 1, True, False), (12.25, 8.192, 165.888)),
((8, 192, True, True), (172.25, 8.192, 1476.608)),
((12, 16, False, False), (26.25, 8.192, 280.576)),
],
),
(
sx1262,
(8, 125, 6, False),
[
((8, 1, True, True), (18.25, 2.048, 53.760)),
((8, 2, True, True), (18.25, 2.048, 53.760)),
((8, 2, True, False), (18.25, 2.048, 53.760)),
((8, 3, True, True), (24.25, 2.048, 66.048)),
((8, 3, True, False), (18.25, 2.048, 53.760)),
((8, 4, True, True), (24.25, 2.048, 66.048)),
((8, 4, True, False), (18.25, 2.048, 53.760)),
((8, 5, True, True), (24.25, 2.048, 66.048)),
((8, 5, True, False), (24.25, 2.048, 66.048)),
((8, 253, True, True), (396.25, 2.048, 827.904)),
((8, 253, True, False), (396.25, 2.048, 827.904)),
((12, 5, False, True), (22.25, 2.048, 61.952)),
((12, 5, False, False), (22.25, 2.048, 61.952)),
((12, 10, False, True), (34.25, 2.048, 86.528)),
((12, 253, False, True), (394.25, 2.048, 823.808)),
],
),
# quick check that sx1276 is the same as sx1262 for SF>6
(
sx1276,
(8, 125, 6, False),
[
((8, 1, True, True), (18.25, 2.048, 53.760)),
((8, 2, True, True), (18.25, 2.048, 53.760)),
((12, 5, False, True), (22.25, 2.048, 61.952)),
((12, 5, False, False), (22.25, 2.048, 61.952)),
],
),
# SF5 on SX1262
(
sx1262,
(5, 500, 5, False),
[
(
(2, 1, True, False),
(13.25, 0.064, 1.360),
), # Shortest possible LoRa packet?
((2, 1, True, True), (18.25, 0.064, 1.680)),
((12, 1, False, False), (18.25, 0.064, 1.680)),
((12, 253, False, True), (523.25, 0.064, 34.000)),
],
),
(
sx1262,
(5, 125, 8, False),
[
((12, 253, False, True), (826.25, 0.256, 213.568)),
],
),
# SF5 on SX1276
#
# Note: SF5 & SF6 settings are different between SX1262 & SX1276.
#
# There's no Semtech official calculator available for SX1276, so the
# symbol length is calculated by copying the formula from the datasheet
# "Time on air" section. Symbol time is the same as SX1262. Then the
# time on air is manually calculated by multiplying the two together.
#
# see the functions sx1276_num_payload and sx1276_num_symbols at end of this module
# for the actual functions used.
(
sx1276,
(5, 500, 5, False),
[
(
(2, 1, True, False),
(19.25 - 8, 0.064, 1.232),
), # Shortest possible LoRa packet?
((2, 1, True, True), (24.25 - 8, 0.064, 1.552)),
((12, 1, False, False), (24.25 - 8, 0.064, 1.552)),
((12, 253, False, True), (534.25 - 8, 0.064, 34.192)),
],
),
(
sx1276,
(5, 125, 8, False),
[
((12, 253, False, True), (840.25 - 8, 0.256, 215.104)),
],
),
(
sx1262,
(12, 7.81, 8, True), # Slowest possible
[
((128, 253, True, True), (540.25, 524.456, 287532.907)),
((1000, 253, True, True), (1412.25, 524.456, 744858.387)),
],
),
(
sx1262,
(11, 10.42, 7, True),
[
((25, 16, True, True), (57.25, 196.545, 12824.568)),
((25, 16, False, False), (50.25, 196.545, 11448.752)),
],
),
]
tests = 0
failures = set()
for modem, modem_settings, packets in CASES:
(sf, bw_khz, coding_rate, low_datarate_optimize) = modem_settings
print(
f"Modem config sf={sf} bw={bw_khz}kHz coding_rate=4/{coding_rate} "
+ f"low_datarate_optimize={low_datarate_optimize}"
)
# We don't call configure() as the Dummy interfaces won't handle it,
# just update the BaseModem fields directly
modem._sf = sf
modem._bw_hz = int(bw_khz * 1000)
modem._coding_rate = coding_rate
# Low datarate optimize on/off is auto-configured in the current driver,
# check the automatic selection matches the test case from the
# calculator
if modem._get_ldr_en() != low_datarate_optimize:
print(
f" -- ERROR: Test case has low_datarate_optimize={low_datarate_optimize} "
+ f"but modem selects {modem._get_ldr_en()}"
)
failures += 1
continue # results will not match so don't run any of the packet test cases
for packet_config, expected_outputs in packets:
preamble_len, payload_len, explicit_header, crc_en = packet_config
print(
f" -- preamble_len={preamble_len} payload_len={payload_len} "
+ f"explicit_header={explicit_header} crc_en={crc_en}"
)
modem._preamble_len = preamble_len
modem._implicit_header = not explicit_header # opposite logic to calculator
modem._crc_en = crc_en
# Now calculate the symbol length and times and compare with the expected valuesd
(
expected_symbols,
expected_symbol_time,
expected_time_on_air,
) = expected_outputs
print(f" ---- calculator shows total length {expected_symbols}")
expected_symbols += 8 # Account for the calculator bug mentioned in the comment above
n_symbols = modem.get_n_symbols_x4(payload_len) / 4.0
symbol_time_us = modem._get_t_sym_us()
time_on_air_us = modem.get_time_on_air_us(payload_len)
tests += 1
if n_symbols == expected_symbols:
print(f" ---- symbols {n_symbols}")
else:
print(f" ---- SYMBOL COUNT ERROR expected {expected_symbols} got {n_symbols}")
failures.add((modem, modem_settings, packet_config))
max_error = expected_symbol_time * 1000 * TIME_ERROR_RATIO
if abs(int(expected_symbol_time * 1000) - symbol_time_us) <= max_error:
print(f" ---- symbol time {expected_symbol_time}ms")
else:
print(
f" ---- SYMBOL TIME ERROR expected {expected_symbol_time}ms "
+ f"got {symbol_time_us}us"
)
failures.add((modem, modem_settings, packet_config))
max_error = expected_time_on_air * 1000 * TIME_ERROR_RATIO
if abs(int(expected_time_on_air * 1000) - time_on_air_us) <= max_error:
print(f" ---- time on air {expected_time_on_air}ms")
else:
print(
f" ---- TIME ON AIR ERROR expected {expected_time_on_air}ms "
+ f"got {time_on_air_us}us"
)
failures.add((modem, modem_settings, packet_config))
print("************************")
print(f"\n{len(failures)}/{tests} tests failed")
if failures:
print("FAILURES:")
for f in failures:
print(f)
raise SystemExit(1)
print("SUCCESS")
class DummySPI:
# Dummy SPI Interface allows us to use normal constructors
#
# Reading will always return the 'always_read' value
def __init__(self, always_read=0x00):
self.always_read = always_read
def write_readinto(self, _wrbuf, rdbuf):
for i in range(len(rdbuf)):
rdbuf[i] = self.always_read
class DummyPin:
# Dummy Pin interface allows us to use normal constructors
def __init__(self):
pass
def __call__(self, _=None):
pass
# Copies of the functions used to calculate SX1276 SF5, SF6 test case symbol counts.
# (see comments above).
#
# These are written as closely to the SX1276 datasheet "Time on air" section as
# possible, quite different from the BaseModem implementation.
def sx1276_n_payload(pl, sf, ih, de, cr, crc):
import math
ceil_arg = 8 * pl - 4 * sf + 28 + 16 * crc - 20 * ih
ceil_arg /= 4 * (sf - 2 * de)
return 8 + max(math.ceil(ceil_arg) * (cr + 4), 0)
def sx1276_n_syms(pl, sf, ih, de, cr, crc, n_preamble):
return sx1276_n_payload(pl, sf, ih, de, cr, crc) + n_preamble + 4.25
if __name__ == "__main__":
main()