Compare commits

...

14 commits

Author SHA1 Message Date
dc8e6163af Another delay injection 2026-05-29 18:04:17 -07:00
ce56e876bd Note tested, added 750ms delay to overcome possible race condition 2026-05-29 17:46:56 -07:00
a52617862f Before further debugging of LINK bug 2026-05-29 17:24:33 -07:00
4e1d9a75db For posterity 2026-05-29 12:30:45 -07:00
294a17660d John draft, I asked Codex to review and it started changing, I intervened and asked to keep Codex review separate, I think it sucessfuly backed out its edits. Committing for posterity, there is a style difference between me and Codex. 2026-05-29 12:30:04 -07:00
197b46b4bb Exercise is reduced to BOB, CY & DAN. LINK is still broken, but writing history_of_a_LINK.md for a forensic analysis and referencing this version 2026-05-29 10:58:14 -07:00
7f78dfb70e Change frame to have prefix showing origin. Tested and all units received from the other tree. I should have committed then, but forgot. Here, the blocking has been activated, but not tested. 2026-05-28 21:23:10 -07:00
fd96e56410 All 4 units, BOB, CY, DAN & ED, are LINKing with the other three. Success. 2026-05-28 20:50:16 -07:00
d8e5cc0a8d Safety 2026-05-28 17:25:31 -07:00
f1670a6123 4 Units are talking, but LINK rarely works. Committing now for safety. 2026-05-28 16:44:41 -07:00
10498b57e1 Additional schema change to reconcile with Perl importer 2026-05-27 11:55:49 -07:00
ff7c6fa0ad Untested revision of schema as of 11:50 AM 2026-05-27 11:51:30 -07:00
58c62ed07d Chat GPT revision, small, after 11:00 a.m. today 2026-05-27 11:47:21 -07:00
e555986b87 Improved workflow for migrating logs into database 2026-05-27 11:16:23 -07:00
26 changed files with 2719 additions and 26 deletions

View file

@ -0,0 +1,18 @@
Prompt:
Referencing /usr/local/src/microreticulum/microReticulumTbeam/exercises/203_microreticulum_link_ping_pong, you are to create a new 204_established_identities (I already created the directory) building upon Exercise 203. You probably should copy Exercise 203's files, except the README.md since I have already started a draft of Exercise 204's README.md. You can, of course, copy portions from the README.md in Exercise 203 into the 204 as appropriate.
Please review Exercise's 204 README.md -- it is a preliminary draft that contains information about identities created for the 7 units. Exercise 204 is to utilize these already created identities. Do not worry about secrecy for the identities, they were simply created for exclusive use in this exercise. Please modify the platformio.ini (copied from Exercise 203 as) so that each build has its correlated identity information.
You need only compile two unit's build, BOB and CY and load same and monitor via a serial interface. I'll compile the other 5 and then load the binaries as needed -- this is to save time and reduce the thought time your have to utilize and which is allocated to me in 5 hour chunks.
Please use the OLED facility. At startup there should be a splash screen withe "Ex 204 v. XX" -- you will use the Python script for versioning of this execerise so at start-up, I can verify which version is loaded.
Please use the disciplined clock approach which means if the RTC has not been disciplined within the last 24 hours, then the unit needs to obtain time from satellites and therefore needs to be taken outside. If a unit has been disciplined in the last 24 hours (using the saved clock file appoach we have used in other exercises), then the unit does not need to be exposed to satellites and my proceed transmitting and receiving messages.
We'll eventually have event written to a log file, but that is a nice-to-have feature we'll implement once we've proven that messages are being transported by intermediate units.
Please configure all units as "transport".
The first iteration of this exercise is to succeed in having BOB and CY exchange messages over LoRa. Once that works, then for a second iteration, what we'll do is prevent BOB's LoRa from being received by CY so that BOB's message must come through another unit in 2 or more hops. We will use hard-coded blocks to simulate the out-of-transmission-range state between BOB and CY for Lora thereby forcing intermediate units, e.g. DAN, ED, FLO or GUY to transport the packets. And then for the utlimate version of this exercise, we'll cause blockages among the units such that for BOB to commmunicate with CY, their packets will have to be transported through several intermediate units.
Messages sent and received should have iteration numbers so we can detect dropped packets. As each unit learns of other units, then each unit should send a "Hi from [unit name]" and the 7 units

View file

@ -0,0 +1,110 @@
# Introduction
Exercise 204 builds upon Exercise 203 where two units generate ids, announce, and then establish a LINK and exchange messages. In Exercise 204, we add:
OLED display
use of established identities
configuration of units as transports
Allow for multiple units to be communicating
# Identities
We have 7 units: AMY...GUY. The following command creates identities for use in this exercise.
# Activate the Python environment that has Reticulum & pio installed
source ~/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/204_established_identities
mkdir identities
cd identities
for n in AMY BOB CY DAN ED FLO GUY
do
rnid -g "./${n}.identity"
rnid -i "./${n}.identity" -p > "./${n}.identity_info.txt"
done
## Example run
```bash
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/204_established_identities/identities $ for n in AMY BOB CY DAN ED FLO GUY
do
rnid -g "./${n}.identity"
rnid -i "./${n}.identity" -p > "./${n}.identity_info.txt"
done
[2026-05-28 11:26:29] New identity <c95d06fb622a80b4d80389fc7fe55d16> written to ./AMY.identity
[2026-05-28 11:26:33] New identity <5769e13e1214e62b96e43c17bd47085e> written to ./BOB.identity
[2026-05-28 11:26:38] New identity <92ce7c2eb62820c2e4476308350cc69d> written to ./CY.identity
[2026-05-28 11:26:42] New identity <d0524d8f1d98fc39f13772655640ea30> written to ./DAN.identity
[2026-05-28 11:26:46] New identity <4ad998c481ff6c71d1acb8cbaf111e1f> written to ./ED.identity
[2026-05-28 11:26:51] New identity <5671752180661e6af4bfe49e962f23dd> written to ./FLO.identity
[2026-05-28 11:26:55] New identity <051cfb95faa527b68368d24efb40f689> written to ./GUY.identity
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/204_established_identities/identities $
```
# Implementation notes
`platformio.ini` defines environments for `amy`, `bob`, `cy`, `dan`, `ed`, `flo`, and `guy`. The pre-build script `scripts/set_build_identity.py` embeds the matching `identities/<BOARD>.identity` file into the firmware and also stamps `FW_BUILD_UTC` for the OLED splash screen.
At boot the OLED shows:
```text
Exercise 204
Build
<build utc>
<BOARD>
<node label>
```
The splash is held for 10 seconds so the full build stamp can be read.
The unit then checks the RTC and `/ex204/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. A successful GPS discipline writes a fresh marker and then starts LoRa. Marker writes replace the old file, and marker reads use the last valid line so stale appended clock data cannot keep forcing GPS discipline.
All units run with `reticulum.transport_enabled(true)`. Path learning remains enabled, but `RNS_PERSIST_PATHS` is intentionally not defined for this exercise. Live announces are sufficient for the single-hop field test, and disabling persistent paths avoids ESP32 LittleFS `/path_store_*.dat` compaction errors during dense multi-unit announce traffic.
Each unit tracks up to six peers, so a full seven-unit field run can form one Link per ordered pair. The lexicographically lower node initiates each pair to avoid both ends opening duplicate Links at the same time; for example `Bob` initiates to `Cy`, `Dan`, `Ed`, `Flo`, and `Guy`, while `Amy` initiates to everyone. If an outbound link request is sent but no link becomes active, the initiator retries after 90 seconds. This matters in field use because the first request can be missed while units are being moved, reset, or connected to monitors.
Announces and application traffic are scheduled from the RTC so all nodes do not transmit at once. Announces run once per minute at `02,06,10,14,18,22,26` for `Amy..Guy`; Link payloads run twice per minute:
```text
Amy: 04,34 Bob: 08,38 Cy: 12,42 Dan: 16,46
Ed: 20,50 Flo: 24,54 Guy: 28,58
```
Follow-up note: DAN/ED field testing showed the software version/build stamp was not visible on the OLED even though `FW_BUILD_UTC` is compiled into the splash text. Recheck the OLED startup/status path on the next firmware modification; do not rebuild solely for this note.
# Build, upload, and monitor
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/204_established_identities -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/204_established_identities -e "${env}" -t upload
done
```
After both units have a fresh clock marker, monitor them:
```bash
pio device monitor -d exercises/204_established_identities -e bob
```
Expected first-run clock gate:
```text
SD ready
No saved clock marker; GPS discipline required
Waiting for GPS UTC before LoRa startup
gps_gate time=0 fix=0 sats=-1 pps_ms=0
```
Expected LoRa/link traffic after the clock gate:
```text
Local Identity: <identity hash>
Local SINGLE destination: <destination hash>
TX ANNOUNCE: Bob
RX ANNOUNCE: label=Cy hash=<destination hash>
TX LINKREQUEST: opening link to Cy
LINK ACTIVE: initiator link established
TX LINK: Hi from Bob iter=0 to=Cy
RX LINK: Hi from Cy iter=0 to=Bob | RSSI=... SNR=...
```

