Compare commits
2 commits
7d8b3eb705
...
2a6fef41c7
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a6fef41c7 | |||
| eecd1a9f4c |
25 changed files with 2092 additions and 0 deletions
|
|
@ -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:'
|
||||
|
||||
69
exercises/205_sustained_link/README.md
Normal file
69
exercises/205_sustained_link/README.md
Normal 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
|
||||
```
|
||||
1
exercises/205_sustained_link/identities/AMY.identity
Normal file
1
exercises/205_sustained_link/identities/AMY.identity
Normal file
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>Ο Υf°TΐdΒΦλ <CEBB>ΡΚ‰BοSl>ϊw»6ΧΗ`d³Ζ¥>Z®υ6ΓΚ%Q©Μο
<07>¦W±‰ ±gy―®λ…
|
||||
|
|
@ -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
|
||||
2
exercises/205_sustained_link/identities/BOB.identity
Normal file
2
exercises/205_sustained_link/identities/BOB.identity
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
àǽt½Æ$Uw™)mi$yz÷3¶Ó§ªQðwK„ïKÉ'Æù<C386>ôgÌù
|
||||
bY¶‘<EFBFBD>D6íÆ¾¹ˆ&ÕÜ!Òüó2ð
|
||||
|
|
@ -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
|
||||
2
exercises/205_sustained_link/identities/CY.identity
Normal file
2
exercises/205_sustained_link/identities/CY.identity
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
øß7EøbI$‡vÛp1ƒ->Ö嫵²¥GësÁ„.Ñ¥|üÝž;‹Y+D¾³¢;¬ôò
|
||||
¼BãPjàH`º:
|
||||
|
|
@ -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
|
||||
1
exercises/205_sustained_link/identities/DAN.identity
Normal file
1
exercises/205_sustained_link/identities/DAN.identity
Normal file
|
|
@ -0,0 +1 @@
|
|||
˜á¸w'™’Úl æNñÓ´Ñ-µÉ¥¼2¥<0E>Évñu¶|,vq£GêDâ1øXϱÒyê§«ëˆÜ….<2E>‚LA
|
||||
|
|
@ -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
|
||||
1
exercises/205_sustained_link/identities/ED.identity
Normal file
1
exercises/205_sustained_link/identities/ED.identity
Normal file
|
|
@ -0,0 +1 @@
|
|||
àßðš¨L”&޵œ£(_ÁßIär¾¤ÙßÂÐiT°S±OëKØDlàÿ×§ÇÍ àË–x¯vœ-å•hþ@¹
|
||||
|
|
@ -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
|
||||
2
exercises/205_sustained_link/identities/FLO.identity
Normal file
2
exercises/205_sustained_link/identities/FLO.identity
Normal 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>
|
||||
|
|
@ -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
|
||||
1
exercises/205_sustained_link/identities/GUY.identity
Normal file
1
exercises/205_sustained_link/identities/GUY.identity
Normal file
|
|
@ -0,0 +1 @@
|
|||
Àv<EFBFBD>p˜³{BQ5<51>ð–î ¼°Ø<C2B0>–Ýñ\ûƉoe´+o„à¯2ž<ÙJ®y•ñ'¥äL¦2ð‹¤
|
||||
|
|
@ -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
|
||||
141
exercises/205_sustained_link/platformio.ini
Normal file
141
exercises/205_sustained_link/platformio.ini
Normal 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
|
||||
16
exercises/205_sustained_link/scripts/handy_log_parsers.txt
Normal file
16
exercises/205_sustained_link/scripts/handy_log_parsers.txt
Normal 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:'
|
||||
|
||||
184
exercises/205_sustained_link/scripts/load_only.sh
Executable file
184
exercises/205_sustained_link/scripts/load_only.sh
Executable 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
|
||||
25
exercises/205_sustained_link/scripts/pull_build_from_ryzdesk.sh
Executable file
25
exercises/205_sustained_link/scripts/pull_build_from_ryzdesk.sh
Executable 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
|
||||
26
exercises/205_sustained_link/scripts/pull_only_from_ryzdesk.sh
Executable file
26
exercises/205_sustained_link/scripts/pull_only_from_ryzdesk.sh
Executable 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
|
||||
23
exercises/205_sustained_link/scripts/set_build_identity.py
Normal file
23
exercises/205_sustained_link/scripts/set_build_identity.py
Normal 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}\\"'),
|
||||
]
|
||||
)
|
||||
245
exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.cpp
Normal file
245
exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.cpp
Normal 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);
|
||||
}
|
||||
63
exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.h
Normal file
63
exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.h
Normal 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;
|
||||
};
|
||||
1253
exercises/205_sustained_link/src/main.cpp
Normal file
1253
exercises/205_sustained_link/src/main.cpp
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue