Exercise 305 works great, need to remove or toggle debugging
This commit is contained in:
parent
7410e820c6
commit
5207f72f14
16 changed files with 3846 additions and 0 deletions
|
|
@ -0,0 +1,480 @@
|
|||
# Exercise 305 post-mortem: BLE file transfer corruption under load
|
||||
|
||||
Date: 2026-05-20
|
||||
|
||||
Exercise 305 was created to move beyond BLE ping-pong messages and test a real bilateral file transfer over a negotiated microReticulum Link. Both ESP32-S3 T-Beam SUPREME boards run the same firmware, form a BLE transport connection, establish a Reticulum Link, and then send the selected text file at the same time.
|
||||
|
||||
The test exposed a failure that looked at first like a Reticulum cryptographic problem:
|
||||
|
||||
```text
|
||||
Decryption failed on link {...}. The contained exception was: Token token HMAC was invalid
|
||||
RX FILE END ... received=0/... chunks=0/... status=VERIFY_FAIL
|
||||
```
|
||||
|
||||
The important observation was that the failure followed BLE role/path, not a particular physical board. The server-side receiver could receive the bilateral US Constitution transfer, while the client-side receiver of server-originated chunks failed. In some runs a receiver accepted a small number of packets and then entered a long run of HMAC failures. That pattern suggested that the negotiated Link could be valid initially, and that later traffic pressure might be corrupting bytes, selecting the wrong Link context, overflowing a queue, or exhausting a constrained runtime resource.
|
||||
|
||||
This post-mortem records what was instrumented, what the live T-Beam serial sessions showed, and what was fixed.
|
||||
|
||||
## Test setup
|
||||
|
||||
The active test was:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_constitution_pi_zero_profile
|
||||
```
|
||||
|
||||
The same firmware image was uploaded to both boards:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_constitution_pi_zero_profile -t upload --upload-port /dev/ttytBOB
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_constitution_pi_zero_profile -t upload --upload-port /dev/ttytDAN
|
||||
```
|
||||
|
||||
The serial devices used for the live debug session were:
|
||||
|
||||
```text
|
||||
/dev/ttytBOB
|
||||
/dev/ttytDAN
|
||||
```
|
||||
|
||||
The Pi-Zero-comparison profile sends:
|
||||
|
||||
```text
|
||||
US_Constitution.txt: 44225 bytes
|
||||
Application chunk size: 300 bytes
|
||||
Application chunk interval: 100 ms
|
||||
Repeat rest: 10000 ms after each completed transmission
|
||||
```
|
||||
|
||||
Those are application-level file chunks. microReticulum then wraps and encrypts them into Link packets, and the BLE adapter fragments those encrypted packets into BLE characteristic writes/notifications.
|
||||
|
||||
## Initial hypotheses
|
||||
|
||||
The requested debug work was designed to distinguish several possible failure classes:
|
||||
|
||||
1. Wrong Link/Fernet/HMAC key or wrong Link/session object.
|
||||
2. Corrupted, truncated, or shifted encrypted packet bytes before decrypt.
|
||||
3. BLE fragmentation or reassembly failure.
|
||||
4. Queue overflow or silent overwrite between BLE callback and Reticulum processing.
|
||||
5. Borrowed buffer lifetime or buffer reuse after a BLE callback returns.
|
||||
6. Heap, stack, or fragmentation pressure.
|
||||
7. Role-specific TX/RX sequencing failure.
|
||||
|
||||
The central idea was to fingerprint the same packet at multiple layers without dumping full payloads. If the sender's encrypted token CRC matched the receiver's token CRC but HMAC failed, the likely cause would be wrong key/session/Link context. If the CRC differed, the likely cause would be mechanical corruption below Reticulum.
|
||||
|
||||
## Instrumentation added
|
||||
|
||||
Exercise 305 now has a lightweight debug layer in:
|
||||
|
||||
```text
|
||||
src/DebugStats.h
|
||||
src/DebugStats.cpp
|
||||
```
|
||||
|
||||
The BLE adapter instrumentation is in:
|
||||
|
||||
```text
|
||||
src/TBeamSupremeBleInterface.h
|
||||
src/TBeamSupremeBleInterface.cpp
|
||||
```
|
||||
|
||||
The exercise application emits Link and application-send debug lines from:
|
||||
|
||||
```text
|
||||
src/main.cpp
|
||||
```
|
||||
|
||||
The local microReticulum fork also has Link encrypt/decrypt instrumentation in:
|
||||
|
||||
```text
|
||||
/usr/local/src/microreticulum/microReticulum/src/Link.cpp
|
||||
```
|
||||
|
||||
The debug output uses stable single-line prefixes so a log analyzer can parse them:
|
||||
|
||||
```text
|
||||
RNSLINK Link establishment, close, announce TX/RX, peer hashes, Link ids.
|
||||
RNSTX Application plaintext sends and encrypted Reticulum packets handed to BLE.
|
||||
RNSRX Reassembled BLE packets immediately before Reticulum receives them.
|
||||
RNSDEC Link encrypt/decrypt attempts and classified decrypt failures.
|
||||
RNSBLE BLE connect, identity handshake, fragment TX/RX, and packet assembly.
|
||||
RNSQUEUE BLE RX queue depth, pushes, pops, drops, allocation failures, high-water marks.
|
||||
RNSMEM Heap, largest block, PSRAM, and current task stack high-water mark.
|
||||
RNSERR Classified adapter errors such as reassembly gap, bad header, queue overflow.
|
||||
```
|
||||
|
||||
All records are one-line `key=value` records. Packet records include length, CRC32, first 4 bytes, and last 4 bytes, but not full payloads. Normal packet fingerprint logging is rate-limited: first 25 packets, every 25th packet after that, and all packets briefly after a failure. It can be made fully verbose with `RNS_DEBUG_VERBOSE=1`.
|
||||
|
||||
## Link/key context fingerprinting
|
||||
|
||||
The Reticulum Link layer now prints non-secret fingerprints around encryption and decryption:
|
||||
|
||||
```text
|
||||
RNSDEC event=encrypt ...
|
||||
RNSDEC event=attempt ...
|
||||
RNSDEC event=HMAC_INVALID ...
|
||||
```
|
||||
|
||||
The fields include:
|
||||
|
||||
```text
|
||||
link_id
|
||||
token_len
|
||||
token_crc32
|
||||
sign_key_crc32
|
||||
enc_key_crc32
|
||||
link_obj
|
||||
```
|
||||
|
||||
The key CRCs are not keys. They are non-secret fingerprints that let the logs answer questions such as:
|
||||
|
||||
```text
|
||||
Did this packet use the same Link object?
|
||||
Did this node try to decrypt with the same negotiated key material?
|
||||
Did the ciphertext bytes change between sender and receiver?
|
||||
```
|
||||
|
||||
This is the crucial distinction. Reticulum can only report that an encrypted token failed authentication. It cannot, by itself, know whether the bytes were damaged in the transport or whether the wrong key/session was selected. The added fingerprints separate those cases.
|
||||
|
||||
## BLE fragmentation instrumentation
|
||||
|
||||
The BLE adapter was changed to use an explicit debug-friendly fragment header:
|
||||
|
||||
```text
|
||||
byte 0 fragment type
|
||||
byte 1 fragment header version
|
||||
bytes 2-3 fragment index
|
||||
bytes 4-5 total fragments
|
||||
bytes 6-9 message id
|
||||
bytes 10-13 full message length
|
||||
```
|
||||
|
||||
The fragment header size is 14 bytes.
|
||||
|
||||
Each transmitted and received fragment can now be logged with:
|
||||
|
||||
```text
|
||||
RNSBLE event=fragment_tx ...
|
||||
RNSBLE event=fragment_rx ...
|
||||
```
|
||||
|
||||
Important fields include:
|
||||
|
||||
```text
|
||||
frag_index
|
||||
frag_total
|
||||
msg_id
|
||||
msg_off
|
||||
msg_total
|
||||
frag_len
|
||||
payload_len
|
||||
frag_crc32
|
||||
```
|
||||
|
||||
The receiver now validates:
|
||||
|
||||
```text
|
||||
fragment header version
|
||||
fragment type
|
||||
fragment count
|
||||
message id
|
||||
exact expected message length
|
||||
payload length versus remaining message bytes
|
||||
stale/mismatched message id
|
||||
mid-message restart
|
||||
assembled packet length
|
||||
```
|
||||
|
||||
When reassembly succeeds, it prints:
|
||||
|
||||
```text
|
||||
RNSBLE event=packet_assembled assembled_len=... assembled_crc32=...
|
||||
```
|
||||
|
||||
When it fails, it prints `RNSERR` with a specific class such as:
|
||||
|
||||
```text
|
||||
rx_fragment_too_short
|
||||
rx_fragment_header_bad
|
||||
rx_reassembly_gap
|
||||
rx_reassembly_restart
|
||||
rx_reassembly_stale_fragment
|
||||
rx_reassembly_len_mismatch
|
||||
```
|
||||
|
||||
## Queue and buffer ownership changes
|
||||
|
||||
The BLE callback boundary was audited so incoming packets are copied into owned storage before being handed upward. The adapter avoids passing a borrowed pointer or view into a BLE callback buffer that may be reused after the callback returns.
|
||||
|
||||
The RX queue now tracks:
|
||||
|
||||
```text
|
||||
depth
|
||||
max_depth
|
||||
pushes
|
||||
pops
|
||||
drops
|
||||
overwrites
|
||||
allocation failures
|
||||
high-water mark
|
||||
```
|
||||
|
||||
It emits `RNSQUEUE` once per second and immediately on errors. Silent overwrite is treated as unacceptable during this test; a queue overflow increments a counter and emits `RNSERR`.
|
||||
|
||||
In the successful run after the fix, queue pressure was not the cause. Representative live stats showed no drops, no overwrites, and no allocation failures:
|
||||
|
||||
```text
|
||||
RNSQUEUE ... pushes=153 pops=153 drops=0 overwrites=0 alloc_failures=0 highwater=1
|
||||
RNSQUEUE ... pushes=302 pops=302 drops=0 overwrites=0 alloc_failures=0 highwater=3
|
||||
```
|
||||
|
||||
## Memory and stack instrumentation
|
||||
|
||||
`RNSMEM` prints once per second and immediately after important failures:
|
||||
|
||||
```text
|
||||
free_heap
|
||||
min_heap
|
||||
largest_block
|
||||
free_psram
|
||||
min_psram
|
||||
task
|
||||
stack_hw_words
|
||||
stack_hw_bytes
|
||||
```
|
||||
|
||||
The "thin stack margin" finding was about per-task stack, not total heap. On FreeRTOS, each task has its own stack. A task can run out of stack while the chip still has plenty of free heap.
|
||||
|
||||
The risky task was the ESP32 BLE/Bluetooth callback task, commonly shown as `BTC_TASK`. Earlier debug output showed this task with a very small stack high-water margin, while overall heap remained healthy. That means the board was not simply out of RAM. Instead, the code running inside a BLE callback had too much local stack pressure for that task.
|
||||
|
||||
One debug helper originally used a local formatting buffer:
|
||||
|
||||
```text
|
||||
char buffer[512]
|
||||
```
|
||||
|
||||
That was acceptable on a large stack, but risky inside `BTC_TASK`. During the live session, that local buffer contributed to a stack canary panic. The fix was to move the formatting buffer to static storage and guard serial writes with a FreeRTOS mutex:
|
||||
|
||||
```text
|
||||
static char debug_log_buffer[512]
|
||||
```
|
||||
|
||||
This reduced callback stack pressure while preserving parseable logs.
|
||||
|
||||
Configurability note:
|
||||
|
||||
```text
|
||||
The Arduino loop task stack is relatively easy to tune.
|
||||
The BLE/Bluetooth host callback task stack is controlled deeper in the ESP32/Arduino/Bluetooth stack.
|
||||
```
|
||||
|
||||
So this was not an immediate hardware RAM ceiling. It was a task-stack budget issue caused by where the code was running. The practical fix was to keep callback-local stack use small and move heavier formatting/storage out of that stack.
|
||||
|
||||
## The decisive finding: MTU was not payload size
|
||||
|
||||
The root transport bug was an MTU accounting error.
|
||||
|
||||
The code previously treated BLE MTU `185` as if `185` bytes could be written as the characteristic value. That is not correct. BLE ATT notifications/writes have protocol overhead. The usable characteristic value payload is:
|
||||
|
||||
```text
|
||||
ATT value size = ATT MTU - 3
|
||||
```
|
||||
|
||||
For this exercise:
|
||||
|
||||
```text
|
||||
BLE_ATT_MTU = 185
|
||||
BLE_VALUE_SIZE = 185 - 3 = 182
|
||||
FRAG_HEADER = 14
|
||||
BLE payload = 182 - 14 = 168
|
||||
```
|
||||
|
||||
Before the fix, the sender attempted to send 185-byte characteristic values. The receiver saw only 182 bytes. That means the first fragment of a multi-fragment Reticulum token lost exactly 3 bytes.
|
||||
|
||||
The live debug logs made this visible. A received fragment showed:
|
||||
|
||||
```text
|
||||
RNSBLE event=fragment_rx ... msg_total=184 frag_len=182 payload_len=168
|
||||
RNSERR class=rx_reassembly_len_mismatch ... assembled_181 expected_184
|
||||
```
|
||||
|
||||
For a larger encrypted Reticulum packet, the same accounting error showed up as token shrinkage. A packet that should have been 384 bytes arrived as 378 bytes after two fragments lost 3 bytes each.
|
||||
|
||||
That observation moved the failure out of Reticulum and into BLE fragmentation:
|
||||
|
||||
```text
|
||||
Reticulum token bytes were being truncated before decrypt.
|
||||
The HMAC failures were a correct symptom of damaged ciphertext.
|
||||
```
|
||||
|
||||
Once the BLE value size was corrected to `MTU - 3`, the receiver's token CRCs matched the sender's token CRCs before decrypt, and the HMAC failures stopped for the bilateral Constitution transfer.
|
||||
|
||||
## Why HMAC failures were expected
|
||||
|
||||
Reticulum Link packets are authenticated. If even one byte of the encrypted token is missing, shifted, or changed, the Link decrypt step must reject it.
|
||||
|
||||
That is what HMAC is for. The HMAC failure did not mean the cryptographic layer was broken. It meant the cryptographic layer detected that the bytes it received were not the bytes that were originally encrypted.
|
||||
|
||||
The new instrumentation makes the distinction concrete:
|
||||
|
||||
```text
|
||||
Same sender token_crc32 and receiver token_crc32, but HMAC_INVALID:
|
||||
suspect wrong key, wrong Link object, or wrong session context.
|
||||
|
||||
Different sender token_crc32 and receiver token_crc32:
|
||||
suspect BLE fragmentation, reassembly, queue, or buffer corruption.
|
||||
```
|
||||
|
||||
In this case the BLE fragment logs and reassembly length mismatch proved that the bytes were truncated before Reticulum saw them.
|
||||
|
||||
## Result after the fix
|
||||
|
||||
After correcting the BLE value size and moving the debug log buffer off the BLE task stack, both boards completed the Pi-Zero-profile US Constitution transfer in both directions.
|
||||
|
||||
Representative successful receiver output:
|
||||
|
||||
```text
|
||||
RX FILE END: from=Node-A0935A43CA48 file=US_Constitution.txt received=44225/44225 chunks=148/148 crc=4C7F2C7A status=OK
|
||||
RX FILE END: from=Node-DC935A43CA48 file=US_Constitution.txt received=44225/44225 chunks=148/148 crc=4C7F2C7A status=OK
|
||||
```
|
||||
|
||||
The live packet fingerprints also showed matching encrypted token CRCs at the sender and receiver for packet after packet. For example, encrypted token CRCs such as:
|
||||
|
||||
```text
|
||||
C3D5244B
|
||||
15472EDA
|
||||
998AFC8C
|
||||
39B9D34D
|
||||
```
|
||||
|
||||
appeared on both sides before decrypt, followed by valid `RX FILE DATA` lines instead of HMAC failures.
|
||||
|
||||
That result is important because it validates the whole stack under bilateral pressure:
|
||||
|
||||
```text
|
||||
same firmware image on both boards
|
||||
deterministic BLE client/server tie-breaker
|
||||
BLE transport
|
||||
microReticulum Link establishment
|
||||
encrypted Link packet send/receive
|
||||
application file protocol
|
||||
full file CRC verification
|
||||
repeat transfer loop
|
||||
```
|
||||
|
||||
## Limitations discovered
|
||||
|
||||
### BLE MTU is not the usable application payload
|
||||
|
||||
The configured ATT MTU is not the number of application bytes that can be placed in a characteristic value. The BLE ATT layer consumes 3 bytes. For MTU 185, the characteristic value size is 182 bytes.
|
||||
|
||||
This matters any time the firmware does its own fragmentation. The fragmenter must budget against the characteristic value size, not the MTU.
|
||||
|
||||
### Fragment headers reduce available payload further
|
||||
|
||||
Exercise 305 now uses a 14-byte fragment header. With MTU 185:
|
||||
|
||||
```text
|
||||
185 ATT MTU
|
||||
- 3 ATT overhead
|
||||
- 14 Exercise 305 fragment header
|
||||
= 168 encrypted Reticulum packet bytes per BLE fragment
|
||||
```
|
||||
|
||||
The application file chunk size is separate from this. A 300-byte file chunk becomes a larger encrypted Reticulum token, and that token is then split into 168-byte BLE payload fragments.
|
||||
|
||||
### Stack pressure is role/task dependent
|
||||
|
||||
The BLE callback path can run on a smaller Bluetooth task stack than ordinary application code. Heavy local variables, formatting buffers, or deep call paths inside that callback can trigger stack canary failures even when global heap looks fine.
|
||||
|
||||
The practical rule for this firmware is:
|
||||
|
||||
```text
|
||||
Keep BLE callback code small.
|
||||
Copy incoming bytes into owned storage at the callback boundary.
|
||||
Do not allocate large local buffers on the BLE callback stack.
|
||||
Move verbose formatting out of callback-local stack where possible.
|
||||
```
|
||||
|
||||
### Logging itself can change the system
|
||||
|
||||
The debug logging was necessary, but the first version added stack pressure. That is an important lesson: instrumentation on a microcontroller is not free. It can expose a bug, hide a bug, or create a new one if it uses too much stack, heap, serial bandwidth, or callback time.
|
||||
|
||||
The current instrumentation tries to stay lightweight by:
|
||||
|
||||
```text
|
||||
printing fingerprints instead of payloads
|
||||
rate-limiting normal packet logs
|
||||
printing every error
|
||||
using a static log buffer
|
||||
tracking queue counters instead of dumping queue contents
|
||||
```
|
||||
|
||||
### Serial output can interleave
|
||||
|
||||
Exercise 305 debug output uses a mutexed logger, but some microReticulum fork output still uses direct `Serial.printf` calls. Under simultaneous BLE, Reticulum, and application output, a few serial lines can interleave or appear visually truncated.
|
||||
|
||||
That is a logging cleanliness issue, not evidence of transport corruption by itself. For a fully machine-parseable long-duration capture, the remaining direct `Serial.printf` calls in the fork should eventually route through the same debug logger or another shared serial lock.
|
||||
|
||||
## Why live access mattered
|
||||
|
||||
The fastest part of this investigation was being able to upload firmware to both T-Beams and monitor both serial consoles immediately:
|
||||
|
||||
```bash
|
||||
pio device monitor -p /dev/ttytBOB -b 115200
|
||||
pio device monitor -p /dev/ttytDAN -b 115200
|
||||
```
|
||||
|
||||
That made the debug loop short:
|
||||
|
||||
```text
|
||||
patch instrumentation
|
||||
build
|
||||
upload both boards
|
||||
watch both roles at once
|
||||
compare sender and receiver CRCs
|
||||
adjust instrumentation
|
||||
repeat
|
||||
```
|
||||
|
||||
Without live dual-console access, the key clue would have been much harder to isolate. The logs had to be compared across both boards at the exact layer boundary where Reticulum handed encrypted bytes to BLE and where BLE handed reassembled bytes back to Reticulum.
|
||||
|
||||
## Final diagnosis
|
||||
|
||||
The primary failure was not a bad Reticulum Link negotiation and not a board-specific hardware fault.
|
||||
|
||||
The main failure was:
|
||||
|
||||
```text
|
||||
BLE fragmentation used the ATT MTU as the characteristic value size.
|
||||
The actual characteristic value size was ATT MTU minus 3.
|
||||
This truncated BLE fragments.
|
||||
Truncated encrypted Reticulum tokens correctly failed HMAC validation.
|
||||
```
|
||||
|
||||
A secondary instrumentation problem was:
|
||||
|
||||
```text
|
||||
The first debug logger used too much BLE callback task stack.
|
||||
Moving the log buffer to static storage removed that stack pressure.
|
||||
```
|
||||
|
||||
After both fixes, the bilateral Exercise 305 US Constitution transfer completed successfully in both directions with full byte count, full chunk count, and matching file checksum.
|
||||
|
||||
## Practical takeaways
|
||||
|
||||
For future ESP32 BLE Reticulum work:
|
||||
|
||||
```text
|
||||
Budget BLE fragments against ATT MTU - 3, not ATT MTU.
|
||||
Keep callback stack usage small.
|
||||
Deep-copy BLE callback bytes before passing them upward.
|
||||
Treat HMAC failures as a symptom, not a root cause.
|
||||
Use packet CRC fingerprints at every layer boundary.
|
||||
Track queues explicitly; never silently overwrite during transport tests.
|
||||
Use full-file verification so a successful run is unambiguous.
|
||||
```
|
||||
|
||||
Exercise 305 is now a useful stress test because it proves more than "a packet got through." It proves that both devices can run the same firmware, negotiate roles, establish a microReticulum Link over BLE, transmit encrypted packets simultaneously, reassemble those packets correctly, and verify a 44 KB file in both directions.
|
||||
187
exercises/305_microReticulum_ble_file_transfer/README.md
Normal file
187
exercises/305_microReticulum_ble_file_transfer/README.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Exercise 305: microReticulum BLE file transfer
|
||||
|
||||
This exercise builds on Exercise 304's equal-peer BLE transport. Both boards run the same dual-role BLE interface, form a Reticulum Link, and then send the selected text file across that Link at the same time.
|
||||
|
||||
The file transfer protocol is intentionally small and visible on the serial console:
|
||||
|
||||
```text
|
||||
FTB -> file begin, with file name, byte count, chunk count, and checksum
|
||||
FTD -> numbered file data chunk
|
||||
FTE -> file end, with verification metadata repeated
|
||||
```
|
||||
|
||||
Each receiver checks byte count, chunk count, and FNV-1a checksum. After a sender completes a transfer, it rests for 10 seconds and then starts the same selected file again.
|
||||
|
||||
## Sample Set
|
||||
|
||||
Exercise 305 uses the same payload files as the Pi Zero BLE Reticulum tests:
|
||||
|
||||
```text
|
||||
texts/If.txt 195 bytes
|
||||
texts/If_full.txt 1583 bytes
|
||||
texts/US_Constitution.txt 44225 bytes
|
||||
```
|
||||
|
||||
The selected file is compiled into the firmware. The transfer code does not care which file is selected; `platformio.ini` chooses the source text through `custom_text_source`, and `scripts/embed_text.py` generates `SelectedText.h` in the build directory before compilation.
|
||||
|
||||
## Transfer Profiles
|
||||
|
||||
The transfer pressure is selected in `platformio.ini` with build flags:
|
||||
|
||||
```ini
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=32
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=500
|
||||
```
|
||||
|
||||
`FILE_TRANSFER_CHUNK_SIZE` is the number of text bytes placed in each application-level `FTD` message before microReticulum wraps and encrypts it as a Link packet. Larger chunks reduce the number of packets needed for a file, but each encrypted packet becomes larger. If it grows beyond what the ESP32 BLE transport can reliably carry under simultaneous two-way traffic, Reticulum may log Link decrypt/HMAC failures because ciphertext arrived damaged or incomplete.
|
||||
|
||||
`FILE_TRANSFER_CHUNK_INTERVAL_MS` is the delay between application-level file chunks. Smaller intervals increase throughput, but also increase BLE write/notify pressure. With both nodes transmitting at the same time, too short a cadence can overflow buffers or expose ordering/loss issues in the current ESP32 BLE transport.
|
||||
|
||||
The conservative bring-up profile uses:
|
||||
|
||||
```text
|
||||
32 byte chunks, 500 ms between chunks
|
||||
```
|
||||
|
||||
The Pi-Zero-comparison profile uses:
|
||||
|
||||
```text
|
||||
300 byte chunks, 100 ms between chunks
|
||||
```
|
||||
|
||||
That is the apples-to-apples starting point for the previous Zero-to-Zero tests. Those commands requested `--message-chunk-size 900`, but the Python sender intentionally applied an internal board/Link-budget cap before sending. The run17 report for the Constitution transfer shows effective chunk data around 300 to 316 bytes, not 900 bytes, with roughly 100 ms sender pacing.
|
||||
|
||||
`VERIFY_FAIL` means the file protocol received an incomplete or corrupted transfer. A Reticulum Link HMAC/decryption error means corruption happened earlier, before the file protocol could parse the packet.
|
||||
|
||||
## Priority
|
||||
|
||||
See Exercise 304_microReticulum_ble_dual_role_ping_pong README.md for explanation of "deterministic tie-breaker" of the role of client and server based on the ESP32 MAC.
|
||||
|
||||
## Environments
|
||||
|
||||
Conservative ESP32 bring-up environments:
|
||||
|
||||
```text
|
||||
tbeam_if
|
||||
tbeam_if_full
|
||||
tbeam_constitution
|
||||
```
|
||||
|
||||
Pi-Zero-comparison environments:
|
||||
|
||||
```text
|
||||
tbeam_if_pi_zero_profile
|
||||
tbeam_if_full_pi_zero_profile
|
||||
tbeam_constitution_pi_zero_profile
|
||||
```
|
||||
|
||||
## Build Once, Upload Twice
|
||||
|
||||
Each selected text environment produces one firmware image. Build it once, then upload that same image to both boards.
|
||||
|
||||
Build the short If sample:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_if
|
||||
```
|
||||
|
||||
Build the Pi-Zero-profile Constitution sample:
|
||||
|
||||
```bash
|
||||
source /home/jlpoole/rnsenv/bin/activate
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_constitution_pi_zero_profile
|
||||
```
|
||||
|
||||
After the build succeeds, upload the same environment to both boards. These commands may be run one after the other:
|
||||
|
||||
```bash
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_if -t upload --upload-port /dev/ttytAMY
|
||||
pio run -d exercises/305_microReticulum_ble_file_transfer -e tbeam_if -t upload --upload-port /dev/ttytBOB
|
||||
```
|
||||
|
||||
Use the same `-e` value in upload commands that you used for the build.
|
||||
|
||||
For strict parallel uploads, use `esptool.py` directly against the already-built artifacts. This avoids two concurrent `pio run` processes touching the same `.pio` build directory:
|
||||
|
||||
```bash
|
||||
cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/305_microReticulum_ble_file_transfer
|
||||
esptool.py --chip esp32s3 --port /dev/ttytAMY --baud 460800 write_flash -z \
|
||||
0x0000 .pio/build/tbeam_if/bootloader.bin \
|
||||
0x8000 .pio/build/tbeam_if/partitions.bin \
|
||||
0xe000 /home/jlpoole/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||
0x10000 .pio/build/tbeam_if/firmware.bin &
|
||||
esptool.py --chip esp32s3 --port /dev/ttytBOB --baud 460800 write_flash -z \
|
||||
0x0000 .pio/build/tbeam_if/bootloader.bin \
|
||||
0x8000 .pio/build/tbeam_if/partitions.bin \
|
||||
0xe000 /home/jlpoole/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
||||
0x10000 .pio/build/tbeam_if/firmware.bin &
|
||||
wait
|
||||
```
|
||||
|
||||
For another environment, replace each `.pio/build/tbeam_if/` path with that environment's build directory.
|
||||
|
||||
Monitor:
|
||||
|
||||
```bash
|
||||
pio device monitor -p /dev/ttytAMY -b 115200
|
||||
pio device monitor -p /dev/ttytBOB -b 115200
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
Once the Link is active, both nodes start sending:
|
||||
|
||||
```text
|
||||
Selected file=If.txt bytes=195 chunk=32 interval_ms=500 repeat_rest_ms=10000
|
||||
TX FILE BEGIN: round=1 file=If.txt bytes=195 chunks=7 crc=...
|
||||
TX FILE DATA: round=1 seq=1/7 bytes=32 preview="If you can keep your head..."
|
||||
TX FILE END: round=1 file=If.txt bytes=195 chunks=7 crc=... next_round_in_ms=10000
|
||||
```
|
||||
|
||||
The receiver verifies the transfer:
|
||||
|
||||
```text
|
||||
RX FILE BEGIN: from=Node-... file=If.txt bytes=195 chunks=7 crc=...
|
||||
RX FILE DATA: from=Node-... seq=1/7 bytes=32 preview="If you can keep your head..."
|
||||
RX FILE END: from=Node-... file=If.txt received=195/195 chunks=7/7 crc=... status=OK
|
||||
```
|
||||
|
||||
Ten seconds after `TX FILE END`, the same selected file starts again. This rest interval is measured after transfer completion, so large files get the same 10-second pause before the next round.
|
||||
|
||||
## Debug Lines
|
||||
|
||||
Exercise 305 includes machine-parseable debug records for the role-dependent BLE/Link failure investigation. Each record is one line of `key=value` fields.
|
||||
|
||||
```text
|
||||
RNSLINK Link/announce events, peer hashes, Link ids, and Link object ids.
|
||||
RNSTX Application plaintext sends and encrypted Reticulum packets handed to BLE.
|
||||
RNSRX Reassembled BLE packets immediately before Reticulum receives them.
|
||||
RNSDEC Link encrypt/decrypt attempts and failures from microReticulum Link.cpp.
|
||||
RNSBLE BLE connect, identity, fragment TX/RX, and packet assembly events.
|
||||
RNSQUEUE BLE RX queue depth, pushes, pops, drops, and high-water marks.
|
||||
RNSMEM Heap, largest block, PSRAM, and current task stack high-water mark.
|
||||
RNSERR Classified adapter errors: reassembly gaps, short fragments, queue overflow, allocation failure.
|
||||
```
|
||||
|
||||
Normal packet logging is rate-limited: first 25 packets, then every 25th packet, then all packets for two seconds after the first failure. Define `RNS_DEBUG_VERBOSE=1` in `platformio.ini` to print every packet fingerprint.
|
||||
|
||||
Debug hooks are inserted at these points:
|
||||
|
||||
```text
|
||||
src/TBeamSupremeBleInterface.cpp BLE callback boundary, fragment TX/RX, reassembly, queue, packet handoff.
|
||||
src/main.cpp board-name mapping, announce/link events, application Link send.
|
||||
/usr/local/src/microreticulum/microReticulum/src/Link.cpp
|
||||
Link encrypt/decrypt token fingerprints and classified decrypt failures.
|
||||
```
|
||||
|
||||
Use the CRC fields to split the failure:
|
||||
|
||||
```text
|
||||
Same RNSTX token crc and RNSRX/RNSDEC token crc, but HMAC_INVALID -> likely wrong key/session/Link context.
|
||||
Different RNSTX token crc and RNSRX/RNSDEC token crc -> BLE fragmentation, reassembly, queue, or buffer corruption.
|
||||
RNSERR rx_reassembly_gap/timeout or queue_overflow -> mechanical adapter failure before Reticulum decrypt.
|
||||
RNSMEM largest_block or min_heap collapse before failures -> heap pressure or fragmentation.
|
||||
```
|
||||
109
exercises/305_microReticulum_ble_file_transfer/platformio.ini
Normal file
109
exercises/305_microReticulum_ble_file_transfer/platformio.ini
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
; Exercise 305: microReticulum BLE file transfer
|
||||
|
||||
[platformio]
|
||||
default_envs = tbeam_if
|
||||
|
||||
[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/embed_text.py
|
||||
custom_text_source = texts/If.txt
|
||||
|
||||
build_flags =
|
||||
-Wall
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-D RNS_USE_FS
|
||||
-D RNS_PERSIST_PATHS
|
||||
-D USTORE_USE_UNIVERSALFS
|
||||
-D MSGPACK_USE_BOOST=OFF
|
||||
-D MCU_ESP32
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
https://github.com/attermann/Crypto.git
|
||||
https://github.com/attermann/microStore.git
|
||||
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
|
||||
|
||||
[env:tbeam_if]
|
||||
extends = env
|
||||
custom_text_source = texts/If.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=32
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=500
|
||||
|
||||
[env:tbeam_if_full]
|
||||
extends = env
|
||||
custom_text_source = texts/If_full.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=32
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=500
|
||||
|
||||
[env:tbeam_constitution]
|
||||
extends = env
|
||||
custom_text_source = texts/US_Constitution.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=32
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=500
|
||||
|
||||
[env:tbeam_if_pi_zero_profile]
|
||||
extends = env
|
||||
custom_text_source = texts/If.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=300
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=100
|
||||
|
||||
[env:tbeam_if_full_pi_zero_profile]
|
||||
extends = env
|
||||
custom_text_source = texts/If_full.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=300
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=100
|
||||
|
||||
[env:tbeam_constitution_pi_zero_profile]
|
||||
extends = env
|
||||
custom_text_source = texts/US_Constitution.txt
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D FILE_TRANSFER_CHUNK_SIZE=300
|
||||
-D FILE_TRANSFER_CHUNK_INTERVAL_MS=100
|
||||
|
||||
[env:tbeam]
|
||||
extends = env
|
||||
|
||||
[env:amy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytAMY
|
||||
monitor_port = /dev/ttytAMY
|
||||
|
||||
[env:bob]
|
||||
extends = env
|
||||
upload_port = /dev/ttytBOB
|
||||
monitor_port = /dev/ttytBOB
|
||||
|
||||
[env:cy]
|
||||
extends = env
|
||||
upload_port = /dev/ttytCY
|
||||
monitor_port = /dev/ttytCY
|
||||
|
||||
[env:dan]
|
||||
extends = env
|
||||
upload_port = /dev/ttytDAN
|
||||
monitor_port = /dev/ttytDAN
|
||||
|
||||
[env:ed]
|
||||
extends = env
|
||||
upload_port = /dev/ttytED
|
||||
monitor_port = /dev/ttytED
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from pathlib import Path
|
||||
|
||||
Import("env")
|
||||
|
||||
project_dir = Path(env.subst("$PROJECT_DIR"))
|
||||
source_name = env.GetProjectOption("custom_text_source", "texts/If.txt")
|
||||
source_path = Path(source_name)
|
||||
if not source_path.is_absolute():
|
||||
source_path = project_dir / source_path
|
||||
|
||||
if not source_path.exists():
|
||||
raise RuntimeError(f"custom_text_source file not found: {source_path}")
|
||||
|
||||
data = source_path.read_bytes()
|
||||
generated_dir = Path(env.subst("$BUILD_DIR")) / "generated"
|
||||
generated_dir.mkdir(parents=True, exist_ok=True)
|
||||
header_path = generated_dir / "SelectedText.h"
|
||||
symbol_name = source_path.name.replace("\\", "/")
|
||||
|
||||
lines = [
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <Arduino.h>",
|
||||
"#include <pgmspace.h>",
|
||||
"",
|
||||
f'static constexpr const char* SELECTED_TEXT_NAME = "{symbol_name}";',
|
||||
f"static constexpr size_t SELECTED_TEXT_SIZE = {len(data)};",
|
||||
"static const uint8_t SELECTED_TEXT[] PROGMEM = {",
|
||||
]
|
||||
|
||||
for offset in range(0, len(data), 16):
|
||||
chunk = data[offset : offset + 16]
|
||||
values = ", ".join(f"0x{byte:02x}" for byte in chunk)
|
||||
lines.append(f" {values},")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"};",
|
||||
"",
|
||||
"static inline uint8_t read_selected_text_byte(size_t offset) {",
|
||||
" return pgm_read_byte(SELECTED_TEXT + offset);",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
header_path.write_text("\n".join(lines))
|
||||
env.Append(CPPPATH=[str(generated_dir)])
|
||||
print(f"Embedded {source_path} ({len(data)} bytes) -> {header_path}")
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
#include "DebugStats.h"
|
||||
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
#include <esp_heap_caps.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#endif
|
||||
|
||||
static String debug_board = "unknown";
|
||||
static const char* debug_role = "unknown";
|
||||
static uint32_t debug_failure_window_until = 0;
|
||||
static uint32_t debug_next_mem_ms = 0;
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
static SemaphoreHandle_t debug_serial_mutex = nullptr;
|
||||
static char debug_log_buffer[512];
|
||||
|
||||
static SemaphoreHandle_t serial_mutex() {
|
||||
if (!debug_serial_mutex) {
|
||||
debug_serial_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
return debug_serial_mutex;
|
||||
}
|
||||
#endif
|
||||
|
||||
void DebugStats::set_board(const String& board) {
|
||||
debug_board = board.length() ? board : String("unknown");
|
||||
}
|
||||
|
||||
void DebugStats::set_role(const char* role) {
|
||||
debug_role = role ? role : "unknown";
|
||||
}
|
||||
|
||||
const char* DebugStats::board() {
|
||||
return debug_board.c_str();
|
||||
}
|
||||
|
||||
const char* DebugStats::role() {
|
||||
return debug_role;
|
||||
}
|
||||
|
||||
uint32_t DebugStats::crc32(const uint8_t* data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320UL & (0UL - (crc & 1UL)));
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
String DebugStats::edge_hex(const uint8_t* data, size_t len, bool first) {
|
||||
char out[9];
|
||||
out[0] = '\0';
|
||||
if (!data || len == 0) {
|
||||
return String("none");
|
||||
}
|
||||
size_t edge_len = len < 4 ? len : 4;
|
||||
size_t start = first ? 0 : len - edge_len;
|
||||
char* cursor = out;
|
||||
for (size_t i = 0; i < edge_len; ++i) {
|
||||
snprintf(cursor, 3, "%02X", data[start + i]);
|
||||
cursor += 2;
|
||||
}
|
||||
*cursor = '\0';
|
||||
return String(out);
|
||||
}
|
||||
|
||||
uint32_t DebugStats::bytes_crc32(const RNS::Bytes& bytes) {
|
||||
return crc32(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
String DebugStats::bytes_first4(const RNS::Bytes& bytes) {
|
||||
return edge_hex(bytes.data(), bytes.size(), true);
|
||||
}
|
||||
|
||||
String DebugStats::bytes_last4(const RNS::Bytes& bytes) {
|
||||
return edge_hex(bytes.data(), bytes.size(), false);
|
||||
}
|
||||
|
||||
bool DebugStats::should_log_packet(uint32_t seq) {
|
||||
#if RNS_DEBUG_VERBOSE
|
||||
(void)seq;
|
||||
return true;
|
||||
#else
|
||||
uint32_t now = millis();
|
||||
return seq <= 25 || (seq % 25) == 0 || (int32_t)(debug_failure_window_until - now) > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugStats::note_failure() {
|
||||
debug_failure_window_until = millis() + 2000;
|
||||
}
|
||||
|
||||
void DebugStats::vlogf(const char* format, va_list args) {
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
SemaphoreHandle_t mutex = serial_mutex();
|
||||
if (mutex) {
|
||||
xSemaphoreTake(mutex, pdMS_TO_TICKS(50));
|
||||
}
|
||||
vsnprintf(debug_log_buffer, sizeof(debug_log_buffer), format, args);
|
||||
Serial.print(debug_log_buffer);
|
||||
if (mutex) {
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
#else
|
||||
(void)format;
|
||||
(void)args;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugStats::logf(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vlogf(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void DebugStats::println(const char* line) {
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
SemaphoreHandle_t mutex = serial_mutex();
|
||||
if (mutex) {
|
||||
xSemaphoreTake(mutex, pdMS_TO_TICKS(50));
|
||||
}
|
||||
Serial.println(line ? line : "");
|
||||
if (mutex) {
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
#else
|
||||
(void)line;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugStats::print_mem(const char* event) {
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
UBaseType_t stack_words = uxTaskGetStackHighWaterMark(NULL);
|
||||
logf("RNSMEM ms=%lu board=%s role=%s event=%s task=%s free_heap=%u min_heap=%u largest_block=%u free_psram=%u min_psram=%u stack_hw_words=%u stack_hw_bytes=%u\r\n",
|
||||
(unsigned long)millis(),
|
||||
board(),
|
||||
role(),
|
||||
event ? event : "periodic",
|
||||
pcTaskGetName(NULL),
|
||||
(unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT),
|
||||
(unsigned)heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT),
|
||||
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT),
|
||||
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
|
||||
(unsigned)heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM),
|
||||
(unsigned)stack_words,
|
||||
(unsigned)(stack_words * sizeof(StackType_t)));
|
||||
#else
|
||||
(void)event;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugStats::maybe_print_periodic() {
|
||||
#if RNS_DEBUG_INSTRUMENTATION
|
||||
uint32_t now = millis();
|
||||
if (debug_next_mem_ms == 0 || (int32_t)(now - debug_next_mem_ms) >= 0) {
|
||||
debug_next_mem_ms = now + 1000;
|
||||
print_mem("periodic");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" const char* rns_debug_board_name() {
|
||||
return DebugStats::board();
|
||||
}
|
||||
|
||||
extern "C" const char* rns_debug_role_name() {
|
||||
return DebugStats::role();
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Bytes.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#ifndef RNS_DEBUG_INSTRUMENTATION
|
||||
#define RNS_DEBUG_INSTRUMENTATION 1
|
||||
#endif
|
||||
|
||||
#ifndef RNS_DEBUG_VERBOSE
|
||||
#define RNS_DEBUG_VERBOSE 0
|
||||
#endif
|
||||
|
||||
class DebugStats {
|
||||
public:
|
||||
static void set_board(const String& board);
|
||||
static void set_role(const char* role);
|
||||
static const char* board();
|
||||
static const char* role();
|
||||
|
||||
static uint32_t crc32(const uint8_t* data, size_t len);
|
||||
static String edge_hex(const uint8_t* data, size_t len, bool first);
|
||||
static uint32_t bytes_crc32(const RNS::Bytes& bytes);
|
||||
static String bytes_first4(const RNS::Bytes& bytes);
|
||||
static String bytes_last4(const RNS::Bytes& bytes);
|
||||
|
||||
static bool should_log_packet(uint32_t seq);
|
||||
static void note_failure();
|
||||
|
||||
static void logf(const char* format, ...);
|
||||
static void vlogf(const char* format, va_list args);
|
||||
static void println(const char* line);
|
||||
static void print_mem(const char* event = "periodic");
|
||||
static void maybe_print_periodic();
|
||||
};
|
||||
|
|
@ -0,0 +1,780 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
#include "DebugStats.h"
|
||||
|
||||
#include <BLE2902.h>
|
||||
#include <BLEService.h>
|
||||
#include <Cryptography/Random.h>
|
||||
#include <Identity.h>
|
||||
#include <Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
|
||||
using namespace RNS;
|
||||
|
||||
static TBeamSupremeBleInterface* active_ble_interface = nullptr;
|
||||
|
||||
static void put_u16_be(uint8_t* data, uint16_t value) {
|
||||
data[0] = (uint8_t)((value >> 8) & 0xFF);
|
||||
data[1] = (uint8_t)(value & 0xFF);
|
||||
}
|
||||
|
||||
static void put_u32_be(uint8_t* data, uint32_t value) {
|
||||
data[0] = (uint8_t)((value >> 24) & 0xFF);
|
||||
data[1] = (uint8_t)((value >> 16) & 0xFF);
|
||||
data[2] = (uint8_t)((value >> 8) & 0xFF);
|
||||
data[3] = (uint8_t)(value & 0xFF);
|
||||
}
|
||||
|
||||
static uint16_t get_u16_be(const uint8_t* data) {
|
||||
return ((uint16_t)data[0] << 8) | data[1];
|
||||
}
|
||||
|
||||
static uint32_t get_u32_be(const uint8_t* data) {
|
||||
return ((uint32_t)data[0] << 24) |
|
||||
((uint32_t)data[1] << 16) |
|
||||
((uint32_t)data[2] << 8) |
|
||||
(uint32_t)data[3];
|
||||
}
|
||||
|
||||
class TBeamSupremeBleInterface::ServerCallbacks : public BLEServerCallbacks {
|
||||
public:
|
||||
explicit ServerCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onConnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
if (_owner->_client && _owner->_client->isConnected()) {
|
||||
Serial.println("BLE dual-role: inbound connection arrived while outbound is active; keeping outbound link");
|
||||
server->disconnect(server->getConnId());
|
||||
return;
|
||||
}
|
||||
_owner->_connected = true;
|
||||
_owner->_server_handshake_received = false;
|
||||
DebugStats::set_role("server");
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=connect conn=%u direction=inbound\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)server->getConnId());
|
||||
Serial.println("BLE dual-role: central connected to local server");
|
||||
INFO("BLE central connected to local server");
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer* server) override {
|
||||
(void)server;
|
||||
if (_owner->_client && _owner->_client->isConnected()) {
|
||||
_owner->_connected = true;
|
||||
DebugStats::set_role("client");
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=disconnect conn=%u direction=inbound action=ignored_outbound_active\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)server->getConnId());
|
||||
return;
|
||||
}
|
||||
_owner->_connected = false;
|
||||
_owner->_server_handshake_received = false;
|
||||
_owner->reset_reassembly();
|
||||
DebugStats::set_role("idle");
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=disconnect conn=%u direction=inbound\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)server->getConnId());
|
||||
Serial.println("BLE dual-role: central disconnected; restarting advertising");
|
||||
INFO("BLE central disconnected; restarting advertising");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::RxCallbacks : public BLECharacteristicCallbacks {
|
||||
public:
|
||||
explicit RxCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onWrite(BLECharacteristic* characteristic) override {
|
||||
std::string value = characteristic->getValue();
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_owner->_server_handshake_received && value.size() == 16) {
|
||||
_owner->_server_handshake_received = true;
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=identity_handshake_rx len=%u crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)value.size(),
|
||||
(unsigned long)DebugStats::crc32(reinterpret_cast<const uint8_t*>(value.data()), value.size()));
|
||||
Serial.println("BLE dual-role: identity handshake received by local server");
|
||||
INFOF("BLE identity handshake received: %d bytes", (int)value.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// BLECharacteristic owns/reuses its callback storage; handle_fragment copies
|
||||
// payload bytes into RNS::Bytes before data can outlive this callback.
|
||||
_owner->handle_fragment(reinterpret_cast<const uint8_t*>(value.data()), value.size());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
class TBeamSupremeBleInterface::AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
explicit AdvertisedDeviceCallbacks(TBeamSupremeBleInterface* owner) : _owner(owner) {}
|
||||
|
||||
void onResult(BLEAdvertisedDevice advertised_device) override {
|
||||
bool has_service = advertised_device.haveServiceUUID() &&
|
||||
advertised_device.isAdvertisingService(BLEUUID(TBeamSupremeBleInterface::SERVICE_UUID));
|
||||
bool has_rns_name = advertised_device.haveName() &&
|
||||
advertised_device.getName().rfind("RNS-", 0) == 0;
|
||||
|
||||
if ((has_service || has_rns_name) || _owner->_scan_report_count < 5) {
|
||||
Serial.printf("BLE scan: addr=%s name=%s service=%s rssi=%d\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
advertised_device.haveName() ? advertised_device.getName().c_str() : "",
|
||||
has_service ? "yes" : "no",
|
||||
advertised_device.getRSSI());
|
||||
_owner->_scan_report_count++;
|
||||
}
|
||||
|
||||
if (!has_service && !has_rns_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
String peer_label = _owner->advertised_node_label(advertised_device);
|
||||
String peer_address = String(advertised_device.getAddress().toString().c_str());
|
||||
if (!_owner->should_connect_to_peer(peer_address)) {
|
||||
Serial.printf("BLE dual-role: peer addr=%s label=%s has lower tie-breaker; staying available\r\n",
|
||||
peer_address.c_str(),
|
||||
peer_label.length() ? peer_label.c_str() : "(unknown)");
|
||||
return;
|
||||
}
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
delete _owner->_advertised_device;
|
||||
_owner->_advertised_device = new BLEAdvertisedDevice(advertised_device);
|
||||
_owner->_do_connect = true;
|
||||
_owner->_scanning = false;
|
||||
Serial.printf("BLE dual-role: peer candidate found: %s label=%s\r\n",
|
||||
advertised_device.getAddress().toString().c_str(),
|
||||
peer_label.c_str());
|
||||
INFOF("BLE peer found: %s", advertised_device.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
TBeamSupremeBleInterface* _owner;
|
||||
};
|
||||
|
||||
TBeamSupremeBleInterface::TBeamSupremeBleInterface(const String& node_label, const char* name) : InterfaceImpl(name) {
|
||||
_node_label = node_label;
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_bitrate = 1000000;
|
||||
_HW_MTU = BLE_PAYLOAD_SIZE;
|
||||
}
|
||||
|
||||
TBeamSupremeBleInterface::~TBeamSupremeBleInterface() {
|
||||
stop();
|
||||
delete _advertised_device;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::start() {
|
||||
if (_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
active_ble_interface = this;
|
||||
BLEDevice::init(std::string("RNS-") + _node_label.c_str());
|
||||
BLEDevice::setMTU(BLE_ATT_MTU);
|
||||
_local_address = String(BLEDevice::getAddress().toString().c_str());
|
||||
|
||||
Serial.printf("BLE dual-role: local addr=%s label=%s\r\n", _local_address.c_str(), _node_label.c_str());
|
||||
Serial.println("BLE dual-role: starting advertiser and scanner");
|
||||
INFO("BLE starting as dual-role client/server");
|
||||
start_peripheral();
|
||||
start_central_scan();
|
||||
|
||||
_started = true;
|
||||
_online = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::stop() {
|
||||
_online = false;
|
||||
_connected = false;
|
||||
if (_client && _client->isConnected()) {
|
||||
_client->disconnect();
|
||||
}
|
||||
DebugStats::set_role("idle");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::loop() {
|
||||
if (!_online) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_do_connect && _advertised_device) {
|
||||
_do_connect = false;
|
||||
if (!connect_to_advertised_device(_advertised_device)) {
|
||||
_connected = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connected && !_scanning && (int32_t)(millis() - _next_scan_ms) >= 0) {
|
||||
start_central_scan();
|
||||
}
|
||||
|
||||
if (_reassembly_started_ms != 0 && millis() - _reassembly_started_ms > REASSEMBLY_TIMEOUT_MS) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_timeout detail=msg_id_%lu received=%u expected=%u bytes=%u expected_bytes=%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)_received_fragments,
|
||||
(unsigned)_expected_total,
|
||||
(unsigned)_reassembly_buffer.size(),
|
||||
(unsigned long)_expected_message_len);
|
||||
DebugStats::print_mem("rx_reassembly_timeout");
|
||||
WARNING("BLE reassembly timeout; dropping partial packet");
|
||||
reset_reassembly();
|
||||
}
|
||||
|
||||
RNS::Bytes packet({RNS::Type::NONE});
|
||||
while (dequeue_packet(packet)) {
|
||||
_packet_rx_seq++;
|
||||
if (DebugStats::should_log_packet(_packet_rx_seq)) {
|
||||
DebugStats::logf("RNSRX ms=%lu board=%s role=%s seq=%lu link_id=unknown len=%u crc32=%08lX first4=%s last4=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_packet_rx_seq,
|
||||
(unsigned)packet.size(),
|
||||
(unsigned long)DebugStats::bytes_crc32(packet),
|
||||
DebugStats::bytes_first4(packet).c_str(),
|
||||
DebugStats::bytes_last4(packet).c_str());
|
||||
DebugStats::logf("RNSDEC ms=%lu board=%s role=%s event=attempt link_id=unknown token_len=%u token_crc32=%08lX link_obj=unknown\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)packet.size(),
|
||||
(unsigned long)DebugStats::bytes_crc32(packet));
|
||||
}
|
||||
InterfaceImpl::handle_incoming(packet);
|
||||
}
|
||||
print_queue_stats(false);
|
||||
DebugStats::maybe_print_periodic();
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_peripheral() {
|
||||
_server = BLEDevice::createServer();
|
||||
_server->setCallbacks(new ServerCallbacks(this));
|
||||
|
||||
BLEService* service = _server->createService(SERVICE_UUID);
|
||||
_tx_characteristic = service->createCharacteristic(
|
||||
TX_UUID,
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
_tx_characteristic->addDescriptor(new BLE2902());
|
||||
|
||||
BLECharacteristic* rx_characteristic = service->createCharacteristic(
|
||||
RX_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
|
||||
rx_characteristic->setCallbacks(new RxCallbacks(this));
|
||||
|
||||
_identity_characteristic = service->createCharacteristic(
|
||||
IDENTITY_UUID,
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_identity_characteristic->setValue((uint8_t*)identity.data(), identity.size());
|
||||
|
||||
service->start();
|
||||
|
||||
BLEAdvertising* advertising = BLEDevice::getAdvertising();
|
||||
advertising->addServiceUUID(SERVICE_UUID);
|
||||
advertising->setScanResponse(true);
|
||||
advertising->setMinPreferred(0x00);
|
||||
BLEDevice::startAdvertising();
|
||||
Serial.println("BLE dual-role: advertising Reticulum service");
|
||||
INFO("BLE advertising Reticulum service");
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::start_central_scan() {
|
||||
_scanning = true;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
_scan_report_count = 0;
|
||||
|
||||
BLEScan* scan = BLEDevice::getScan();
|
||||
scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks(this), true);
|
||||
scan->setInterval(1349);
|
||||
scan->setWindow(449);
|
||||
scan->setActiveScan(true);
|
||||
Serial.println("BLE dual-role: scanning for Reticulum service");
|
||||
scan->start(5, false);
|
||||
if (!_do_connect && !_connected) {
|
||||
_scanning = false;
|
||||
_next_scan_ms = millis() + SCAN_RETRY_MS;
|
||||
Serial.println("BLE dual-role: scan complete; no eligible peer found");
|
||||
}
|
||||
scan->clearResults();
|
||||
INFO("BLE scanning for Reticulum service");
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::connect_to_advertised_device(BLEAdvertisedDevice* device) {
|
||||
Serial.printf("BLE dual-role: connecting to %s\r\n", device->getAddress().toString().c_str());
|
||||
INFOF("BLE connecting to %s", device->getAddress().toString().c_str());
|
||||
|
||||
_client = BLEDevice::createClient();
|
||||
if (!_client->connect(device)) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_connect_failed detail=connect_failed\r\n",
|
||||
(unsigned long)millis(), DebugStats::board(), DebugStats::role());
|
||||
Serial.println("BLE dual-role: connect failed");
|
||||
WARNING("BLE connect failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_client->setMTU(BLE_ATT_MTU);
|
||||
BLERemoteService* service = _client->getService(BLEUUID(SERVICE_UUID));
|
||||
if (!service) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_service_missing detail=reticulum_service_not_found\r\n",
|
||||
(unsigned long)millis(), DebugStats::board(), DebugStats::role());
|
||||
Serial.println("BLE dual-role: Reticulum service not found after connect");
|
||||
WARNING("BLE Reticulum service not found after connect");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
_remote_rx_characteristic = service->getCharacteristic(BLEUUID(RX_UUID));
|
||||
_remote_tx_characteristic = service->getCharacteristic(BLEUUID(TX_UUID));
|
||||
BLERemoteCharacteristic* identity_characteristic = service->getCharacteristic(BLEUUID(IDENTITY_UUID));
|
||||
|
||||
if (!_remote_rx_characteristic || !_remote_tx_characteristic) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=ble_characteristic_missing detail=rx_or_tx_missing\r\n",
|
||||
(unsigned long)millis(), DebugStats::board(), DebugStats::role());
|
||||
Serial.println("BLE dual-role: RX/TX characteristics not found");
|
||||
WARNING("BLE Reticulum RX/TX characteristics not found");
|
||||
_client->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identity_characteristic && identity_characteristic->canRead()) {
|
||||
std::string peer_identity = identity_characteristic->readValue();
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=client event=identity_read len=%u crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
(unsigned)peer_identity.size(),
|
||||
(unsigned long)DebugStats::crc32(reinterpret_cast<const uint8_t*>(peer_identity.data()), peer_identity.size()));
|
||||
Serial.printf("BLE dual-role: peer identity read: %d bytes\r\n", (int)peer_identity.size());
|
||||
INFOF("BLE peer identity read: %d bytes", (int)peer_identity.size());
|
||||
}
|
||||
|
||||
if (_remote_tx_characteristic->canNotify()) {
|
||||
_remote_tx_characteristic->registerForNotify(client_notify_callback);
|
||||
} else {
|
||||
Serial.println("BLE dual-role: peer TX characteristic does not notify");
|
||||
}
|
||||
|
||||
RNS::Bytes identity = local_identity_hash();
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)identity.data(), identity.size(), true);
|
||||
_connected = true;
|
||||
_scanning = false;
|
||||
DebugStats::set_role("client");
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=connect conn=client direction=outbound peer=%s identity_crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
device->getAddress().toString().c_str(),
|
||||
(unsigned long)DebugStats::bytes_crc32(identity));
|
||||
Serial.println("BLE dual-role: connected and identity handshake sent");
|
||||
INFO("BLE connected and identity handshake sent");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::should_connect_to_peer(const String& peer_address) const {
|
||||
if (peer_address.length() == 0 || peer_address == _local_address) {
|
||||
return false;
|
||||
}
|
||||
return strcmp(_local_address.c_str(), peer_address.c_str()) < 0;
|
||||
}
|
||||
|
||||
String TBeamSupremeBleInterface::advertised_node_label(BLEAdvertisedDevice& device) const {
|
||||
if (!device.haveName()) {
|
||||
return "";
|
||||
}
|
||||
std::string name = device.getName();
|
||||
if (name.rfind("RNS-", 0) != 0) {
|
||||
return "";
|
||||
}
|
||||
return String(name.substr(4).c_str());
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_outgoing(const Bytes& data) {
|
||||
if (!_online || !_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
if (total == 0 || total > 65535) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=tx_fragment_count_invalid detail=packet_len_%u fragments_%u\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)data.size(),
|
||||
(unsigned)total);
|
||||
DebugStats::print_mem("tx_fragment_count_invalid");
|
||||
WARNINGF("BLE cannot fragment packet of size %d", (int)data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
_packet_tx_seq++;
|
||||
uint32_t msg_id = ++_tx_message_id;
|
||||
if (DebugStats::should_log_packet(_packet_tx_seq)) {
|
||||
DebugStats::logf("RNSTX ms=%lu board=%s role=%s seq=%lu link_id=unknown msg_id=%lu len=%u crc32=%08lX first4=%s last4=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_packet_tx_seq,
|
||||
(unsigned long)msg_id,
|
||||
(unsigned)data.size(),
|
||||
(unsigned long)DebugStats::bytes_crc32(data),
|
||||
DebugStats::bytes_first4(data).c_str(),
|
||||
DebugStats::bytes_last4(data).c_str());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
uint8_t fragment[BLE_VALUE_SIZE];
|
||||
uint8_t fragment_type = FRAG_CONTINUE;
|
||||
if (i == 0) {
|
||||
fragment_type = FRAG_START;
|
||||
} else if (i == total - 1) {
|
||||
fragment_type = FRAG_END;
|
||||
}
|
||||
|
||||
size_t offset = i * BLE_PAYLOAD_SIZE;
|
||||
size_t chunk = std::min(BLE_PAYLOAD_SIZE, data.size() - offset);
|
||||
fragment[0] = fragment_type;
|
||||
fragment[1] = FRAG_HEADER_VERSION;
|
||||
put_u16_be(fragment + 2, (uint16_t)i);
|
||||
put_u16_be(fragment + 4, (uint16_t)total);
|
||||
put_u32_be(fragment + 6, msg_id);
|
||||
put_u32_be(fragment + 10, (uint32_t)data.size());
|
||||
memcpy(fragment + FRAG_HEADER_SIZE, data.data() + offset, chunk);
|
||||
_ble_frag_tx_seq++;
|
||||
if (DebugStats::should_log_packet(_packet_tx_seq)) {
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=fragment_tx conn=%s frag_seq=%lu frag_index=%u frag_total=%u msg_id=%lu msg_off=%u msg_total=%u frag_len=%u payload_len=%u frag_crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
_client && _client->isConnected() ? "client" : "server",
|
||||
(unsigned long)_ble_frag_tx_seq,
|
||||
(unsigned)i,
|
||||
(unsigned)total,
|
||||
(unsigned long)msg_id,
|
||||
(unsigned)offset,
|
||||
(unsigned)data.size(),
|
||||
(unsigned)(FRAG_HEADER_SIZE + chunk),
|
||||
(unsigned)chunk,
|
||||
(unsigned long)DebugStats::crc32(fragment, FRAG_HEADER_SIZE + chunk));
|
||||
}
|
||||
send_fragment(fragment, FRAG_HEADER_SIZE + chunk);
|
||||
delay(8);
|
||||
}
|
||||
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::send_fragment(const uint8_t* data, size_t len) {
|
||||
if (_client && _client->isConnected() && _remote_rx_characteristic) {
|
||||
_remote_rx_characteristic->writeValue((uint8_t*)data, len, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tx_characteristic) {
|
||||
_tx_characteristic->setValue((uint8_t*)data, len);
|
||||
_tx_characteristic->notify();
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::handle_fragment(const uint8_t* data, size_t len) {
|
||||
if (len < FRAG_HEADER_SIZE) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_fragment_too_short detail=len_%u\r\n",
|
||||
(unsigned long)millis(), DebugStats::board(), DebugStats::role(), (unsigned)len);
|
||||
DebugStats::print_mem("rx_fragment_too_short");
|
||||
WARNINGF("BLE fragment too short: %d", (int)len);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fragment_type = data[0];
|
||||
uint8_t version = data[1];
|
||||
uint16_t sequence = get_u16_be(data + 2);
|
||||
uint16_t total = get_u16_be(data + 4);
|
||||
uint32_t msg_id = get_u32_be(data + 6);
|
||||
uint32_t msg_len = get_u32_be(data + 10);
|
||||
const uint8_t* payload = data + FRAG_HEADER_SIZE;
|
||||
size_t payload_len = len - FRAG_HEADER_SIZE;
|
||||
_ble_frag_rx_seq++;
|
||||
uint32_t expected_fragments = (msg_len + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
|
||||
size_t expected_offset = (size_t)sequence * BLE_PAYLOAD_SIZE;
|
||||
|
||||
if ((fragment_type != FRAG_START && fragment_type != FRAG_CONTINUE && fragment_type != FRAG_END) ||
|
||||
version != FRAG_HEADER_VERSION ||
|
||||
total == 0 || sequence >= total ||
|
||||
msg_id == 0 || msg_len == 0 ||
|
||||
expected_fragments == 0 || expected_fragments != total ||
|
||||
expected_offset >= msg_len || payload_len > (msg_len - expected_offset)) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_fragment_header_bad detail=type_%u_ver_%u_seq_%u_total_%u_msg_id_%lu_msg_len_%lu_len_%u_payload_%u_expected_frags_%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)fragment_type,
|
||||
(unsigned)version,
|
||||
(unsigned)sequence,
|
||||
(unsigned)total,
|
||||
(unsigned long)msg_id,
|
||||
(unsigned long)msg_len,
|
||||
(unsigned)len,
|
||||
(unsigned)payload_len,
|
||||
(unsigned long)expected_fragments);
|
||||
DebugStats::print_mem("rx_fragment_header_bad");
|
||||
WARNING("BLE invalid fragment header");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequence == 0) {
|
||||
if (_reassembly_started_ms != 0 && _received_fragments != _expected_total) {
|
||||
_reassembly_gaps++;
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_restart detail=old_msg_id_%lu_new_msg_id_%lu_received_%u_expected_%u bytes_%u gaps=%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned long)msg_id,
|
||||
(unsigned)_received_fragments,
|
||||
(unsigned)_expected_total,
|
||||
(unsigned)_reassembly_buffer.size(),
|
||||
(unsigned long)_reassembly_gaps);
|
||||
DebugStats::print_mem("rx_reassembly_restart");
|
||||
}
|
||||
reset_reassembly();
|
||||
_expected_total = total;
|
||||
_received_fragments = 0;
|
||||
_expected_message_len = msg_len;
|
||||
_reassembly_started_ms = millis();
|
||||
_current_rx_message_id = msg_id;
|
||||
if (msg_id > _rx_message_id) {
|
||||
_rx_message_id = msg_id;
|
||||
}
|
||||
} else if (_reassembly_started_ms == 0 || msg_id != _current_rx_message_id) {
|
||||
_reassembly_out_of_order++;
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_stale_fragment detail=msg_id_%lu_current_%lu_seq_%u_received_%u total_%u expected_total_%u stale=%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)msg_id,
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)sequence,
|
||||
(unsigned)_received_fragments,
|
||||
(unsigned)total,
|
||||
(unsigned)_expected_total,
|
||||
(unsigned long)_reassembly_out_of_order);
|
||||
DebugStats::print_mem("rx_reassembly_stale_fragment");
|
||||
return;
|
||||
}
|
||||
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=fragment_rx conn=%s frag_seq=%lu frag_index=%u frag_total=%u msg_id=%lu msg_off=%u msg_total=%lu frag_len=%u payload_len=%u frag_crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
_client && _client->isConnected() ? "client" : "server",
|
||||
(unsigned long)_ble_frag_rx_seq,
|
||||
(unsigned)sequence,
|
||||
(unsigned)total,
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)expected_offset,
|
||||
(unsigned long)msg_len,
|
||||
(unsigned)len,
|
||||
(unsigned)payload_len,
|
||||
(unsigned long)DebugStats::crc32(data, len));
|
||||
|
||||
if (_expected_total != total || _expected_message_len != msg_len || sequence != _received_fragments) {
|
||||
if (sequence < _received_fragments) {
|
||||
_reassembly_duplicates++;
|
||||
} else if (sequence > _received_fragments) {
|
||||
_reassembly_gaps++;
|
||||
} else {
|
||||
_reassembly_out_of_order++;
|
||||
}
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_gap detail=msg_id_%lu_seq_%u_expected_seq_%u_total_%u_expected_total_%u_msg_len_%lu_expected_len_%lu gaps=%lu duplicates=%lu out_of_order=%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)sequence,
|
||||
(unsigned)_received_fragments,
|
||||
(unsigned)total,
|
||||
(unsigned)_expected_total,
|
||||
(unsigned long)msg_len,
|
||||
(unsigned long)_expected_message_len,
|
||||
(unsigned long)_reassembly_gaps,
|
||||
(unsigned long)_reassembly_duplicates,
|
||||
(unsigned long)_reassembly_out_of_order);
|
||||
DebugStats::print_mem("rx_reassembly_gap");
|
||||
WARNING("BLE out-of-order fragment; dropping partial packet");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
|
||||
// Own the bytes from this point forward. The next Reticulum layer receives
|
||||
// a deep-copied RNS::Bytes packet, never a pointer into a BLE callback buffer.
|
||||
_reassembly_buffer.append(payload, payload_len);
|
||||
_received_fragments++;
|
||||
|
||||
if (_received_fragments == _expected_total) {
|
||||
if (_reassembly_buffer.size() != _expected_message_len) {
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=rx_reassembly_len_mismatch detail=msg_id_%lu assembled_%u expected_%lu fragments_%u\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)_reassembly_buffer.size(),
|
||||
(unsigned long)_expected_message_len,
|
||||
(unsigned)_received_fragments);
|
||||
DebugStats::print_mem("rx_reassembly_len_mismatch");
|
||||
reset_reassembly();
|
||||
return;
|
||||
}
|
||||
DebugStats::logf("RNSBLE ms=%lu board=%s role=%s event=packet_assembled msg_id=%lu assembled_len=%u assembled_crc32=%08lX fragments=%u\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_current_rx_message_id,
|
||||
(unsigned)_reassembly_buffer.size(),
|
||||
(unsigned long)DebugStats::bytes_crc32(_reassembly_buffer),
|
||||
(unsigned)_received_fragments);
|
||||
enqueue_packet(_reassembly_buffer);
|
||||
reset_reassembly();
|
||||
}
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::enqueue_packet(const RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.size() >= MAX_INCOMING_QUEUE_DEPTH) {
|
||||
_queue_drops++;
|
||||
size_t depth = _incoming_packets.size();
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=queue_overflow detail=ble_rx_depth_%u_drops_%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)depth,
|
||||
(unsigned long)_queue_drops);
|
||||
DebugStats::print_mem("queue_overflow");
|
||||
print_queue_stats(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_incoming_packets.push_back(packet);
|
||||
_queue_pushes++;
|
||||
if (_incoming_packets.size() > _queue_highwater) {
|
||||
_queue_highwater = _incoming_packets.size();
|
||||
}
|
||||
} catch (...) {
|
||||
_queue_alloc_failures++;
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
DebugStats::note_failure();
|
||||
DebugStats::logf("RNSERR ms=%lu board=%s role=%s class=queue_alloc_failure detail=ble_rx_push_failed failures=%lu\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)_queue_alloc_failures);
|
||||
DebugStats::print_mem("queue_alloc_failure");
|
||||
print_queue_stats(true);
|
||||
return;
|
||||
}
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
}
|
||||
|
||||
bool TBeamSupremeBleInterface::dequeue_packet(RNS::Bytes& packet) {
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
if (_incoming_packets.empty()) {
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return false;
|
||||
}
|
||||
packet = _incoming_packets.front();
|
||||
_incoming_packets.pop_front();
|
||||
_queue_pops++;
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::print_queue_stats(bool force) {
|
||||
uint32_t now = millis();
|
||||
if (!force && _next_queue_log_ms != 0 && (int32_t)(now - _next_queue_log_ms) < 0) {
|
||||
return;
|
||||
}
|
||||
_next_queue_log_ms = now + 1000;
|
||||
portENTER_CRITICAL(&_queue_mux);
|
||||
size_t depth = _incoming_packets.size();
|
||||
size_t highwater = _queue_highwater;
|
||||
uint32_t pushes = _queue_pushes;
|
||||
uint32_t pops = _queue_pops;
|
||||
uint32_t drops = _queue_drops;
|
||||
uint32_t overwrites = _queue_overwrites;
|
||||
uint32_t alloc_failures = _queue_alloc_failures;
|
||||
portEXIT_CRITICAL(&_queue_mux);
|
||||
DebugStats::logf("RNSQUEUE ms=%lu board=%s role=%s name=ble_rx depth=%u max_depth=%u pushes=%lu pops=%lu drops=%lu overwrites=%lu alloc_failures=%lu highwater=%u\r\n",
|
||||
(unsigned long)now,
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned)depth,
|
||||
(unsigned)MAX_INCOMING_QUEUE_DEPTH,
|
||||
(unsigned long)pushes,
|
||||
(unsigned long)pops,
|
||||
(unsigned long)drops,
|
||||
(unsigned long)overwrites,
|
||||
(unsigned long)alloc_failures,
|
||||
(unsigned)highwater);
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::reset_reassembly() {
|
||||
_reassembly_buffer.clear();
|
||||
_expected_total = 0;
|
||||
_received_fragments = 0;
|
||||
_expected_message_len = 0;
|
||||
_reassembly_started_ms = 0;
|
||||
}
|
||||
|
||||
RNS::Bytes TBeamSupremeBleInterface::local_identity_hash() const {
|
||||
String material = String("microReticulum BLE ") + _node_label;
|
||||
return Identity::truncated_hash(RNS::bytesFromString(material.c_str()));
|
||||
}
|
||||
|
||||
void TBeamSupremeBleInterface::client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify) {
|
||||
(void)characteristic;
|
||||
(void)is_notify;
|
||||
if (active_ble_interface) {
|
||||
// NimBLE/Arduino BLE owns this callback buffer. handle_fragment copies it
|
||||
// synchronously into owned reassembly storage before returning.
|
||||
active_ble_interface->handle_fragment(data, length);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
#pragma once
|
||||
|
||||
#include <Bytes.h>
|
||||
#include <Interface.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEClient.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLERemoteCharacteristic.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
class TBeamSupremeBleInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
explicit TBeamSupremeBleInterface(const String& node_label,
|
||||
const char* name = "TBeamSupremeBLE");
|
||||
~TBeamSupremeBleInterface() override;
|
||||
|
||||
bool start() override;
|
||||
void stop() override;
|
||||
void loop() override;
|
||||
|
||||
bool connected() const { return _connected; }
|
||||
const char* role_name() const { return "dual-role"; }
|
||||
|
||||
private:
|
||||
void send_outgoing(const RNS::Bytes& data) override;
|
||||
|
||||
void start_peripheral();
|
||||
void start_central_scan();
|
||||
bool connect_to_advertised_device(BLEAdvertisedDevice* device);
|
||||
bool should_connect_to_peer(const String& peer_address) const;
|
||||
String advertised_node_label(BLEAdvertisedDevice& device) const;
|
||||
|
||||
void send_fragment(const uint8_t* data, size_t len);
|
||||
void handle_fragment(const uint8_t* data, size_t len);
|
||||
void enqueue_packet(const RNS::Bytes& packet);
|
||||
bool dequeue_packet(RNS::Bytes& packet);
|
||||
void reset_reassembly();
|
||||
void print_queue_stats(bool force);
|
||||
RNS::Bytes local_identity_hash() const;
|
||||
|
||||
static void client_notify_callback(BLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool is_notify);
|
||||
|
||||
class ServerCallbacks;
|
||||
class RxCallbacks;
|
||||
class AdvertisedDeviceCallbacks;
|
||||
|
||||
static constexpr const char* SERVICE_UUID = "37145b00-442d-4a94-917f-8f42c5da28e3";
|
||||
static constexpr const char* TX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e4";
|
||||
static constexpr const char* RX_UUID = "37145b00-442d-4a94-917f-8f42c5da28e5";
|
||||
static constexpr const char* IDENTITY_UUID = "37145b00-442d-4a94-917f-8f42c5da28e6";
|
||||
|
||||
static constexpr uint8_t FRAG_START = 0x01;
|
||||
static constexpr uint8_t FRAG_CONTINUE = 0x02;
|
||||
static constexpr uint8_t FRAG_END = 0x03;
|
||||
static constexpr uint8_t FRAG_HEADER_VERSION = 0x02;
|
||||
static constexpr size_t FRAG_HEADER_SIZE = 14;
|
||||
static constexpr size_t BLE_ATT_MTU = 185;
|
||||
static constexpr size_t BLE_VALUE_SIZE = BLE_ATT_MTU - 3;
|
||||
static constexpr size_t BLE_PAYLOAD_SIZE = BLE_VALUE_SIZE - FRAG_HEADER_SIZE;
|
||||
static constexpr uint32_t REASSEMBLY_TIMEOUT_MS = 30000;
|
||||
static constexpr uint32_t SCAN_RETRY_MS = 5000;
|
||||
static constexpr size_t MAX_INCOMING_QUEUE_DEPTH = 32;
|
||||
|
||||
String _node_label;
|
||||
String _local_address;
|
||||
bool _started = false;
|
||||
bool _connected = false;
|
||||
bool _do_connect = false;
|
||||
bool _scanning = false;
|
||||
bool _server_handshake_received = false;
|
||||
uint8_t _scan_report_count = 0;
|
||||
uint32_t _next_scan_ms = 0;
|
||||
|
||||
BLEServer* _server = nullptr;
|
||||
BLECharacteristic* _tx_characteristic = nullptr;
|
||||
BLECharacteristic* _identity_characteristic = nullptr;
|
||||
BLEAdvertisedDevice* _advertised_device = nullptr;
|
||||
BLEClient* _client = nullptr;
|
||||
BLERemoteCharacteristic* _remote_rx_characteristic = nullptr;
|
||||
BLERemoteCharacteristic* _remote_tx_characteristic = nullptr;
|
||||
|
||||
RNS::Bytes _reassembly_buffer;
|
||||
uint16_t _expected_total = 0;
|
||||
uint16_t _received_fragments = 0;
|
||||
uint32_t _expected_message_len = 0;
|
||||
uint32_t _reassembly_started_ms = 0;
|
||||
uint32_t _rx_message_id = 0;
|
||||
uint32_t _current_rx_message_id = 0;
|
||||
uint32_t _tx_message_id = 0;
|
||||
uint32_t _ble_frag_tx_seq = 0;
|
||||
uint32_t _ble_frag_rx_seq = 0;
|
||||
uint32_t _packet_tx_seq = 0;
|
||||
uint32_t _packet_rx_seq = 0;
|
||||
uint32_t _reassembly_gaps = 0;
|
||||
uint32_t _reassembly_duplicates = 0;
|
||||
uint32_t _reassembly_out_of_order = 0;
|
||||
uint32_t _queue_pushes = 0;
|
||||
uint32_t _queue_pops = 0;
|
||||
uint32_t _queue_drops = 0;
|
||||
uint32_t _queue_overwrites = 0;
|
||||
uint32_t _queue_alloc_failures = 0;
|
||||
size_t _queue_highwater = 0;
|
||||
uint32_t _next_queue_log_ms = 0;
|
||||
|
||||
std::deque<RNS::Bytes> _incoming_packets;
|
||||
portMUX_TYPE _queue_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
};
|
||||
602
exercises/305_microReticulum_ble_file_transfer/src/main.cpp
Normal file
602
exercises/305_microReticulum_ble_file_transfer/src/main.cpp
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
#include "TBeamSupremeBleInterface.h"
|
||||
#include "DebugStats.h"
|
||||
#include "SelectedText.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Destination.h>
|
||||
#include <Identity.h>
|
||||
#include <Link.h>
|
||||
#include <Log.h>
|
||||
#include <Packet.h>
|
||||
#include <Reticulum.h>
|
||||
#include <Transport.h>
|
||||
#include <Type.h>
|
||||
#include <Utilities/OS.h>
|
||||
#include <microStore/Adapters/UniversalFileSystem.h>
|
||||
#include <microStore/FileSystem.h>
|
||||
|
||||
static constexpr const char* APP_NAME = "microreticulum";
|
||||
static constexpr const char* APP_ASPECT = "filetransfer";
|
||||
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.filetransfer";
|
||||
|
||||
#ifndef FILE_TRANSFER_CHUNK_SIZE
|
||||
#define FILE_TRANSFER_CHUNK_SIZE 32
|
||||
#endif
|
||||
|
||||
#ifndef FILE_TRANSFER_CHUNK_INTERVAL_MS
|
||||
#define FILE_TRANSFER_CHUNK_INTERVAL_MS 500
|
||||
#endif
|
||||
|
||||
#ifndef FILE_TRANSFER_REPEAT_INTERVAL_MS
|
||||
#define FILE_TRANSFER_REPEAT_INTERVAL_MS 10000
|
||||
#endif
|
||||
|
||||
static constexpr size_t TRANSFER_CHUNK_SIZE = FILE_TRANSFER_CHUNK_SIZE;
|
||||
static constexpr uint32_t TRANSFER_CHUNK_INTERVAL_MS = FILE_TRANSFER_CHUNK_INTERVAL_MS;
|
||||
static constexpr uint32_t TRANSFER_REPEAT_INTERVAL_MS = FILE_TRANSFER_REPEAT_INTERVAL_MS;
|
||||
static constexpr uint32_t FNV1A_OFFSET = 2166136261UL;
|
||||
static constexpr uint32_t FNV1A_PRIME = 16777619UL;
|
||||
|
||||
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
||||
static RNS::Interface ble_interface({RNS::Type::NONE});
|
||||
static RNS::Identity local_identity({RNS::Type::NONE});
|
||||
static RNS::Destination inbound_destination({RNS::Type::NONE});
|
||||
static RNS::Destination peer_destination({RNS::Type::NONE});
|
||||
static RNS::Link active_link({RNS::Type::NONE});
|
||||
static RNS::Link pending_link({RNS::Type::NONE});
|
||||
static RNS::Bytes peer_hash;
|
||||
static String peer_label;
|
||||
static bool have_peer = false;
|
||||
static bool link_active = false;
|
||||
static bool link_attempted = false;
|
||||
static TBeamSupremeBleInterface* ble_impl = nullptr;
|
||||
static String node_label;
|
||||
|
||||
struct RxTransferState {
|
||||
bool active = false;
|
||||
String sender;
|
||||
String file_name;
|
||||
size_t expected_size = 0;
|
||||
uint32_t expected_chunks = 0;
|
||||
uint32_t expected_crc = 0;
|
||||
uint32_t received_chunks = 0;
|
||||
size_t received_size = 0;
|
||||
uint32_t crc = FNV1A_OFFSET;
|
||||
};
|
||||
|
||||
static RxTransferState rx_transfer;
|
||||
|
||||
struct TxTransferState {
|
||||
bool active = false;
|
||||
bool complete = false;
|
||||
size_t offset = 0;
|
||||
uint32_t sequence = 0;
|
||||
uint32_t total_chunks = 0;
|
||||
uint32_t crc = 0;
|
||||
uint32_t next_start_ms = 0;
|
||||
uint32_t round = 0;
|
||||
};
|
||||
|
||||
static TxTransferState tx_transfer;
|
||||
|
||||
static String default_node_label() {
|
||||
uint64_t mac = ESP.getEfuseMac();
|
||||
char label[24];
|
||||
snprintf(label, sizeof(label), "Node-%012llX", (unsigned long long)(mac & 0xFFFFFFFFFFFFULL));
|
||||
return String(label);
|
||||
}
|
||||
|
||||
static String board_name_from_node_label(const String& label) {
|
||||
if (label == "Node-68BF5B43CA48") return "AMY";
|
||||
if (label == "Node-DC935A43CA48") return "BOB";
|
||||
if (label == "Node-44915A43CA48") return "CY";
|
||||
if (label == "Node-A0935A43CA48") return "DAN";
|
||||
if (label == "Node-D0905A43CA48") return "ED";
|
||||
if (label == "Node-748C5B43CA48") return "FLO";
|
||||
if (label == "Node-E0405A43CA48") return "GUY";
|
||||
return label;
|
||||
}
|
||||
|
||||
static bool should_initiate_link_to(const String& label) {
|
||||
return strcmp(node_label.c_str(), label.c_str()) < 0;
|
||||
}
|
||||
|
||||
static uint32_t fnv1a_update(uint32_t crc, uint8_t byte) {
|
||||
crc ^= byte;
|
||||
crc *= FNV1A_PRIME;
|
||||
return crc;
|
||||
}
|
||||
|
||||
static uint32_t selected_text_crc() {
|
||||
uint32_t crc = FNV1A_OFFSET;
|
||||
for (size_t i = 0; i < SELECTED_TEXT_SIZE; ++i) {
|
||||
crc = fnv1a_update(crc, read_selected_text_byte(i));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static String hex32(uint32_t value) {
|
||||
char buffer[9];
|
||||
snprintf(buffer, sizeof(buffer), "%08lX", (unsigned long)value);
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
static String link_id_hex(const RNS::Link& link) {
|
||||
if (!link) {
|
||||
return String("none");
|
||||
}
|
||||
return String(link.link_id().toHex().c_str());
|
||||
}
|
||||
|
||||
static uint32_t bytes_crc32(const RNS::Bytes& bytes) {
|
||||
return DebugStats::bytes_crc32(bytes);
|
||||
}
|
||||
|
||||
static String preview_payload(const String& payload) {
|
||||
String preview;
|
||||
for (size_t i = 0; i < payload.length() && preview.length() < 56; ++i) {
|
||||
char c = payload.charAt(i);
|
||||
if (c == '\n') {
|
||||
preview += "\\n";
|
||||
} else if (c == '\r') {
|
||||
preview += "\\r";
|
||||
} else if ((uint8_t)c < 32) {
|
||||
preview += '?';
|
||||
} else {
|
||||
preview += c;
|
||||
}
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
static void handle_file_begin(const String& sender,
|
||||
const String& file_name,
|
||||
size_t size,
|
||||
uint32_t chunks,
|
||||
uint32_t crc) {
|
||||
rx_transfer.active = true;
|
||||
rx_transfer.sender = sender;
|
||||
rx_transfer.file_name = file_name;
|
||||
rx_transfer.expected_size = size;
|
||||
rx_transfer.expected_chunks = chunks;
|
||||
rx_transfer.expected_crc = crc;
|
||||
rx_transfer.received_chunks = 0;
|
||||
rx_transfer.received_size = 0;
|
||||
rx_transfer.crc = FNV1A_OFFSET;
|
||||
|
||||
Serial.printf("RX FILE BEGIN: from=%s file=%s bytes=%u chunks=%lu crc=%s\r\n",
|
||||
sender.c_str(),
|
||||
file_name.c_str(),
|
||||
(unsigned)size,
|
||||
(unsigned long)chunks,
|
||||
hex32(crc).c_str());
|
||||
}
|
||||
|
||||
static void handle_file_data(const String& sender,
|
||||
uint32_t sequence,
|
||||
uint32_t total,
|
||||
const String& payload) {
|
||||
if (!rx_transfer.active || rx_transfer.sender != sender) {
|
||||
Serial.printf("RX FILE DATA ignored: sender=%s seq=%lu no active transfer\r\n",
|
||||
sender.c_str(), (unsigned long)sequence);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < payload.length(); ++i) {
|
||||
rx_transfer.crc = fnv1a_update(rx_transfer.crc, (uint8_t)payload.charAt(i));
|
||||
}
|
||||
rx_transfer.received_chunks++;
|
||||
rx_transfer.received_size += payload.length();
|
||||
|
||||
Serial.printf("RX FILE DATA: from=%s seq=%lu/%lu bytes=%u preview=\"%s\"\r\n",
|
||||
sender.c_str(),
|
||||
(unsigned long)sequence,
|
||||
(unsigned long)total,
|
||||
(unsigned)payload.length(),
|
||||
preview_payload(payload).c_str());
|
||||
}
|
||||
|
||||
static void handle_file_end(const String& sender,
|
||||
const String& file_name,
|
||||
size_t size,
|
||||
uint32_t chunks,
|
||||
uint32_t crc) {
|
||||
bool ok = rx_transfer.active &&
|
||||
rx_transfer.sender == sender &&
|
||||
rx_transfer.file_name == file_name &&
|
||||
rx_transfer.expected_size == size &&
|
||||
rx_transfer.expected_chunks == chunks &&
|
||||
rx_transfer.expected_crc == crc &&
|
||||
rx_transfer.received_size == size &&
|
||||
rx_transfer.received_chunks == chunks &&
|
||||
rx_transfer.crc == crc;
|
||||
|
||||
Serial.printf("RX FILE END: from=%s file=%s received=%u/%u chunks=%lu/%lu crc=%s status=%s\r\n",
|
||||
sender.c_str(),
|
||||
file_name.c_str(),
|
||||
(unsigned)rx_transfer.received_size,
|
||||
(unsigned)size,
|
||||
(unsigned long)rx_transfer.received_chunks,
|
||||
(unsigned long)chunks,
|
||||
hex32(rx_transfer.crc).c_str(),
|
||||
ok ? "OK" : "VERIFY_FAIL");
|
||||
rx_transfer.active = false;
|
||||
}
|
||||
|
||||
static void parse_file_transfer_packet(const String& message) {
|
||||
int p1 = message.indexOf('|');
|
||||
int p2 = message.indexOf('|', p1 + 1);
|
||||
if (p1 < 0 || p2 < 0) {
|
||||
Serial.printf("RX LINK BLE: %s\r\n", message.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
String type = message.substring(0, p1);
|
||||
String sender = message.substring(p1 + 1, p2);
|
||||
|
||||
if (type == "FTB") {
|
||||
int p3 = message.indexOf('|', p2 + 1);
|
||||
int p4 = message.indexOf('|', p3 + 1);
|
||||
int p5 = message.indexOf('|', p4 + 1);
|
||||
if (p3 < 0 || p4 < 0 || p5 < 0) {
|
||||
Serial.printf("RX FILE malformed BEGIN: %s\r\n", message.c_str());
|
||||
return;
|
||||
}
|
||||
handle_file_begin(sender,
|
||||
message.substring(p2 + 1, p3),
|
||||
(size_t)message.substring(p3 + 1, p4).toInt(),
|
||||
(uint32_t)message.substring(p4 + 1, p5).toInt(),
|
||||
(uint32_t)strtoul(message.substring(p5 + 1).c_str(), nullptr, 16));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "FTD") {
|
||||
int p3 = message.indexOf('|', p2 + 1);
|
||||
int p4 = message.indexOf('|', p3 + 1);
|
||||
if (p3 < 0 || p4 < 0) {
|
||||
Serial.printf("RX FILE malformed DATA: %s\r\n", message.c_str());
|
||||
return;
|
||||
}
|
||||
handle_file_data(sender,
|
||||
(uint32_t)message.substring(p2 + 1, p3).toInt(),
|
||||
(uint32_t)message.substring(p3 + 1, p4).toInt(),
|
||||
message.substring(p4 + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "FTE") {
|
||||
int p3 = message.indexOf('|', p2 + 1);
|
||||
int p4 = message.indexOf('|', p3 + 1);
|
||||
int p5 = message.indexOf('|', p4 + 1);
|
||||
if (p3 < 0 || p4 < 0 || p5 < 0) {
|
||||
Serial.printf("RX FILE malformed END: %s\r\n", message.c_str());
|
||||
return;
|
||||
}
|
||||
handle_file_end(sender,
|
||||
message.substring(p2 + 1, p3),
|
||||
(size_t)message.substring(p3 + 1, p4).toInt(),
|
||||
(uint32_t)message.substring(p4 + 1, p5).toInt(),
|
||||
(uint32_t)strtoul(message.substring(p5 + 1).c_str(), nullptr, 16));
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("RX LINK BLE: %s\r\n", message.c_str());
|
||||
}
|
||||
|
||||
static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
|
||||
(void)packet;
|
||||
parse_file_transfer_packet(String(data.toString().c_str()));
|
||||
}
|
||||
|
||||
static void on_link_closed(RNS::Link& link) {
|
||||
Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_closed peer=%s link_id=%s link_obj=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
peer_label.length() ? peer_label.c_str() : "unknown",
|
||||
link_id_hex(link).c_str(),
|
||||
link.toString().c_str());
|
||||
Serial.println("LINK CLOSED");
|
||||
active_link = {RNS::Type::NONE};
|
||||
pending_link = {RNS::Type::NONE};
|
||||
link_active = false;
|
||||
link_attempted = false;
|
||||
}
|
||||
|
||||
static void on_outbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_established direction=outbound peer=%s link_id=%s link_hash_crc32=%08lX peer_dest_crc32=%08lX sign_key_crc32=unavailable enc_key_crc32=unavailable link_obj=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
peer_label.length() ? peer_label.c_str() : "unknown",
|
||||
link_id_hex(active_link).c_str(),
|
||||
(unsigned long)bytes_crc32(active_link.hash()),
|
||||
(unsigned long)bytes_crc32(peer_hash),
|
||||
active_link.toString().c_str());
|
||||
Serial.println("LINK ACTIVE: initiator link established");
|
||||
}
|
||||
|
||||
static void on_inbound_link_established(RNS::Link& link) {
|
||||
active_link = link;
|
||||
active_link.set_packet_callback(on_link_packet);
|
||||
active_link.set_link_closed_callback(on_link_closed);
|
||||
link_active = true;
|
||||
link_attempted = true;
|
||||
Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_established direction=inbound peer=%s link_id=%s link_hash_crc32=%08lX peer_dest_crc32=%08lX sign_key_crc32=unavailable enc_key_crc32=unavailable link_obj=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
peer_label.length() ? peer_label.c_str() : "unknown",
|
||||
link_id_hex(active_link).c_str(),
|
||||
(unsigned long)bytes_crc32(active_link.hash()),
|
||||
(unsigned long)bytes_crc32(peer_hash),
|
||||
active_link.toString().c_str());
|
||||
Serial.println("RX LINK: inbound link established");
|
||||
}
|
||||
|
||||
class FileAnnounceHandler : public RNS::AnnounceHandler {
|
||||
public:
|
||||
FileAnnounceHandler() : 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;
|
||||
}
|
||||
|
||||
peer_hash = destination_hash;
|
||||
peer_label = label;
|
||||
peer_destination = RNS::Destination(announced_identity,
|
||||
RNS::Type::Destination::OUT,
|
||||
RNS::Type::Destination::SINGLE,
|
||||
destination_hash);
|
||||
have_peer = true;
|
||||
|
||||
Serial.printf("RNSLINK ms=%lu board=%s role=%s event=announce_rx peer=%s peer_dest=%s peer_dest_crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
peer_label.c_str(),
|
||||
peer_hash.toHex().c_str(),
|
||||
(unsigned long)bytes_crc32(peer_hash));
|
||||
Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
|
||||
peer_label.c_str(), peer_hash.toHex().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
static RNS::HAnnounceHandler announce_handler(new FileAnnounceHandler());
|
||||
|
||||
static void print_config() {
|
||||
Serial.printf("Node=%s\r\n", node_label.c_str());
|
||||
Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
|
||||
ble_impl ? ble_impl->role_name() : "?");
|
||||
Serial.printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\r\n",
|
||||
SELECTED_TEXT_NAME,
|
||||
(unsigned)SELECTED_TEXT_SIZE,
|
||||
(unsigned)TRANSFER_CHUNK_SIZE,
|
||||
(unsigned long)TRANSFER_CHUNK_INTERVAL_MS,
|
||||
(unsigned long)TRANSFER_REPEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
static void send_announce() {
|
||||
if (!inbound_destination) {
|
||||
return;
|
||||
}
|
||||
Serial.printf("RNSLINK ms=%lu board=%s role=%s event=announce_tx peer=all local_dest=%s local_dest_crc32=%08lX\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
inbound_destination.hash().toHex().c_str(),
|
||||
(unsigned long)bytes_crc32(inbound_destination.hash()));
|
||||
Serial.printf("TX ANNOUNCE: %s\r\n", node_label.c_str());
|
||||
inbound_destination.announce(RNS::bytesFromString(node_label.c_str()));
|
||||
}
|
||||
|
||||
static void maybe_open_link() {
|
||||
if (!have_peer || link_active || link_attempted || !peer_destination) {
|
||||
return;
|
||||
}
|
||||
if (!should_initiate_link_to(peer_label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("TX LINKREQUEST: opening link to %s\r\n", peer_label.c_str());
|
||||
pending_link = RNS::Link(peer_destination);
|
||||
pending_link.set_packet_callback(on_link_packet);
|
||||
pending_link.set_link_established_callback(on_outbound_link_established);
|
||||
pending_link.set_link_closed_callback(on_link_closed);
|
||||
link_attempted = true;
|
||||
}
|
||||
|
||||
static void send_link_message(const String& message) {
|
||||
RNS::Bytes plaintext = RNS::bytesFromString(message.c_str());
|
||||
static uint32_t app_tx_seq = 0;
|
||||
app_tx_seq++;
|
||||
if (DebugStats::should_log_packet(app_tx_seq)) {
|
||||
Serial.printf("RNSTX ms=%lu board=%s role=%s seq=%lu stage=link_plain link_id=%s len=%u crc32=%08lX first4=%s last4=%s\r\n",
|
||||
(unsigned long)millis(),
|
||||
DebugStats::board(),
|
||||
DebugStats::role(),
|
||||
(unsigned long)app_tx_seq,
|
||||
link_id_hex(active_link).c_str(),
|
||||
(unsigned)plaintext.size(),
|
||||
(unsigned long)DebugStats::bytes_crc32(plaintext),
|
||||
DebugStats::bytes_first4(plaintext).c_str(),
|
||||
DebugStats::bytes_last4(plaintext).c_str());
|
||||
}
|
||||
RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
|
||||
}
|
||||
|
||||
static bool send_next_file_packet(uint32_t now) {
|
||||
if (tx_transfer.complete && (int32_t)(now - tx_transfer.next_start_ms) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tx_transfer.active) {
|
||||
tx_transfer.active = true;
|
||||
tx_transfer.complete = false;
|
||||
tx_transfer.offset = 0;
|
||||
tx_transfer.sequence = 0;
|
||||
tx_transfer.crc = selected_text_crc();
|
||||
tx_transfer.total_chunks = (SELECTED_TEXT_SIZE + TRANSFER_CHUNK_SIZE - 1) / TRANSFER_CHUNK_SIZE;
|
||||
tx_transfer.round++;
|
||||
|
||||
String begin = String("FTB|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
|
||||
String((unsigned)SELECTED_TEXT_SIZE) + "|" + String((unsigned long)tx_transfer.total_chunks) +
|
||||
"|" + hex32(tx_transfer.crc);
|
||||
Serial.printf("TX FILE BEGIN: round=%lu file=%s bytes=%u chunks=%lu crc=%s\r\n",
|
||||
(unsigned long)tx_transfer.round,
|
||||
SELECTED_TEXT_NAME,
|
||||
(unsigned)SELECTED_TEXT_SIZE,
|
||||
(unsigned long)tx_transfer.total_chunks,
|
||||
hex32(tx_transfer.crc).c_str());
|
||||
send_link_message(begin);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tx_transfer.offset < SELECTED_TEXT_SIZE) {
|
||||
size_t remaining = SELECTED_TEXT_SIZE - tx_transfer.offset;
|
||||
size_t count = remaining < TRANSFER_CHUNK_SIZE ? remaining : TRANSFER_CHUNK_SIZE;
|
||||
String payload;
|
||||
payload.reserve(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
payload += (char)read_selected_text_byte(tx_transfer.offset + i);
|
||||
}
|
||||
tx_transfer.sequence++;
|
||||
tx_transfer.offset += count;
|
||||
|
||||
String data = String("FTD|") + node_label + "|" + String((unsigned long)tx_transfer.sequence) +
|
||||
"|" + String((unsigned long)tx_transfer.total_chunks) + "|" + payload;
|
||||
Serial.printf("TX FILE DATA: round=%lu seq=%lu/%lu bytes=%u preview=\"%s\"\r\n",
|
||||
(unsigned long)tx_transfer.round,
|
||||
(unsigned long)tx_transfer.sequence,
|
||||
(unsigned long)tx_transfer.total_chunks,
|
||||
(unsigned)count,
|
||||
preview_payload(payload).c_str());
|
||||
send_link_message(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
String end = String("FTE|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
|
||||
String((unsigned)SELECTED_TEXT_SIZE) + "|" + String((unsigned long)tx_transfer.total_chunks) +
|
||||
"|" + hex32(tx_transfer.crc);
|
||||
Serial.printf("TX FILE END: round=%lu file=%s bytes=%u chunks=%lu crc=%s next_round_in_ms=%lu\r\n",
|
||||
(unsigned long)tx_transfer.round,
|
||||
SELECTED_TEXT_NAME,
|
||||
(unsigned)SELECTED_TEXT_SIZE,
|
||||
(unsigned long)tx_transfer.total_chunks,
|
||||
hex32(tx_transfer.crc).c_str(),
|
||||
(unsigned long)TRANSFER_REPEAT_INTERVAL_MS);
|
||||
send_link_message(end);
|
||||
tx_transfer.active = false;
|
||||
tx_transfer.complete = true;
|
||||
tx_transfer.next_start_ms = now + TRANSFER_REPEAT_INTERVAL_MS;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void setup_reticulum() {
|
||||
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
|
||||
filesystem.init();
|
||||
RNS::Utilities::OS::register_filesystem(filesystem);
|
||||
|
||||
ble_impl = new TBeamSupremeBleInterface(node_label);
|
||||
ble_interface = ble_impl;
|
||||
ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
RNS::Transport::register_interface(ble_interface);
|
||||
ble_interface.start();
|
||||
|
||||
reticulum = RNS::Reticulum();
|
||||
reticulum.transport_enabled(false);
|
||||
reticulum.probe_destination_enabled(false);
|
||||
reticulum.start();
|
||||
|
||||
local_identity = RNS::Identity();
|
||||
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 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();
|
||||
RNS::loglevel(RNS::LOG_NOTICE);
|
||||
node_label = default_node_label();
|
||||
DebugStats::set_board(board_name_from_node_label(node_label));
|
||||
DebugStats::set_role("boot");
|
||||
setup_reticulum();
|
||||
Serial.println("Exercise 305: microReticulum BLE file transfer");
|
||||
print_config();
|
||||
Serial.println("microReticulum ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
reticulum.loop();
|
||||
|
||||
static uint32_t next_announce_ms = 0;
|
||||
static uint32_t next_transfer_ms = 0;
|
||||
static uint32_t next_wait_log_ms = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (ble_impl && !ble_impl->connected()) {
|
||||
if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
|
||||
next_wait_log_ms = now + 10000;
|
||||
Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
|
||||
}
|
||||
delay(5);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_announce_ms == 0) {
|
||||
uint32_t offset = 700 + ((uint32_t)node_label.charAt(node_label.length() - 1) % 5) * 400;
|
||||
next_announce_ms = now + offset;
|
||||
}
|
||||
if (!link_active && (int32_t)(now - next_announce_ms) >= 0) {
|
||||
next_announce_ms = now + 15000;
|
||||
send_announce();
|
||||
}
|
||||
|
||||
maybe_open_link();
|
||||
|
||||
if (link_active && next_transfer_ms == 0) {
|
||||
next_transfer_ms = now + (should_initiate_link_to(peer_label) ? 900 : 1200);
|
||||
}
|
||||
|
||||
if (link_active && (int32_t)(now - next_transfer_ms) >= 0) {
|
||||
next_transfer_ms = now + TRANSFER_CHUNK_INTERVAL_MS;
|
||||
send_next_file_packet(now);
|
||||
}
|
||||
|
||||
delay(5);
|
||||
}
|
||||
|
||||
int _write(int file, char* ptr, int len) {
|
||||
(void)file;
|
||||
int wrote = Serial.write(ptr, len);
|
||||
Serial.flush();
|
||||
return wrote;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
If you can wait and not be tired by waiting,
|
||||
Or being lied about, don’t deal in lies,
|
||||
Or being hated, don’t give way to hating,
|
||||
And yet don’t look too good, nor talk too wise:
|
||||
|
||||
If you can dream—and not make dreams your master;
|
||||
If you can think—and not make thoughts your aim;
|
||||
If you can meet with Triumph and Disaster
|
||||
And treat those two impostors just the same;
|
||||
If you can bear to hear the truth you’ve spoken
|
||||
Twisted by knaves to make a trap for fools,
|
||||
Or watch the things you gave your life to, broken,
|
||||
And stoop and build ’em up with worn-out tools:
|
||||
|
||||
If you can make one heap of all your winnings
|
||||
And risk it on one turn of pitch-and-toss,
|
||||
And lose, and start again at your beginnings
|
||||
And never breathe a word about your loss;
|
||||
If you can force your heart and nerve and sinew
|
||||
To serve your turn long after they are gone,
|
||||
And so hold on when there is nothing in you
|
||||
Except the Will which says to them: ‘Hold on!’
|
||||
|
||||
If you can talk with crowds and keep your virtue,
|
||||
Or walk with Kings—nor lose the common touch,
|
||||
If neither foes nor loving friends can hurt you,
|
||||
If all men count with you, but none too much;
|
||||
If you can fill the unforgiving minute
|
||||
With sixty seconds’ worth of distance run,
|
||||
Yours is the Earth and everything that’s in it,
|
||||
And—which is more—you’ll be a Man, my son!
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
The United States Constitution
|
||||
|
||||
We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.
|
||||
The Constitutional Convention
|
||||
Article I
|
||||
Section 1: Congress
|
||||
|
||||
All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives.
|
||||
Section 2: The House of Representatives
|
||||
|
||||
The House of Representatives shall be composed of Members chosen every second Year by the People of the several States, and the Electors in each State shall have the Qualifications requisite for Electors of the most numerous Branch of the State Legislature.
|
||||
|
||||
No Person shall be a Representative who shall not have attained to the Age of twenty five Years, and been seven Years a Citizen of the United States, and who shall not, when elected, be an Inhabitant of that State in which he shall be chosen.
|
||||
|
||||
Representatives and direct Taxes shall be apportioned among the several States which may be included within this Union, according to their respective Numbers, which shall be determined by adding to the whole Number of free Persons, including those bound to Service for a Term of Years, and excluding Indians not taxed, three fifths of all other Persons. The actual Enumeration shall be made within three Years after the first Meeting of the Congress of the United States, and within every subsequent Term of ten Years, in such Manner as they shall by Law direct.The Number of Representatives shall not exceed one for every thirty Thousand, but each State shall have at Least one Representative; and until such enumeration shall be made, the State of New Hampshire shall be entitled to chuse three, Massachusetts eight, Rhode-Island and Providence Plantations one, Connecticut five, New-York six, New Jersey four, Pennsylvania eight, Delaware one, Maryland six, Virginia ten, North Carolina five, South Carolina five, and Georgia three.
|
||||
|
||||
When vacancies happen in the Representation from any State, the Executive Authority thereof shall issue Writs of Election to fill such Vacancies.
|
||||
|
||||
The House of Representatives shall chuse their Speaker and other Officers;and shall have the sole Power of Impeachment.
|
||||
Section 3: The Senate
|
||||
|
||||
The Senate of the United States shall be composed of two Senators from each State, chosen by the Legislature thereof, for six Years; and each Senator shall have one Vote.
|
||||
|
||||
Immediately after they shall be assembled in Consequence of the first Election, they shall be divided as equally as may be into three Classes. The Seats of the Senators of the first Class shall be vacated at the Expiration of the second Year, of the second Class at the Expiration of the fourth Year, and of the third Class at the Expiration of the sixth Year, so that one third may be chosen every second Year; and if Vacancies happen by Resignation, or otherwise, during the Recess of the Legislature of any State, the Executive thereof may make temporary Appointments until the next Meeting of the Legislature, which shall then fill such Vacancies.
|
||||
|
||||
No Person shall be a Senator who shall not have attained to the Age of thirty Years, and been nine Years a Citizen of the United States, and who shall not, when elected, be an Inhabitant of that State for which he shall be chosen.
|
||||
|
||||
The Vice President of the United States shall be President of the Senate, but shall have no Vote, unless they be equally divided.
|
||||
|
||||
The Senate shall chuse their other Officers, and also a President pro tempore, in the Absence of the Vice President, or when he shall exercise the Office of President of the United States.
|
||||
|
||||
The Senate shall have the sole Power to try all Impeachments. When sitting for that Purpose, they shall be on Oath or Affirmation. When the President of the United States is tried, the Chief Justice shall preside: And no Person shall be convicted without the Concurrence of two thirds of the Members present.
|
||||
|
||||
Judgment in Cases of Impeachment shall not extend further than to removal from Office, and disqualification to hold and enjoy any Office of honor, Trust or Profit under the United States: but the Party convicted shall nevertheless be liable and subject to Indictment, Trial, Judgment and Punishment, according to Law.
|
||||
Section 4: Elections
|
||||
|
||||
The Times, Places and Manner of holding Elections for Senators and Representatives, shall be prescribed in each State by the Legislature thereof; but the Congress may at any time by Law make or alter such Regulations, except as to the Places of chusing Senators.
|
||||
|
||||
The Congress shall assemble at least once in every Year, and such Meeting shall be on the first Monday in December, unless they shall by Law appoint a different Day.
|
||||
Section 5: Powers and Duties of Congress
|
||||
|
||||
Each House shall be the Judge of the Elections, Returns and Qualifications of its own Members,and a Majority of each shall constitute a Quorum to do Business; but a smaller Number may adjourn from day to day, and may be authorized to compel the Attendance of absent Members, in such Manner, and under such Penalties as each House may provide.
|
||||
|
||||
Each House may determine the Rules of its Proceedings, punish its Members for disorderly Behaviour, and, with the Concurrence of two thirds, expel a Member.
|
||||
|
||||
Each House shall keep a Journal of its Proceedings, and from time to time publish the same, excepting such Parts as may in their Judgment require Secrecy; and the Yeas and Nays of the Members of either House on any question shall, at the Desire of one fifth of those Present, be entered on the Journal.
|
||||
|
||||
Neither House, during the Session of Congress, shall, without the Consent of the other, adjourn for more than three days, nor to any other Place than that in which the two Houses shall be sitting.
|
||||
Section 6: Rights and Disabilities of Members
|
||||
|
||||
The Senators and Representatives shall receive a Compensation for their Services, to be ascertained by Law, and paid out of the Treasury of the United States.They shall in all Cases, except Treason, Felony and Breach of the Peace, be privileged from Arrest during their Attendance at the Session of their respective Houses, and in going to and returning from the same; and for any Speech or Debate in either House, they shall not be questioned in any other Place.
|
||||
|
||||
No Senator or Representative shall, during the Time for which he was elected, be appointed to any civil Office under the Authority of the United States, which shall have been created, or the Emoluments whereof shall have been encreased during such time; and no Person holding any Office under the United States, shall be a Member of either House during his Continuance in Office.
|
||||
Section 7: Legislative Process
|
||||
|
||||
All Bills for raising Revenue shall originate in the House of Representatives; but the Senate may propose or concur with Amendments as on other Bills.
|
||||
|
||||
Every Bill which shall have passed the House of Representatives and the Senate, shall, before it become a Law, be presented to the President of the United States; If he approve he shall sign it, but if not he shall return it, with his Objections to that House in which it shall have originated, who shall enter the Objections at large on their Journal, and proceed to reconsider it. If after such Reconsideration two thirds of that House shall agree to pass the Bill, it shall be sent, together with the Objections, to the other House, by which it shall likewise be reconsidered, and if approved by two thirds of that House, it shall become a Law. But in all such Cases the Votes of both Houses shall be determined by yeas and Nays, and the Names of the Persons voting for and against the Bill shall be entered on the Journal of each House respectively. If any Bill shall not be returned by the President within ten Days (Sundays excepted) after it shall have been presented to him, the Same shall be a Law, in like Manner as if he had signed it, unless the Congress by their Adjournment prevent its Return, in which Case it shall not be a Law.
|
||||
|
||||
Every Order, Resolution, or Vote to which the Concurrence of the Senate and House of Representatives may be necessary (except on a question of Adjournment) shall be presented to the President of the United States; and before the Same shall take Effect, shall be approved by him, or being disapproved by him, shall be repassed by two thirds of the Senate and House of Representatives, according to the Rules and Limitations prescribed in the Case of a Bill.
|
||||
Section 8: Powers of Congress
|
||||
|
||||
The Congress shall have Power To lay and collect Taxes, Duties, Imposts and Excises, to pay the Debts and provide for the common Defence and general Welfare of the United States; but all Duties, Imposts and Excises shall be uniform throughout the United States;
|
||||
|
||||
To borrow Money on the credit of the United States;
|
||||
|
||||
To regulate Commerce with foreign Nations, and among the several States, and with the Indian Tribes;
|
||||
|
||||
To establish a uniform Rule of Naturalization, and uniform Laws on the subject of Bankruptcies throughout the United States;
|
||||
|
||||
To coin Money, regulate the Value thereof, and of foreign Coin, and fix the Standard of Weights and Measures;
|
||||
|
||||
To provide for the Punishment of counterfeiting the Securities and current Coin of the United States;
|
||||
|
||||
To establish Post Offices and post Roads;
|
||||
|
||||
To promote the Progress of Science and useful Arts, by securing for limited Times to Authors and Inventors the exclusive Right to their respective Writings and Discoveries;
|
||||
|
||||
To constitute Tribunals inferior to the supreme Court;
|
||||
|
||||
To define and punish Piracies and Felonies committed on the high Seas, and Offences against the Law of Nations;
|
||||
|
||||
To declare War, grant Letters of Marque and Reprisal, and make Rules concerning Captures on Land and Water;
|
||||
|
||||
To raise and support Armies, but no Appropriation of Money to that Use shall be for a longer Term than two Years;
|
||||
|
||||
To provide and maintain a Navy;
|
||||
|
||||
To make Rules for the Government and Regulation of the land and naval Forces;
|
||||
|
||||
To provide for calling forth the Militia to execute the Laws of the Union, suppress Insurrections and repel Invasions;
|
||||
|
||||
To provide for organizing, arming, and disciplining, the Militia, and for governing such Part of them as may be employed in the Service of the United States, reserving to the States respectively, the Appointment of the Officers, and the Authority of training the Militia according to the discipline prescribed by Congress;
|
||||
|
||||
To exercise exclusive Legislation in all Cases whatsoever, over such District (not exceeding ten Miles square) as may, by Cession of particular States, and the Acceptance of Congress, become the Seat of the Government of the United States, and to exercise like Authority over all Places purchased by the Consent of the Legislature of the State in which the Same shall be, for the Erection of Forts, Magazines, Arsenals, dock-Yards and other needful Buildings;-And
|
||||
|
||||
To make all Laws which shall be necessary and proper for carrying into Execution the foregoing Powers, and all other Powers vested by this Constitution in the Government of the United States, or in any Department or Officer thereof.
|
||||
Section 9: Powers Denied Congress
|
||||
|
||||
The Migration or Importation of such Persons as any of the States now existing shall think proper to admit, shall not be prohibited by the Congress prior to the Year one thousand eight hundred and eight, but a Tax or duty may be imposed on such Importation, not exceeding ten dollars for each Person.
|
||||
|
||||
The Privilege of the Writ of Habeas Corpus shall not be suspended, unless when in Cases of Rebellion or Invasion the public Safety may require it.
|
||||
|
||||
No Bill of Attainder or ex post facto Law shall be passed.
|
||||
|
||||
No Capitation, or other direct, Tax shall be laid, unless in Proportion to the Census or enumeration herein before directed to be taken.
|
||||
|
||||
No Tax or Duty shall be laid on Articles exported from any State.
|
||||
|
||||
No Preference shall be given by any Regulation of Commerce or Revenue to the Ports of one State over those of another: nor shall Vessels bound to, or from, one State, be obliged to enter, clear, or pay Duties in another.
|
||||
|
||||
No Money shall be drawn from the Treasury, but in Consequence of Appropriations made by Law; and a regular Statement and Account of the Receipts and Expenditures of all public Money shall be published from time to time.
|
||||
|
||||
No Title of Nobility shall be granted by the United States: And no Person holding any Office of Profit or Trust under them, shall, without the Consent of the Congress, accept of any present, Emolument, Office, or Title, of any kind whatever, from any King, Prince, or foreign State.
|
||||
Section 10: Powers Denied to the States
|
||||
|
||||
No State shall enter into any Treaty, Alliance, or Confederation; grant Letters of Marque and Reprisal; coin Money; emit Bills of Credit; make any Thing but gold and silver Coin a Tender in Payment of Debts; pass any Bill of Attainder, ex post facto Law, or Law impairing the Obligation of Contracts, or grant any Title of Nobility.
|
||||
|
||||
No State shall, without the Consent of the Congress, lay any Imposts or Duties on Imports or Exports, except what may be absolutely necessary for executing it's inspection Laws: and the net Produce of all Duties and Imposts, laid by any State on Imports or Exports, shall be for the Use of the Treasury of the United States; and all such Laws shall be subject to the Revision and Controul of the Congress.
|
||||
|
||||
No State shall, without the Consent of Congress, lay any Duty of Tonnage, keep Troops, or Ships of War in time of Peace, enter into any Agreement or Compact with another State, or with a foreign Power, or engage in War, unless actually invaded, or in such imminent Danger as will not admit of delay.
|
||||
Article II
|
||||
Section 1
|
||||
|
||||
The executive Power shall be vested in a President of the United States of America.
|
||||
|
||||
He shall hold his Office during the Term of four Years, and, together with the Vice President, chosen for the same Term, be elected, as follows:
|
||||
|
||||
Each State shall appoint, in such Manner as the Legislature thereof may direct, a Number of Electors, equal to the whole Number of Senators and Representatives to which the State may be entitled in the Congress: but no Senator or Representative, or Person holding an Office of Trust or Profit under the United States, shall be appointed an Elector.
|
||||
|
||||
The Electors shall meet in their respective States, and vote by Ballot for two Persons, of whom one at least shall not be an Inhabitant of the same State with themselves. And they shall make a List of all the Persons voted for, and of the Number of Votes for each; which List they shall sign and certify, and transmit sealed to the Seat of the Government of the United States, directed to the President of the Senate. The President of the Senate shall, in the Presence of the Senate and House of Representatives, open all the Certificates, and the Votes shall then be counted. The Person having the greatest Number of Votes shall be the President, if such Number be a Majority of the whole Number of Electors appointed; and if there be more than one who have such Majority, and have an equal Number of Votes, then the House of Representatives shall immediately chuse by Ballot one of them for President; and if no Person have a Majority, then from the five highest on the List the said House shall in like Manner chuse the President. But in chusing the President, the Votes shall be taken by States, the Representation from each State having one Vote; A quorum for this Purpose shall consist of a Member or Members from two thirds of the States, and a Majority of all the States shall be necessary to a Choice. In every Case, after the Choice of the President, the Person having the greatest Number of Votes of the Electors shall be the Vice President. But if there should remain two or more who have equal Votes, the Senate shall chuse from them by Ballot the Vice President.
|
||||
|
||||
The Congress may determine the Time of chusing the Electors, and the Day on which they shall give their Votes; which Day shall be the same throughout the United States.
|
||||
|
||||
No Person except a natural born Citizen, or a Citizen of the United States, at the time of the Adoption of this Constitution, shall be eligible to the Office of President; neither shall any Person be eligible to that Office who shall not have attained to the Age of thirty five Years, and been fourteen Years a Resident within the United States.
|
||||
|
||||
In Case of the Removal of the President from Office, or of his Death, Resignation, or Inability to discharge the Powers and Duties of the said Office, the Same shall devolve on the Vice President, and the Congress may by Law provide for the Case of Removal, Death, Resignation or Inability, both of the President and Vice President, declaring what Officer shall then act as President, and such Officer shall act accordingly, until the Disability be removed, or a President shall be elected.
|
||||
|
||||
The President shall, at stated Times, receive for his Services, a Compensation, which shall neither be encreased nor diminished during the Period for which he shall have been elected, and he shall not receive within that Period any other Emolument from the United States, or any of them.
|
||||
|
||||
Before he enter on the Execution of his Office, he shall take the following Oath or Affirmation:--"I do solemnly swear (or affirm) that I will faithfully execute the Office of President of the United States, and will to the best of my Ability, preserve, protect and defend the Constitution of the United States."
|
||||
Section 2
|
||||
|
||||
The President shall be Commander in Chief of the Army and Navy of the United States, and of the Militia of the several States, when called into the actual Service of the United States; he may require the Opinion, in writing, of the principal Officer in each of the executive Departments, upon any Subject relating to the Duties of their respective Offices, and he shall have Power to grant Reprieves and Pardons for Offences against the United States, except in Cases of Impeachment.
|
||||
|
||||
He shall have Power, by and with the Advice and Consent of the Senate, to make Treaties, provided two thirds of the Senators present concur; and he shall nominate, and by and with the Advice and Consent of the Senate, shall appoint Ambassadors, other public Ministers and Consuls, Judges of the supreme Court, and all other Officers of the United States, whose Appointments are not herein otherwise provided for, and which shall be established by Law: but the Congress may by Law vest the Appointment of such inferior Officers, as they think proper, in the President alone, in the Courts of Law, or in the Heads of Departments.
|
||||
|
||||
The President shall have Power to fill up all Vacancies that may happen during the Recess of the Senate, by granting Commissions which shall expire at the End of their next Session.
|
||||
Section 3
|
||||
|
||||
He shall from time to time give to the Congress Information of the State of the Union, and recommend to their Consideration such Measures as he shall judge necessary and expedient; he may, on extraordinary Occasions, convene both Houses, or either of them, and in Case of Disagreement between them, with Respect to the Time of Adjournment, he may adjourn them to such Time as he shall think proper; he shall receive Ambassadors and other public Ministers; he shall take Care that the Laws be faithfully executed, and shall Commission all the Officers of the United States.
|
||||
Section 4
|
||||
|
||||
The President, Vice President and all civil Officers of the United States, shall be removed from Office on Impeachment for, and Conviction of, Treason, Bribery, or other high Crimes and Misdemeanors.
|
||||
Article III
|
||||
Section 1
|
||||
|
||||
The judicial Power of the United States, shall be vested in one supreme Court, and in such inferior Courts as the Congress may from time to time ordain and establish. The Judges, both of the supreme and inferior Courts, shall hold their Offices during good Behaviour, and shall, at stated Times, receive for their Services, a Compensation, which shall not be diminished during their Continuance in Office.
|
||||
Section 2
|
||||
|
||||
The judicial Power shall extend to all Cases, in Law and Equity, arising under this Constitution, the Laws of the United States, and Treaties made, or which shall be made, under their Authority;--to all Cases affecting Ambassadors, other public Ministers and Consuls;--to all Cases of admiralty and maritime Jurisdiction;--to Controversies to which the United States shall be a Party;--to Controversies between two or more States;--between a State and Citizens of another State;--between Citizens of different States;--between Citizens of the same State claiming Lands under Grants of different States, and between a State, or the Citizens thereof, and foreign States, Citizens or Subjects.
|
||||
|
||||
In all Cases affecting Ambassadors, other public Ministers and Consuls, and those in which a State shall be Party, the supreme Court shall have original Jurisdiction. In all the other Cases before mentioned, the supreme Court shall have appellate Jurisdiction, both as to Law and Fact, with such Exceptions, and under such Regulations as the Congress shall make.
|
||||
|
||||
The Trial of all Crimes, except in Cases of Impeachment; shall be by Jury; and such Trial shall be held in the State where the said Crimes shall have been committed; but when not committed within any State, the Trial shall be at such Place or Places as the Congress may by Law have directed.
|
||||
Section 3
|
||||
|
||||
Treason against the United States, shall consist only in levying War against them, or in adhering to their Enemies, giving them Aid and Comfort. No Person shall be convicted of Treason unless on the Testimony of two Witnesses to the same overt Act, or on Confession in open Court.
|
||||
|
||||
The Congress shall have Power to declare the Punishment of Treason, but no Attainder of Treason shall work Corruption of Blood, or Forfeiture except during the Life of the Person attainted.
|
||||
Article IV
|
||||
Section 1
|
||||
|
||||
Full Faith and Credit shall be given in each State to the public Acts, Records, and judicial Proceedings of every other State. And the Congress may by general Laws prescribe the Manner in which such Acts, Records and Proceedings shall be proved, and the Effect thereof.
|
||||
Section 2
|
||||
|
||||
The Citizens of each State shall be entitled to all Privileges and Immunities of Citizens in the several States.
|
||||
|
||||
A Person charged in any State with Treason, Felony, or other Crime, who shall flee from Justice, and be found in another State, shall on Demand of the executive Authority of the State from which he fled, be delivered up, to be removed to the State having Jurisdiction of the Crime.
|
||||
|
||||
No Person held to Service or Labour in one State, under the Laws thereof, escaping into another, shall, in Consequence of any Law or Regulation therein, be discharged from such Service or Labour, but shall be delivered up on Claim of the Party to whom such Service or Labour may be due.
|
||||
Section 3
|
||||
|
||||
New States may be admitted by the Congress into this Union; but no new State shall be formed or erected within the Jurisdiction of any other State; nor any State be formed by the Junction of two or more States, or Parts of States, without the Consent of the Legislatures of the States concerned as well as of the Congress.
|
||||
|
||||
The Congress shall have Power to dispose of and make all needful Rules and Regulations respecting the Territory or other Property belonging to the United States; and nothing in this Constitution shall be so construed as to Prejudice any Claims of the United States, or of any particular State.
|
||||
Section 4
|
||||
|
||||
The United States shall guarantee to every State in this Union a Republican Form of Government, and shall protect each of them against Invasion; and on Application of the Legislature, or of the Executive (when the Legislature cannot be convened) against domestic Violence.
|
||||
Article V
|
||||
|
||||
The Congress, whenever two thirds of both Houses shall deem it necessary, shall propose Amendments to this Constitution, or, on the Application of the Legislatures of two thirds of the several States, shall call a Convention for proposing Amendments, which, in either Case, shall be valid to all Intents and Purposes, as Part of this Constitution, when ratified by the Legislatures of three fourths of the several States, or by Conventions in three fourths thereof, as the one or the other Mode of Ratification may be proposed by the Congress; Provided that no Amendment which may be made prior to the Year One thousand eight hundred and eight shall in any Manner affect the first and fourth Clauses in the Ninth Section of the first Article; and that no State, without its Consent, shall be deprived of its equal Suffrage in the Senate.
|
||||
Article VI
|
||||
|
||||
All Debts contracted and Engagements entered into, before the Adoption of this Constitution, shall be as valid against the United States under this Constitution, as under the Confederation.
|
||||
|
||||
This Constitution, and the Laws of the United States which shall be made in Pursuance thereof; and all Treaties made, or which shall be made, under the Authority of the United States, shall be the supreme Law of the Land; and the Judges in every State shall be bound thereby, any Thing in the Constitution or Laws of any State to the Contrary notwithstanding.
|
||||
|
||||
The Senators and Representatives before mentioned, and the Members of the several State Legislatures, and all executive and judicial Officers, both of the United States and of the several States, shall be bound by Oath or Affirmation, to support this Constitution; but no religious Test shall ever be required as a Qualification to any Office or public Trust under the United States.
|
||||
Article VII
|
||||
|
||||
The Ratification of the Conventions of nine States, shall be sufficient for the Establishment of this Constitution between the States so ratifying the Same.
|
||||
First Amendment
|
||||
|
||||
Congress shall make no law respecting an establishment of religion, or prohibiting the free exercise thereof; or abridging the freedom of speech, or of the press; or the right of the people peaceably to assemble, and to petition the Government for a redress of grievances.
|
||||
Second Amendment
|
||||
|
||||
A well regulated Militia, being necessary to the security of a free State, the right of the people to keep and bear Arms, shall not be infringed.
|
||||
Third Amendment
|
||||
|
||||
No Soldier shall, in time of peace be quartered in any house, without the consent of the Owner, nor in time of war, but in a manner to be prescribed by law.
|
||||
Fourth Amendment
|
||||
|
||||
The right of the people to be secure in their persons, houses, papers, and effects, against unreasonable searches and seizures, shall not be violated, and no Warrants shall issue, but upon probable cause, supported by Oath or affirmation, and particularly describing the place to be searched, and the persons or things to be seized.
|
||||
Fifth Amendment
|
||||
|
||||
No person shall be held to answer for a capital, or otherwise infamous crime, unless on a presentment or indictment of a Grand Jury, except in cases arising in the land or naval forces, or in the Militia, when in actual service in time of War or public danger; nor shall any person be subject for the same offence to be twice put in jeopardy of life or limb; nor shall be compelled in any criminal case to be a witness against himself, nor be deprived of life, liberty, or property, without due process of law; nor shall private property be taken for public use, without just compensation.
|
||||
Sixth Amendment
|
||||
|
||||
In all criminal prosecutions, the accused shall enjoy the right to a speedy and public trial, by an impartial jury of the State and district wherein the crime shall have been committed, which district shall have been previously ascertained by law, and to be informed of the nature and cause of the accusation; to be confronted with the witnesses against him; to have compulsory process for obtaining witnesses in his favor, and to have the Assistance of Counsel for his defence.
|
||||
Seventh Amendment
|
||||
|
||||
In Suits at common law, where the value in controversy shall exceed twenty dollars, the right of trial by jury shall be preserved, and no fact tried by a jury, shall be otherwise re-examined in any Court of the United States, than according to the rules of the common law.
|
||||
Eighth Amendment
|
||||
|
||||
Excessive bail shall not be required, nor excessive fines imposed, nor cruel and unusual punishments inflicted.
|
||||
Ninth Amendment
|
||||
|
||||
The enumeration in the Constitution, of certain rights, shall not be construed to deny or disparage others retained by the people.
|
||||
10th Amendment
|
||||
|
||||
The powers not delegated to the United States by the Constitution, nor prohibited by it to the States, are reserved to the States respectively, or to the people.
|
||||
11th Amendment
|
||||
|
||||
The Judicial power of the United States shall not be construed to extend to any suit in law or equity, commenced or prosecuted against one of the United States by Citizens of another State, or by Citizens or Subjects of any Foreign State.
|
||||
12th Amendment
|
||||
|
||||
The Electors shall meet in their respective states and vote by ballot for President and Vice-President, one of whom, at least, shall not be an inhabitant of the same state with themselves; they shall name in their ballots the person voted for as President, and in distinct ballots the person voted for as Vice-President, and they shall make distinct lists of all persons voted for as President, and of all persons voted for as Vice-President, and of the number of votes for each, which lists they shall sign and certify, and transmit sealed to the seat of the government of the United States, directed to the President of the Senate; -- the President of the Senate shall, in the presence of the Senate and House of Representatives, open all the certificates and the votes shall then be counted; -- The person having the greatest number of votes for President, shall be the President, if such number be a majority of the whole number of Electors appointed; and if no person have such majority, then from the persons having the highest numbers not exceeding three on the list of those voted for as President, the House of Representatives shall choose immediately, by ballot, the President. But in choosing the President, the votes shall be taken by states, the representation from each state having one vote; a quorum for this purpose shall consist of a member or members from two-thirds of the states, and a majority of all the states shall be necessary to a choice. And if the House of Representatives shall not choose a President whenever the right of choice shall devolve upon them, before the fourth day of March next following, then the Vice-President shall act as President, as in the case of the death or other constitutional disability of the President.-- The person having the greatest number of votes as Vice-President, shall be the Vice-President, if such number be a majority of the whole number of Electors appointed, and if no person have a majority, then from the two highest numbers on the list, the Senate shall choose the Vice-President; a quorum for the purpose shall consist of two-thirds of the whole number of Senators, and a majority of the whole number shall be necessary to a choice. But no person constitutionally ineligible to the office of President shall be eligible to that of Vice-President of the United States.
|
||||
13th Amendment
|
||||
Section 1
|
||||
|
||||
Neither slavery nor involuntary servitude, except as a punishment for crime whereof the party shall have been duly convicted, shall exist within the United States, or any place subject to their jurisdiction.
|
||||
Section 2
|
||||
|
||||
Congress shall have power to enforce this article by appropriate legislation.
|
||||
14th Amendment
|
||||
Section 1
|
||||
|
||||
All persons born or naturalized in the United States, and subject to the jurisdiction thereof, are citizens of the United States and of the State wherein they reside. No State shall make or enforce any law which shall abridge the privileges or immunities of citizens of the United States; nor shall any State deprive any person of life, liberty, or property, without due process of law; nor deny to any person within its jurisdiction the equal protection of the laws.
|
||||
Section 2
|
||||
|
||||
Representatives shall be apportioned among the several States according to their respective numbers, counting the whole number of persons in each State, excluding Indians not taxed. But when the right to vote at any election for the choice of electors for President and Vice-President of the United States, Representatives in Congress, the Executive and Judicial officers of a State, or the members of the Legislature thereof, is denied to any of the male inhabitants of such State, being twenty-one years of age, and citizens of the United States, or in any way abridged, except for participation in rebellion, or other crime, the basis of representation therein shall be reduced in the proportion which the number of such male citizens shall bear to the whole number of male citizens twenty-one years of age in such State.
|
||||
Section 3
|
||||
|
||||
No person shall be a Senator or Representative in Congress, or elector of President and Vice-President, or hold any office, civil or military, under the United States, or under any State, who, having previously taken an oath, as a member of Congress, or as an officer of the United States, or as a member of any State legislature, or as an executive or judicial officer of any State, to support the Constitution of the United States, shall have engaged in insurrection or rebellion against the same, or given aid or comfort to the enemies thereof. But Congress may by a vote of two-thirds of each House, remove such disability.
|
||||
Section 4
|
||||
|
||||
The validity of the public debt of the United States, authorized by law, including debts incurred for payment of pensions and bounties for services in suppressing insurrection or rebellion, shall not be questioned. But neither the United States nor any State shall assume or pay any debt or obligation incurred in aid of insurrection or rebellion against the United States, or any claim for the loss or emancipation of any slave; but all such debts, obligations and claims shall be held illegal and void.
|
||||
Section 5
|
||||
|
||||
The Congress shall have power to enforce, by appropriate legislation, the provisions of this article.
|
||||
15th Amendment
|
||||
Section 1
|
||||
|
||||
The right of citizens of the United States to vote shall not be denied or abridged by the United States or by any State on account of race, color, or previous condition of servitude.
|
||||
Section 2
|
||||
|
||||
The Congress shall have power to enforce this article by appropriate legislation.
|
||||
16th Amendment
|
||||
|
||||
The Congress shall have power to lay and collect taxes on incomes, from whatever source derived, without apportionment among the several States, and without regard to any census or enumeration.
|
||||
17th Amendment
|
||||
|
||||
The Senate of the United States shall be composed of two Senators from each State, elected by the people thereof, for six years; and each Senator shall have one vote. The electors in each State shall have the qualifications requisite for electors of the most numerous branch of the State legislatures.
|
||||
|
||||
When vacancies happen in the representation of any State in the Senate, the executive authority of such State shall issue writs of election to fill such vacancies: Provided, That the legislature of any State may empower the executive thereof to make temporary appointments until the people fill the vacancies by election as the legislature may direct.
|
||||
|
||||
This amendment shall not be so construed as to affect the election or term of any Senator chosen before it becomes valid as part of the Constitution.
|
||||
18th Amendment
|
||||
Section 1
|
||||
|
||||
After one year from the ratification of this article the manufacture, sale, or transportation of intoxicating liquors within, the importation thereof into, or the exportation thereof from the United States and all territory subject to the jurisdiction thereof for beverage purposes is hereby prohibited.
|
||||
Section 2
|
||||
|
||||
The Congress and the several States shall have concurrent power to enforce this article by appropriate legislation.
|
||||
Section 3
|
||||
|
||||
This article shall be inoperative unless it shall have been ratified as an amendment to the Constitution by the legislatures of the several States, as provided in the Constitution, within seven years from the date of the submission hereof to the States by the Congress.
|
||||
19th Amendment
|
||||
|
||||
The right of citizens of the United States to vote shall not be denied or abridged by the United States or by any State on account of sex.
|
||||
|
||||
Congress shall have power to enforce this article by appropriate legislation.
|
||||
20th Amendment
|
||||
Section 1
|
||||
|
||||
The terms of the President and the Vice President shall end at noon on the 20th day of January, and the terms of Senators and Representatives at noon on the 3d day of January, of the years in which such terms would have ended if this article had not been ratified; and the terms of their successors shall then begin.
|
||||
Section 2
|
||||
|
||||
The Congress shall assemble at least once in every year, and such meeting shall begin at noon on the 3d day of January, unless they shall by law appoint a different day.
|
||||
Section 3
|
||||
|
||||
If, at the time fixed for the beginning of the term of the President, the President elect shall have died, the Vice President elect shall become President. If a President shall not have been chosen before the time fixed for the beginning of his term, or if the President elect shall have failed to qualify, then the Vice President elect shall act as President until a President shall have qualified; and the Congress may by law provide for the case wherein neither a President elect nor a Vice President elect shall have qualified, declaring who shall then act as President, or the manner in which one who is to act shall be selected, and such person shall act accordingly until a President or Vice President shall have qualified.
|
||||
Section 4
|
||||
|
||||
The Congress may by law provide for the case of the death of any of the persons from whom the House of Representatives may choose a President whenever the right of choice shall have devolved upon them, and for the case of the death of any of the persons from whom the Senate may choose a Vice President whenever the right of choice shall have devolved upon them.
|
||||
Section 5
|
||||
|
||||
Sections 1 and 2 shall take effect on the 15th day of October following the ratification of this article.
|
||||
Section 6
|
||||
|
||||
This article shall be inoperative unless it shall have been ratified as an amendment to the Constitution by the legislatures of three-fourths of the several States within seven years from the date of its submission.
|
||||
21st Amendment
|
||||
Section 1
|
||||
|
||||
The eighteenth article of amendment to the Constitution of the United States is hereby repealed.
|
||||
Section 2
|
||||
|
||||
The transportation or importation into any State, Territory, or possession of the United States for delivery or use therein of intoxicating liquors, in violation of the laws thereof, is hereby prohibited.
|
||||
Section 3
|
||||
|
||||
This article shall be inoperative unless it shall have been ratified as an amendment to the Constitution by conventions in the several States, as provided in the Constitution, within seven years from the date of the submission hereof to the States by the Congress.
|
||||
22nd Amendment
|
||||
Section 1
|
||||
|
||||
No person shall be elected to the office of the President more than twice, and no person who has held the office of President, or acted as President, for more than two years of a term to which some other person was elected President shall be elected to the office of the President more than once. But this Article shall not apply to any person holding the office of President when this Article was proposed by the Congress, and shall not prevent any person who may be holding the office of President, or acting as President, during the term within which this Article becomes operative from holding the office of President or acting as President during the remainder of such term.
|
||||
Section 2
|
||||
|
||||
This article shall be inoperative unless it shall have been ratified as an amendment to the Constitution by the legislatures of three-fourths of the several States within seven years from the date of its submission to the States by the Congress.
|
||||
23rd Amendment
|
||||
Section 1
|
||||
|
||||
The District constituting the seat of Government of the United States shall appoint in such manner as Congress may direct:
|
||||
|
||||
A number of electors of President and Vice President equal to the whole number of Senators and Representatives in Congress to which the District would be entitled if it were a State, but in no event more than the least populous State; they shall be in addition to those appointed by the States, but they shall be considered, for the purposes of the election of President and Vice President, to be electors appointed by a State; and they shall meet in the District and perform such duties as provided by the twelfth article of amendment.
|
||||
Section 2
|
||||
|
||||
The Congress shall have power to enforce this article by appropriate legislation.
|
||||
24th Amendment
|
||||
Section 1
|
||||
|
||||
The right of citizens of the United States to vote in any primary or other election for President or Vice President, for electors for President or Vice President, or for Senator or Representative in Congress, shall not be denied or abridged by the United States or any State by reason of failure to pay poll tax or other tax.
|
||||
Section 2
|
||||
|
||||
The Congress shall have power to enforce this article by appropriate legislation.
|
||||
25th Amendment
|
||||
Section 1
|
||||
|
||||
In case of the removal of the President from office or of his death or resignation, the Vice President shall become President.
|
||||
Section 2
|
||||
|
||||
Whenever there is a vacancy in the office of the Vice President, the President shall nominate a Vice President who shall take office upon confirmation by a majority vote of both Houses of Congress.
|
||||
Section 3
|
||||
|
||||
Whenever the President transmits to the President pro tempore of the Senate and the Speaker of the House of Representatives his written declaration that he is unable to discharge the powers and duties of his office, and until he transmits to them a written declaration to the contrary, such powers and duties shall be discharged by the Vice President as Acting President.
|
||||
Section 4
|
||||
|
||||
Whenever the Vice President and a majority of either the principal officers of the executive departments or of such other body as Congress may by law provide, transmit to the President pro tempore of the Senate and the Speaker of the House of Representatives their written declaration that the President is unable to discharge the powers and duties of his office, the Vice President shall immediately assume the powers and duties of the office as Acting President.
|
||||
|
||||
Thereafter, when the President transmits to the President pro tempore of the Senate and the Speaker of the House of Representatives his written declaration that no inability exists, he shall resume the powers and duties of his office unless the Vice President and a majority of either the principal officers of the executive department or of such other body as Congress may by law provide, transmit within four days to the President pro tempore of the Senate and the Speaker of the House of Representatives their written declaration that the President is unable to discharge the powers and duties of his office. Thereupon Congress shall decide the issue, assembling within forty-eight hours for that purpose if not in session. If the Congress, within twenty-one days after receipt of the latter written declaration, or, if Congress is not in session, within twenty-one days after Congress is required to assemble, determines by two-thirds vote of both Houses that the President is unable to discharge the powers and duties of his office, the Vice President shall continue to discharge the same as Acting President; otherwise, the President shall resume the powers and duties of his office.
|
||||
26th Amendment
|
||||
Section 1
|
||||
|
||||
The right of citizens of the United States, who are eighteen years of age or older, to vote shall not be denied or abridged by the United States or by any State on account of age.
|
||||
Section 2
|
||||
|
||||
The Congress shall have power to enforce this article by appropriate legislation.
|
||||
27th Amendment
|
||||
|
||||
No law, varying the compensation for the services of the Senators and Representatives, shall take effect, until an election of Representatives shall have intervened.
|
||||
43
exercises/BLE_MAC_address_Differentiator.md
Normal file
43
exercises/BLE_MAC_address_Differentiator.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
Yes. In the current dual-role BLE exercises, every T-Beam starts as both:
|
||||
|
||||
- a BLE **peripheral/server**, advertising the Reticulum BLE service
|
||||
- a BLE **central/client**, scanning for peers
|
||||
|
||||
The tie-breaker is the BLE MAC address string.
|
||||
|
||||
In [TBeamSupremeBleInterface.cpp](/usr/local/src/microreticulum/microReticulumTbeam/exercises/305_microReticulum_ble_file_transfer/src/TBeamSupremeBleInterface.cpp:293):
|
||||
|
||||
```cpp
|
||||
return strcmp(_local_address.c_str(), peer_address.c_str()) < 0;
|
||||
```
|
||||
|
||||
So the unit with the **lower BLE MAC address** initiates the BLE connection as the **client/central**. The unit with the higher BLE MAC stays available as the **server/peripheral**.
|
||||
|
||||
Sorted from lowest to highest priority by your list:
|
||||
|
||||
```text
|
||||
1. GUY 48:ca:43:5a:40:e0
|
||||
2. ED 48:ca:43:5a:90:d0
|
||||
3. CY 48:ca:43:5a:91:44
|
||||
4. DAN 48:ca:43:5a:93:a0
|
||||
5. FLO 48:ca:43:5b:8c:74
|
||||
6. BOB 48:ca:43:5b:93:dc
|
||||
7. AMY 48:ca:43:5b:bf:68
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
```text
|
||||
GUY will initiate BLE connections to everyone.
|
||||
AMY will never initiate against this set; everyone else has lower priority and will connect to AMY.
|
||||
BOB initiates to AMY, but stays server for GUY/ED/CY/DAN/FLO.
|
||||
FLO initiates to BOB and AMY, but stays server for GUY/ED/CY/DAN.
|
||||
```
|
||||
|
||||
There is also a second, similar tie-breaker for the Reticulum Link itself in [main.cpp](/usr/local/src/microreticulum/microReticulumTbeam/exercises/305_microReticulum_ble_file_transfer/src/main.cpp:88):
|
||||
|
||||
```cpp
|
||||
return strcmp(node_label.c_str(), label.c_str()) < 0;
|
||||
```
|
||||
|
||||
That compares the `Node-...` labels. In practice those labels are derived from the ESP MAC, so the same "lower identity initiates" idea is used again after the BLE connection exists.
|
||||
BIN
exercises/t-beam_MAC_addresses.ods
Normal file
BIN
exercises/t-beam_MAC_addresses.ods
Normal file
Binary file not shown.
805
tools/analyze_tbeam_bilateral_transfer_20260520_0412.pl
Executable file
805
tools/analyze_tbeam_bilateral_transfer_20260520_0412.pl
Executable file
|
|
@ -0,0 +1,805 @@
|
|||
#!/usr/bin/env perl
|
||||
# ./analyze_tbeam_bilateral_transfer_20260520_0412.pl BLE_CY_DAN_20260519_2051.zip
|
||||
# ./analyze_tbeam_bilateral_transfer_20260520_0412.pl --detail BLE_CY_DAN_20260519_2051.zip
|
||||
# ./analyze_tbeam_bilateral_transfer_20260520_0412.pl --csv tbeam_chunks_$(date +%Y%m%d_%H%M).csv BLE_CY_DAN_20260519_2051.zip
|
||||
# ./analyze_tbeam_bilateral_transfer_20260520_0412.pl --report tbeam_report_$(date +%Y%m%d_%H%M).txt BLE_CY_DAN_20260519_2051.zip
|
||||
#
|
||||
# 20260520 ChatGPT
|
||||
# $Header$
|
||||
# $HeadURL$
|
||||
#
|
||||
=pod
|
||||
|
||||
Purpose:
|
||||
Analyze two or more ESP32/T-Beam serial monitor logs from a bilateral
|
||||
microReticulum + Reticulum BLE file transfer.
|
||||
|
||||
The script is intentionally parallel in spirit to
|
||||
analyze_reticulum_file_transfer_20260518_1930.pl, but the T-Beam logs have a
|
||||
different shape:
|
||||
|
||||
[1779249101.7991] TX FILE BEGIN: round=3 file=US_Constitution.txt ...
|
||||
[1779249101.9005] TX FILE DATA: round=3 seq=1/148 bytes=300 ...
|
||||
[1779249105.8511] RX FILE END: from=Node-... received=0/44225 chunks=0/148 ... status=VERIFY_FAIL
|
||||
[1779249090.6498] 00:21:47.653 [ERR] Decryption failed ... Token token HMAC was invalid
|
||||
|
||||
It accepts either log files directly or a .zip archive containing log files.
|
||||
Zip extraction uses the system unzip command because that is usually already
|
||||
present on Gentoo/Debian systems and avoids a non-core Perl dependency.
|
||||
|
||||
Interpretation note:
|
||||
HMAC failures mean the receiver rejected encrypted Reticulum traffic before it
|
||||
became valid application payload. Therefore, a transfer can show many Reticulum
|
||||
decrypt errors while showing zero RX FILE DATA records.
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use POSIX qw(strftime);
|
||||
use List::Util qw(min max sum);
|
||||
use File::Basename qw(basename dirname);
|
||||
use File::Temp qw(tempdir);
|
||||
use Cwd qw(abs_path getcwd);
|
||||
|
||||
my $detail = 0;
|
||||
my $csv_file = '';
|
||||
my $report_file = '';
|
||||
my $keep_extract = 0;
|
||||
|
||||
GetOptions(
|
||||
'detail!' => \$detail,
|
||||
'csv=s' => \$csv_file,
|
||||
'report=s' => \$report_file,
|
||||
'keep-extract' => \$keep_extract,
|
||||
'help' => \my $help,
|
||||
) or die usage();
|
||||
|
||||
if ($help || !@ARGV) {
|
||||
print usage();
|
||||
exit($help ? 0 : 1);
|
||||
}
|
||||
|
||||
my @input_files = expand_inputs(@ARGV);
|
||||
@input_files or die "No readable log files found.\n";
|
||||
|
||||
my @reports;
|
||||
for my $file (@input_files) {
|
||||
push @reports, parse_tbeam_log($file);
|
||||
}
|
||||
|
||||
my %node_to_board = infer_node_to_board(@reports);
|
||||
annotate_remote_boards(\@reports, \%node_to_board);
|
||||
annotate_errors(\@reports);
|
||||
|
||||
my $text = build_report(\@reports, \%node_to_board, $detail);
|
||||
|
||||
if ($report_file ne '') {
|
||||
open my $rfh, '>', $report_file or die "Cannot write $report_file: $!\n";
|
||||
print {$rfh} $text;
|
||||
close $rfh;
|
||||
print "Report written: $report_file\n";
|
||||
}
|
||||
else {
|
||||
print $text;
|
||||
}
|
||||
|
||||
write_csv($csv_file, \@reports) if $csv_file ne '';
|
||||
|
||||
exit 0;
|
||||
|
||||
sub expand_inputs {
|
||||
my (@args) = @_;
|
||||
my @files;
|
||||
|
||||
for my $arg (@args) {
|
||||
if (-d $arg) {
|
||||
push @files, grep { -f $_ && -r $_ } glob("$arg/*");
|
||||
next;
|
||||
}
|
||||
if ($arg =~ /\.zip\z/i) {
|
||||
push @files, extract_zip_logs($arg);
|
||||
next;
|
||||
}
|
||||
push @files, $arg if -f $arg && -r $arg;
|
||||
}
|
||||
|
||||
@files = grep { /(?:\.log|\.txt)\z/i || file_looks_like_tbeam_log($_) } @files;
|
||||
return sort @files;
|
||||
}
|
||||
|
||||
sub extract_zip_logs {
|
||||
my ($zip) = @_;
|
||||
-r $zip or die "Cannot read zip archive $zip\n";
|
||||
|
||||
my $tdir = tempdir('tbeam_ble_logs_XXXXXX', TMPDIR => 1, CLEANUP => !$keep_extract);
|
||||
my $cmd = sprintf('unzip -q %s -d %s', shellq($zip), shellq($tdir));
|
||||
system($cmd) == 0 or die "unzip failed for $zip\n";
|
||||
|
||||
my @found;
|
||||
open my $find, '-|', 'find', $tdir, '-type', 'f' or die "Cannot run find: $!\n";
|
||||
while (my $path = <$find>) {
|
||||
chomp $path;
|
||||
push @found, $path if $path =~ /(?:\.log|\.txt)\z/i || file_looks_like_tbeam_log($path);
|
||||
}
|
||||
close $find;
|
||||
|
||||
warn "Extracted zip under $tdir\n" if $keep_extract;
|
||||
return @found;
|
||||
}
|
||||
|
||||
sub file_looks_like_tbeam_log {
|
||||
my ($file) = @_;
|
||||
open my $fh, '<', $file or return 0;
|
||||
my $n = 0;
|
||||
while (my $line = <$fh>) {
|
||||
++$n;
|
||||
if ($line =~ /^#\s+Board:/ || $line =~ /^\[\d+\.\d+\]\s+(?:TX|RX) FILE / || $line =~ /Decryption failed on link/) {
|
||||
close $fh;
|
||||
return 1;
|
||||
}
|
||||
last if $n > 40;
|
||||
}
|
||||
close $fh;
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub parse_tbeam_log {
|
||||
my ($file) = @_;
|
||||
|
||||
open my $fh, '<', $file or die "Cannot open $file: $!\n";
|
||||
|
||||
my %r = (
|
||||
file => $file,
|
||||
base => basename($file),
|
||||
header => {},
|
||||
tx_rounds => {},
|
||||
rx_sessions => [],
|
||||
errors => [],
|
||||
events => [],
|
||||
line_count => 0,
|
||||
);
|
||||
|
||||
my %active_rx_by_node;
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
++$r{line_count};
|
||||
chomp $line;
|
||||
|
||||
if ($line =~ /^#\s*([^:]+):\s*(.*)$/) {
|
||||
my ($k, $v) = (trim($1), trim($2));
|
||||
$r{header}{$k} = $v;
|
||||
next;
|
||||
}
|
||||
|
||||
next unless $line =~ /^\[([0-9]+(?:\.[0-9]+)?)\]\s+(.*)$/;
|
||||
my ($epoch, $msg) = ($1 + 0.0, $2);
|
||||
|
||||
$r{first_epoch} = $epoch if !defined $r{first_epoch};
|
||||
$r{last_epoch} = $epoch;
|
||||
|
||||
if ($msg =~ /^TX FILE BEGIN:\s+round=(\d+)\s+file=(\S+)\s+bytes=(\d+)\s+chunks=(\d+)\s+crc=([0-9A-Fa-f]+)/) {
|
||||
my ($round, $file_name, $bytes, $chunks, $crc) = ($1+0, $2, $3+0, $4+0, uc($5));
|
||||
my $tx = ($r{tx_rounds}{$round} ||= new_tx_round($round));
|
||||
@{$tx}{qw(begin_epoch file bytes_expected chunks_expected crc_expected)} = ($epoch, $file_name, $bytes, $chunks, $crc);
|
||||
push @{$r{events}}, { type => 'tx_begin', epoch => $epoch, round => $round, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /^TX FILE DATA:\s+round=(\d+)\s+seq=(\d+)\/(\d+)\s+bytes=(\d+)/) {
|
||||
my ($round, $seq, $total, $bytes) = ($1+0, $2+0, $3+0, $4+0);
|
||||
my $tx = ($r{tx_rounds}{$round} ||= new_tx_round($round));
|
||||
push @{$tx->{data}}, { epoch => $epoch, seq => $seq, total => $total, bytes => $bytes, line => $r{line_count} };
|
||||
$tx->{chunks_expected} ||= $total;
|
||||
push @{$r{events}}, { type => 'tx_data', epoch => $epoch, round => $round, seq => $seq, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /^TX FILE END:\s+round=(\d+)\s+file=(\S+)\s+bytes=(\d+)\s+chunks=(\d+)\s+crc=([0-9A-Fa-f]+)(?:\s+next_round_in_ms=(\d+))?/) {
|
||||
my ($round, $file_name, $bytes, $chunks, $crc, $next_ms) = ($1+0, $2, $3+0, $4+0, uc($5), $6);
|
||||
my $tx = ($r{tx_rounds}{$round} ||= new_tx_round($round));
|
||||
@{$tx}{qw(end_epoch file bytes_expected chunks_expected crc_expected)} = ($epoch, $file_name, $bytes, $chunks, $crc);
|
||||
$tx->{next_round_in_ms} = $next_ms + 0 if defined $next_ms;
|
||||
push @{$r{events}}, { type => 'tx_end', epoch => $epoch, round => $round, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /^RX FILE BEGIN:\s+from=(\S+)\s+file=(\S+)\s+bytes=(\d+)\s+chunks=(\d+)\s+crc=([0-9A-Fa-f]+)/) {
|
||||
my ($from, $file_name, $bytes, $chunks, $crc) = ($1, $2, $3+0, $4+0, uc($5));
|
||||
my $sess = {
|
||||
idx => scalar(@{$r{rx_sessions}}) + 1,
|
||||
from_node => $from,
|
||||
file => $file_name,
|
||||
begin_epoch => $epoch,
|
||||
bytes_expected => $bytes,
|
||||
chunks_expected => $chunks,
|
||||
crc_expected => $crc,
|
||||
data => [],
|
||||
errors => [],
|
||||
begin_line => $r{line_count},
|
||||
};
|
||||
push @{$r{rx_sessions}}, $sess;
|
||||
$active_rx_by_node{$from} = $sess;
|
||||
push @{$r{events}}, { type => 'rx_begin', epoch => $epoch, rx_idx => $sess->{idx}, from_node => $from, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /^RX FILE DATA:\s+from=(\S+)\s+seq=(\d+)\/(\d+)\s+bytes=(\d+)/) {
|
||||
my ($from, $seq, $total, $bytes) = ($1, $2+0, $3+0, $4+0);
|
||||
my $sess = $active_rx_by_node{$from};
|
||||
if (!$sess) {
|
||||
$sess = {
|
||||
idx => scalar(@{$r{rx_sessions}}) + 1,
|
||||
from_node => $from,
|
||||
file => 'UNKNOWN',
|
||||
begin_epoch => undef,
|
||||
bytes_expected => undef,
|
||||
chunks_expected => $total,
|
||||
crc_expected => undef,
|
||||
data => [],
|
||||
errors => [],
|
||||
synthetic => 1,
|
||||
};
|
||||
push @{$r{rx_sessions}}, $sess;
|
||||
$active_rx_by_node{$from} = $sess;
|
||||
}
|
||||
push @{$sess->{data}}, { epoch => $epoch, seq => $seq, total => $total, bytes => $bytes, line => $r{line_count} };
|
||||
$sess->{chunks_expected} ||= $total;
|
||||
push @{$r{events}}, { type => 'rx_data', epoch => $epoch, rx_idx => $sess->{idx}, from_node => $from, seq => $seq, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /^RX FILE END:\s+from=(\S+)\s+file=(\S+)\s+received=(\d+)\/(\d+)\s+chunks=(\d+)\/(\d+)\s+crc=([0-9A-Fa-f]+)\s+status=(\S+)/) {
|
||||
my ($from, $file_name, $rx_bytes, $expect_bytes, $rx_chunks, $expect_chunks, $crc, $status) = ($1, $2, $3+0, $4+0, $5+0, $6+0, uc($7), $8);
|
||||
my $sess = $active_rx_by_node{$from};
|
||||
if (!$sess) {
|
||||
$sess = {
|
||||
idx => scalar(@{$r{rx_sessions}}) + 1,
|
||||
from_node => $from,
|
||||
file => $file_name,
|
||||
begin_epoch => undef,
|
||||
bytes_expected => $expect_bytes,
|
||||
chunks_expected => $expect_chunks,
|
||||
crc_expected => undef,
|
||||
data => [],
|
||||
errors => [],
|
||||
synthetic => 1,
|
||||
};
|
||||
push @{$r{rx_sessions}}, $sess;
|
||||
}
|
||||
@{$sess}{qw(end_epoch file bytes_received bytes_expected chunks_received chunks_expected crc_seen status end_line)} =
|
||||
($epoch, $file_name, $rx_bytes, $expect_bytes, $rx_chunks, $expect_chunks, $crc, $status, $r{line_count});
|
||||
delete $active_rx_by_node{$from};
|
||||
push @{$r{events}}, { type => 'rx_end', epoch => $epoch, rx_idx => $sess->{idx}, from_node => $from, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
|
||||
if ($msg =~ /\[ERR\]\s+Decryption failed on link\s+\{Link:([^}]+)\}\.\s+The contained exception was:\s+(.+)$/) {
|
||||
my ($link, $exception) = ($1, $2);
|
||||
my $class = classify_error($exception);
|
||||
my $err = {
|
||||
epoch => $epoch,
|
||||
link => $link,
|
||||
exception => $exception,
|
||||
class => $class,
|
||||
line => $r{line_count},
|
||||
raw => $msg,
|
||||
};
|
||||
push @{$r{errors}}, $err;
|
||||
push @{$r{events}}, { type => 'error', epoch => $epoch, error => $err, line => $r{line_count} };
|
||||
next;
|
||||
}
|
||||
}
|
||||
close $fh;
|
||||
|
||||
$r{board} = $r{header}{Board} || board_from_filename($file);
|
||||
summarize_tx_round($_) for values %{$r{tx_rounds}};
|
||||
summarize_rx_session($_) for @{$r{rx_sessions}};
|
||||
return \%r;
|
||||
}
|
||||
|
||||
sub new_tx_round {
|
||||
my ($round) = @_;
|
||||
return {
|
||||
round => $round,
|
||||
data => [],
|
||||
};
|
||||
}
|
||||
|
||||
sub classify_error {
|
||||
my ($e) = @_;
|
||||
return 'HMAC_INVALID' if $e =~ /HMAC/i;
|
||||
return 'PADDING_INVALID' if $e =~ /pad|padding/i;
|
||||
return 'TOKEN_INVALID' if $e =~ /token/i;
|
||||
return 'OTHER';
|
||||
}
|
||||
|
||||
sub summarize_tx_round {
|
||||
my ($tx) = @_;
|
||||
my @d = sort { $a->{seq} <=> $b->{seq} || $a->{epoch} <=> $b->{epoch} } @{$tx->{data}};
|
||||
$tx->{data} = \@d;
|
||||
$tx->{data_count} = scalar @d;
|
||||
$tx->{bytes_sent_logged} = sum0(map { $_->{bytes} } @d);
|
||||
$tx->{first_data_epoch} = @d ? $d[0]{epoch} : undef;
|
||||
$tx->{last_data_epoch} = @d ? $d[-1]{epoch} : undef;
|
||||
$tx->{first_seq} = @d ? min(map { $_->{seq} } @d) : undef;
|
||||
$tx->{last_seq} = @d ? max(map { $_->{seq} } @d) : undef;
|
||||
$tx->{chunk_total_seen} = @d ? max(map { $_->{total} } @d) : ($tx->{chunks_expected} || 0);
|
||||
$tx->{missing} = missing_ranges([ map { $_->{seq} } @d ], $tx->{chunk_total_seen});
|
||||
$tx->{duplicates} = duplicate_list(map { $_->{seq} } @d);
|
||||
$tx->{data_span} = defined($tx->{first_data_epoch}) && defined($tx->{last_data_epoch}) ? $tx->{last_data_epoch} - $tx->{first_data_epoch} : undef;
|
||||
$tx->{round_span} = defined($tx->{begin_epoch}) && defined($tx->{end_epoch}) ? $tx->{end_epoch} - $tx->{begin_epoch} : undef;
|
||||
$tx->{gaps} = [ consecutive_deltas(map { $_->{epoch} } @d) ];
|
||||
}
|
||||
|
||||
sub summarize_rx_session {
|
||||
my ($s) = @_;
|
||||
my @d = sort { $a->{seq} <=> $b->{seq} || $a->{epoch} <=> $b->{epoch} } @{$s->{data}};
|
||||
$s->{data} = \@d;
|
||||
$s->{data_count} = scalar @d;
|
||||
$s->{bytes_data_logged} = sum0(map { $_->{bytes} } @d);
|
||||
$s->{first_data_epoch} = @d ? $d[0]{epoch} : undef;
|
||||
$s->{last_data_epoch} = @d ? $d[-1]{epoch} : undef;
|
||||
$s->{first_seq} = @d ? min(map { $_->{seq} } @d) : undef;
|
||||
$s->{last_seq} = @d ? max(map { $_->{seq} } @d) : undef;
|
||||
$s->{missing} = missing_ranges([ map { $_->{seq} } @d ], $s->{chunks_expected} || 0);
|
||||
$s->{duplicates} = duplicate_list(map { $_->{seq} } @d);
|
||||
$s->{data_span} = defined($s->{first_data_epoch}) && defined($s->{last_data_epoch}) ? $s->{last_data_epoch} - $s->{first_data_epoch} : undef;
|
||||
$s->{session_span} = defined($s->{begin_epoch}) && defined($s->{end_epoch}) ? $s->{end_epoch} - $s->{begin_epoch} : undef;
|
||||
$s->{gaps} = [ consecutive_deltas(map { $_->{epoch} } @d) ];
|
||||
}
|
||||
|
||||
sub infer_node_to_board {
|
||||
my (@reports) = @_;
|
||||
my %received_from;
|
||||
for my $r (@reports) {
|
||||
my $board = $r->{board};
|
||||
for my $s (@{$r->{rx_sessions}}) {
|
||||
next unless defined $s->{from_node};
|
||||
$received_from{$board}{$s->{from_node}}++;
|
||||
}
|
||||
}
|
||||
|
||||
my %node_to_board;
|
||||
if (@reports == 2) {
|
||||
my ($a, $b) = @reports;
|
||||
my ($board_a, $board_b) = ($a->{board}, $b->{board});
|
||||
for my $node (keys %{$received_from{$board_a} || {}}) {
|
||||
$node_to_board{$node} = $board_b;
|
||||
}
|
||||
for my $node (keys %{$received_from{$board_b} || {}}) {
|
||||
$node_to_board{$node} = $board_a;
|
||||
}
|
||||
}
|
||||
return %node_to_board;
|
||||
}
|
||||
|
||||
sub annotate_remote_boards {
|
||||
my ($reports, $node_to_board) = @_;
|
||||
for my $r (@$reports) {
|
||||
for my $s (@{$r->{rx_sessions}}) {
|
||||
$s->{from_board} = $node_to_board->{$s->{from_node}} if defined $s->{from_node} && exists $node_to_board->{$s->{from_node}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub annotate_errors {
|
||||
my ($reports) = @_;
|
||||
for my $r (@$reports) {
|
||||
my @tx = values %{$r->{tx_rounds}};
|
||||
my @rx = @{$r->{rx_sessions}};
|
||||
for my $e (@{$r->{errors}}) {
|
||||
for my $tx (@tx) {
|
||||
next unless defined $tx->{begin_epoch} && defined $tx->{end_epoch};
|
||||
if ($e->{epoch} >= $tx->{begin_epoch} && $e->{epoch} <= $tx->{end_epoch}) {
|
||||
$e->{tx_round} = $tx->{round};
|
||||
$tx->{error_count}++;
|
||||
}
|
||||
}
|
||||
for my $s (@rx) {
|
||||
next unless defined $s->{begin_epoch} && defined $s->{end_epoch};
|
||||
if ($e->{epoch} >= $s->{begin_epoch} && $e->{epoch} <= $s->{end_epoch}) {
|
||||
$e->{rx_idx} = $s->{idx};
|
||||
push @{$s->{errors}}, $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub build_report {
|
||||
my ($reports, $node_to_board, $detail) = @_;
|
||||
my $out = '';
|
||||
my $emit = sub { $out .= join('', @_) };
|
||||
|
||||
$emit->("Reticulum BLE T-Beam bilateral transfer analysis\n");
|
||||
$emit->("Generated: ", strftime('%Y-%m-%d %H:%M:%S %Z', localtime), "\n");
|
||||
$emit->("Input files:\n");
|
||||
for my $r (@$reports) {
|
||||
$emit->(" $r->{file}\n");
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("Capture summary:\n");
|
||||
for my $r (@$reports) {
|
||||
my $duration = defined($r->{first_epoch}) && defined($r->{last_epoch}) ? $r->{last_epoch} - $r->{first_epoch} : 0;
|
||||
$emit->(sprintf " %-36s board=%-6s port=%-12s exercise=%-6s lines=%6d duration=%8.3f s\n",
|
||||
$r->{base}, $r->{board} || 'UNKNOWN', $r->{header}{Port} || 'UNKNOWN', $r->{header}{Exercise} || 'n/a', $r->{line_count}, $duration);
|
||||
$emit->(sprintf " started : %s\n", $r->{header}{Started} || 'not seen');
|
||||
$emit->(sprintf " command : %s\n", $r->{header}{Command} || 'not seen');
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("Inferred Reticulum node identity map:\n");
|
||||
if (%$node_to_board) {
|
||||
for my $node (sort keys %$node_to_board) {
|
||||
$emit->(sprintf " %-22s => %s\n", $node, $node_to_board->{$node});
|
||||
}
|
||||
}
|
||||
else {
|
||||
$emit->(" not enough information to infer node-to-board mapping\n");
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("Per-board high-level totals:\n");
|
||||
for my $r (@$reports) {
|
||||
my @tx_rounds = values %{$r->{tx_rounds}};
|
||||
my @rx_sessions = @{$r->{rx_sessions}};
|
||||
my $tx_data = sum0(map { $_->{data_count} } @tx_rounds);
|
||||
my $rx_data = sum0(map { $_->{data_count} } @rx_sessions);
|
||||
my $rx_ok = scalar grep { ($_->{status} || '') eq 'OK' } @rx_sessions;
|
||||
my $rx_fail = scalar grep { ($_->{status} || '') ne '' && ($_->{status} || '') ne 'OK' } @rx_sessions;
|
||||
my %err_class;
|
||||
$err_class{$_->{class}}++ for @{$r->{errors}};
|
||||
$emit->(sprintf " %-6s tx_rounds=%3d tx_data=%4d rx_sessions=%3d rx_data=%4d rx_ok=%3d rx_fail=%3d decrypt_errors=%5d",
|
||||
$r->{board}, scalar(@tx_rounds), $tx_data, scalar(@rx_sessions), $rx_data, $rx_ok, $rx_fail, scalar(@{$r->{errors}}));
|
||||
if (%err_class) {
|
||||
$emit->(" ", join(' ', map { "$err_class{$_} $_" } sort keys %err_class));
|
||||
}
|
||||
$emit->("\n");
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("TX round summary:\n");
|
||||
for my $r (@$reports) {
|
||||
$emit->(" Board $r->{board}\n");
|
||||
for my $round (sort { $a <=> $b } keys %{$r->{tx_rounds}}) {
|
||||
my $tx = $r->{tx_rounds}{$round};
|
||||
my $expected = $tx->{chunks_expected} || $tx->{chunk_total_seen} || 0;
|
||||
my $complete = $expected ? 100.0 * $tx->{data_count} / $expected : 0;
|
||||
$emit->(sprintf " round=%-3d file=%-22s chunks=%3d/%-3d completeness=%6.2f%% bytes_logged=%6d span=%8s data_span=%8s errors_during_tx=%4d missing=%s dupes=%s\n",
|
||||
$round,
|
||||
$tx->{file} || 'UNKNOWN',
|
||||
$tx->{data_count} || 0,
|
||||
$expected,
|
||||
$complete,
|
||||
$tx->{bytes_sent_logged} || 0,
|
||||
fmt_s($tx->{round_span}),
|
||||
fmt_s($tx->{data_span}),
|
||||
$tx->{error_count} || 0,
|
||||
@{$tx->{missing}} ? join(',', @{$tx->{missing}}) : 'none',
|
||||
@{$tx->{duplicates}} ? join(',', @{$tx->{duplicates}}) : 'none');
|
||||
emit_gap_stats_to_string(\$out, " tx inter-data gap", $tx->{gaps});
|
||||
}
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("RX session summary:\n");
|
||||
for my $r (@$reports) {
|
||||
$emit->(" Board $r->{board}\n");
|
||||
for my $s (sort { ($a->{begin_epoch} || 0) <=> ($b->{begin_epoch} || 0) } @{$r->{rx_sessions}}) {
|
||||
my $expected = $s->{chunks_expected} || 0;
|
||||
my $rx_chunks = defined($s->{chunks_received}) ? $s->{chunks_received} : $s->{data_count};
|
||||
my $complete = $expected ? 100.0 * $rx_chunks / $expected : 0;
|
||||
$emit->(sprintf " rx#=%-2d from=%-6s node=%-22s file=%-22s status=%-11s chunks=%3d/%-3d completeness=%6.2f%% bytes=%6s/%-6s crc=%-8s expect=%-8s session_span=%8s data_span=%8s errors=%4d missing=%s dupes=%s\n",
|
||||
$s->{idx},
|
||||
$s->{from_board} || '?',
|
||||
$s->{from_node} || 'UNKNOWN',
|
||||
$s->{file} || 'UNKNOWN',
|
||||
$s->{status} || 'OPEN/UNKNOWN',
|
||||
$rx_chunks,
|
||||
$expected,
|
||||
$complete,
|
||||
defined($s->{bytes_received}) ? $s->{bytes_received} : $s->{bytes_data_logged} || 0,
|
||||
defined($s->{bytes_expected}) ? $s->{bytes_expected} : '?',
|
||||
$s->{crc_seen} || '?',
|
||||
$s->{crc_expected} || '?',
|
||||
fmt_s($s->{session_span}),
|
||||
fmt_s($s->{data_span}),
|
||||
scalar(@{$s->{errors} || []}),
|
||||
@{$s->{missing}} ? join(',', @{$s->{missing}}) : 'none',
|
||||
@{$s->{duplicates}} ? join(',', @{$s->{duplicates}}) : 'none');
|
||||
emit_gap_stats_to_string(\$out, " rx inter-data gap", $s->{gaps});
|
||||
}
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("Decrypt error summary:\n");
|
||||
for my $r (@$reports) {
|
||||
my @e = @{$r->{errors}};
|
||||
$emit->(" Board $r->{board}\n");
|
||||
if (!@e) {
|
||||
$emit->(" none\n");
|
||||
next;
|
||||
}
|
||||
my %by_class; $by_class{$_->{class}}++ for @e;
|
||||
my %by_link; $by_link{$_->{link}}++ for @e;
|
||||
my $span = $e[-1]{epoch} - $e[0]{epoch};
|
||||
my $rate = $span > 0 ? @e / $span : 0;
|
||||
$emit->(sprintf " total=%d first=%s last=%s span=%.3f s rate=%.2f errors/s\n",
|
||||
scalar(@e), fmt_epoch($e[0]{epoch}), fmt_epoch($e[-1]{epoch}), $span, $rate);
|
||||
$emit->(" by class: ", join(', ', map { "$by_class{$_} $_" } sort keys %by_class), "\n");
|
||||
$emit->(" by link : ", join(', ', map { "$by_link{$_} $_" } sort keys %by_link), "\n");
|
||||
|
||||
my %by_tx;
|
||||
my %by_rx;
|
||||
for my $err (@e) {
|
||||
$by_tx{defined($err->{tx_round}) ? $err->{tx_round} : 'no-active-tx'}++;
|
||||
$by_rx{defined($err->{rx_idx}) ? $err->{rx_idx} : 'no-active-rx'}++;
|
||||
}
|
||||
$emit->(" during TX rounds: ", join(', ', map { "$_=$by_tx{$_}" } sort by_mixed keys %by_tx), "\n");
|
||||
$emit->(" during RX sessions: ", join(', ', map { "$_=$by_rx{$_}" } sort by_mixed keys %by_rx), "\n");
|
||||
emit_error_bursts(\$out, \@e);
|
||||
}
|
||||
$emit->("\n");
|
||||
|
||||
$emit->("Bilateral asymmetry notes:\n");
|
||||
$emit->(build_asymmetry_notes($reports));
|
||||
$emit->("\n");
|
||||
|
||||
if ($detail) {
|
||||
$emit->("Detailed RX DATA rows:\n");
|
||||
for my $r (@$reports) {
|
||||
$emit->(" Board $r->{board}\n");
|
||||
for my $s (@{$r->{rx_sessions}}) {
|
||||
for my $d (@{$s->{data}}) {
|
||||
$emit->(sprintf " rx#=%-2d t=%s from=%-6s seq=%3d/%-3d bytes=%3d line=%d\n",
|
||||
$s->{idx}, fmt_epoch($d->{epoch}), $s->{from_board} || '?', $d->{seq}, $d->{total}, $d->{bytes}, $d->{line});
|
||||
}
|
||||
}
|
||||
}
|
||||
$emit->("\n");
|
||||
}
|
||||
|
||||
$emit->("Caution:\n");
|
||||
$emit->(" This report uses the host-side epoch prefix from the serial monitor. That is excellent for ordering and gap analysis,\n");
|
||||
$emit->(" but it is not the same as an on-device timestamp captured before BLE queueing and Reticulum decrypt work.\n");
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
sub build_asymmetry_notes {
|
||||
my ($reports) = @_;
|
||||
my $s = '';
|
||||
for my $r (@$reports) {
|
||||
my $board = $r->{board};
|
||||
my $err_count = scalar @{$r->{errors}};
|
||||
my $rx_ok = scalar grep { ($_->{status} || '') eq 'OK' } @{$r->{rx_sessions}};
|
||||
my $rx_fail = scalar grep { ($_->{status} || '') ne '' && ($_->{status} || '') ne 'OK' } @{$r->{rx_sessions}};
|
||||
my $zero_chunk_fail = scalar grep { (($_->{status} || '') ne '' && ($_->{status} || '') ne 'OK') && (($_->{chunks_received} || 0) == 0) } @{$r->{rx_sessions}};
|
||||
$s .= sprintf(" %-6s rx_ok=%d rx_fail=%d zero_chunk_fail=%d decrypt_errors=%d\n", $board, $rx_ok, $rx_fail, $zero_chunk_fail, $err_count);
|
||||
}
|
||||
|
||||
my @bad = grep { @{$_->{errors}} > 0 } @$reports;
|
||||
my @good = grep { @{$_->{errors}} == 0 } @$reports;
|
||||
if (@bad && @good) {
|
||||
$s .= " Observation: decrypt failures are asymmetric. At least one board receives complete files while another reports Reticulum HMAC failures.\n";
|
||||
$s .= " Working hypothesis: investigate the receiver/server side before blaming the Constitution payload or the sender text chunker.\n";
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
sub emit_error_bursts {
|
||||
my ($out_ref, $errors) = @_;
|
||||
my %bin;
|
||||
for my $e (@$errors) {
|
||||
$bin{int($e->{epoch})}++;
|
||||
}
|
||||
my @top = sort { $bin{$b} <=> $bin{$a} || $a <=> $b } keys %bin;
|
||||
@top = @top[0 .. min(4, $#top)] if @top > 5;
|
||||
$$out_ref .= " busiest seconds: ";
|
||||
$$out_ref .= @top ? join(', ', map { fmt_epoch($_) . "=$bin{$_}" } @top) : 'none';
|
||||
$$out_ref .= "\n";
|
||||
}
|
||||
|
||||
sub write_csv {
|
||||
my ($csv, $reports) = @_;
|
||||
open my $out, '>', $csv or die "Cannot write $csv: $!\n";
|
||||
print {$out} join(',', qw(board event direction peer file round rx_idx seq total bytes epoch time status crc expected_crc error_class link line source_log)), "\n";
|
||||
for my $r (@$reports) {
|
||||
for my $round (sort { $a <=> $b } keys %{$r->{tx_rounds}}) {
|
||||
my $tx = $r->{tx_rounds}{$round};
|
||||
for my $d (@{$tx->{data}}) {
|
||||
print {$out} join(',', map { csvq($_) } (
|
||||
$r->{board}, 'TX_FILE_DATA', 'tx', '', $tx->{file} || '', $round, '',
|
||||
$d->{seq}, $d->{total}, $d->{bytes}, sprintf('%.4f', $d->{epoch}), fmt_epoch($d->{epoch}), '', '', '', '', '', $d->{line}, $r->{base}
|
||||
)), "\n";
|
||||
}
|
||||
}
|
||||
for my $s (@{$r->{rx_sessions}}) {
|
||||
for my $d (@{$s->{data}}) {
|
||||
print {$out} join(',', map { csvq($_) } (
|
||||
$r->{board}, 'RX_FILE_DATA', 'rx', $s->{from_board} || $s->{from_node} || '', $s->{file} || '', '', $s->{idx},
|
||||
$d->{seq}, $d->{total}, $d->{bytes}, sprintf('%.4f', $d->{epoch}), fmt_epoch($d->{epoch}), $s->{status} || '', $s->{crc_seen} || '', $s->{crc_expected} || '', '', '', $d->{line}, $r->{base}
|
||||
)), "\n";
|
||||
}
|
||||
}
|
||||
for my $e (@{$r->{errors}}) {
|
||||
print {$out} join(',', map { csvq($_) } (
|
||||
$r->{board}, 'DECRYPT_ERROR', 'rx', '', '', $e->{tx_round} || '', $e->{rx_idx} || '',
|
||||
'', '', '', sprintf('%.4f', $e->{epoch}), fmt_epoch($e->{epoch}), '', '', '', $e->{class}, $e->{link}, $e->{line}, $r->{base}
|
||||
)), "\n";
|
||||
}
|
||||
}
|
||||
close $out;
|
||||
print "CSV written: $csv\n";
|
||||
}
|
||||
|
||||
# Build-report-local gap appender. Kept separate from emit_gap_stats to avoid passing closures everywhere.
|
||||
sub emit_gap_stats_to_string {
|
||||
my ($out_ref, $label, $values) = @_;
|
||||
return if !$values || !@$values;
|
||||
$$out_ref .= sprintf "%s min/median/mean/p95/max/stddev: %.3f / %.3f / %.3f / %.3f / %.3f / %.3f ms\n",
|
||||
$label,
|
||||
min(@$values) * 1000.0,
|
||||
percentile(50, @$values) * 1000.0,
|
||||
mean(@$values) * 1000.0,
|
||||
percentile(95, @$values) * 1000.0,
|
||||
max(@$values) * 1000.0,
|
||||
stddev(@$values) * 1000.0;
|
||||
}
|
||||
|
||||
# Monkey-patch wrapper used by build_report's lexical $out. This is clearer than a global in usage.
|
||||
# Perl has no closures by name, so build_report calls this helper via explicit code below.
|
||||
|
||||
sub fmt_s {
|
||||
my ($v) = @_;
|
||||
return 'n/a' unless defined $v;
|
||||
return sprintf('%.3f s', $v);
|
||||
}
|
||||
|
||||
sub fmt_epoch {
|
||||
my ($epoch) = @_;
|
||||
return 'n/a' unless defined $epoch;
|
||||
my $whole = int($epoch);
|
||||
my $ms = int(($epoch - $whole) * 1000.0 + 0.5);
|
||||
if ($ms >= 1000) { ++$whole; $ms -= 1000; }
|
||||
return strftime('%H:%M:%S', localtime($whole)) . sprintf('.%03d', $ms);
|
||||
}
|
||||
|
||||
sub consecutive_deltas {
|
||||
my (@v) = @_;
|
||||
my @d;
|
||||
for (my $i = 1; $i < @v; ++$i) {
|
||||
push @d, $v[$i] - $v[$i - 1];
|
||||
}
|
||||
return @d;
|
||||
}
|
||||
|
||||
sub missing_ranges {
|
||||
my ($seen_ref, $expected) = @_;
|
||||
return [] if !$expected;
|
||||
my %seen = map { $_ => 1 } @$seen_ref;
|
||||
my @missing = grep { !$seen{$_} } 1 .. $expected;
|
||||
return [] unless @missing;
|
||||
|
||||
my @ranges;
|
||||
my ($start, $prev) = ($missing[0], $missing[0]);
|
||||
for my $m (@missing[1 .. $#missing]) {
|
||||
if ($m == $prev + 1) {
|
||||
$prev = $m;
|
||||
}
|
||||
else {
|
||||
push @ranges, $start == $prev ? $start : "$start-$prev";
|
||||
($start, $prev) = ($m, $m);
|
||||
}
|
||||
}
|
||||
push @ranges, $start == $prev ? $start : "$start-$prev";
|
||||
return \@ranges;
|
||||
}
|
||||
|
||||
sub duplicate_list {
|
||||
my (@v) = @_;
|
||||
my %count;
|
||||
++$count{$_} for @v;
|
||||
return [ sort { $a <=> $b } grep { $count{$_} > 1 } keys %count ];
|
||||
}
|
||||
|
||||
sub percentile {
|
||||
my ($p, @values) = @_;
|
||||
@values = sort { $a <=> $b } @values;
|
||||
return 0 unless @values;
|
||||
return $values[0] if @values == 1;
|
||||
my $rank = ($p / 100.0) * (@values - 1);
|
||||
my $lo = int($rank);
|
||||
my $hi = $lo + 1;
|
||||
return $values[$lo] if $hi > $#values;
|
||||
my $frac = $rank - $lo;
|
||||
return $values[$lo] + (($values[$hi] - $values[$lo]) * $frac);
|
||||
}
|
||||
|
||||
sub mean {
|
||||
my (@values) = @_;
|
||||
return 0 unless @values;
|
||||
return sum0(@values) / @values;
|
||||
}
|
||||
|
||||
sub stddev {
|
||||
my (@values) = @_;
|
||||
return 0 if @values < 2;
|
||||
my $mean = mean(@values);
|
||||
my $ss = 0;
|
||||
$ss += ($_ - $mean) ** 2 for @values;
|
||||
return sqrt($ss / @values);
|
||||
}
|
||||
|
||||
sub sum0 {
|
||||
return 0 unless @_;
|
||||
my $s = sum(@_);
|
||||
return defined($s) ? $s : 0;
|
||||
}
|
||||
|
||||
sub by_mixed {
|
||||
my $aa = $a;
|
||||
my $bb = $b;
|
||||
return $aa <=> $bb if $aa =~ /^\d+\z/ && $bb =~ /^\d+\z/;
|
||||
return $aa cmp $bb;
|
||||
}
|
||||
|
||||
sub board_from_filename {
|
||||
my ($file) = @_;
|
||||
my $base = basename($file);
|
||||
return $1 if $base =~ /_(AMY|BOB|CY|DAN|ED|FLO|GUY)_/i;
|
||||
return $1 if $base =~ /\b(AMY|BOB|CY|DAN|ED|FLO|GUY)\b/i;
|
||||
return $base;
|
||||
}
|
||||
|
||||
sub trim {
|
||||
my ($v) = @_;
|
||||
$v = '' unless defined $v;
|
||||
$v =~ s/^\s+//;
|
||||
$v =~ s/\s+\z//;
|
||||
return $v;
|
||||
}
|
||||
|
||||
sub shellq {
|
||||
my ($s) = @_;
|
||||
$s =~ s/'/'"'"'/g;
|
||||
return "'$s'";
|
||||
}
|
||||
|
||||
sub csvq {
|
||||
my ($v) = @_;
|
||||
$v = '' unless defined $v;
|
||||
$v =~ s/"/""/g;
|
||||
return '"' . $v . '"';
|
||||
}
|
||||
|
||||
sub usage {
|
||||
return <<'USAGE';
|
||||
Usage:
|
||||
analyze_tbeam_bilateral_transfer_20260520_0412.pl [options] log1.log log2.log
|
||||
analyze_tbeam_bilateral_transfer_20260520_0412.pl [options] BLE_CY_DAN_20260519_2051.zip
|
||||
|
||||
Options:
|
||||
--detail Print detailed RX DATA rows.
|
||||
--csv FILE Write normalized TX/RX/error rows to CSV.
|
||||
--report FILE Write the report to FILE instead of STDOUT.
|
||||
--keep-extract Keep temporary extraction directory when reading .zip input.
|
||||
--help Show this help.
|
||||
|
||||
Examples:
|
||||
chmod 755 analyze_tbeam_bilateral_transfer_20260520_0412.pl
|
||||
|
||||
./analyze_tbeam_bilateral_transfer_20260520_0412.pl \
|
||||
BLE_CY_DAN_20260519_2051.zip
|
||||
|
||||
./analyze_tbeam_bilateral_transfer_20260520_0412.pl \
|
||||
--detail \
|
||||
--csv tbeam_chunks_$(date +%Y%m%d_%H%M).csv \
|
||||
--report tbeam_report_$(date +%Y%m%d_%H%M).txt \
|
||||
BLE_CY_DAN_20260519_2051.zip
|
||||
USAGE
|
||||
}
|
||||
59
tools/monitor_t-beam_with_epoch.sh
Executable file
59
tools/monitor_t-beam_with_epoch.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
# 20260519 ChatGPT
|
||||
# $Header$
|
||||
#
|
||||
# Example:
|
||||
# chmod 755 monitor_tbeam_with_epoch.sh
|
||||
# ./monitor_tbeam_with_epoch.sh AMY
|
||||
# ./monitor_tbeam_with_epoch.sh BOB
|
||||
# ./monitor_tbeam_with_epoch.sh CY
|
||||
#
|
||||
# Optional:
|
||||
# BAUD=115200 EXERCISE=305 ./monitor_tbeam_with_epoch.sh AMY
|
||||
|
||||
ORIG_STTY=$(stty -g)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BOARD="${1:-AMY}"
|
||||
BAUD="${BAUD:-115200}"
|
||||
EXERCISE="${EXERCISE:-305}"
|
||||
|
||||
PORT="/dev/ttyt${BOARD}"
|
||||
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
LOGDIR="$HOME/logs/tbeam_exercise_${EXERCISE}"
|
||||
LOGFILE="${LOGDIR}/${TS}_${BOARD}_exercise_${EXERCISE}_serial.log"
|
||||
|
||||
mkdir -p "$LOGDIR"
|
||||
|
||||
if [ ! -e "$PORT" ]; then
|
||||
echo "ERROR: serial device does not exist: $PORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CMD="pio device monitor -p ${PORT} -b ${BAUD}"
|
||||
cleanup() {
|
||||
stty "$ORIG_STTY" 2>/dev/null || stty sane 2>/dev/null || true
|
||||
echo
|
||||
echo "# Terminal settings restored."
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM HUP
|
||||
|
||||
{
|
||||
echo "# Started: $(date)"
|
||||
echo "# Epoch start: $(perl -MTime::HiRes=time -e 'printf "%.4f\n", time')"
|
||||
echo "# Host: $(hostname)"
|
||||
echo "# Board: ${BOARD}"
|
||||
echo "# Port: ${PORT}"
|
||||
echo "# Baud: ${BAUD}"
|
||||
echo "# Exercise: ${EXERCISE}"
|
||||
echo "# Command: ${CMD}"
|
||||
echo "# Logfile: ${LOGFILE}"
|
||||
echo "# ---- serial output follows ----"
|
||||
} | tee "$LOGFILE"
|
||||
|
||||
${CMD} 2>&1 \
|
||||
| perl -MTime::HiRes=time -ne '$|=1; printf "[%.4f] %s", time, $_' \
|
||||
| tee -a "$LOGFILE"
|
||||
Loading…
Add table
Add a link
Reference in a new issue