View file

@ -0,0 +1,692 @@
# History of a BOB to CY Link
This is a forensic analysis to help understand and fix the failure of a LINK between two isolated units forced to use an intermediary for transport. This arrangement is the heart of a "mesh" network. The units are LilyGO T-Beam Supremes named: BOB, CY, DAN & ED. Additional units of AMY, FLO & GUY may be involved in later testing.
## Background & Tools
The version of this exercise, 204_established_identities, is:
```bash
commit 197b46b4bba2b49f38cddd51b25a7f264b3f348b (HEAD -> feature/fieldtest-beacon-sd-provision)
Author: John Poole <jlpoole56@gmail.com>
Date: Fri May 29 10:58:14 2026 -0700
```
`platformio.ini` was modified so that DAN & CY had additional debugging activated in microReticulum's Transport.cpp through the use of the build directive of `-D EX204_RNS_TRACE=1`.
The log captures were performed using 4 serial console connections (ED was defunct) using an bash script, `monitor_tbeam`, that adds colors using the Perl script: `color_rx_tx.pl`.
Finally all the logs were moved into their own run directory with the Perl script: `clump_tbeam_logs.pl`.
Source logs: `/home/jlpoole/logs/20260528_2319`
This run used BOB, DAN, and CY only. ED was offline. The simulated physical
block between BOB and CY was enabled, so direct BOB<->CY LoRa frames were
dropped below microReticulum in `TBeamSupremeLoRaInterface`. Packets physically
transmitted by DAN were still accepted by both BOB and CY.
### Time
`main.cpp` disciplines the time among the T-Beams, up to 24 hour drift is allowed. The workstation, jp, that the logging and serial port monitor occurred is disciplined to a local stratum 1 time server. Time stamps from the bash logging script exhibited some delay.
## Identity Legend
The relevant destination and transport identities in this run were:
```text
BOB destination hash: e431430abeca68dca8411f50ca9864b0
CY destination hash: f04ce61418eecae211de3f78d0e652e6
DAN destination hash: d7217d5b6372e94aa78df9a43110723d
DAN transport ID: d0524d8f1d98fc39f13772655640ea30
BOB->CY Link ID: 387de6033cddc91ac6c583721fa79eb8
```
The application policy is that BOB is the initiator for this BOB/CY pair. The
logs do not show the identity comparison itself, but they do show BOB initiating
the LinkRequest and CY accepting the inbound request.
## Software isolation of BOB and CY
To simulate BOB and CY being out of radio range, we resorted to blocking packets at each unit. Here is a Codex explanation of the blocking mechanism designed to be outside of the Reticulum code.
Blocking Implementation
Here is Codex's explanation of our blockage:
We are simulating BOB/CY RF separation below microReticulum, inside the T-Beam LoRa interface adapter. microReticulum packets are not inspected, parsed, or altered for the block decision.
Each transmitted LoRa frame gets a small 4-byte simulation envelope prepended by TBeamSupremeLoRaInterface:
[ SIM PHY ENVELOPE ][ local LoRa fragment header ][ raw microReticulum packet bytes ... ]
SIM PHY ENVELOPE:
byte 0: 0xC2 magic
byte 1: 0x04 exercise marker
byte 2: 0x01 envelope version
byte 3: NODE_SLOT physical transmitter ID
NODE_SLOT:
1 = BOB
2 = CY
3 = DAN
4 = ED
On receive, the LoRa adapter strips that 4-byte envelope before passing bytes to microReticulum. So microReticulum receives the same byte stream it would have received without this simulation wrapper.
The block rule is only:
if local node is BOB and physical transmitter is CY: drop before microReticulum
if local node is CY and physical transmitter is BOB: drop before microReticulum
otherwise: strip envelope and deliver to microReticulum
Example: direct BOB -> CY is blocked before microReticulum sees it:
LoRa air frame:
c2 04 01 01 | H | <microReticulum packet bytes>
^^
physical_tx = 1 = BOB
CY receives:
local = 2 = CY
physical_tx = 1 = BOB
decision = DROP
Result:
InterfaceImpl::handle_incoming() is not called.
microReticulum never sees this packet.
Example: BOB traffic transported/rebroadcast by DAN is allowed:
LoRa air frame:
c2 04 01 03 | H | <microReticulum packet bytes>
^^
physical_tx = 3 = DAN
CY receives:
local = 2 = CY
physical_tx = 3 = DAN
decision = ACCEPT
Adapter strips envelope:
H | <microReticulum packet bytes>
Then calls:
InterfaceImpl::handle_incoming(<microReticulum packet bytes>)
So the important point is: the BOB/CY block is a physical-link simulation in TBeamSupremeLoRaInterface, not a microReticulum-layer rule. A packet physically emitted by DAN or ED is allowed through even if the Reticulum payload originated from BOB or CY.
# Analysis
## 1. Announcements Establish the Path
### ANNOUNCEMENT
An ANNOUNCEMENT is a packet that contains a DESTINATION, aka "DESTINATION HASH", and PUBLIC KEY, and possibly other information. See https://reticulum.network/manual/understanding.html#public-key-announcements
#### DESTINATION
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead Reticulum uses the singular concept of destinations. Destinations are represented as a 16 byte hash. The inbound_destination is computed at main.cpp (line 1020):
```cpp
inbound_destination = RNS::Destination(local_identity,
RNS::Type::Destination::IN,
RNS::Type::Destination::SINGLE,
APP_NAME,
APP_ASPECT);
```
and APP_NAME and APP_ASPECT are arbitrary strings defined at:
```cpp
static constexpr const char* APP_NAME = "microreticulum";
static constexpr const char* APP_ASPECT = "linkping";
```
So BOB's destination hash is derived from:
```bash
Bob's fixed identity
+ app name: microreticulum
+ aspect: linkping
```
microReticulum expands that into a destination name and hashes it. The relevant library code is:
```cpp
Bytes addr_hash_material = name_hash(app_name, aspects);
if (identity) {
addr_hash_material << identity.hash();
}
return Identity::truncated_hash(addr_hash_material);
```
For purposes of this analysis, BOB's destination hash is: e431430abeca68dca8411f50ca9864b0
#### PUUBLIC KEY
PUBLIC KEY was generated from the Reticulum utility `rnid`. BOB's identity consists of:
```bash
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/204_established_identities/identities $ cat BOB.identity_info.txt
[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
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/204_established_identities/identities $
```
Therein is BOB's Public KEY: 4705...4814.
#### BOB's ANNOUNCEMENT
BOB announces directly. DAN receives the BOB announce and records a direct
one-hop path to BOB:
```text
DAN line 1404: packet destination=e431430abeca68dca8411f50ca9864b0 hops=0
DAN line 1414: Valid announce for e431430abeca68dca8411f50ca9864b0 1 hops away
DAN line 1425: MR TRANSPORT PATH: dest=e431430abeca68dca8411f50ca9864b0 hops=1 via=e431430abeca68dca8411f50ca9864b0 iface=Interface[TBeamSupremeLoRa]
DAN line 1436: RX ANNOUNCE: label=Bob hash=e431430abeca68dca8411f50ca9864b0 phy=Bob(1)
```
DAN rebroadcasts BOB's announce with DAN as the transport ID (d052...a30):
```text
DAN line 1444: Rebroadcasting announce for e431430abeca68dca8411f50ca9864b0 with hop count 1
DAN line 1447: Packet::pack: transport id: d0524d8f1d98fc39f13772655640ea30
DAN line 1448: Packet::pack: destination hash: e431430abeca68dca8411f50ca9864b0
```
CY receives BOB's announce through DAN and records a two-hop path to BOB via
DAN:
```text
CY line 1497: packet: ... hp=1 ti=d0524d8f1d98fc39f13772655640ea30 dh=e431430abeca68dca8411f50ca9864b0
CY line 1507: Valid announce for e431430abeca68dca8411f50ca9864b0 2 hops away, received via d0524d8f1d98fc39f13772655640ea30
CY line 1517: Destination e431430abeca68dca8411f50ca9864b0 is now 2 hops away via d0524d8f1d98fc39f13772655640ea30
CY line 1518: MR TRANSPORT PATH: dest=e431430abeca68dca8411f50ca9864b0 hops=2 via=d0524d8f1d98fc39f13772655640ea30 iface=Interface[TBeamSupremeLoRa]
CY line 1529: RX ANNOUNCE: label=Bob hash=e431430abeca68dca8411f50ca9864b0 phy=Dan(3)
```
Line 1529 above confirms that CY received that rebroadcast from DAN and CY now knows Bob is reachable via DAN.
Likewise, for BOB, at line 89 below, the reverse direction is also known. BOB repeatedly receives CY announces via DAN and records a two-hop path to CY:
```text
BOB line 88: MR TRANSPORT PATH: dest=f04ce61418eecae211de3f78d0e652e6 hops=2 via=d0524d8f1d98fc39f13772655640ea30 iface=Interface[TBeamSupremeLoRa]
BOB line 89: RX ANNOUNCE: label=Cy hash=f04ce61418eecae211de3f78d0e652e6 phy=Dan(3)
```
At this point, BOB & CY have each other's destinations and know that the other is reachable through DAN. The rule employed in this exercise is that the "lower" identity is the unit that initiates a LINK. (This would not be a real life scenario, but this rule is adopted to keep transmissions over the LoRa to a minimum and to easily progress with linking.) In this exercise, each unit is aware of the other's id, sort of an address book, so BOB can determine it is its responsibility to initiate a LINK with CY.
## 2. BOB Initiates the LinkRequest
BOB decides to open a Link to CY. Since BOB is the initiator for this pair, BOB
creates a LinkRequest targeting CY's destination hash. Because CY is known as a
two-hop destination via DAN, BOB sends the LinkRequest to DAN as the next hop:
```text
BOB line 97: TX LINKREQUEST: opening link to Cy slot=19
BOB line 98: MR TRANSPORT OUT: type=LINKREQUEST dest=f04ce61418eecae211de3f78d0e652e6 hops=2 next=d0524d8f1d98fc39f13772655640ea30 iface=Interface[TBeamSupremeLoRa]
```
In line 98 above, "dest" stands for destination, e.g. "f04c...e6", and utilizes the value from the "hash" value in BOB's log, line 89 above.
DAN receives that transported LinkRequest. DAN recognizes that it is the
designated next hop and forwards the request toward CY:
```text
DAN line 1680: MR TRANSPORT FWD: type=LINKREQUEST dest=f04ce61418eecae211de3f78d0e652e6 remaining=1 next=f04ce61418eecae211de3f78d0e652e6 iface=Interface[TBeamSupremeLoRa]
DAN line 1681: Transport::inbound: Packet is next-hop LINKREQUEST
```
Be aware that CY also sees one direct physical BOB frame and drops it at the simulated PHY
layer. This is expected and is not a microReticulum decision:
```text
CY line 1310: SIM PHY DROP: rx=2 tx=1 len=107 blocked=113
```
## 3. CY Accepts the LinkRequest
CY receives the forwarded LinkRequest from DAN. It is no longer a direct BOB
physical frame; it is physically transmitted by DAN, so it passes the simulated
PHY block and reaches microReticulum:
```text
CY line 1314: packet: ht=0 tt=0 dt=0 pt=2 hp=1 ... dh=f04ce61418eecae211de3f78d0e652e6 ph=387de6033cddc91ac6c583721fa79eb8...
CY line 1315: destination=f04ce61418eecae211de3f78d0e652e6 hops=1
CY line 1320: Transport::inbound: Packet is LINKREQUEST
CY line 1322: Transport::inbound: Found local destination for LINKREQUEST
CY line 1323: ***** Accepting link request
CY line 1327: Validating link request 387de6033cddc91ac6c583721fa79eb8
CY line 1335: Link 387de6033cddc91ac6c583721fa79eb8 requesting proof
```
CY sends a proof for the LinkRequest:
```text
CY line 1342: Packet::pack: destination link id: 387de6033cddc91ac6c583721fa79eb8
CY line 1345: Transport::outbound: destination=387de6033cddc91ac6c583721fa79eb8 hops=0
CY line 1347: MR TRANSPORT OUT: type=PROOF dest=387de6033cddc91ac6c583721fa79eb8 path=unknown
CY line 1353: Incoming link request {Link:387de6033cddc91ac6c583721fa79eb8} accepted
```
BOB receives enough of the proof/handshake to mark the Link active:
```text
BOB line 100: RNSDEC ... event=encrypt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=64 ...
BOB line 101: RNSDEC ... event=attempt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=64 ...
BOB line 102: MR TRANSPORT OUT: type=DATA dest=387de6033cddc91ac6c583721fa79eb8 path=unknown
BOB line 103: LINK ACTIVE: initiator link established to Cy hash=387de6033cddc91ac6c583721fa79eb8
```
BOB successfully establishes a Link to CY through DAN. (Previously in the 4 unit test, this event never occurred. This is the key success point: with ED removed.)
## 4. First Data Packet After Link Establishment
After the Link is active, BOB sends an application payload over the Link:
```text
BOB line 110: TX LINK: Hi from Bob iter=5 to=Cy
BOB line 111: RNSDEC ... event=encrypt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=80 ...
BOB line 112: MR TRANSPORT OUT: type=DATA dest=387de6033cddc91ac6c583721fa79eb8 path=unknown
```
DAN receives the Link data and identifies it as a packet that should be link
transported. The inbound and outbound interfaces are the same LoRa interface, so
DAN forwards the packet back out on the same interface:
```text
DAN line 1885: packet: ht=0 tt=0 dt=3 pt=0 hp=0 ... dh=387de6033cddc91ac6c583721fa79eb8
DAN line 1886: destination=387de6033cddc91ac6c583721fa79eb8 hops=0
DAN line 1894: MR TRANSPORT LINKFWD: dest=387de6033cddc91ac6c583721fa79eb8 hops=1 link_remaining=1 link_hops=1 iface=Interface[TBeamSupremeLoRa]
```
## 5. Failure Point
BOB hears the forwarded Link packet on the shared LoRa channel. Since the
plaintext says it is from Bob and to Cy, BOB's application rejects it as
self/wrong-recipient:
```text
BOB line 113: RNSDEC ... event=attempt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=80 ...
BOB line 114: RX LINK ignored: self_or_wrong_recipient text=Hi from Bob iter=5 to=Cy
```
CY also receives a packet for the same Link ID around the same time:
```text
CY line 1532: Transport::inbound: received 99 bytes
CY line 1534: packet: ht=0 tt=0 dt=3 pt=0 hp=1 ... dh=387de6033cddc91ac6c583721fa79eb8
CY line 1535: destination=387de6033cddc91ac6c583721fa79eb8 hops=1
CY line 1545: RNSDEC ... event=attempt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=80 ...
```
But CY does not log the expected application-level receive:
```text
expected but absent:
RX LINK: Hi from Bob iter=5 to=Cy
```
The failure is therefore not in initial path discovery and not in LinkRequest
delivery. The BOB->DAN->CY Link establishment succeeds. The observed failure is
in post-establishment Link data handling after DAN forwards the Link data on the
same broadcast LoRa interface. BOB hears the forwarded packet and ignores it as
self/wrong-recipient; CY attempts to process the packet but does not emit the
expected `RX LINK` application payload line.
## Working Interpretation
1. Announcement transport works: BOB and CY learn each other through DAN.
2. LinkRequest transport works in the single-intermediary case: DAN forwards
BOB's LinkRequest to CY.
3. Link establishment works: BOB marks the BOB->CY Link active.
4. Link data transport partially works: DAN recognizes and forwards Link data.
5. The failure point is after Link data forwarding on the same LoRa interface.
The forwarded packet is heard by BOB and apparently reaches CY, but CY does
not produce the expected decrypted application payload log.
# Appendix
## Key Evidence With Exact Log Lines
These are the most pertinent lines in `file:line:entry` form.
```text
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:85:20260528_232309.309 TX ANNOUNCE: Bob
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:88:20260528_232316.679 01-02:10:30.334 [NOT] MR TRANSPORT PATH: dest=f04ce61418eecae211de3f78d0e652e6 hops=2 via=d0524d8f1d98fc39f13772655640ea30 iface=Interface[TBeamSupremeLoRa]
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:89:20260528_232316.682 RX ANNOUNCE: label=Cy hash=f04ce61418eecae211de3f78d0e652e6 phy=Dan(3)
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:97:20260528_232322.256 TX LINKREQUEST: opening link to Cy slot=19
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:98:20260528_232322.386 01-02:10:36.041 [NOT] MR TRANSPORT OUT: type=LINKREQUEST dest=f04ce61418eecae211de3f78d0e652e6 hops=2 next=d0524d8f1d98fc39f13772655640ea30 iface=Interface[TBeamSupremeLoRa]
/home/jlpoole/logs/20260528_2319/DAN_raw_20260528_231958.log:1680:20260528_232322.594 23:23:59.788 [NOT] MR TRANSPORT FWD: type=LINKREQUEST dest=f04ce61418eecae211de3f78d0e652e6 remaining=1 next=f04ce61418eecae211de3f78d0e652e6 iface=Interface[TBeamSupremeLoRa]
/home/jlpoole/logs/20260528_2319/DAN_raw_20260528_231958.log:1681:20260528_232322.595 23:23:59.789 [---] Transport::inbound: Packet is next-hop LINKREQUEST
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1314:20260528_232322.767 18:52:08.577 [---] Transport::inbound: packet: ht=0 tt=0 dt=0 pt=2 hp=1 ti= dh=f04ce61418eecae211de3f78d0e652e6 ph=387de6033cddc91ac6c583721fa79eb84b58bfc84dc4b8f35fb1d98ef0f3bffc
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1320:20260528_232322.773 18:52:08.583 [---] Transport::inbound: Packet is LINKREQUEST
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1322:20260528_232322.774 18:52:08.584 [---] Transport::inbound: Found local destination for LINKREQUEST
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1323:20260528_232322.775 18:52:08.585 [---] ***** Accepting link request
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1335:20260528_232322.846 18:52:08.656 [DBG] Link 387de6033cddc91ac6c583721fa79eb8 requesting proof
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1347:20260528_232322.910 18:52:08.720 [NOT] MR TRANSPORT OUT: type=PROOF dest=387de6033cddc91ac6c583721fa79eb8 path=unknown
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:103:20260528_232323.747 LINK ACTIVE: initiator link established to Cy hash=387de6033cddc91ac6c583721fa79eb8
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:110:20260528_232411.271 TX LINK: Hi from Bob iter=5 to=Cy
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:112:20260528_232411.306 01-02:11:24.961 [NOT] MR TRANSPORT OUT: type=DATA dest=387de6033cddc91ac6c583721fa79eb8 path=unknown
/home/jlpoole/logs/20260528_2319/DAN_raw_20260528_231958.log:1885:20260528_232411.501 23:24:48.694 [---] Transport::inbound: packet: ht=0 tt=0 dt=3 pt=0 hp=0 ti= dh=387de6033cddc91ac6c583721fa79eb8 ph=838aaf52dc8f5a71ca7d878154504fce9471a5e40cad556f55739287c01720f2
/home/jlpoole/logs/20260528_2319/DAN_raw_20260528_231958.log:1893:20260528_232411.507 23:24:48.700 [---] Transport::inbound: Link inbound/outbound interfaes are same, transporting on same interface
/home/jlpoole/logs/20260528_2319/DAN_raw_20260528_231958.log:1894:20260528_232411.509 23:24:48.701 [NOT] MR TRANSPORT LINKFWD: dest=387de6033cddc91ac6c583721fa79eb8 hops=1 link_remaining=1 link_hops=1 iface=Interface[TBeamSupremeLoRa]
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:113:20260528_232411.702 RNSDEC ms=3378664 board=library role=unknown event=attempt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=80 token_crc32=808C9304 sign_key_crc32=928C394D enc_key_crc32=D6DE582A link_obj={Link:387de6033cddc91ac6c583721fa79eb8}
/home/jlpoole/logs/20260528_2319/BOB_raw_20260528_231955.log:114:20260528_232411.704 RX LINK ignored: self_or_wrong_recipient text=Hi from Bob iter=5 to=Cy
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1534:20260528_232411.704 18:52:57.513 [---] Transport::inbound: packet: ht=0 tt=0 dt=3 pt=0 hp=1 ti= dh=387de6033cddc91ac6c583721fa79eb8 ph=838aaf52dc8f5a71ca7d878154504fce9471a5e40cad556f55739287c01720f2
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1541:20260528_232411.711 18:52:57.519 [---] Transport::inbound: Packet is DATA
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1542:20260528_232411.712 18:52:57.520 [---] Transport::inbound: Packet is DATA for a LINK
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1543:20260528_232411.712 18:52:57.520 [---] Transport::inbound: Packet is DATA for an active LINK
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1545:20260528_232411.714 RNSDEC ms=1197054 board=library role=unknown event=attempt link_id=387de6033cddc91ac6c583721fa79eb8 token_len=80 token_crc32=808C9304 sign_key_crc32=928C394D enc_key_crc32=D6DE582A link_obj={Link:387de6033cddc91ac6c583721fa79eb8}
```
## bash script monitor_tbeam
```bash
(rnsenv) jlpoole@jp ~ $ cat /usr/bin/monitor_tbeam
#!/bin/bash
# 20260528 ChatGPT
# $Header: https://ares/svn/workstation/trunk/scripts/monitor_tbeam.sh 46 2026-05-29 05:04:50Z jlpoole $
# $HeadURL: https://ares/svn/workstation/trunk/scripts/monitor_tbeam.sh $
#
# Example:
# monitor_tbeam ED
#
# Install:
# sudo ln -s /home/jlpoole/workstation/scripts/monitor_tbeam.sh /usr/bin/monitor_tbeam
# sudo chmod +x /home/jlpoole/workstation/scripts/monitor_tbeam.sh
#
export TBEAM="$1"
if [ -z "${TBEAM}" ]; then
echo "Usage: monitor_tbeam ED"
echo "Example: monitor_tbeam GUY"
exit 1
fi
source ~/rnsenv/bin/activate
mkdir -p ~/logs
LOGFILE=~/logs/${TBEAM}_raw_$(date +%Y%m%d_%H%M%S).log
pio device monitor -b 115200 -p "/dev/ttyt${TBEAM}" 2>&1 \
| perl -MTime::HiRes=time -MPOSIX=strftime -ne '
BEGIN {
$| = 1;
}
s/\r?\n$//;
my $t = time();
my $s = int($t);
my $ms = int(($t - $s) * 1000);
my $stamp = strftime("%Y%m%d_%H%M%S", localtime($s));
printf "%s.%03d %s\n", $stamp, $ms, $_;
' \
| tee "$LOGFILE" \
| /usr/bin/color_rx_tx.pl
(rnsenv) jlpoole@jp ~ $
```
## Perl script color_rx_tx.pl
```perl
jlpoole@jp ~/workstation/perl $ cat color_rx_tx.pl
#!/usr/bin/env perl
# 20260528 ChatGPT
# $Header: https://ares/svn/workstation/trunk/perl/color_rx_tx.pl 44 2026-05-28 23:36:24Z jlpoole $
# $HeadURL: https://ares/svn/workstation/trunk/perl/color_rx_tx.pl $
#
# sudo ln -s /home/jlpoole/workstation/perl/color_rx_tx.pl /usr/bin/color_rx_tx.pl
#
#
use strict;
use warnings;
$| = 1;
my $BLUE = "\e[96m"; # light blue / cyan
my $GREEN = "\e[92m"; # light green
my $RESET = "\e[0m";
while (my $line = <STDIN>) {
$line =~ s/\r?\n$//;
if ($line =~ /\bTX\b/) {
print $BLUE, $line, $RESET, "\n";
}
elsif ($line =~ /\bRX\b/) {
print $GREEN, $line, $RESET, "\n";
}
else {
print $line, "\n";
}
}
jlpoole@jp ~/workstation/perl $
```
## Perl script clump_tbeam_logs.pl
```perl
jlpoole@jp ~/workstation/perl $ cat clump_tbeam_logs.pl
#!/usr/bin/env perl
#
# Example:
# chmod 755 clump_tbeam_logs.pl
# ./clump_tbeam_logs.pl
# ./clump_tbeam_logs.pl --dry-run
# ./clump_tbeam_logs.pl --dir ~/logs --window 60 --min-files 4
#
# 20260528 ChatGPT
# $Header: https://ares/svn/workstation/trunk/perl/clump_tbeam_logs.pl 44 2026-05-28 23:36:24Z jlpoole $
# $HeadURL: https://ares/svn/workstation/trunk/perl/clump_tbeam_logs.pl $
#
=pod
chmod +x clump_tbeam_logs.pl
sudo ln -s /home/jlpoole/workstation/perl/clump_tbeam_logs.pl /usr/bin/clump_tbeam_logs.pl
=cut
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use File::Basename qw(basename);
use File::Path qw(make_path);
use File::Spec;
use Time::Piece;
use List::Util qw(max);
my $log_dir = "$ENV{HOME}/logs";
my $window = 60; # seconds
my $min_files = 2; # use 4 if you want to require four consoles
my $dry_run = 0;
my $help = 0;
GetOptions(
'dir=s' => \$log_dir,
'window=i' => \$window,
'min-files=i' => \$min_files,
'dry-run' => \$dry_run,
'help' => \$help,
) or die "Bad option. Try --help\n";
if ($help) {
print <<"HELP";
Usage:
./clump_tbeam_logs.pl [options]
Options:
--dir DIR Log directory. Default: ~/logs
--window SECONDS Timestamp grouping window. Default: 60
--min-files N Minimum matching files required. Default: 2
--dry-run Show what would happen, but do not move files.
--help Show this help.
Example:
./clump_tbeam_logs.pl --dir ~/logs --window 60 --min-files 4
HELP
exit 0;
}
die "Directory not found: $log_dir\n" unless -d $log_dir;
my @units = qw(AMY BOB CY DAN ED FLO GUY);
my $unit_re = join '|', @units;
print "\nContents of $log_dir:\n";
print "-" x 72, "\n";
my @all_paths = glob(File::Spec->catfile($log_dir, '*'));
my @listed = sort {
(stat($b))[9] <=> (stat($a))[9]
} @all_paths;
for my $path (@listed) {
my @st = stat($path);
next unless @st;
my $mtime = localtime($st[9])->strftime('%Y-%m-%d %H:%M:%S');
my $size = $st[7];
my $name = basename($path);
printf "%s %10d %s\n", $mtime, $size, $name;
}
print "-" x 72, "\n\n";
my @candidates;
for my $path (@all_paths) {
next unless -f $path;
my $base = basename($path);
next unless $base =~ /(?:^|[_\-.])($unit_re)(?:[_\-.]|$)/i;
my $unit = uc($1);
next unless $base =~ /(\d{8}_\d{6})/;
my $stamp = $1;
my $tp;
eval {
$tp = Time::Piece->strptime($stamp, '%Y%m%d_%H%M%S');
};
if ($@) {
warn "Skipping file with bad timestamp: $base\n";
next;
}
my $minute_stamp = substr($stamp, 0, 13); # YYYYMMDD_HHMM
push @candidates, {
path => $path,
base => $base,
unit => $unit,
stamp => $stamp,
minute_stamp => $minute_stamp,
epoch => $tp->epoch,
};
}
die "No matching T-Beam log files found in $log_dir\n"
unless @candidates;
@candidates = sort {
$b->{epoch} <=> $a->{epoch}
} @candidates;
my $seed = $candidates[0];
my @window_matches = grep {
abs($_->{epoch} - $seed->{epoch}) <= $window
} @candidates;
# Keep only the newest file per unit inside the time window.
my %best_for_unit;
for my $item (@window_matches) {
my $unit = $item->{unit};
if (!exists $best_for_unit{$unit}
|| $item->{epoch} > $best_for_unit{$unit}->{epoch}) {
$best_for_unit{$unit} = $item;
}
}
my @selected = sort {
$a->{epoch} <=> $b->{epoch}
} values %best_for_unit;
if (@selected < $min_files) {
print "Newest matching file:\n";
printf " %s %s %s\n", $seed->{unit}, $seed->{stamp}, $seed->{base};
print "\nFiles found within +/- $window seconds:\n";
for my $item (@selected) {
printf " %-3s %s %s\n",
$item->{unit}, $item->{stamp}, $item->{base};
}
die "\nOnly found " . scalar(@selected) .
" matching file(s), but --min-files is $min_files. No files moved.\n";
}
my %minute_count;
for my $item (@selected) {
$minute_count{ $item->{minute_stamp} }++;
}
my $max_count = max(values %minute_count);
my @mode_minutes = sort grep {
$minute_count{$_} == $max_count
} keys %minute_count;
# Tie-breaker: use the newest file's minute if it is one of the modes;
# otherwise use the latest mode minute.
my $dir_stamp = $seed->{minute_stamp};
if (!grep { $_ eq $dir_stamp } @mode_minutes) {
$dir_stamp = $mode_minutes[-1];
}
my $target_dir = File::Spec->catdir($log_dir, $dir_stamp);
print "Seed file:\n";
printf " %-3s %s %s\n", $seed->{unit}, $seed->{stamp}, $seed->{base};
print "\nSelected files within +/- $window seconds:\n";
for my $item (@selected) {
printf " %-3s %s %s\n",
$item->{unit}, $item->{stamp}, $item->{base};
}
print "\nMost common minute stamp: $dir_stamp\n";
print "Target directory: $target_dir\n\n";
if ($dry_run) {
print "DRY RUN: no files will be moved.\n\n";
} else {
if (!-d $target_dir) {
print "Creating directory: $target_dir\n";
make_path($target_dir) or die "Unable to create $target_dir: $!\n";
}
}
for my $item (@selected) {
my $src = $item->{path};
my $dst = File::Spec->catfile($target_dir, $item->{base});
if (-e $dst) {
die "Refusing to overwrite existing file: $dst\n";
}
if ($dry_run) {
print "Would move: $src\n";
print " to: $dst\n";
} else {
print "Moving: $item->{base}\n";
rename($src, $dst) or die "rename failed for $src -> $dst: $!\n";
}
}
print "\nDone.\n";
jlpoole@jp ~/workstation/perl $
```

