Compare commits

...

2 commits

Author SHA1 Message Date
2a6fef41c7 What changed from 204:
Kept all units in transport mode with reticulum.transport_enabled(true).
Removed physical blocking: SIM_PHY_BLOCK_BOB_CY=0, no per-unit block.
Removed the extra deep transmission/debug flags from platformio.ini.
Kept the 204 announcement schedule/protocol.
Removed intentional Link teardown after message cycles.
Every unit now attempts an outbound Link to every peer that announces.
Added retry health behavior: 3 Link attempts within 3 minutes, then wait for a fresh announce before trying that peer again.
Preserved substantive parseable logs: TX ANNOUNCE, RX ANNOUNCE, TX LINKREQUEST, LINK ACTIVE, TX LINK, RX LINK, retry/failure/reset events.
Updated copied helper scripts to point at 205_sustained_link.
Updated README for the new exercise behavior.
2026-06-03 09:19:33 -07:00
eecd1a9f4c awk parsers for logs 2026-06-03 09:00:28 -07:00
25 changed files with 2092 additions and 0 deletions

View file

@ -0,0 +1,16 @@
Using awk:
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep Hi
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep 'TX LINK:'
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep 'RX LINK:'

View file

@ -0,0 +1,69 @@
# Introduction
Exercise 205 builds on Exercise 204 established identities and LoRa Link traffic. It keeps the same identity, announce, RTC/GPS, OLED, and machine-parseable TX/RX event style, but changes the link policy for longer multi-unit field runs.
All seven units, `AMY` through `GUY`, are supported. Every unit runs in transport mode and every unit attempts to open a Link to any other unit that announces.
# Behavior
The announce protocol is intentionally the same as the last Exercise 204 protocol:
```text
startup announce: immediate
second announce: ANNOUNCEMENT_2 seconds after startup, default 300
repeat announce: ANNOUNCEMENT_REPEAT seconds after that, default 3600
```
There is no simulated BOB/CY physical block in this exercise. `SIM_PHY_ENVELOPE` remains enabled so log records can report the physical sender slot, but `SIM_PHY_BLOCK_BOB_CY=0` for every environment.
Exercise 205 does not intentionally tear down Links after a message count. A Link is reused while it remains active. If a Link becomes stale or closes, the unit clears local state and attempts to recreate the outbound Link.
The outbound Link retry budget is:
```text
retry interval: 60 seconds
failure window: 3 minutes
max attempts: 3
```
After three failed attempts within the window, the peer is marked failed and no more Link requests are sent to that peer until another announce for that peer is received. A fresh announce resets the retry budget and starts over.
# Clock Gate
The unit checks the RTC and `/ex205/clock.txt` on the SD card. If the saved discipline marker is less than 24 hours old according to the RTC, LoRa/microReticulum starts immediately. If not, the OLED shows `Take outside` and serial prints `gps_gate ...` until GPS UTC/fix is available.
# Log Events
Substantive events retain the Exercise 204 style so multi-unit log parsing can correlate TX and RX:
```text
TX ANNOUNCE: Bob
RX ANNOUNCE: label=Cy hash=<destination hash> phy=Cy(2)
TX LINKREQUEST: opening link to Cy slot=19 attempt=1/3
LINK ACTIVE: initiator link established to Cy hash=<link hash>
RX LINK: inbound link established hash=<link hash> phy=Bob(1)
TX LINK: Hi from Bob iter=0 to=Cy via=outbound hash=<link hash> status=2
RX LINK: Hi from Cy iter=0 to=Bob | phy=Cy(2) RSSI=... SNR=...
LINK RETRY: no establishment after 60000 ms; retrying Cy attempts=1/3
LINK FAILED: peer=Cy attempts=3 window_ms=... waiting_for_announce=1
LINK RETRY RESET: fresh announce from Cy
```
# Build, Upload, And Monitor
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/205_sustained_link -e amy -e bob -e cy -e dan -e ed -e flo -e guy
for env in amy bob cy dan ed flo guy
do
pio run -d exercises/205_sustained_link -e "${env}" -t upload
done
```
Monitor one unit:
```bash
pio device monitor -d exercises/205_sustained_link -e bob
```

View file

@ -0,0 +1 @@
 <EFBFBD>Ο Υf°TΐdΒΦλ <CEBB>ΡΚ‰B οSl>ϊw»6ΧΗ`d³Ζ¥>Z®υΚ%Μο <07>¦W±‰ ±gy―®λ…

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:31] Loaded Identity <c95d06fb622a80b4d80389fc7fe55d16> from ./AMY.identity
[2026-05-28 11:26:31] Public Key : f25adccd75eefaf9fafe5a4b22f0a16c43bf0094810c5f9279eb30ad3fd97312ba71cdd7940bb139c15949d433f8ffb57d75441d1b84a1d091b234420d22a608
[2026-05-28 11:26:31] Private Key : Hidden

