kopia lustrzana https://github.com/micropython/micropython-lib
311 wiersze
12 KiB
Python
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()
|