diff --git a/exercises/204_established_identities/scripts/handy_log_parsers.txt b/exercises/204_established_identities/scripts/handy_log_parsers.txt deleted file mode 100644 index 7924472..0000000 --- a/exercises/204_established_identities/scripts/handy_log_parsers.txt +++ /dev/null @@ -1,16 +0,0 @@ -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:' - diff --git a/exercises/205_sustained_link/README.md b/exercises/205_sustained_link/README.md deleted file mode 100644 index 973d6c5..0000000 --- a/exercises/205_sustained_link/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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= phy=Cy(2) -TX LINKREQUEST: opening link to Cy slot=19 attempt=1/3 -LINK ACTIVE: initiator link established to Cy hash= -RX LINK: inbound link established hash= phy=Bob(1) -TX LINK: Hi from Bob iter=0 to=Cy via=outbound 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 -``` diff --git a/exercises/205_sustained_link/identities/AMY.identity b/exercises/205_sustained_link/identities/AMY.identity deleted file mode 100644 index a13546a..0000000 --- a/exercises/205_sustained_link/identities/AMY.identity +++ /dev/null @@ -1 +0,0 @@ - fTd렐ʉB Sl>w6`d>Z6%Q W gy \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/AMY.identity_info.txt b/exercises/205_sustained_link/identities/AMY.identity_info.txt deleted file mode 100644 index decfea7..0000000 --- a/exercises/205_sustained_link/identities/AMY.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[2026-05-28 11:26:31] Loaded Identity from ./AMY.identity -[2026-05-28 11:26:31] Public Key : f25adccd75eefaf9fafe5a4b22f0a16c43bf0094810c5f9279eb30ad3fd97312ba71cdd7940bb139c15949d433f8ffb57d75441d1b84a1d091b234420d22a608 -[2026-05-28 11:26:31] Private Key : Hidden diff --git a/exercises/205_sustained_link/identities/BOB.identity b/exercises/205_sustained_link/identities/BOB.identity deleted file mode 100644 index 565253f..0000000 --- a/exercises/205_sustained_link/identities/BOB.identity +++ /dev/null @@ -1,2 +0,0 @@ -ǽt$Uw)mi$yz3ӧQwKK'g -bYD6ƾ&!2 \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/BOB.identity_info.txt b/exercises/205_sustained_link/identities/BOB.identity_info.txt deleted file mode 100644 index 3cb6bda..0000000 --- a/exercises/205_sustained_link/identities/BOB.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[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 diff --git a/exercises/205_sustained_link/identities/CY.identity b/exercises/205_sustained_link/identities/CY.identity deleted file mode 100644 index 73a6a68..0000000 --- a/exercises/205_sustained_link/identities/CY.identity +++ /dev/null @@ -1,2 +0,0 @@ -7EbI$vp1->嫵Gs.ѥ|ݞ;Y+D; -BPjH`: \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/CY.identity_info.txt b/exercises/205_sustained_link/identities/CY.identity_info.txt deleted file mode 100644 index b66e212..0000000 --- a/exercises/205_sustained_link/identities/CY.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[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 diff --git a/exercises/205_sustained_link/identities/DAN.identity b/exercises/205_sustained_link/identities/DAN.identity deleted file mode 100644 index 091fe60..0000000 --- a/exercises/205_sustained_link/identities/DAN.identity +++ /dev/null @@ -1 +0,0 @@ -w'lNӴ-ɥ2vu|,vqGD1Xϱyꧫ܅.LA \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/DAN.identity_info.txt b/exercises/205_sustained_link/identities/DAN.identity_info.txt deleted file mode 100644 index 1689740..0000000 --- a/exercises/205_sustained_link/identities/DAN.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[2026-05-28 11:26:44] Loaded Identity from ./DAN.identity -[2026-05-28 11:26:44] Public Key : 14483f044c5ea19c12a2c89ba539ca1ee2cea613bde7eb8d5d700058351d1067b5415b26b0ae8667c2cac4d4cc932f24b48ca727f3c5e42c614750d05e475d80 -[2026-05-28 11:26:44] Private Key : Hidden diff --git a/exercises/205_sustained_link/identities/ED.identity b/exercises/205_sustained_link/identities/ED.identity deleted file mode 100644 index 7fbee55..0000000 --- a/exercises/205_sustained_link/identities/ED.identity +++ /dev/null @@ -1 +0,0 @@ -L&(_IriTSOKDlק ˖xv-h@ \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/ED.identity_info.txt b/exercises/205_sustained_link/identities/ED.identity_info.txt deleted file mode 100644 index 3962a6b..0000000 --- a/exercises/205_sustained_link/identities/ED.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[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 diff --git a/exercises/205_sustained_link/identities/FLO.identity b/exercises/205_sustained_link/identities/FLO.identity deleted file mode 100644 index b1b8612..0000000 --- a/exercises/205_sustained_link/identities/FLO.identity +++ /dev/null @@ -1,2 +0,0 @@ - -057X!##CM t{%+X6T!ͪE MP6[\^Z& \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/FLO.identity_info.txt b/exercises/205_sustained_link/identities/FLO.identity_info.txt deleted file mode 100644 index a2d1630..0000000 --- a/exercises/205_sustained_link/identities/FLO.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[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 diff --git a/exercises/205_sustained_link/identities/GUY.identity b/exercises/205_sustained_link/identities/GUY.identity deleted file mode 100644 index 2986e72..0000000 --- a/exercises/205_sustained_link/identities/GUY.identity +++ /dev/null @@ -1 +0,0 @@ -vp{BQ5 ؍\oe+o2<٭Jy'L2 \ No newline at end of file diff --git a/exercises/205_sustained_link/identities/GUY.identity_info.txt b/exercises/205_sustained_link/identities/GUY.identity_info.txt deleted file mode 100644 index 24340b4..0000000 --- a/exercises/205_sustained_link/identities/GUY.identity_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -[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 diff --git a/exercises/205_sustained_link/platformio.ini b/exercises/205_sustained_link/platformio.ini deleted file mode 100644 index cd1a30e..0000000 --- a/exercises/205_sustained_link/platformio.ini +++ /dev/null @@ -1,141 +0,0 @@ -; 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 diff --git a/exercises/205_sustained_link/scripts/handy_log_parsers.txt b/exercises/205_sustained_link/scripts/handy_log_parsers.txt deleted file mode 100644 index 7924472..0000000 --- a/exercises/205_sustained_link/scripts/handy_log_parsers.txt +++ /dev/null @@ -1,16 +0,0 @@ -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:' - diff --git a/exercises/205_sustained_link/scripts/load_only.sh b/exercises/205_sustained_link/scripts/load_only.sh deleted file mode 100755 index 6ffa96e..0000000 --- a/exercises/205_sustained_link/scripts/load_only.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/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 diff --git a/exercises/205_sustained_link/scripts/pull_build_from_ryzdesk.sh b/exercises/205_sustained_link/scripts/pull_build_from_ryzdesk.sh deleted file mode 100755 index 17d08a6..0000000 --- a/exercises/205_sustained_link/scripts/pull_build_from_ryzdesk.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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 diff --git a/exercises/205_sustained_link/scripts/pull_only_from_ryzdesk.sh b/exercises/205_sustained_link/scripts/pull_only_from_ryzdesk.sh deleted file mode 100755 index 9cbe0aa..0000000 --- a/exercises/205_sustained_link/scripts/pull_only_from_ryzdesk.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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 diff --git a/exercises/205_sustained_link/scripts/set_build_identity.py b/exercises/205_sustained_link/scripts/set_build_identity.py deleted file mode 100644 index e877182..0000000 --- a/exercises/205_sustained_link/scripts/set_build_identity.py +++ /dev/null @@ -1,23 +0,0 @@ -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}\\"'), - ] -) diff --git a/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.cpp b/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.cpp deleted file mode 100644 index 8735459..0000000 --- a/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.cpp +++ /dev/null @@ -1,245 +0,0 @@ -#include "TBeamSupremeLoRaInterface.h" - -#include -#include - -#include - -#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); -} diff --git a/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.h b/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.h deleted file mode 100644 index eb87b5c..0000000 --- a/exercises/205_sustained_link/src/TBeamSupremeLoRaInterface.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -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; -}; diff --git a/exercises/205_sustained_link/src/main.cpp b/exercises/205_sustained_link/src/main.cpp deleted file mode 100644 index 2a038cf..0000000 --- a/exercises/205_sustained_link/src/main.cpp +++ /dev/null @@ -1,1253 +0,0 @@ -#include "TBeamSupremeLoRaInterface.h" -#include "TBeamDisplay.h" -#include "tbeam_supreme_adapter.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef NODE_LABEL -#define NODE_LABEL "?" -#endif - -#ifndef BOARD_ID -#define BOARD_ID "UNKNOWN" -#endif - -#ifndef LOCAL_IDENTITY_HEX -#error "LOCAL_IDENTITY_HEX must be supplied by scripts/set_build_identity.py" -#endif - -#ifndef FW_BUILD_UTC -#define FW_BUILD_UTC "unknown" -#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 - -#ifndef EX205_RNS_TRACE -#define EX205_RNS_TRACE 0 -#endif -#ifndef MR_LINKFWD_DELAY_MS -#define MR_LINKFWD_DELAY_MS 0 -#endif -#ifndef ANNOUNCEMENT_2 -#define ANNOUNCEMENT_2 300 -#endif -#ifndef ANNOUNCEMENT_REPEAT -#define ANNOUNCEMENT_REPEAT 3600 -#endif - -static constexpr const char* APP_NAME = "microreticulum"; -static constexpr const char* APP_ASPECT = "linkping"; -static constexpr const char* ANNOUNCE_FILTER = "microreticulum.linkping"; -static constexpr const char* CLOCK_MARKER_PATH = "/ex205/clock.txt"; -static constexpr uint32_t DISCIPLINE_HOLDOVER_SECONDS = 24UL * 60UL * 60UL; -static constexpr uint32_t GPS_STATUS_PERIOD_MS = 2000; -static constexpr uint32_t PPS_WAIT_MS = 1500; -static constexpr uint32_t LINK_RETRY_MS = 60000; -static constexpr uint32_t LINK_RETRY_WINDOW_MS = 180000; -static constexpr uint32_t LINK_RX_STALE_MS = 75000; -static constexpr uint32_t LINK_REOPEN_DELAY_MS = 5000; -static constexpr uint8_t LINK_MAX_ATTEMPTS_PER_WINDOW = 3; -static constexpr uint32_t ANNOUNCEMENT_2_MS = (uint32_t)ANNOUNCEMENT_2 * 1000UL; -static constexpr uint32_t ANNOUNCEMENT_REPEAT_MS = (uint32_t)ANNOUNCEMENT_REPEAT * 1000UL; - -struct DateTime { - uint16_t year = 0; - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; -}; - -struct GpsState { - bool valid_time = false; - bool valid_fix = false; - int sats_used = -1; - DateTime utc; - uint32_t last_time_ms = 0; -}; - -static RNS::Reticulum reticulum({RNS::Type::NONE}); -static RNS::Interface lora_interface({RNS::Type::NONE}); -static RNS::Identity local_identity({RNS::Type::NONE}); -static RNS::Identity transport_identity({RNS::Type::NONE}); -static RNS::Destination inbound_destination({RNS::Type::NONE}); -static bool clock_ready = false; -static bool sd_ready = false; -static TBeamSupremeLoRaInterface* lora_impl = nullptr; -static XPowersLibInterface* pmu = nullptr; -static tbeam::TBeamDisplay oled_display; -static SPIClass sd_spi(FSPI); -static HardwareSerial gps_serial(1); -static GpsState gps; -static char gps_line[128]; -static size_t gps_line_len = 0; -static volatile uint32_t last_pps_ms = 0; - -static constexpr uint8_t MAX_PEERS = 6; - -struct PeerState { - String label; - RNS::Bytes destination_hash; - RNS::Destination destination = {RNS::Type::NONE}; - RNS::Link outbound_link = {RNS::Type::NONE}; - RNS::Link inbound_link = {RNS::Type::NONE}; - bool announced = false; - bool outbound_attempted = false; - bool outbound_active = false; - bool inbound_active = false; - bool outbound_failed = false; - uint8_t outbound_attempts = 0; - uint32_t outbound_attempt_window_ms = 0; - uint32_t last_link_attempt_ms = 0; - uint32_t last_link_active_ms = 0; - uint32_t next_link_open_ms = 0; - uint32_t last_rx_ms = 0; - uint32_t last_tx_ms = 0; - uint32_t tx_iter = 0; - uint8_t last_tx_second = 255; - uint8_t last_skip_second = 255; -}; - -static PeerState peers[MAX_PEERS]; - -static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet); -static void on_link_closed(RNS::Link& link); - -static const char* node_label_for_slot(uint8_t slot) { - switch (slot) { - case 0: return "Amy"; - case 1: return "Bob"; - case 2: return "Cy"; - case 3: return "Dan"; - case 4: return "Ed"; - case 5: return "Fay"; - case 6: return "Guy"; - default: return "unknown"; - } -} - -static void IRAM_ATTR on_pps_edge() { - last_pps_ms = millis(); -} - -static uint8_t to_bcd(uint8_t value) { - return (uint8_t)(((value / 10U) << 4U) | (value % 10U)); -} - -static uint8_t from_bcd(uint8_t value) { - return (uint8_t)(((value >> 4U) * 10U) + (value & 0x0FU)); -} - -static bool is_leap_year(uint16_t year) { - return ((year % 4U) == 0U && (year % 100U) != 0U) || ((year % 400U) == 0U); -} - -static uint8_t days_in_month(uint16_t year, uint8_t month) { - static const uint8_t days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2U) { - return is_leap_year(year) ? 29U : 28U; - } - if (month >= 1U && month <= 12U) { - return days[month - 1U]; - } - return 0; -} - -static bool valid_datetime(const DateTime& dt) { - return dt.year >= 2024U && dt.year <= 2099U && - dt.month >= 1U && dt.month <= 12U && - dt.day >= 1U && dt.day <= days_in_month(dt.year, dt.month) && - dt.hour <= 23U && dt.minute <= 59U && dt.second <= 59U; -} - -static int64_t days_from_civil(int year, unsigned month, unsigned day) { - year -= (month <= 2U); - const int era = (year >= 0 ? year : year - 399) / 400; - const unsigned yoe = (unsigned)(year - era * 400); - const unsigned doy = (153U * (month + (month > 2U ? (unsigned)-3 : 9U)) + 2U) / 5U + day - 1U; - const unsigned doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; - return era * 146097 + (int)doe - 719468; -} - -static int64_t to_epoch_seconds(const DateTime& dt) { - const int64_t days = days_from_civil((int)dt.year, dt.month, dt.day); - return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + dt.second; -} - -static bool from_epoch_seconds(int64_t seconds, DateTime& out) { - if (seconds < 0) { - return false; - } - - int64_t days = seconds / 86400LL; - int64_t rem = seconds % 86400LL; - out.hour = (uint8_t)(rem / 3600LL); - rem %= 3600LL; - out.minute = (uint8_t)(rem / 60LL); - out.second = (uint8_t)(rem % 60LL); - - days += 719468; - const int era = (days >= 0 ? days : days - 146096) / 146097; - const unsigned doe = (unsigned)(days - era * 146097); - const unsigned yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U; - int year = (int)yoe + era * 400; - const unsigned doy = doe - (365U * yoe + yoe / 4U - yoe / 100U); - const unsigned mp = (5U * doy + 2U) / 153U; - const unsigned day = doy - (153U * mp + 2U) / 5U + 1U; - const unsigned month = mp + (mp < 10U ? 3U : (unsigned)-9); - year += (month <= 2U); - - out.year = (uint16_t)year; - out.month = (uint8_t)month; - out.day = (uint8_t)day; - return valid_datetime(out); -} - -static void format_iso(const DateTime& dt, char* out, size_t out_size) { - snprintf(out, out_size, "%04u-%02u-%02uT%02u:%02u:%02uZ", - (unsigned)dt.year, (unsigned)dt.month, (unsigned)dt.day, - (unsigned)dt.hour, (unsigned)dt.minute, (unsigned)dt.second); -} - -static bool read_rtc(DateTime& out, int64_t* epoch_out = nullptr) { - Wire1.beginTransmission(RTC_I2C_ADDR); - Wire1.write(0x02); - if (Wire1.endTransmission(false) != 0) { - return false; - } - - if (Wire1.requestFrom((int)RTC_I2C_ADDR, 7) != 7) { - return false; - } - - const uint8_t sec = Wire1.read(); - const uint8_t min = Wire1.read(); - const uint8_t hour = Wire1.read(); - const uint8_t day = Wire1.read(); - (void)Wire1.read(); - const uint8_t month = Wire1.read(); - const uint8_t year = Wire1.read(); - - if (sec & 0x80U) { - return false; - } - out.second = from_bcd(sec & 0x7FU); - out.minute = from_bcd(min & 0x7FU); - out.hour = from_bcd(hour & 0x3FU); - out.day = from_bcd(day & 0x3FU); - out.month = from_bcd(month & 0x1FU); - out.year = (uint16_t)((month & 0x80U) ? 1900U : 2000U) + from_bcd(year); - if (!valid_datetime(out)) { - return false; - } - if (epoch_out) { - *epoch_out = to_epoch_seconds(out); - } - return true; -} - -static bool write_rtc(const DateTime& dt) { - if (!valid_datetime(dt)) { - return false; - } - Wire1.beginTransmission(RTC_I2C_ADDR); - Wire1.write(0x02); - Wire1.write(to_bcd(dt.second)); - Wire1.write(to_bcd(dt.minute)); - Wire1.write(to_bcd(dt.hour)); - Wire1.write(to_bcd(dt.day)); - Wire1.write(0x00); - Wire1.write(to_bcd(dt.month)); - Wire1.write(to_bcd((uint8_t)(dt.year % 100U))); - return Wire1.endTransmission() == 0; -} - -static bool mount_sd() { - pinMode(tbeam_supreme::sdCs(), OUTPUT); - digitalWrite(tbeam_supreme::sdCs(), HIGH); - pinMode(tbeam_supreme::imuCs(), OUTPUT); - digitalWrite(tbeam_supreme::imuCs(), HIGH); - sd_spi.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs()); - sd_ready = SD.begin(tbeam_supreme::sdCs(), sd_spi, 4000000); - Serial.printf("SD %s\r\n", sd_ready ? "ready" : "not mounted"); - return sd_ready; -} - -static int64_t read_clock_marker() { - if (!sd_ready || !SD.exists(CLOCK_MARKER_PATH)) { - return 0; - } - File f = SD.open(CLOCK_MARKER_PATH, FILE_READ); - if (!f) { - return 0; - } - int64_t marker_epoch = 0; - while (f.available()) { - String line = f.readStringUntil('\n'); - line.trim(); - const int64_t value = (int64_t)strtoll(line.c_str(), nullptr, 10); - if (value > 0) { - marker_epoch = value; - } - } - f.close(); - return marker_epoch; -} - -static bool write_clock_marker(int64_t epoch) { - if (!sd_ready) { - return false; - } - SD.mkdir("/ex205"); - if (SD.exists(CLOCK_MARKER_PATH)) { - SD.remove(CLOCK_MARKER_PATH); - } - File f = SD.open(CLOCK_MARKER_PATH, FILE_WRITE); - if (!f) { - return false; - } - f.printf("%lld\n", (long long)epoch); - f.close(); - return true; -} - -static void show_status(const char* left, const char* right = nullptr, const char* footer = nullptr) { - oled_display.showStatus("Ex 205 " BOARD_ID, left, right, footer); -} - -static void show_splash() { - oled_display.showLines("Exercise 205", "Build", FW_BUILD_UTC, BOARD_ID, NODE_LABEL, "sustained"); -} - -static bool hex_nibble(char c, uint8_t& out) { - if (c >= '0' && c <= '9') { - out = (uint8_t)(c - '0'); - return true; - } - if (c >= 'a' && c <= 'f') { - out = (uint8_t)(10 + c - 'a'); - return true; - } - if (c >= 'A' && c <= 'F') { - out = (uint8_t)(10 + c - 'A'); - return true; - } - return false; -} - -static bool identity_bytes_from_hex(RNS::Bytes& out) { - const char* hex = LOCAL_IDENTITY_HEX; - const size_t len = strlen(hex); - if (len == 0 || (len % 2U) != 0U) { - return false; - } - uint8_t buffer[96]; - const size_t byte_count = len / 2U; - if (byte_count > sizeof(buffer)) { - return false; - } - for (size_t i = 0; i < byte_count; ++i) { - uint8_t hi = 0; - uint8_t lo = 0; - if (!hex_nibble(hex[i * 2U], hi) || !hex_nibble(hex[i * 2U + 1U], lo)) { - return false; - } - buffer[i] = (uint8_t)((hi << 4U) | lo); - } - out = RNS::Bytes(buffer, byte_count); - return true; -} - -static bool parse_utc_time(const char* value, DateTime& out) { - if (!value || strlen(value) < 6) { - return false; - } - out.hour = (uint8_t)((value[0] - '0') * 10 + (value[1] - '0')); - out.minute = (uint8_t)((value[2] - '0') * 10 + (value[3] - '0')); - out.second = (uint8_t)((value[4] - '0') * 10 + (value[5] - '0')); - return out.hour <= 23U && out.minute <= 59U && out.second <= 59U; -} - -static bool parse_utc_date(const char* value, DateTime& out) { - if (!value || strlen(value) < 6) { - return false; - } - out.day = (uint8_t)((value[0] - '0') * 10 + (value[1] - '0')); - out.month = (uint8_t)((value[2] - '0') * 10 + (value[3] - '0')); - const uint8_t yy = (uint8_t)((value[4] - '0') * 10 + (value[5] - '0')); - out.year = (uint16_t)(2000U + yy); - return true; -} - -static void parse_gga(char* line) { - char* fields[16] = {}; - size_t count = 0; - char* save = nullptr; - for (char* tok = strtok_r(line, ",", &save); tok && count < 16; tok = strtok_r(nullptr, ",", &save)) { - fields[count++] = tok; - } - if (count > 7 && fields[7] && fields[7][0]) { - gps.sats_used = atoi(fields[7]); - } -} - -static void parse_rmc(char* line) { - char* fields[16] = {}; - size_t count = 0; - char* save = nullptr; - for (char* tok = strtok_r(line, ",", &save); tok && count < 16; tok = strtok_r(nullptr, ",", &save)) { - fields[count++] = tok; - } - if (count <= 9) { - return; - } - DateTime dt = gps.utc; - const bool has_time = parse_utc_time(fields[1], dt); - const bool active = fields[2] && fields[2][0] == 'A'; - const bool has_date = parse_utc_date(fields[9], dt); - if (has_time && has_date && valid_datetime(dt)) { - gps.utc = dt; - gps.valid_time = true; - gps.valid_fix = active; - gps.last_time_ms = millis(); - } -} - -static void process_nmea(char* line) { - if (strncmp(line, "$GPRMC", 6) == 0 || strncmp(line, "$GNRMC", 6) == 0) { - parse_rmc(line); - } else if (strncmp(line, "$GPGGA", 6) == 0 || strncmp(line, "$GNGGA", 6) == 0) { - parse_gga(line); - } -} - -static void poll_gps() { - while (gps_serial.available() > 0) { - const char c = (char)gps_serial.read(); - if (c == '\r') { - continue; - } - if (c == '\n') { - if (gps_line_len > 0) { - gps_line[gps_line_len] = '\0'; - process_nmea(gps_line); - gps_line_len = 0; - } - } else if (gps_line_len + 1U < sizeof(gps_line)) { - gps_line[gps_line_len++] = c; - } else { - gps_line_len = 0; - } - } -} - -static bool wait_for_pps(uint32_t timeout_ms) { - const uint32_t start = millis(); - const uint32_t prior = last_pps_ms; - while ((uint32_t)(millis() - start) < timeout_ms) { - if (last_pps_ms != prior) { - return true; - } - poll_gps(); - delay(5); - } - return false; -} - -static bool saved_clock_is_fresh() { - DateTime rtc_now{}; - int64_t rtc_epoch = 0; - if (!read_rtc(rtc_now, &rtc_epoch)) { - Serial.println("RTC invalid; GPS discipline required"); - return false; - } - char iso[32]; - format_iso(rtc_now, iso, sizeof(iso)); - if (!sd_ready) { - Serial.printf("Clock holdover accepted without SD marker: rtc=%s reason=sd_unavailable\r\n", iso); - show_status("Clock OK", "RTC only", iso); - return true; - } - const int64_t marker_epoch = read_clock_marker(); - if (marker_epoch <= 0) { - Serial.println("No saved clock marker; GPS discipline required"); - return false; - } - const int64_t age = rtc_epoch - marker_epoch; - if (age < 0 || age > (int64_t)DISCIPLINE_HOLDOVER_SECONDS) { - Serial.printf("Saved clock stale: age_s=%lld\r\n", (long long)age); - return false; - } - Serial.printf("Clock holdover accepted: rtc=%s age_s=%lld\r\n", iso, (long long)age); - (void)write_clock_marker(rtc_epoch); - show_status("Clock OK", "holdover", iso); - return true; -} - -static void discipline_clock_from_gps() { - Serial.println("Waiting for GPS UTC before LoRa startup"); - gps_serial.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); -#ifdef GPS_1PPS_PIN - pinMode(GPS_1PPS_PIN, INPUT); - attachInterrupt(digitalPinToInterrupt(GPS_1PPS_PIN), on_pps_edge, RISING); -#endif - - uint32_t last_status = 0; - while (true) { - poll_gps(); - const uint32_t now = millis(); - if ((uint32_t)(now - last_status) >= GPS_STATUS_PERIOD_MS) { - last_status = now; - char sats[16]; - snprintf(sats, sizeof(sats), "sats=%d", gps.sats_used); - show_status("Take outside", gps.valid_time ? "GPS time" : "No GPS", sats); - Serial.printf("gps_gate time=%u fix=%u sats=%d pps_ms=%lu\r\n", - gps.valid_time ? 1U : 0U, - gps.valid_fix ? 1U : 0U, - gps.sats_used, - (unsigned long)last_pps_ms); - } - - if (gps.valid_time && gps.valid_fix && (uint32_t)(now - gps.last_time_ms) < 5000U) { - DateTime disciplined = gps.utc; - bool used_pps = wait_for_pps(PPS_WAIT_MS); - if (used_pps) { - int64_t snapped = to_epoch_seconds(gps.utc) + 1LL; - (void)from_epoch_seconds(snapped, disciplined); - } - if (write_rtc(disciplined)) { - const int64_t epoch = to_epoch_seconds(disciplined); - (void)write_clock_marker(epoch); - char iso[32]; - format_iso(disciplined, iso, sizeof(iso)); - Serial.printf("Clock disciplined from GPS: utc=%s pps=%u marker_written=%u\r\n", - iso, - used_pps ? 1U : 0U, - sd_ready ? 1U : 0U); - show_status("Clock set", used_pps ? "GPS+PPS" : "GPS", iso); - delay(1000); - return; - } - Serial.println("RTC write failed; retrying GPS discipline"); - } - delay(10); - } -} - -static int8_t node_index_for_label(const String& label) { - if (label == "Amy") { - return 0; - } - if (label == "Bob") { - return 1; - } - if (label == "Cy") { - return 2; - } - if (label == "Dan") { - return 3; - } - if (label == "Ed") { - return 4; - } - if (label == "Flo") { - return 5; - } - if (label == "Guy") { - return 6; - } - return -1; -} - -static uint8_t directed_link_open_second(const String& label) { - int8_t a = node_index_for_label(String(NODE_LABEL)); - int8_t b = node_index_for_label(label); - if (a < 0 || b < 0) { - return 1; - } - return (uint8_t)(1U + (uint8_t)(((uint8_t)a * 7U + (uint8_t)b) % 29U) * 2U); -} - -static uint8_t node_send_slot_second() { - if (strcmp(NODE_LABEL, "Amy") == 0) { - return 4; - } - if (strcmp(NODE_LABEL, "Bob") == 0) { - return 8; - } - if (strcmp(NODE_LABEL, "Cy") == 0) { - return 12; - } - if (strcmp(NODE_LABEL, "Dan") == 0) { - return 16; - } - if (strcmp(NODE_LABEL, "Ed") == 0) { - return 20; - } - if (strcmp(NODE_LABEL, "Flo") == 0) { - return 24; - } - if (strcmp(NODE_LABEL, "Guy") == 0) { - return 28; - } - return (uint8_t)(4U + ((uint8_t)NODE_SLOT_INDEX * 4U)); -} - -static int find_peer_by_label(const String& label) { - for (uint8_t i = 0; i < MAX_PEERS; ++i) { - if (peers[i].label == label) { - return i; - } - } - return -1; -} - -static int find_peer_by_link_hash(const RNS::Bytes& link_hash) { - for (uint8_t i = 0; i < MAX_PEERS; ++i) { - if (peers[i].outbound_link && peers[i].outbound_link.hash() == link_hash) { - return i; - } - if (peers[i].inbound_link && peers[i].inbound_link.hash() == link_hash) { - return i; - } - } - return -1; -} - -static int first_free_peer_slot() { - for (uint8_t i = 0; i < MAX_PEERS; ++i) { - if (peers[i].label.length() == 0 && !peers[i].outbound_link && !peers[i].inbound_link) { - return i; - } - } - return -1; -} - -static void clear_peer_slot(uint8_t index) { - peers[index].label = ""; - peers[index].destination_hash.clear(); - peers[index].destination = {RNS::Type::NONE}; - peers[index].outbound_link = {RNS::Type::NONE}; - peers[index].inbound_link = {RNS::Type::NONE}; - peers[index].announced = false; - peers[index].outbound_attempted = false; - peers[index].outbound_active = false; - peers[index].inbound_active = false; - peers[index].outbound_failed = false; - peers[index].outbound_attempts = 0; - peers[index].outbound_attempt_window_ms = 0; - peers[index].last_link_attempt_ms = 0; - peers[index].last_link_active_ms = 0; - peers[index].next_link_open_ms = 0; - peers[index].last_rx_ms = 0; - peers[index].last_tx_ms = 0; - peers[index].tx_iter = 0; - peers[index].last_tx_second = 255; - peers[index].last_skip_second = 255; -} - -static int ensure_peer_for_label(const String& label) { - int index = find_peer_by_label(label); - if (index >= 0) { - return index; - } - index = first_free_peer_slot(); - if (index >= 0) { - peers[index].label = label; - } - return index; -} - -static String parse_sender_label(const String& text) { - const int from_pos = text.indexOf("from "); - if (from_pos < 0) { - return ""; - } - const int start = from_pos + 5; - int end = text.indexOf(' ', start); - if (end < 0) { - end = text.length(); - } - return text.substring(start, end); -} - -static String parse_recipient_label(const String& text) { - const int to_pos = text.indexOf("to="); - if (to_pos < 0) { - return ""; - } - const int start = to_pos + 3; - int end = text.indexOf(' ', start); - if (end < 0) { - end = text.length(); - } - return text.substring(start, end); -} - -static void attach_link_callbacks(RNS::Link& link) { - if (link) { - Serial.printf("APP LINK CALLBACKS: attach hash=%s status=%u initiator=%u\r\n", - link.hash().toHex().c_str(), - (unsigned)link.status(), - link.initiator() ? 1U : 0U); - } else { - Serial.println("APP LINK CALLBACKS: attach skipped null link"); - } - link.set_packet_callback(on_link_packet); - link.set_link_closed_callback(on_link_closed); -} - -static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) { - String peer = "(unknown)"; - int peer_index = -1; - if (packet.link()) { - peer_index = find_peer_by_link_hash(packet.link().hash()); - } - String text = data.toString().c_str(); - String sender = parse_sender_label(text); - String recipient = parse_recipient_label(text); - Serial.printf("APP RX LINK ENTER: link=%s status=%u initiator=%u peer_index=%d sender=%s recipient=%s text=%s\r\n", - packet.link() ? packet.link().hash().toHex().c_str() : "(none)", - packet.link() ? (unsigned)packet.link().status() : 255U, - packet.link() && packet.link().initiator() ? 1U : 0U, - peer_index, - sender.c_str(), - recipient.c_str(), - text.c_str()); - if (sender == NODE_LABEL || (recipient.length() > 0 && recipient != NODE_LABEL)) { - Serial.printf("RX LINK ignored: self_or_wrong_recipient text=%s\r\n", text.c_str()); - return; - } - if (peer_index >= 0 && peers[peer_index].label.length() == 0 && - sender.length() > 0 && sender != NODE_LABEL) { - const int existing_index = find_peer_by_label(sender); - if (existing_index >= 0 && existing_index != peer_index) { - if (packet.link()) { - peers[existing_index].inbound_link = packet.link(); - peers[existing_index].inbound_active = true; - attach_link_callbacks(peers[existing_index].inbound_link); - } - clear_peer_slot((uint8_t)peer_index); - peer_index = existing_index; - } else { - peers[peer_index].label = sender; - } - } - if (peer_index < 0) { - if (sender.length() > 0 && sender != NODE_LABEL) { - peer_index = ensure_peer_for_label(sender); - if (peer_index >= 0 && packet.link()) { - peers[peer_index].inbound_link = packet.link(); - peers[peer_index].inbound_active = true; - attach_link_callbacks(peers[peer_index].inbound_link); - } - } - } - if (peer_index >= 0) { - peer = peers[peer_index].label; - peers[peer_index].last_rx_ms = millis(); - } - float rssi = lora_impl ? lora_impl->last_rssi() : 0.0f; - float snr = lora_impl ? lora_impl->last_snr() : 0.0f; - uint8_t physical_tx = lora_impl ? lora_impl->last_physical_tx() : 255; - Serial.printf("RX LINK: %s | phy=%s(%u) RSSI=%.1f SNR=%.1f\r\n", - text.c_str(), - node_label_for_slot(physical_tx), - (unsigned)physical_tx, - rssi, - snr); - show_status("RX LINK", peer.c_str(), text.c_str()); -} - -static void on_link_closed(RNS::Link& link) { - if (!link) { - Serial.println("LINK CLOSED: null link callback"); - show_status("LINK CLOSED", "null"); - return; - } - - const RNS::Bytes link_hash = link.hash(); - const String link_hash_hex = link_hash.toHex().c_str(); - int peer_index = find_peer_by_link_hash(link_hash); - const char* label = "(unknown)"; - if (peer_index >= 0) { - label = peers[peer_index].label.c_str(); - if (peers[peer_index].outbound_link && peers[peer_index].outbound_link.hash() == link_hash) { - peers[peer_index].outbound_link = {RNS::Type::NONE}; - peers[peer_index].outbound_active = false; - peers[peer_index].outbound_attempted = false; - peers[peer_index].last_link_attempt_ms = 0; - peers[peer_index].last_link_active_ms = 0; - peers[peer_index].next_link_open_ms = millis() + LINK_REOPEN_DELAY_MS; - } - if (peers[peer_index].inbound_link && peers[peer_index].inbound_link.hash() == link_hash) { - peers[peer_index].inbound_link = {RNS::Type::NONE}; - peers[peer_index].inbound_active = false; - peers[peer_index].last_link_active_ms = 0; - } - } - Serial.printf("LINK CLOSED: %s hash=%s\r\n", label, link_hash_hex.c_str()); - show_status("LINK CLOSED", label); -} - -static void on_outbound_link_established(RNS::Link& link) { - int peer_index = find_peer_by_link_hash(link.hash()); - Serial.printf("APP LINK ESTABLISHED CB: direction=outbound hash=%s status=%u initiator=%u peer_index=%d\r\n", - link.hash().toHex().c_str(), - (unsigned)link.status(), - link.initiator() ? 1U : 0U, - peer_index); - if (peer_index >= 0) { - peers[peer_index].outbound_link = link; - attach_link_callbacks(peers[peer_index].outbound_link); - peers[peer_index].outbound_active = true; - peers[peer_index].outbound_attempted = true; - peers[peer_index].outbound_failed = false; - peers[peer_index].outbound_attempts = 0; - peers[peer_index].outbound_attempt_window_ms = 0; - peers[peer_index].last_link_attempt_ms = 0; - peers[peer_index].last_link_active_ms = millis(); - Serial.printf("LINK ACTIVE: initiator link established to %s hash=%s\r\n", - peers[peer_index].label.c_str(), link.hash().toHex().c_str()); - show_status("LINK ACTIVE", peers[peer_index].label.c_str(), "initiator"); - } else { - Serial.printf("LINK ACTIVE: initiator link established hash=%s peer=unknown\r\n", - link.hash().toHex().c_str()); - } -} - -static void on_inbound_link_established(RNS::Link& link) { - int peer_index = find_peer_by_link_hash(link.hash()); - if (peer_index < 0) { - peer_index = first_free_peer_slot(); - } - Serial.printf("APP LINK ESTABLISHED CB: direction=inbound hash=%s status=%u initiator=%u peer_index=%d\r\n", - link.hash().toHex().c_str(), - (unsigned)link.status(), - link.initiator() ? 1U : 0U, - peer_index); - if (peer_index >= 0) { - peers[peer_index].inbound_link = link; - attach_link_callbacks(peers[peer_index].inbound_link); - peers[peer_index].inbound_active = true; - peers[peer_index].last_link_active_ms = millis(); - } - uint8_t physical_tx = lora_impl ? lora_impl->last_physical_tx() : 255; - Serial.printf("RX LINK: inbound link established hash=%s phy=%s(%u)\r\n", - link.hash().toHex().c_str(), - node_label_for_slot(physical_tx), - (unsigned)physical_tx); - show_status("LINK ACTIVE", "inbound", link.hash().toHex().c_str()); -} - -class LinkAnnounceHandler : public RNS::AnnounceHandler { - public: - LinkAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {} - - void received_announce(const RNS::Bytes& destination_hash, - const RNS::Identity& announced_identity, - const RNS::Bytes& app_data) override { - String label = app_data ? String(app_data.toString().c_str()) : String("(no label)"); - if (label == NODE_LABEL) { - return; - } - if (!announced_identity) { - Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n", - destination_hash.toHex().c_str()); - return; - } - - int peer_index = ensure_peer_for_label(label); - if (peer_index < 0) { - Serial.printf("RX ANNOUNCE ignored: peer table full label=%s hash=%s\r\n", - label.c_str(), destination_hash.toHex().c_str()); - return; - } - - peers[peer_index].destination_hash = destination_hash; - peers[peer_index].destination = RNS::Destination(announced_identity, - RNS::Type::Destination::OUT, - RNS::Type::Destination::SINGLE, - destination_hash); - peers[peer_index].announced = true; - if (peers[peer_index].outbound_failed) { - peers[peer_index].outbound_failed = false; - peers[peer_index].outbound_attempts = 0; - peers[peer_index].outbound_attempt_window_ms = 0; - peers[peer_index].outbound_attempted = false; - peers[peer_index].last_link_attempt_ms = 0; - peers[peer_index].next_link_open_ms = millis() + LINK_REOPEN_DELAY_MS; - Serial.printf("LINK RETRY RESET: fresh announce from %s\r\n", peers[peer_index].label.c_str()); - } - - uint8_t physical_tx = lora_impl ? lora_impl->last_physical_tx() : 255; - Serial.printf("RX ANNOUNCE: label=%s hash=%s phy=%s(%u)\r\n", - peers[peer_index].label.c_str(), - peers[peer_index].destination_hash.toHex().c_str(), - node_label_for_slot(physical_tx), - (unsigned)physical_tx); - show_status("RX ANNOUNCE", peers[peer_index].label.c_str(), peers[peer_index].destination_hash.toHex().c_str()); - } -}; - -static RNS::HAnnounceHandler announce_handler(new LinkAnnounceHandler()); - -static void print_config() { - Serial.printf("Node=%s Board=%s Build=%s\r\n", NODE_LABEL, BOARD_ID, FW_BUILD_UTC); - Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n", - (int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY, - (int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI); - Serial.printf("LoRa: freq=%.1f BW=%.1f SF=%d CR=%d sync=0x%02x txp=%d\r\n", - (double)LORA_FREQ_MHZ, (double)LORA_BW_KHZ, (int)LORA_SF, - (int)LORA_CR, (int)LORA_SYNC_WORD, (int)LORA_TX_POWER_DBM); - Serial.printf("Sim: phy_envelope=%d phy_block_bob_cy=%d node_slot=%d rns_trace=%d linkfwd_delay_ms=%u transport=1\r\n", - (int)SIM_PHY_ENVELOPE, - (int)SIM_PHY_BLOCK_BOB_CY, - (int)NODE_SLOT_INDEX, - (int)EX205_RNS_TRACE, - (unsigned)MR_LINKFWD_DELAY_MS); - Serial.printf("Announce: startup=1 second=%lu repeat=%lu seconds\r\n", - (unsigned long)ANNOUNCEMENT_2, - (unsigned long)ANNOUNCEMENT_REPEAT); -} - -static void send_announce() { - if (!inbound_destination || !clock_ready) { - return; - } - Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL); - show_status("TX ANNOUNCE", NODE_LABEL); - inbound_destination.announce(RNS::bytesFromString(NODE_LABEL)); -} - -static void maybe_send_scheduled_announce() { - if (!clock_ready || !inbound_destination) { - return; - } - - static bool startup_announce_sent = false; - static uint8_t announce_count = 0; - static uint32_t next_announce_ms = 0; - - const uint32_t now = millis(); - if (!startup_announce_sent) { - startup_announce_sent = true; - announce_count = 1; - next_announce_ms = now + ANNOUNCEMENT_2_MS; - send_announce(); - return; - } - - if ((int32_t)(now - next_announce_ms) < 0) { - return; - } - - send_announce(); - if (announce_count < 2) { - announce_count = 2; - } - next_announce_ms = now + ANNOUNCEMENT_REPEAT_MS; -} - -static void maybe_open_link(const DateTime& rtc_now, bool have_rtc_now) { - const uint32_t now = millis(); - static uint32_t last_open_ms = 0; - if (!clock_ready) { - return; - } - for (uint8_t i = 0; i < MAX_PEERS; ++i) { - PeerState& peer = peers[i]; - if (!peer.announced || !peer.destination) { - continue; - } - if (peer.outbound_failed) { - continue; - } - uint32_t stale_reference_ms = peer.last_rx_ms; - if (stale_reference_ms == 0) { - stale_reference_ms = peer.last_tx_ms; - } - if (stale_reference_ms == 0) { - stale_reference_ms = peer.last_link_active_ms; - } - if (stale_reference_ms == 0) { - stale_reference_ms = peer.last_link_attempt_ms; - } - if ((peer.outbound_link || peer.inbound_link) && stale_reference_ms != 0 && - (uint32_t)(now - stale_reference_ms) >= LINK_RX_STALE_MS) { - Serial.printf("LINK STALE: no RX from %s after %lu ms ref=%lu; clearing link state\r\n", - peer.label.c_str(), - (unsigned long)LINK_RX_STALE_MS, - (unsigned long)stale_reference_ms); - peer.outbound_link = {RNS::Type::NONE}; - peer.inbound_link = {RNS::Type::NONE}; - peer.outbound_active = false; - peer.inbound_active = false; - peer.outbound_attempted = false; - peer.last_link_attempt_ms = 0; - peer.last_link_active_ms = 0; - peer.last_rx_ms = 0; - peer.last_tx_ms = 0; - peer.next_link_open_ms = now + LINK_REOPEN_DELAY_MS; - } - if (peer.next_link_open_ms != 0 && (int32_t)(now - peer.next_link_open_ms) < 0) { - continue; - } - if (peer.outbound_active || (peer.outbound_link && peer.outbound_link.status() == RNS::Type::Link::ACTIVE)) { - if (!peer.outbound_active) { - Serial.printf("APP LINK STATE: outbound already active peer=%s hash=%s status=%u\r\n", - peer.label.c_str(), - peer.outbound_link.hash().toHex().c_str(), - (unsigned)peer.outbound_link.status()); - } - peer.outbound_active = true; - continue; - } - if (peer.outbound_attempted && peer.last_link_attempt_ms != 0 && - (uint32_t)(now - peer.last_link_attempt_ms) >= LINK_RETRY_MS) { - Serial.printf("LINK RETRY: no establishment after %lu ms; retrying %s attempts=%u/%u\r\n", - (unsigned long)LINK_RETRY_MS, - peer.label.c_str(), - (unsigned)peer.outbound_attempts, - (unsigned)LINK_MAX_ATTEMPTS_PER_WINDOW); - peer.outbound_link = {RNS::Type::NONE}; - peer.outbound_attempted = false; - peer.last_link_attempt_ms = 0; - if (peer.outbound_attempts >= LINK_MAX_ATTEMPTS_PER_WINDOW && - peer.outbound_attempt_window_ms != 0 && - (uint32_t)(now - peer.outbound_attempt_window_ms) <= LINK_RETRY_WINDOW_MS) { - peer.outbound_failed = true; - Serial.printf("LINK FAILED: peer=%s attempts=%u window_ms=%lu waiting_for_announce=1\r\n", - peer.label.c_str(), - (unsigned)peer.outbound_attempts, - (unsigned long)(now - peer.outbound_attempt_window_ms)); - show_status("LINK FAILED", peer.label.c_str(), "wait announce"); - continue; - } - } - if (peer.outbound_attempted || peer.outbound_link) { - continue; - } - const uint8_t open_second = directed_link_open_second(peer.label); - if (!have_rtc_now || rtc_now.second != open_second) { - continue; - } - if (last_open_ms != 0 && (uint32_t)(now - last_open_ms) < 1500U) { - return; - } - if (peer.outbound_attempt_window_ms == 0 || - (uint32_t)(now - peer.outbound_attempt_window_ms) >= LINK_RETRY_WINDOW_MS) { - peer.outbound_attempt_window_ms = now; - peer.outbound_attempts = 0; - } - if (peer.outbound_attempts >= LINK_MAX_ATTEMPTS_PER_WINDOW) { - peer.outbound_failed = true; - Serial.printf("LINK FAILED: peer=%s attempts=%u window_ms=%lu waiting_for_announce=1\r\n", - peer.label.c_str(), - (unsigned)peer.outbound_attempts, - (unsigned long)(now - peer.outbound_attempt_window_ms)); - show_status("LINK FAILED", peer.label.c_str(), "wait announce"); - continue; - } - - ++peer.outbound_attempts; - Serial.printf("TX LINKREQUEST: opening link to %s slot=%u attempt=%u/%u\r\n", - peer.label.c_str(), - (unsigned)open_second, - (unsigned)peer.outbound_attempts, - (unsigned)LINK_MAX_ATTEMPTS_PER_WINDOW); - show_status("TX LINKREQ", peer.label.c_str()); - peer.outbound_link = RNS::Link(peer.destination); - peer.outbound_link.set_packet_callback(on_link_packet); - peer.outbound_link.set_link_established_callback(on_outbound_link_established); - peer.outbound_link.set_link_closed_callback(on_link_closed); - peer.outbound_attempted = true; - peer.last_link_attempt_ms = now; - last_open_ms = now; - return; - } -} - -static void setup_reticulum() { - microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()}; - filesystem.init(); - RNS::Utilities::OS::register_filesystem(filesystem); - - lora_impl = new TBeamSupremeLoRaInterface(); - lora_interface = lora_impl; - lora_interface.mode(RNS::Type::Interface::MODE_GATEWAY); - RNS::Transport::register_interface(lora_interface); - lora_interface.start(); - - reticulum = RNS::Reticulum(); - reticulum.transport_enabled(true); - reticulum.probe_destination_enabled(false); - - RNS::Bytes private_key; - if (!identity_bytes_from_hex(private_key)) { - Serial.println("FATAL: identity hex decode failed"); - show_status("IDENTITY FAIL", BOARD_ID); - while (true) { - delay(1000); - } - } - local_identity = RNS::Identity(false); - if (!local_identity.load_private_key(private_key)) { - Serial.println("FATAL: identity load failed"); - show_status("IDENTITY FAIL", BOARD_ID); - while (true) { - delay(1000); - } - } - transport_identity = RNS::Identity(false); - transport_identity.load_private_key(private_key); - RNS::Transport::identity(transport_identity); - - reticulum.start(); - - inbound_destination = RNS::Destination(local_identity, - RNS::Type::Destination::IN, - RNS::Type::Destination::SINGLE, - APP_NAME, - APP_ASPECT); - inbound_destination.set_link_established_callback(on_inbound_link_established); - inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE); - - RNS::Transport::register_announce_handler(announce_handler); - - Serial.printf("Local Identity: %s\r\n", local_identity.hash().toHex().c_str()); - Serial.printf("Local SINGLE destination: %s\r\n", - inbound_destination.hash().toHex().c_str()); -} - -void setup() { - Serial.begin(115200); - while (!Serial && millis() < 5000) { - delay(100); - } - delay(250); - - Serial.println(); - Serial.println("Exercise 205: sustained transport Links"); -#if EX205_RNS_TRACE - RNS::loglevel(RNS::LOG_TRACE); -#else - RNS::loglevel(RNS::LOG_NOTICE); -#endif - - (void)tbeam_supreme::initPmuForPeripherals(pmu, &Serial); - tbeam::DisplayConfig display_config; - display_config.powerSave = false; - oled_display.begin(display_config); - show_splash(); - delay(10000); - - print_config(); - mount_sd(); - - if (saved_clock_is_fresh()) { - clock_ready = true; - } else { - discipline_clock_from_gps(); - clock_ready = true; - } - - show_status("Starting LoRa", NODE_LABEL); - setup_reticulum(); - show_status("microR ready", NODE_LABEL, local_identity.hash().toHex().c_str()); - Serial.println("microReticulum ready"); -} - -void loop() { - reticulum.loop(); - - static uint32_t last_rtc_poll_ms = 0; - static DateTime rtc_now{}; - static bool have_rtc_now = false; - uint32_t now = millis(); - - if ((uint32_t)(now - last_rtc_poll_ms) >= 200U) { - last_rtc_poll_ms = now; - have_rtc_now = read_rtc(rtc_now); - } - maybe_open_link(rtc_now, have_rtc_now); - - maybe_send_scheduled_announce(); - const uint8_t send_slot = node_send_slot_second(); - const uint8_t second_slot = (uint8_t)((send_slot + 30U) % 60U); - if (have_rtc_now && (rtc_now.second == send_slot || rtc_now.second == second_slot)) { - for (uint8_t i = 0; i < MAX_PEERS; ++i) { - PeerState& peer = peers[i]; - if (peer.label.length() == 0 || peer.last_tx_second == rtc_now.second) { - continue; - } - - RNS::Link* link = nullptr; - if (peer.outbound_link && peer.outbound_link.status() == RNS::Type::Link::ACTIVE) { - peer.outbound_active = true; - link = &peer.outbound_link; - } else if (peer.inbound_link && peer.inbound_link.status() == RNS::Type::Link::ACTIVE) { - peer.inbound_active = true; - link = &peer.inbound_link; - } - if (!link) { - if (peer.last_skip_second != rtc_now.second) { - peer.last_skip_second = rtc_now.second; - Serial.printf("TX LINK SKIP: peer=%s outbound=%u outbound_status=%u inbound=%u inbound_status=%u attempted=%u active_out=%u active_in=%u\r\n", - peer.label.c_str(), - peer.outbound_link ? 1U : 0U, - peer.outbound_link ? (unsigned)peer.outbound_link.status() : 255U, - peer.inbound_link ? 1U : 0U, - peer.inbound_link ? (unsigned)peer.inbound_link.status() : 255U, - peer.outbound_attempted ? 1U : 0U, - peer.outbound_active ? 1U : 0U, - peer.inbound_active ? 1U : 0U); - } - continue; - } - - peer.last_tx_second = rtc_now.second; - peer.last_tx_ms = now; - String message = String("Hi from ") + NODE_LABEL + " iter=" + String(peer.tx_iter++) + " to=" + peer.label; - Serial.printf("TX LINK: %s via=%s hash=%s status=%u\r\n", - message.c_str(), - link == &peer.outbound_link ? "outbound" : "inbound", - link->hash().toHex().c_str(), - (unsigned)link->status()); - show_status("TX LINK", peer.label.c_str(), message.c_str()); - RNS::Packet(*link, RNS::bytesFromString(message.c_str())).send(); - delay(120); - } - } - - delay(5); -} - -int _write(int file, char* ptr, int len) { - (void)file; - int wrote = Serial.write(ptr, len); - Serial.flush(); - return wrote; -}