View file

@ -0,0 +1,2 @@
àǽt½Æ$Uw™)mi$yz÷3¶Ó§ªQðwK„ïKÉ'Æù<C386>ôgÌù
bY¶<EFBFBD>D6íÆ¾¹ˆ&ÕÜ!Òüó2ð

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:35] Loaded Identity <5769e13e1214e62b96e43c17bd47085e> from ./BOB.identity
[2026-05-28 11:26:35] Public Key : 4705bf1ab8a17cc3bb07d1fa51d4fe59dc92e4e93fd9d55124c808079d33744e894241f52583b3abc7bcaf48a5fad31554c2dee142dbbc3c4d4c5e3855d94814
[2026-05-28 11:26:35] Private Key : Hidden

View file

@ -0,0 +1,2 @@
øß7EøbI$‡vÛp1ƒ->Ö嫵²¥GësÁ„.Ñ¥|üÝž;Y+­­³¢;¬ôò
¼BãPjàH`º:

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:40] Loaded Identity <92ce7c2eb62820c2e4476308350cc69d> from ./CY.identity
[2026-05-28 11:26:40] Public Key : 22dc1ae58534f27562ee37edc7b072eb53502847d7aa718bd84e4baf3064867ab5945f1fc842213b10d7faeebc35d8a71903eb3cee30c112d21e39575c44131a
[2026-05-28 11:26:40] Private Key : Hidden

View file

@ -0,0 +1 @@
˜á¸w'Úl æNñÓ´Ñ-µÉ¥¼2¥<0E>Évñu¶|,vq£GêDâ1øXϱÒyê§«ëˆÜ….<2E>LA

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:44] Loaded Identity <d0524d8f1d98fc39f13772655640ea30> from ./DAN.identity
[2026-05-28 11:26:44] Public Key : 14483f044c5ea19c12a2c89ba539ca1ee2cea613bde7eb8d5d700058351d1067b5415b26b0ae8667c2cac4d4cc932f24b48ca727f3c5e42c614750d05e475d80
[2026-05-28 11:26:44] Private Key : Hidden

View file

@ -0,0 +1 @@
àßðš¨L”&޵œ£(_ÁßIär¾¤ÙßÂÐiT°S±OëKØDlàÿ×§ÇÍ àËx¯vœ-å•hþ@¹

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:48] Loaded Identity <4ad998c481ff6c71d1acb8cbaf111e1f> from ./ED.identity
[2026-05-28 11:26:48] Public Key : c90deacc4f0ccfd552eb12205b92c31d10485ea71778bfb494a1fa6af7217c39b0a5fc1b770486cc2d0a8cb29e52ab2d44c2907e8bce6524397eabe507ff1384
[2026-05-28 11:26:48] Private Key : Hidden

View file

@ -0,0 +1,2 @@
<EFBFBD>
0<03><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>5<EFBFBD><35><EFBFBD>7X<37>!##<23><>C<EFBFBD>M<EFBFBD> t{%+X<>6T!<21>ͪE <0C>M<EFBFBD>P6<50>[\^<5E>Z<EFBFBD><5A>&<26>

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:53] Loaded Identity <5671752180661e6af4bfe49e962f23dd> from ./FLO.identity
[2026-05-28 11:26:53] Public Key : 3ea3edb5198020bf0f2f87219ed2d5ea2cff6eb16116702992f14eb2a43fcd69530d1e8a8b0db60065bdbbf75332c559498cc42f171f283189a62b54f14bf28f
[2026-05-28 11:26:53] Private Key : Hidden

View file

@ -0,0 +1 @@
Àv<EFBFBD>p˜³{BQ5<51>ðî ¼°Ø<C2B0>Ýñ\ûƉoe´+o„à¯2ž<Ù­J®y•ñ'­¥äL¦2ð¤

View file

@ -0,0 +1,3 @@
[2026-05-28 11:26:57] Loaded Identity <051cfb95faa527b68368d24efb40f689> from ./GUY.identity
[2026-05-28 11:26:57] Public Key : cfba3c49750b6ddd3fe7a5083af447b927ffca57e2459bc16bc731aff574cd67abd4cc6198e93721c1f0bb143ddce574a629d8ecf9aa5fea530821cd1fef60f2
[2026-05-28 11:26:57] Private Key : Hidden

View file

@ -0,0 +1,141 @@
; Exercise 205: sustained microReticulum Links over established identities
[platformio]
default_envs = bob
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
upload_speed = 460800
board_build.partitions = huge_app.csv
extra_scripts = pre:scripts/set_build_identity.py
lib_extra_dirs =
../../lib
build_flags =
-Wall
-Wno-missing-field-initializers
-Wno-format
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-I ../../lib/tbeam_display/src
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D RNS_USE_FS
-D USTORE_USE_UNIVERSALFS
-D MSGPACK_USE_BOOST=OFF
-D MCU_ESP32
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
-D OLED_SDA=17
-D OLED_SCL=18
-D OLED_ADDR=0x3C
-D RTC_I2C_ADDR=0x51
-D GPS_RX_PIN=9
-D GPS_TX_PIN=8
-D GPS_1PPS_PIN=6
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D LORA_FREQ_MHZ=915.0
-D LORA_BW_KHZ=125.0
-D LORA_SF=7
-D LORA_CR=5
-D LORA_SYNC_WORD=0x12
-D LORA_TX_POWER_DBM=14
-D USTORE_MAX_VALUE_LEN=1200
-D SIM_PHY_ENVELOPE=1
-D SIM_PHY_BLOCK_BOB_CY=0
-D ANNOUNCEMENT_2=300
-D ANNOUNCEMENT_REPEAT=3600
; Live announces are enough for this single-hop field exercise. Do not define
; RNS_PERSIST_PATHS here: the LittleFS-backed path_store compactor can leave an
; active segment FD open while unlinking /path_store_*.dat on ESP32.
lib_deps =
Wire
SD
olikraus/U8g2@^2.36.4
lewisxhe/XPowersLib@0.3.3
ArduinoJson@^7.4.2
MsgPack@^0.4.2
jgromes/RadioLib@^7.0.0
https://github.com/attermann/Crypto.git
https://github.com/attermann/microStore.git
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
[env:amy]
extends = env
upload_port = /dev/ttytAMY
monitor_port = /dev/ttytAMY
build_flags =
${env.build_flags}
-D BOARD_ID=\"AMY\"
-D NODE_LABEL=\"Amy\"
-D NODE_SLOT_INDEX=0
[env:bob]
extends = env
upload_port = /dev/ttytBOB
monitor_port = /dev/ttytBOB
build_flags =
${env.build_flags}
-D BOARD_ID=\"BOB\"
-D NODE_LABEL=\"Bob\"
-D NODE_SLOT_INDEX=1
[env:cy]
extends = env
upload_port = /dev/ttytCY
monitor_port = /dev/ttytCY
build_flags =
${env.build_flags}
-D BOARD_ID=\"CY\"
-D NODE_LABEL=\"Cy\"
-D NODE_SLOT_INDEX=2
[env:dan]
extends = env
upload_port = /dev/ttytDAN
monitor_port = /dev/ttytDAN
build_flags =
${env.build_flags}
-D BOARD_ID=\"DAN\"
-D NODE_LABEL=\"Dan\"
-D NODE_SLOT_INDEX=3
[env:ed]
extends = env
upload_port = /dev/ttytED
monitor_port = /dev/ttytED
build_flags =
${env.build_flags}
-D BOARD_ID=\"ED\"
-D NODE_LABEL=\"Ed\"
-D NODE_SLOT_INDEX=4
[env:flo]
extends = env
upload_port = /dev/ttytFLO
monitor_port = /dev/ttytFLO
build_flags =
${env.build_flags}
-D BOARD_ID=\"FLO\"
-D NODE_LABEL=\"Flo\"
-D NODE_SLOT_INDEX=5
[env:guy]
extends = env
upload_port = /dev/ttytGUY
monitor_port = /dev/ttytGUY
build_flags =
${env.build_flags}
-D BOARD_ID=\"GUY\"
-D NODE_LABEL=\"Guy\"
-D NODE_SLOT_INDEX=6

View file

@ -0,0 +1,16 @@
Using awk:
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep Hi
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep 'TX LINK:'
awk -v start="20260603_080000" -v end="20260603_083000" '
$1 >= start && $1 < end { print }
' BOB_raw_20260602_191631.log | grep 'RX LINK:'

View file

@ -0,0 +1,184 @@
#!/bin/bash
#
# 20260602 ChatGPT
# $Id$
# $HeadURL$
#
# Example command lines:
#
# ./load_only.sh --tbeams "CY"
# ./load_only.sh --tbeams "DAN"
# ./load_only.sh --tbeams "BOB"
# ./load_only.sh --tbeams "BOB CY DAN"
# ./load_only.sh --tbeams "bob,cy,dan"
#
# If pio/platformio is not in PATH:
#
# ./load_only.sh --pio-bin "$HOME/pioenv/bin/platformio" --tbeams "DAN"
#
set -euo pipefail
EXERCISE="/usr/local/src/microreticulum/microReticulumTbeam/exercises/205_sustained_link"
REMOTE_HOST="ryzdesk"
TBEAMS_RAW=""
PIO_BIN=""
usage() {
cat <<'EOF'
Usage:
./load_only.sh --tbeams "CY"
./load_only.sh --tbeams "BOB CY DAN"
./load_only.sh --tbeams "bob,cy,dan"
Required:
--tbeams One or more of: AMY BOB CY DAN ED FLO GUY
Optional:
--pio-bin Full path to pio/platformio executable
--exercise PlatformIO project directory
--remote Remote build host, default: ryzdesk
EOF
}
while [ "$#" -gt 0 ]
do
case "$1" in
--tbeams)
shift
[ "$#" -gt 0 ] || { echo "ERROR: --tbeams requires a value" >&2; exit 1; }
TBEAMS_RAW="$1"
;;
--pio-bin)
shift
[ "$#" -gt 0 ] || { echo "ERROR: --pio-bin requires a value" >&2; exit 1; }
PIO_BIN="$1"
;;
--exercise)
shift
[ "$#" -gt 0 ] || { echo "ERROR: --exercise requires a value" >&2; exit 1; }
EXERCISE="$1"
;;
--remote)
shift
[ "$#" -gt 0 ] || { echo "ERROR: --remote requires a value" >&2; exit 1; }
REMOTE_HOST="$1"
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown option: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
if [ -z "$TBEAMS_RAW" ]; then
echo "ERROR: missing required option --tbeams" >&2
usage >&2
exit 1
fi
if [ ! -d "$EXERCISE" ]; then
echo "ERROR: EXERCISE directory not found: $EXERCISE" >&2
exit 1
fi
if [ ! -f "$EXERCISE/platformio.ini" ]; then
echo "ERROR: platformio.ini not found under: $EXERCISE" >&2
exit 1
fi
if [ -n "$PIO_BIN" ]; then
if [ ! -x "$PIO_BIN" ]; then
echo "ERROR: --pio-bin is not executable: $PIO_BIN" >&2
exit 1
fi
else
if command -v pio >/dev/null 2>&1; then
PIO_BIN="$(command -v pio)"
elif command -v platformio >/dev/null 2>&1; then
PIO_BIN="$(command -v platformio)"
else
echo "ERROR: cannot find pio or platformio in PATH" >&2
echo " Try: --pio-bin \"\$HOME/pioenv/bin/platformio\"" >&2
exit 1
fi
fi
echo "EXERCISE: $EXERCISE"
echo "REMOTE_HOST: $REMOTE_HOST"
echo "PIO_BIN: $PIO_BIN"
echo
#
# Accept either:
# "BOB CY DAN"
# "bob,cy,dan"
#
TBEAMS_NORMALIZED="$(echo "$TBEAMS_RAW" | tr ',' ' ')"
for env_raw in $TBEAMS_NORMALIZED
do
env="$(echo "$env_raw" | tr '[:upper:]' '[:lower:]')"
ENV="$(echo "$env" | tr '[:lower:]' '[:upper:]')"
case "$env" in
amy|bob|cy|dan|ed|flo|guy)
;;
*)
echo "ERROR: invalid T-Beam name: $env_raw" >&2
echo " Allowed names: AMY BOB CY DAN ED FLO GUY" >&2
exit 1
;;
esac
dev="/dev/ttyt${ENV}"
remote_build_dir="${EXERCISE}/.pio/build/${env}"
local_build_dir="${EXERCISE}/.pio/build/${env}"
if [ ! -e "$dev" ]; then
echo "ERROR: expected device not found for $ENV: $dev" >&2
exit 1
fi
if ! ssh "$REMOTE_HOST" "test -d '$remote_build_dir'"; then
echo "ERROR: remote build directory missing on ${REMOTE_HOST}: $remote_build_dir" >&2
exit 1
fi
if [ ! -d "$local_build_dir" ]; then
echo "Creating missing local build directory: $local_build_dir"
mkdir -p "$local_build_dir" || {
echo "ERROR: failed to create $local_build_dir" >&2
exit 1
}
fi
echo "===== copy built artifact tree for $env from $REMOTE_HOST ====="
rsync -a \
"${REMOTE_HOST}:${remote_build_dir}/" \
"${local_build_dir}/"
if [ ! -f "${local_build_dir}/firmware.bin" ] &&
[ ! -f "${local_build_dir}/firmware.elf" ]; then
echo "ERROR: copied build tree does not appear to contain firmware output:" >&2
echo " $local_build_dir" >&2
exit 1
fi
echo "===== loader-only upload $ENV on $dev ====="
"$PIO_BIN" run \
-d "$EXERCISE" \
-e "$env" \
-t nobuild \
-t upload \
--upload-port "$dev"
echo "===== finished loader-only upload for $ENV ====="
echo
done

View file

@ -0,0 +1,25 @@
#!/bin/bash
# $Header$
# $HeadURL$
set -e
EXERCISE="/usr/local/src/microreticulum/microReticulumTbeam/exercises/205_sustained_link"
for env in amy bob cy dan ed flo guy
do
ENV="$(echo "$env" | tr '[:lower:]' '[:upper:]')"
echo "===== copy artifact $env from ryzdesk ====="
rsync -a \
"ryzdesk:${EXERCISE}/.pio/build/${env}/" \
"${EXERCISE}/.pio/build/${env}/"
echo "===== upload $env / $ENV ====="
pio run \
-d "$EXERCISE" \
-e "$env" \
-t nobuild \
-t upload \
--upload-port "/dev/ttyt${ENV}"
done

View file

@ -0,0 +1,26 @@
#!/bin/bash
# $Header$
# $HeadURL$
set -e
EXERCISE="/usr/local/src/microreticulum/microReticulumTbeam/exercises/205_sustained_link"
for env in amy bob cy dan ed flo guy
do
ENV="$(echo "$env" | tr '[:lower:]' '[:upper:]')"
local_build_dir="${EXERCISE}/.pio/build/${env}"
if [ ! -d "$local_build_dir" ]; then
echo "Creating missing local build directory: $local_build_dir"
mkdir -p "$local_build_dir" || {
echo "ERROR: failed to create $local_build_dir" >&2
exit 1
}
fi
echo "===== copy artifact $env from ryzdesk ====="
rsync -a \
"ryzdesk:${EXERCISE}/.pio/build/${env}/" \
"${EXERCISE}/.pio/build/${env}/"
done

View file

@ -0,0 +1,23 @@
import binascii
import time
from pathlib import Path
Import("env")
pioenv = env.subst("$PIOENV").upper()
identity_path = Path(env.subst("$PROJECT_DIR")) / "identities" / f"{pioenv}.identity"
if not identity_path.exists():
raise RuntimeError(f"Missing identity file for {pioenv}: {identity_path}")
identity_hex = binascii.hexlify(identity_path.read_bytes()).decode("ascii")
epoch = int(time.time())
utc_tag = time.strftime("%Y%m%d_%H%M%S", time.gmtime(epoch))
env.Append(
CPPDEFINES=[
("LOCAL_IDENTITY_HEX", f'\\"{identity_hex}\\"'),
("FW_BUILD_EPOCH", str(epoch)),
("FW_BUILD_UTC", f'\\"{utc_tag}\\"'),
]
)

View file

@ -0,0 +1,245 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Cryptography/Random.h>
#include <Log.h>
#include <string.h>
#ifndef LORA_CS
#error "LORA_CS not defined"
#endif
#ifndef LORA_DIO1
#error "LORA_DIO1 not defined"
#endif
#ifndef LORA_RESET
#error "LORA_RESET not defined"
#endif
#ifndef LORA_BUSY
#error "LORA_BUSY not defined"
#endif
#ifndef NODE_SLOT_INDEX
#define NODE_SLOT_INDEX 255
#endif
#ifndef SIM_PHY_ENVELOPE
#define SIM_PHY_ENVELOPE 0
#endif
#ifndef SIM_PHY_BLOCK_BOB_CY
#define SIM_PHY_BLOCK_BOB_CY 0
#endif
using namespace RNS;
TBeamSupremeLoRaInterface::TBeamSupremeLoRaInterface(const char* name) : InterfaceImpl(name) {
_IN = true;
_OUT = true;
_bitrate = (double)LORA_SF * ((4.0 / LORA_CR) / (pow(2, LORA_SF) / LORA_BW_KHZ)) * 1000.0;
_HW_MTU = (uint16_t)(LORA_MAX_PAYLOAD * 2);
}
TBeamSupremeLoRaInterface::~TBeamSupremeLoRaInterface() {
stop();
delete _radio;
delete _module;
}
bool TBeamSupremeLoRaInterface::start() {
_online = false;
INFO("LoRa initializing for T-Beam Supreme...");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
_module = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY, SPI);
_radio = new SX1262(_module);
int state = _radio->begin(
LORA_FREQ_MHZ,
LORA_BW_KHZ,
LORA_SF,
LORA_CR,
LORA_SYNC_WORD,
LORA_TX_POWER_DBM);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa init failed, code %d", state);
return false;
}
state = _radio->startReceive();
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa startReceive failed, code %d", state);
return false;
}
_online = true;
INFO("LoRa init succeeded.");
return true;
}
void TBeamSupremeLoRaInterface::stop() {
if (_radio) {
_radio->standby();
}
_online = false;
}
void TBeamSupremeLoRaInterface::loop() {
if (!_online || !_radio) {
return;
}
if (!_radio->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
return;
}
int len = _radio->getPacketLength();
uint8_t rx_buf[RADIO_MAX_PAYLOAD];
int state = _radio->readData(rx_buf, len);
if (state == RADIOLIB_ERR_NONE) {
_last_rssi = _radio->getRSSI();
_last_snr = _radio->getSNR();
uint8_t physical_tx = 255;
if (!unpack_frame(rx_buf, len, physical_tx)) {
_radio->startReceive();
return;
}
if (len <= 1) {
_radio->startReceive();
return;
}
uint8_t header = rx_buf[0];
uint8_t seq = packet_sequence(header);
if (is_split_packet(header)) {
if (_rx_seq == SEQ_UNSET || _rx_seq != seq) {
_rx_seq = seq;
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
} else {
_rx_buffer.append(rx_buf + 1, len - 1);
_rx_seq = SEQ_UNSET;
on_incoming(_rx_buffer);
}
} else {
if (_rx_seq != SEQ_UNSET) {
_rx_buffer.clear();
_rx_seq = SEQ_UNSET;
}
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
on_incoming(_rx_buffer);
}
} else if (state != RADIOLIB_ERR_NONE) {
DEBUGF("LoRa readData failed, code %d", state);
}
_radio->startReceive();
}
int TBeamSupremeLoRaInterface::transmit_frame(uint8_t header, const uint8_t* payload, size_t payload_len) {
uint8_t tx_buf[RADIO_MAX_PAYLOAD];
#if SIM_PHY_ENVELOPE
tx_buf[0] = PHY_MAGIC_0;
tx_buf[1] = PHY_MAGIC_1;
tx_buf[2] = PHY_VERSION;
tx_buf[3] = (uint8_t)NODE_SLOT_INDEX;
tx_buf[PHY_ENVELOPE_LEN] = header;
memcpy(tx_buf + PHY_ENVELOPE_LEN + 1, payload, payload_len);
return _radio->transmit(tx_buf, PHY_ENVELOPE_LEN + 1 + payload_len);
#else
tx_buf[0] = header;
memcpy(tx_buf + 1, payload, payload_len);
return _radio->transmit(tx_buf, 1 + payload_len);
#endif
}
bool TBeamSupremeLoRaInterface::unpack_frame(uint8_t* frame, int& len, uint8_t& physical_tx) {
#if SIM_PHY_ENVELOPE
if (len < PHY_ENVELOPE_LEN + 1) {
++_phy_bad_frames;
DEBUGF("SIM PHY malformed: short frame len=%d", len);
return false;
}
if (frame[0] != PHY_MAGIC_0 || frame[1] != PHY_MAGIC_1 || frame[2] != PHY_VERSION) {
++_phy_bad_frames;
DEBUGF("SIM PHY malformed: bad envelope len=%d", len);
return false;
}
physical_tx = frame[3];
if (should_drop_physical_tx(physical_tx)) {
++_phy_blocked_frames;
Serial.printf("SIM PHY DROP: rx=%u tx=%u len=%d blocked=%lu\r\n",
(unsigned)NODE_SLOT_INDEX,
(unsigned)physical_tx,
len,
(unsigned long)_phy_blocked_frames);
DEBUGF("SIM PHY DROP: rx=%u tx=%u len=%d",
(unsigned)NODE_SLOT_INDEX,
(unsigned)physical_tx,
len);
return false;
}
_last_physical_tx = physical_tx;
++_phy_rx_frames;
len -= PHY_ENVELOPE_LEN;
memmove(frame, frame + PHY_ENVELOPE_LEN, len);
return true;
#else
(void)frame;
physical_tx = 255;
_last_physical_tx = physical_tx;
++_phy_rx_frames;
return true;
#endif
}
bool TBeamSupremeLoRaInterface::should_drop_physical_tx(uint8_t physical_tx) {
#if SIM_PHY_BLOCK_BOB_CY
const uint8_t local = (uint8_t)NODE_SLOT_INDEX;
return (local == 1U && physical_tx == 2U) || (local == 2U && physical_tx == 1U);
#else
(void)physical_tx;
return false;
#endif
}
void TBeamSupremeLoRaInterface::send_outgoing(const Bytes& data) {
if (!_online || !_radio) {
return;
}
try {
uint8_t rand_nibble = (uint8_t)(Cryptography::randomnum(256)) & 0xF0;
if ((int)data.size() <= LORA_MAX_PAYLOAD) {
int state = transmit_frame(rand_nibble, data.data(), data.size());
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit failed, code %d", state);
}
} else {
uint8_t seq = (_tx_seq_ctr++) & HEADER_SEQ_MASK;
uint8_t split_header = rand_nibble | HEADER_SPLIT | seq;
int state = transmit_frame(split_header, data.data(), LORA_MAX_PAYLOAD);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 1 failed, code %d", state);
}
size_t remainder = data.size() - LORA_MAX_PAYLOAD;
state = transmit_frame(split_header, data.data() + LORA_MAX_PAYLOAD, remainder);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 2 failed, code %d", state);
}
}
_radio->startReceive();
InterfaceImpl::handle_outgoing(data);
} catch (const std::exception& e) {
ERRORF("LoRa transmit exception: %s", e.what());
}
}
void TBeamSupremeLoRaInterface::on_incoming(const Bytes& data) {
InterfaceImpl::handle_incoming(data);
}

View file

@ -0,0 +1,63 @@
#pragma once
#include <Bytes.h>
#include <Interface.h>
#include <Arduino.h>
#include <RadioLib.h>
#include <SPI.h>
class TBeamSupremeLoRaInterface : public RNS::InterfaceImpl {
public:
explicit TBeamSupremeLoRaInterface(const char* name = "TBeamSupremeLoRa");
~TBeamSupremeLoRaInterface() override;
bool start() override;
void stop() override;
void loop() override;
float last_rssi() const { return _last_rssi; }
float last_snr() const { return _last_snr; }
uint8_t last_physical_tx() const { return _last_physical_tx; }
uint32_t phy_rx_frames() const { return _phy_rx_frames; }
uint32_t phy_blocked_frames() const { return _phy_blocked_frames; }
uint32_t phy_bad_frames() const { return _phy_bad_frames; }
private:
void send_outgoing(const RNS::Bytes& data) override;
void on_incoming(const RNS::Bytes& data);
int transmit_frame(uint8_t header, const uint8_t* payload, size_t payload_len);
bool unpack_frame(uint8_t* frame, int& len, uint8_t& physical_tx);
static bool should_drop_physical_tx(uint8_t physical_tx);
static constexpr uint8_t HEADER_SPLIT = 0x08;
static constexpr uint8_t HEADER_SEQ_MASK = 0x07;
static constexpr uint8_t SEQ_UNSET = 0xFF;
static constexpr int RADIO_MAX_PAYLOAD = 255;
#if defined(SIM_PHY_ENVELOPE) && SIM_PHY_ENVELOPE
static constexpr uint8_t PHY_MAGIC_0 = 0xC2;
static constexpr uint8_t PHY_MAGIC_1 = 0x04;
static constexpr uint8_t PHY_VERSION = 0x01;
static constexpr int PHY_ENVELOPE_LEN = 4;
static constexpr int LORA_MAX_PAYLOAD = RADIO_MAX_PAYLOAD - PHY_ENVELOPE_LEN - 1;
#else
static constexpr int PHY_ENVELOPE_LEN = 0;
static constexpr int LORA_MAX_PAYLOAD = RADIO_MAX_PAYLOAD - 1;
#endif
static bool is_split_packet(uint8_t header) { return (header & HEADER_SPLIT) != 0; }
static uint8_t packet_sequence(uint8_t header) { return header & HEADER_SEQ_MASK; }
RNS::Bytes _rx_buffer;
uint8_t _rx_seq = SEQ_UNSET;
uint8_t _tx_seq_ctr = 0;
uint8_t _last_physical_tx = 255;
uint32_t _phy_rx_frames = 0;
uint32_t _phy_blocked_frames = 0;
uint32_t _phy_bad_frames = 0;
float _last_rssi = 0.0f;
float _last_snr = 0.0f;
Module* _module = nullptr;
SX1262* _radio = nullptr;
};

File diff suppressed because it is too large Load diff