View file

@ -0,0 +1,101 @@
# Review Notes for `history_of_a_LINK.md`
These notes are intentionally separate from the draft.
## Strong Points
The document now gives a useful foundation for a new reader. The added background explains the run setup, the three-node test condition, the purpose of the simulated BOB/CY isolation, and the difference between direct physical LoRa reception and transported microReticulum packets.
The analysis also correctly frames the main result from run `20260528_2319`: announcement transport works, the BOB-to-CY LinkRequest reaches CY through DAN, CY accepts the LinkRequest, and BOB marks the Link active. The remaining failure is later, after DAN forwards Link data on the shared LoRa interface.
## Citation Improvements
For an appellate-brief style record, the main body should not rely on shorthand citations such as:
```text
CY line 1529: RX ANNOUNCE: label=Bob hash=e431430abeca68dca8411f50ca9864b0 phy=Dan(3)
```
Prefer the full citation format already used in the appendix:
```text
/home/jlpoole/logs/20260528_2319/CY_raw_20260528_231957.log:1529:RX ANNOUNCE: label=Bob hash=e431430abeca68dca8411f50ca9864b0 phy=Dan(3)
```
That makes each quoted line independently traceable without requiring the reader to cross-reference the appendix.
For quoted source code, use the same idea:
```cpp
// exercises/204_established_identities/src/main.cpp:1020-1024
inbound_destination = RNS::Destination(local_identity,
RNS::Type::Destination::IN,
RNS::Type::Destination::SINGLE,
APP_NAME,
APP_ASPECT);
```
Useful source citations for the current document:
```text
exercises/204_established_identities/src/main.cpp:50-51
exercises/204_established_identities/src/main.cpp:1020-1024
exercises/204_established_identities/src/TBeamSupremeLoRaInterface.h:37-42
exercises/204_established_identities/src/TBeamSupremeLoRaInterface.cpp:142-149
exercises/204_established_identities/src/TBeamSupremeLoRaInterface.cpp:157-187
exercises/204_established_identities/src/TBeamSupremeLoRaInterface.cpp:197-200
/usr/local/src/microreticulum/microReticulum/src/Destination.cpp:115-126
```
## Terminology Notes
`phy=Dan(3)` means CY received that LoRa frame from physical transmitter DAN, slot 3. It does not mean CY sent a confirmation to DAN or BOB. In the cited BOB announcement example, it means CY learned BOB's destination from a frame physically transmitted by DAN.
`destination hash` should be introduced as the stable Reticulum destination identifier for this application endpoint. In this exercise, BOB's destination hash is derived from BOB's fixed identity plus the exercise's app name and aspect strings.
The `addr_hash_material << identity.hash();` line appends the identity hash bytes to the destination-name hash material before truncating the final hash.
## Suggested Wording Fixes
`PUUBLIC KEY` should be `PUBLIC KEY`.
`using an bash script` should be `using a bash script`.
`up to 24 hour drift is allowed` would read more clearly as `up to 24 hours of holdover is allowed`.
`ED was defunct` may read as hardware failure. If ED was intentionally offline for the test, say `ED was offline for this run`.
`This would not be a real life scenario` can be softened to `This is a test policy, not an inherent Reticulum rule`.
## One Claim to Qualify
The draft says the lower identity initiates the Link. The logs prove the result, namely BOB initiates and CY accepts. Unless the document cites the application code that performs the ordering comparison, phrase this as an exercise policy observed in the logs rather than as a fact proved by the quoted log lines alone.
Suggested wording:
```text
The exercise policy is that one side of a known peer pair initiates the Link,
avoiding simultaneous LinkRequests and reducing LoRa airtime. The logs do not
show the peer-ordering comparison itself; they show the resulting behavior:
BOB initiates, and CY accepts.
```
## Blocking Section
The blocking explanation is good and worth keeping. To make it more rigorous, cite the source lines that show:
```text
1. The envelope constants: TBeamSupremeLoRaInterface.h:37-42
2. The transmit-side envelope prepend: TBeamSupremeLoRaInterface.cpp:142-149
3. The receive-side envelope strip: TBeamSupremeLoRaInterface.cpp:157-187
4. The BOB/CY physical drop rule: TBeamSupremeLoRaInterface.cpp:197-200
```
The strongest single sentence in that section is:
```text
The BOB/CY block is a physical-link simulation in TBeamSupremeLoRaInterface,
not a microReticulum-layer rule.
```
That distinction should remain prominent.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,30 @@
BOB is having problem with using the clock sentinel. Every time BOB starts anew, I have to take it to a window to get satellite time. I tried erasing the clock.txt file and the directory, and a new directory and file were created, but it does not rely upon the clock.txt and forces a satellite time reset. I tried clicking RESET on BOB, and it needs satellites. RESET on the three others does not, they resume without needing satellites.
Also, the splash screen with version needs:
1) the version needes to be on a 2nd line, it goes off to the right of the screen, so i cannot determine its full version.
2) the splash screen needs to hold for 10 seconds, it flashes by too quickly, so please introduce a sleep.
ED never seems to link.
I changed in main.cpp:
//static constexpr uint32_t LINK_RETRY_MS = 30000;
static constexpr uint32_t LINK_RETRY_MS = 90000;
because the LINKS were timing out. Still, with 90 seconds, they time out.
Is the continued ANNOUNCEMENT something that is interfering with the longevity of LINKS? It seems that if a LINK is established, then both units on the LINK would just continue sending back and forth; that is not happening, the LINK seems to be torn down.
I have a 3 minute set of logs with the lastest code at: ~/logs/20260528_1707. Of the four 180 second logs, only one entry of:
jlpoole@jp ~/logs/20260528_1707 $ grep -i 'RX LINK' *
CY_raw_20260528_170755.log:RX LINK: inbound link established hash=3255588dd68ff2bc6ab95b8712120343
jlpoole@jp ~/logs/20260528_1707 $
Are we hitting some bugs in microreticulum?
When you are ready to compile, please compile only 1 unit, if that compiles, then so will all the rest. Notify me and I can run a bash script that will compile the others -- it takes about 4 minutes to complete.
Please leave all uploading of newly compiled firmware to me.
I am trying to maximize your time within my 5 Hour allowance and which recently has been consumed at a rapid rate and I think having you monitor units and wait for compiles and upload may be eating into the budget. I've developed a logging system that isolates logs from a run into its own directory, e.g. ~/logs/20260528_1707, so we can use that to efficiently evaluate what is going on.

View file

@ -0,0 +1,144 @@
; Exercise 204: established identities over microReticulum Link transport
[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=1
-D MR_TRANSPORT_PROBE=1
-D MR_LINKFWD_DELAY_MS=750
-D MR_LRPROOF_DELAY_MS=750
; 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 EX204_RNS_TRACE=1
-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 EX204_RNS_TRACE=1
-D BOARD_ID=\"DAN\"
-D NODE_LABEL=\"Dan\"
-D NODE_SLOT_INDEX=3
[env:ed]
extends = env
upload_port = /dev/ttytED
monitor_port = /dev/ttytED
build_flags =
${env.build_flags}
-D BOARD_ID=\"ED\"
-D NODE_LABEL=\"Ed\"
-D NODE_SLOT_INDEX=4
[env:flo]
extends = env
upload_port = /dev/ttytFLO
monitor_port = /dev/ttytFLO
build_flags =
${env.build_flags}
-D BOARD_ID=\"FLO\"
-D NODE_LABEL=\"Flo\"
-D NODE_SLOT_INDEX=5
[env:guy]
extends = env
upload_port = /dev/ttytGUY
monitor_port = /dev/ttytGUY
build_flags =
${env.build_flags}
-D BOARD_ID=\"GUY\"
-D NODE_LABEL=\"Guy\"
-D NODE_SLOT_INDEX=6

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -5,14 +5,15 @@
-- sqlite3 ble_fieldtest_YYYYMMDD_HHMM.sqlite < create_exercise_26_ble_schema.sql
--
-- SQLite schema for Exercise 26 BLE Discovery field-test logs.
-- 20260527_1156: aligned derived delta column name with importer: rx_tx_delta_ms.
--
-- This schema targets the current 18-column Exercise 26 log format only.
-- This schema targets the current 19-column Exercise 26 log format only.
--
-- Current Exercise 26 log format:
--
-- human_time,rx_epoch_ms,receiver,rx_lat,rx_lon,gps_fix_age_ms,
-- clock_valid,gps_valid,discipline_age_ms,last_discipline_epoch_ms,
-- heard,rssi,avg_rssi,age_s,count,seq,tx_payload_epoch_ms,payload
-- heard,rssi,avg_rssi,age_s,count,seq,tx_payload_epoch_ms,payload,vbat_mv
--
-- Current payload format:
--
@ -27,7 +28,7 @@
-- advertising payload was generated/configured. It is not a per-RF-packet
-- transmit timestamp.
--
-- rx_tx_payload_delta_ms is diagnostic only. It is useful for clock,
-- rx_tx_delta_ms is diagnostic only. It is useful for clock,
-- scheduling, payload-age, and repeated-observation analysis. It is not
-- BLE propagation delay.
--
@ -144,7 +145,7 @@ CREATE TABLE IF NOT EXISTS log_manifest_kv (
-- ---------------------------------------------------------------------------
-- ble_observation_raw
--
-- Faithful row-level import from the current 18-column CSV log.
-- Faithful row-level import from the current 19-column CSV log.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS ble_observation_raw (
@ -175,6 +176,7 @@ CREATE TABLE IF NOT EXISTS ble_observation_raw (
seq INTEGER,
tx_payload_epoch_ms INTEGER,
payload TEXT,
vbat_mv INTEGER,
UNIQUE(log_file_id, source_line_no)
);
@ -196,7 +198,7 @@ ON ble_observation_raw(clock_valid, gps_valid, gps_fix_age_ms);
--
-- Parsed and normalized BLE observation table.
--
-- rx_tx_payload_delta_ms is the receiver row timestamp minus the sender
-- rx_tx_delta_ms is the receiver row timestamp minus the sender
-- payload-generation timestamp. It is diagnostic only.
-- ---------------------------------------------------------------------------
@ -216,7 +218,7 @@ CREATE TABLE IF NOT EXISTS ble_observation (
tx_payload_epoch_ms INTEGER,
tx_payload_epoch_s REAL,
rx_tx_payload_delta_ms INTEGER,
rx_tx_delta_ms INTEGER,
receiver TEXT NOT NULL REFERENCES unit(unit_name),
heard TEXT NOT NULL REFERENCES unit(unit_name),
@ -230,6 +232,8 @@ CREATE TABLE IF NOT EXISTS ble_observation (
discipline_age_ms INTEGER,
last_discipline_epoch_ms INTEGER,
vbat_mv INTEGER,
rssi INTEGER,
avg_rssi INTEGER,
age_s INTEGER,
@ -241,7 +245,7 @@ CREATE TABLE IF NOT EXISTS ble_observation (
payload_kind TEXT,
payload_node TEXT,
payload_seq INTEGER,
payload_tx_payload_epoch_ms INTEGER,
payload_tx_epoch_ms INTEGER,
payload_legacy_uptime INTEGER,
parse_warning TEXT
@ -266,7 +270,7 @@ CREATE INDEX IF NOT EXISTS idx_obs_validity
ON ble_observation(clock_valid, gps_valid, gps_fix_age_ms);
CREATE INDEX IF NOT EXISTS idx_obs_payload_time
ON ble_observation(tx_payload_epoch_ms, rx_tx_payload_delta_ms);
ON ble_observation(tx_payload_epoch_ms, rx_tx_delta_ms);
-- ---------------------------------------------------------------------------
-- v_log_file_summary
@ -375,7 +379,7 @@ GROUP BY trial_id, receiver, heard;
--
-- Per receiver/heard timing-delta summary.
--
-- rx_tx_payload_delta_ms is useful for detecting clock disagreement,
-- rx_tx_delta_ms is useful for detecting clock disagreement,
-- scheduling, repeated payload observations, and payload-age behavior.
-- It is not RF propagation delay.
-- ---------------------------------------------------------------------------
@ -388,9 +392,9 @@ SELECT
COUNT(*) AS observation_count,
MIN(rx_tx_payload_delta_ms) AS min_rx_tx_payload_delta_ms,
AVG(rx_tx_payload_delta_ms) AS avg_rx_tx_payload_delta_ms,
MAX(rx_tx_payload_delta_ms) AS max_rx_tx_payload_delta_ms
MIN(rx_tx_delta_ms) AS min_rx_tx_delta_ms,
AVG(rx_tx_delta_ms) AS avg_rx_tx_delta_ms,
MAX(rx_tx_delta_ms) AS max_rx_tx_delta_ms
FROM ble_observation
WHERE tx_payload_epoch_ms IS NOT NULL
AND tx_payload_epoch_ms > 0
@ -407,9 +411,9 @@ SELECT
heard,
payload_seq,
COUNT(*) AS observations,
MIN(rx_tx_payload_delta_ms) AS min_delta_ms,
AVG(rx_tx_payload_delta_ms) AS avg_delta_ms,
MAX(rx_tx_payload_delta_ms) AS max_delta_ms,
MIN(rx_tx_delta_ms) AS min_delta_ms,
AVG(rx_tx_delta_ms) AS avg_delta_ms,
MAX(rx_tx_delta_ms) AS max_delta_ms,
MIN(rx_epoch_ms) AS first_rx_epoch_ms,
MAX(rx_epoch_ms) AS last_rx_epoch_ms
FROM ble_observation
@ -451,9 +455,9 @@ SELECT
MIN(rx_epoch_ms) AS first_rx_epoch_ms,
MAX(rx_epoch_ms) AS last_rx_epoch_ms,
MAX(rx_epoch_ms) - MIN(rx_epoch_ms) AS rx_spread_ms,
MIN(rx_tx_payload_delta_ms) AS min_delta_ms,
AVG(rx_tx_payload_delta_ms) AS avg_delta_ms,
MAX(rx_tx_payload_delta_ms) AS max_delta_ms
MIN(rx_tx_delta_ms) AS min_delta_ms,
AVG(rx_tx_delta_ms) AS avg_delta_ms,
MAX(rx_tx_delta_ms) AS max_delta_ms
FROM ble_observation
WHERE tx_payload_epoch_ms IS NOT NULL
AND tx_payload_epoch_ms > 0
@ -473,26 +477,26 @@ WITH x AS (
receiver,
heard,
payload_seq,
MIN(payload_tx_payload_epoch_ms) AS payload_tx_payload_epoch_ms
MIN(payload_tx_epoch_ms) AS payload_tx_epoch_ms
FROM ble_observation
WHERE payload_tx_payload_epoch_ms IS NOT NULL
AND payload_tx_payload_epoch_ms > 0
WHERE payload_tx_epoch_ms IS NOT NULL
AND payload_tx_epoch_ms > 0
GROUP BY trial_id, receiver, heard, payload_seq
),
y AS (
SELECT
x.*,
LAG(payload_tx_payload_epoch_ms) OVER (
LAG(payload_tx_epoch_ms) OVER (
PARTITION BY trial_id, receiver, heard
ORDER BY payload_seq
) AS prev_payload_tx_payload_epoch_ms
) AS prev_payload_tx_epoch_ms
FROM x
)
SELECT
*,
payload_tx_payload_epoch_ms - prev_payload_tx_payload_epoch_ms AS tx_payload_epoch_step_ms
payload_tx_epoch_ms - prev_payload_tx_epoch_ms AS tx_payload_epoch_step_ms
FROM y
WHERE prev_payload_tx_payload_epoch_ms IS NOT NULL;
WHERE prev_payload_tx_epoch_ms IS NOT NULL;
-- ---------------------------------------------------------------------------
-- v_map_observation_points
@ -517,11 +521,12 @@ SELECT
gps_valid,
discipline_age_ms,
last_discipline_epoch_ms,
vbat_mv,
rssi,
avg_rssi,
payload_seq,
tx_payload_epoch_ms,
rx_tx_payload_delta_ms
rx_tx_delta_ms
FROM ble_observation
WHERE rx_lat IS NOT NULL
AND rx_lon IS NOT NULL;

View file

@ -23,6 +23,31 @@
--db ${DB_BLE} \
--log "${BOB_LOG}"
Improved steps:
export DB_BLE=ble_fieldtest_20260527_1651.sqlite
sqlite3 ${DB_BLE} < create_exercise_26_ble_schema.sql
export LOG_DIR=/home/jlpoole/work/tbeam/logs/20260527_1651
export BOB_LOG=${LOG_DIR}/20260527_165156_BOB_ble_search.log
export CY_LOG=${LOG_DIR}/20260527_165201_CY_ble_search.log
export ED_LOG=${LOG_DIR}/20260527_165206_ED_ble_search.log
export FLO_LOG=${LOG_DIR}/20260527_165213_FLO_ble_search.log
# smoke test
date; awk -f exercise_26_smoke_test.awk ${BOB_LOG}
date; awk -f exercise_26_smoke_test.awk ${CY_LOG}
date; awk -f exercise_26_smoke_test.awk ${ED_LOG}
date; awk -f exercise_26_smoke_test.awk ${FLO_LOG}
for NODE in BOB CY ED FLO
do
LOG_VAR="${NODE}_LOG"
./import_exercise_26_ble_log.pl \
--db "${DB_BLE}" \
--log "${!LOG_VAR}"
done
=cut
# Manifest:
#