Catchup, today has been stampeding horses of productivity

This commit is contained in:
John Poole 2026-05-19 13:22:39 -07:00
commit 31f881233e
19 changed files with 1699 additions and 0 deletions

View file

@ -0,0 +1,72 @@
# Exercise 201: microReticulum LoRa ping-pong
This exercise proves that two T-Beam Supreme units can exchange Reticulum packets over a LoRa interface using Chad Attermann's microReticulum tree staged at:
`/usr/local/src/microreticulum/microReticulum`
It intentionally mirrors the radio settings and node-label workflow from `01_lora_ascii_pingpong`, but sends Reticulum `PLAIN` destination data packets instead of raw ASCII LoRa frames.
Note that in Reticulum terms, `PLAIN` is the simplest destination type. It is addressable by a shared app/aspect name only, and payloads are not encrypted to a node identity. Exercise 202 moves to announced `SINGLE` destinations. Exercise 203 is intended to add a negotiated `Link`.
## Build and upload
Use two environments so the serial output identifies each unit:
In one console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/201_microReticulum_ping_pong -e amy -t upload --upload-port /dev/ttytAMY && \
pio device monitor -d exercises/201_microReticulum_ping_pong -e amy
```
In another console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/201_microReticulum_ping_pong -e bob -t upload --upload-port /dev/ttytBOB && \
pio device monitor -d exercises/201_microReticulum_ping_pong -e bob
```
Note: if the T-Beam unit was previously running a program that prints to the OLED, the OLED as last used will continue to appear even though this exercise has been loaded and a RESET performed. Therefore, do not let the OLED fool you as to the fact that this exercise has been loaded and is running. To avoid a zombie OLED, power off the unit and power on then run the commands above.
The `platformio.ini` maps each environment to its stable USB symlink:
```text
amy -> /dev/ttytAMY
bob -> /dev/ttytBOB
cy -> /dev/ttytCY
dan -> /dev/ttytDAN
ed -> /dev/ttytED
```
For a one-off override, pass the port as a PlatformIO option, not as a positional argument:
```bash
pio run -d exercises/201_microReticulum_ping_pong -e amy -t upload --upload-port /dev/ttytAMY
```
Open a monitor on each unit:
```bash
pio device monitor -d exercises/201_microReticulum_ping_pong -e amy
pio device monitor -d exercises/201_microReticulum_ping_pong -e bob
```
Expected output includes periodic transmit lines like:
```text
TX RNS: Amy says hi. iter=0
```
and received Reticulum payloads like:
```text
RX RNS: Bob says hi. iter=0 | RSSI=-42.0 SNR=9.5
```
![](../../img/20260519_115429_Tue.png)
## Notes
- The exercise uses the same T-Beam Supreme LoRa pins as exercise 01.
- Radio settings are `915.0 MHz`, `125 kHz`, `SF7`, `CR5`, sync word `0x12`, and `14 dBm`.
- The destination is `PLAIN`, so both nodes can send and receive without an announce/key exchange. This keeps the proof focused on Reticulum packet serialization, transport dispatch, and the LoRa interface path.

View file

@ -0,0 +1,86 @@
; Exercise 201: microReticulum LoRa ping-pong on T-Beam Supreme
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
upload_speed = 460800
board_build.partitions = huge_app.csv
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
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D LORA_FREQ_MHZ=915.0
-D LORA_BW_KHZ=125.0
-D LORA_SF=7
-D LORA_CR=5
-D LORA_SYNC_WORD=0x12
-D LORA_TX_POWER_DBM=14
lib_deps =
ArduinoJson@^7.4.2
MsgPack@^0.4.2
jgromes/RadioLib@^7.0.0
https://github.com/attermann/Crypto.git
https://github.com/attermann/microStore.git
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
[env:amy]
extends = env
upload_port = /dev/ttytAMY
monitor_port = /dev/ttytAMY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Amy\"
[env:bob]
extends = env
upload_port = /dev/ttytBOB
monitor_port = /dev/ttytBOB
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Bob\"
[env:cy]
extends = env
upload_port = /dev/ttytCY
monitor_port = /dev/ttytCY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Cy\"
[env:dan]
extends = env
upload_port = /dev/ttytDAN
monitor_port = /dev/ttytDAN
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Dan\"
[env:ed]
extends = env
upload_port = /dev/ttytED
monitor_port = /dev/ttytED
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Ed\"

View file

@ -0,0 +1,166 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Cryptography/Random.h>
#include <Log.h>
#include <string.h>
#ifndef LORA_CS
#error "LORA_CS not defined"
#endif
#ifndef LORA_DIO1
#error "LORA_DIO1 not defined"
#endif
#ifndef LORA_RESET
#error "LORA_RESET not defined"
#endif
#ifndef LORA_BUSY
#error "LORA_BUSY not defined"
#endif
using namespace RNS;
TBeamSupremeLoRaInterface::TBeamSupremeLoRaInterface(const char* name) : InterfaceImpl(name) {
_IN = true;
_OUT = true;
_bitrate = (double)LORA_SF * ((4.0 / LORA_CR) / (pow(2, LORA_SF) / LORA_BW_KHZ)) * 1000.0;
_HW_MTU = 508;
}
TBeamSupremeLoRaInterface::~TBeamSupremeLoRaInterface() {
stop();
delete _radio;
delete _module;
}
bool TBeamSupremeLoRaInterface::start() {
_online = false;
INFO("LoRa initializing for T-Beam Supreme...");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
_module = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY, SPI);
_radio = new SX1262(_module);
int state = _radio->begin(
LORA_FREQ_MHZ,
LORA_BW_KHZ,
LORA_SF,
LORA_CR,
LORA_SYNC_WORD,
LORA_TX_POWER_DBM);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa init failed, code %d", state);
return false;
}
state = _radio->startReceive();
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa startReceive failed, code %d", state);
return false;
}
_online = true;
INFO("LoRa init succeeded.");
return true;
}
void TBeamSupremeLoRaInterface::stop() {
if (_radio) {
_radio->standby();
}
_online = false;
}
void TBeamSupremeLoRaInterface::loop() {
if (!_online || !_radio) {
return;
}
if (!_radio->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
return;
}
int len = _radio->getPacketLength();
uint8_t rx_buf[255];
int state = _radio->readData(rx_buf, len);
if (state == RADIOLIB_ERR_NONE && len > 1) {
_last_rssi = _radio->getRSSI();
_last_snr = _radio->getSNR();
uint8_t header = rx_buf[0];
uint8_t seq = packet_sequence(header);
if (is_split_packet(header)) {
if (_rx_seq == SEQ_UNSET || _rx_seq != seq) {
_rx_seq = seq;
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
} else {
_rx_buffer.append(rx_buf + 1, len - 1);
_rx_seq = SEQ_UNSET;
on_incoming(_rx_buffer);
}
} else {
if (_rx_seq != SEQ_UNSET) {
_rx_buffer.clear();
_rx_seq = SEQ_UNSET;
}
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
on_incoming(_rx_buffer);
}
} else if (state != RADIOLIB_ERR_NONE) {
DEBUGF("LoRa readData failed, code %d", state);
}
_radio->startReceive();
}
void TBeamSupremeLoRaInterface::send_outgoing(const Bytes& data) {
if (!_online || !_radio) {
return;
}
try {
uint8_t tx_buf[255];
uint8_t rand_nibble = (uint8_t)(Cryptography::randomnum(256)) & 0xF0;
if ((int)data.size() <= LORA_MAX_PAYLOAD) {
tx_buf[0] = rand_nibble;
memcpy(tx_buf + 1, data.data(), data.size());
int state = _radio->transmit(tx_buf, 1 + data.size());
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit failed, code %d", state);
}
} else {
uint8_t seq = (_tx_seq_ctr++) & HEADER_SEQ_MASK;
uint8_t split_header = rand_nibble | HEADER_SPLIT | seq;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data(), LORA_MAX_PAYLOAD);
int state = _radio->transmit(tx_buf, 1 + LORA_MAX_PAYLOAD);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 1 failed, code %d", state);
}
size_t remainder = data.size() - LORA_MAX_PAYLOAD;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data() + LORA_MAX_PAYLOAD, remainder);
state = _radio->transmit(tx_buf, 1 + remainder);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 2 failed, code %d", state);
}
}
_radio->startReceive();
InterfaceImpl::handle_outgoing(data);
} catch (const std::exception& e) {
ERRORF("LoRa transmit exception: %s", e.what());
}
}
void TBeamSupremeLoRaInterface::on_incoming(const Bytes& data) {
InterfaceImpl::handle_incoming(data);
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <Bytes.h>
#include <Interface.h>
#include <Arduino.h>
#include <RadioLib.h>
#include <SPI.h>
class TBeamSupremeLoRaInterface : public RNS::InterfaceImpl {
public:
explicit TBeamSupremeLoRaInterface(const char* name = "TBeamSupremeLoRa");
~TBeamSupremeLoRaInterface() override;
bool start() override;
void stop() override;
void loop() override;
float last_rssi() const { return _last_rssi; }
float last_snr() const { return _last_snr; }
private:
void send_outgoing(const RNS::Bytes& data) override;
void on_incoming(const RNS::Bytes& data);
static constexpr uint8_t HEADER_SPLIT = 0x08;
static constexpr uint8_t HEADER_SEQ_MASK = 0x07;
static constexpr uint8_t SEQ_UNSET = 0xFF;
static constexpr int LORA_MAX_PAYLOAD = 254;
static bool is_split_packet(uint8_t header) { return (header & HEADER_SPLIT) != 0; }
static uint8_t packet_sequence(uint8_t header) { return header & HEADER_SEQ_MASK; }
RNS::Bytes _rx_buffer;
uint8_t _rx_seq = SEQ_UNSET;
uint8_t _tx_seq_ctr = 0;
float _last_rssi = 0.0f;
float _last_snr = 0.0f;
Module* _module = nullptr;
SX1262* _radio = nullptr;
};

View file

@ -0,0 +1,125 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Arduino.h>
#include <Destination.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>
#ifndef NODE_LABEL
#define NODE_LABEL "?"
#endif
static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface lora_interface({RNS::Type::NONE});
static RNS::Destination inbound_destination({RNS::Type::NONE});
static RNS::Destination outbound_destination({RNS::Type::NONE});
static TBeamSupremeLoRaInterface* lora_impl = nullptr;
static void print_config() {
Serial.printf("Node=%s\r\n", NODE_LABEL);
Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n",
(int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY,
(int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI);
Serial.printf("LoRa: freq=%.1f BW=%.1f SF=%d CR=%d sync=0x%02x txp=%d\r\n",
(double)LORA_FREQ_MHZ, (double)LORA_BW_KHZ, (int)LORA_SF,
(int)LORA_CR, (int)LORA_SYNC_WORD, (int)LORA_TX_POWER_DBM);
}
static void on_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
(void)packet;
float rssi = lora_impl ? lora_impl->last_rssi() : 0.0f;
float snr = lora_impl ? lora_impl->last_snr() : 0.0f;
Serial.printf("RX RNS: %s | RSSI=%.1f SNR=%.1f\r\n",
data.toString().c_str(), rssi, snr);
}
static void setup_reticulum() {
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
filesystem.init();
RNS::Utilities::OS::register_filesystem(filesystem);
lora_impl = new TBeamSupremeLoRaInterface();
lora_interface = lora_impl;
lora_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(lora_interface);
lora_interface.start();
reticulum = RNS::Reticulum();
reticulum.transport_enabled(false);
reticulum.probe_destination_enabled(false);
reticulum.start();
inbound_destination = RNS::Destination({RNS::Type::NONE},
RNS::Type::Destination::IN,
RNS::Type::Destination::PLAIN,
"microreticulum",
"pingpong");
inbound_destination.set_packet_callback(on_packet);
outbound_destination = RNS::Destination({RNS::Type::NONE},
RNS::Type::Destination::OUT,
RNS::Type::Destination::PLAIN,
"microreticulum",
"pingpong");
}
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 5000) {
delay(100);
}
delay(250);
Serial.println();
Serial.println("Exercise 201: microReticulum LoRa ping-pong");
print_config();
RNS::loglevel(RNS::LOG_NOTICE);
setup_reticulum();
Serial.println("microReticulum ready");
}
void loop() {
reticulum.loop();
static uint32_t next_tx_ms = 0;
static uint32_t iter = 0;
uint32_t now = millis();
if (next_tx_ms == 0) {
uint32_t offset = (NODE_LABEL[0] == 'A') ? 500 : 1500;
next_tx_ms = now + offset;
}
if ((int32_t)(now - next_tx_ms) >= 0) {
next_tx_ms = now + 2000;
String message = String(NODE_LABEL) + " says hi. iter=" + String(iter++);
Serial.printf("TX RNS: %s\r\n", message.c_str());
RNS::Packet packet(outbound_destination,
RNS::bytesFromString(message.c_str()),
RNS::Type::Packet::DATA,
RNS::Type::Packet::CONTEXT_NONE,
RNS::Type::Transport::BROADCAST,
RNS::Type::Packet::HEADER_1,
{RNS::Bytes::NONE},
false);
packet.send();
}
delay(5);
}
int _write(int file, char* ptr, int len) {
(void)file;
int wrote = Serial.write(ptr, len);
Serial.flush();
return wrote;
}

View file

@ -0,0 +1,73 @@
# Exercise 202: microReticulum announce + SINGLE destination ping-pong
This exercise builds on Exercise 201.
Exercise 201 used a Reticulum `PLAIN` destination. That proved that Reticulum packets could move over the LoRa interface, but it avoided identity discovery. `PLAIN` packets are addressed by a shared app/aspect name and are not encrypted to a node identity.
Exercise 202 uses a Reticulum `SINGLE` destination. Each node creates an identity, announces an inbound destination, learns the other node's announced destination hash and public key, then sends Reticulum `DATA` packets to that learned destination. This proves the next layer:
```text
Identity -> SINGLE destination -> announce -> peer learns identity/path -> encrypted DATA packet -> local destination callback
```
This is still not a negotiated `Link`; that belongs in Exercise 203. `SINGLE` proves announce/key discovery and destination-addressed packets. A `Link` adds a negotiated connection on top of an announced `SINGLE` destination.
## Build, upload, and monitor
In one console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/202_microReticulum_announce_single -e amy -t upload --upload-port /dev/ttytAMY && \
pio device monitor -d exercises/202_microReticulum_announce_single -e amy
```
In another console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/202_microReticulum_announce_single -e bob -t upload --upload-port /dev/ttytBOB && \
pio device monitor -d exercises/202_microReticulum_announce_single -e bob
```
The `platformio.ini` also maps each environment to its stable USB symlink, so the shorter form should work once the symlinks exist:
```bash
pio run -d exercises/202_microReticulum_announce_single -e amy -t upload
pio device monitor -d exercises/202_microReticulum_announce_single -e amy
```
## Expected output
At startup each node prints its local `SINGLE` destination hash and periodically announces it:
```text
Local SINGLE destination: 2f6c...
TX ANNOUNCE: Amy
```
After the peer's announce is received, the node prints the learned destination:
```text
RX ANNOUNCE: label=Bob hash=91a4...
```
Once a peer is known, it sends encrypted `DATA` packets to that destination:
```text
TX SINGLE: Amy -> Bob iter=0
RX SINGLE: Bob -> Amy iter=0 | RSSI=-42.0 SNR=9.5
```
If both nodes only print `TX ANNOUNCE`, announce reception or validation has not succeeded yet. Confirm both are using the same radio settings and that Exercise 201 works first.
![](../../img/20260519_123829_Tue.png)
## Notes
- The exercise uses the same T-Beam Supreme LoRa pins and radio settings as Exercises 01 and 201.
- The identity is generated on each boot. The destination hash can change after reset because the identity changes.
- App data in the announce carries the human-readable node label, such as `Amy` or `Bob`.
- This exercise intentionally stops before `Link` establishment. Exercise 203 should use the announced `SINGLE` destination as the target for a negotiated link.

View file

@ -0,0 +1,86 @@
; Exercise 202: microReticulum announce + SINGLE destination ping-pong
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
upload_speed = 460800
board_build.partitions = huge_app.csv
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
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D LORA_FREQ_MHZ=915.0
-D LORA_BW_KHZ=125.0
-D LORA_SF=7
-D LORA_CR=5
-D LORA_SYNC_WORD=0x12
-D LORA_TX_POWER_DBM=14
lib_deps =
ArduinoJson@^7.4.2
MsgPack@^0.4.2
jgromes/RadioLib@^7.0.0
https://github.com/attermann/Crypto.git
https://github.com/attermann/microStore.git
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
[env:amy]
extends = env
upload_port = /dev/ttytAMY
monitor_port = /dev/ttytAMY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Amy\"
[env:bob]
extends = env
upload_port = /dev/ttytBOB
monitor_port = /dev/ttytBOB
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Bob\"
[env:cy]
extends = env
upload_port = /dev/ttytCY
monitor_port = /dev/ttytCY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Cy\"
[env:dan]
extends = env
upload_port = /dev/ttytDAN
monitor_port = /dev/ttytDAN
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Dan\"
[env:ed]
extends = env
upload_port = /dev/ttytED
monitor_port = /dev/ttytED
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Ed\"

View file

@ -0,0 +1,166 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Cryptography/Random.h>
#include <Log.h>
#include <string.h>
#ifndef LORA_CS
#error "LORA_CS not defined"
#endif
#ifndef LORA_DIO1
#error "LORA_DIO1 not defined"
#endif
#ifndef LORA_RESET
#error "LORA_RESET not defined"
#endif
#ifndef LORA_BUSY
#error "LORA_BUSY not defined"
#endif
using namespace RNS;
TBeamSupremeLoRaInterface::TBeamSupremeLoRaInterface(const char* name) : InterfaceImpl(name) {
_IN = true;
_OUT = true;
_bitrate = (double)LORA_SF * ((4.0 / LORA_CR) / (pow(2, LORA_SF) / LORA_BW_KHZ)) * 1000.0;
_HW_MTU = 508;
}
TBeamSupremeLoRaInterface::~TBeamSupremeLoRaInterface() {
stop();
delete _radio;
delete _module;
}
bool TBeamSupremeLoRaInterface::start() {
_online = false;
INFO("LoRa initializing for T-Beam Supreme...");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
_module = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY, SPI);
_radio = new SX1262(_module);
int state = _radio->begin(
LORA_FREQ_MHZ,
LORA_BW_KHZ,
LORA_SF,
LORA_CR,
LORA_SYNC_WORD,
LORA_TX_POWER_DBM);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa init failed, code %d", state);
return false;
}
state = _radio->startReceive();
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa startReceive failed, code %d", state);
return false;
}
_online = true;
INFO("LoRa init succeeded.");
return true;
}
void TBeamSupremeLoRaInterface::stop() {
if (_radio) {
_radio->standby();
}
_online = false;
}
void TBeamSupremeLoRaInterface::loop() {
if (!_online || !_radio) {
return;
}
if (!_radio->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
return;
}
int len = _radio->getPacketLength();
uint8_t rx_buf[255];
int state = _radio->readData(rx_buf, len);
if (state == RADIOLIB_ERR_NONE && len > 1) {
_last_rssi = _radio->getRSSI();
_last_snr = _radio->getSNR();
uint8_t header = rx_buf[0];
uint8_t seq = packet_sequence(header);
if (is_split_packet(header)) {
if (_rx_seq == SEQ_UNSET || _rx_seq != seq) {
_rx_seq = seq;
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
} else {
_rx_buffer.append(rx_buf + 1, len - 1);
_rx_seq = SEQ_UNSET;
on_incoming(_rx_buffer);
}
} else {
if (_rx_seq != SEQ_UNSET) {
_rx_buffer.clear();
_rx_seq = SEQ_UNSET;
}
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
on_incoming(_rx_buffer);
}
} else if (state != RADIOLIB_ERR_NONE) {
DEBUGF("LoRa readData failed, code %d", state);
}
_radio->startReceive();
}
void TBeamSupremeLoRaInterface::send_outgoing(const Bytes& data) {
if (!_online || !_radio) {
return;
}
try {
uint8_t tx_buf[255];
uint8_t rand_nibble = (uint8_t)(Cryptography::randomnum(256)) & 0xF0;
if ((int)data.size() <= LORA_MAX_PAYLOAD) {
tx_buf[0] = rand_nibble;
memcpy(tx_buf + 1, data.data(), data.size());
int state = _radio->transmit(tx_buf, 1 + data.size());
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit failed, code %d", state);
}
} else {
uint8_t seq = (_tx_seq_ctr++) & HEADER_SEQ_MASK;
uint8_t split_header = rand_nibble | HEADER_SPLIT | seq;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data(), LORA_MAX_PAYLOAD);
int state = _radio->transmit(tx_buf, 1 + LORA_MAX_PAYLOAD);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 1 failed, code %d", state);
}
size_t remainder = data.size() - LORA_MAX_PAYLOAD;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data() + LORA_MAX_PAYLOAD, remainder);
state = _radio->transmit(tx_buf, 1 + remainder);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 2 failed, code %d", state);
}
}
_radio->startReceive();
InterfaceImpl::handle_outgoing(data);
} catch (const std::exception& e) {
ERRORF("LoRa transmit exception: %s", e.what());
}
}
void TBeamSupremeLoRaInterface::on_incoming(const Bytes& data) {
InterfaceImpl::handle_incoming(data);
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <Bytes.h>
#include <Interface.h>
#include <Arduino.h>
#include <RadioLib.h>
#include <SPI.h>
class TBeamSupremeLoRaInterface : public RNS::InterfaceImpl {
public:
explicit TBeamSupremeLoRaInterface(const char* name = "TBeamSupremeLoRa");
~TBeamSupremeLoRaInterface() override;
bool start() override;
void stop() override;
void loop() override;
float last_rssi() const { return _last_rssi; }
float last_snr() const { return _last_snr; }
private:
void send_outgoing(const RNS::Bytes& data) override;
void on_incoming(const RNS::Bytes& data);
static constexpr uint8_t HEADER_SPLIT = 0x08;
static constexpr uint8_t HEADER_SEQ_MASK = 0x07;
static constexpr uint8_t SEQ_UNSET = 0xFF;
static constexpr int LORA_MAX_PAYLOAD = 254;
static bool is_split_packet(uint8_t header) { return (header & HEADER_SPLIT) != 0; }
static uint8_t packet_sequence(uint8_t header) { return header & HEADER_SEQ_MASK; }
RNS::Bytes _rx_buffer;
uint8_t _rx_seq = SEQ_UNSET;
uint8_t _tx_seq_ctr = 0;
float _last_rssi = 0.0f;
float _last_snr = 0.0f;
Module* _module = nullptr;
SX1262* _radio = nullptr;
};

View file

@ -0,0 +1,184 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Arduino.h>
#include <Destination.h>
#include <Identity.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>
#ifndef NODE_LABEL
#define NODE_LABEL "?"
#endif
static constexpr const char* APP_NAME = "microreticulum";
static constexpr const char* APP_ASPECT = "singleping";
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.singleping";
static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface lora_interface({RNS::Type::NONE});
static RNS::Identity local_identity({RNS::Type::NONE});
static RNS::Destination inbound_destination({RNS::Type::NONE});
static RNS::Destination peer_destination({RNS::Type::NONE});
static RNS::Bytes peer_hash;
static String peer_label;
static bool have_peer = false;
static TBeamSupremeLoRaInterface* lora_impl = nullptr;
class SingleAnnounceHandler : public RNS::AnnounceHandler {
public:
SingleAnnounceHandler() : 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("RX ANNOUNCE: label=%s hash=%s\r\n",
peer_label.c_str(), peer_hash.toHex().c_str());
}
};
static RNS::HAnnounceHandler announce_handler(new SingleAnnounceHandler());
static void print_config() {
Serial.printf("Node=%s\r\n", NODE_LABEL);
Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n",
(int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY,
(int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI);
Serial.printf("LoRa: freq=%.1f BW=%.1f SF=%d CR=%d sync=0x%02x txp=%d\r\n",
(double)LORA_FREQ_MHZ, (double)LORA_BW_KHZ, (int)LORA_SF,
(int)LORA_CR, (int)LORA_SYNC_WORD, (int)LORA_TX_POWER_DBM);
}
static void on_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
(void)packet;
float rssi = lora_impl ? lora_impl->last_rssi() : 0.0f;
float snr = lora_impl ? lora_impl->last_snr() : 0.0f;
Serial.printf("RX SINGLE: %s | RSSI=%.1f SNR=%.1f\r\n",
data.toString().c_str(), rssi, snr);
}
static void send_announce() {
if (!inbound_destination) {
return;
}
Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL);
inbound_destination.announce(RNS::bytesFromString(NODE_LABEL));
}
static void setup_reticulum() {
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
filesystem.init();
RNS::Utilities::OS::register_filesystem(filesystem);
lora_impl = new TBeamSupremeLoRaInterface();
lora_interface = lora_impl;
lora_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(lora_interface);
lora_interface.start();
reticulum = RNS::Reticulum();
reticulum.transport_enabled(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_packet_callback(on_packet);
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();
Serial.println("Exercise 202: microReticulum announce + SINGLE destination ping-pong");
print_config();
RNS::loglevel(RNS::LOG_NOTICE);
setup_reticulum();
Serial.println("microReticulum ready");
}
void loop() {
reticulum.loop();
static uint32_t next_announce_ms = 0;
static uint32_t next_tx_ms = 0;
static uint32_t iter = 0;
uint32_t now = millis();
if (next_announce_ms == 0) {
uint32_t offset = (NODE_LABEL[0] == 'A') ? 700 : 1700;
next_announce_ms = now + offset;
}
if ((int32_t)(now - next_announce_ms) >= 0) {
next_announce_ms = now + 15000;
send_announce();
}
if (have_peer && next_tx_ms == 0) {
uint32_t offset = (NODE_LABEL[0] == 'A') ? 2500 : 4000;
next_tx_ms = now + offset;
}
if (have_peer && (int32_t)(now - next_tx_ms) >= 0) {
next_tx_ms = now + 3000;
String message = String(NODE_LABEL) + " -> " + peer_label + " iter=" + String(iter++);
Serial.printf("TX SINGLE: %s\r\n", message.c_str());
RNS::Packet packet(peer_destination,
RNS::bytesFromString(message.c_str()),
RNS::Type::Packet::DATA,
RNS::Type::Packet::CONTEXT_NONE,
RNS::Type::Transport::BROADCAST,
RNS::Type::Packet::HEADER_1,
{RNS::Bytes::NONE},
false);
packet.send();
}
delay(5);
}
int _write(int file, char* ptr, int len) {
(void)file;
int wrote = Serial.write(ptr, len);
Serial.flush();
return wrote;
}

View file

@ -0,0 +1,80 @@
# Exercise 203: microReticulum negotiated Link ping-pong
This exercise builds on Exercises 201 and 202.
Exercise 201 used a `PLAIN` destination to prove that Reticulum packets can move over the LoRa interface. Exercise 202 used announced `SINGLE` destinations to prove identity discovery and encrypted destination-addressed packets.
Exercise 203 adds a negotiated Reticulum `Link`. The nodes still begin by announcing `SINGLE` destinations. After a peer announce is learned, one node opens a `Link` to the peer's `SINGLE` destination. When the link becomes active, both nodes exchange encrypted link packets.
```text
Identity -> SINGLE destination -> announce -> peer learns identity/path -> LINKREQUEST/LRPROOF handshake -> ACTIVE Link -> link DATA packets
```
With AMY and BOB, AMY initiates the link and BOB accepts it. For other pairs, the lower lexical node label initiates, which avoids both peers opening duplicate links at the same time.
## Build, upload, and monitor
In one console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/203_microReticulum_link_ping_pong -e amy -t upload --upload-port /dev/ttytAMY && \
pio device monitor -d exercises/203_microReticulum_link_ping_pong -e amy
```
In another console:
```bash
source /home/jlpoole/rnsenv/bin/activate
cd /usr/local/src/microreticulum/microReticulumTbeam
pio run -d exercises/203_microReticulum_link_ping_pong -e bob -t upload --upload-port /dev/ttytBOB && \
pio device monitor -d exercises/203_microReticulum_link_ping_pong -e bob
```
The `platformio.ini` maps each environment to its stable USB symlink, so this shorter form should also work once the symlinks exist:
```bash
pio run -d exercises/203_microReticulum_link_ping_pong -e amy -t upload
pio device monitor -d exercises/203_microReticulum_link_ping_pong -e amy
```
## Expected output
At startup each node announces its `SINGLE` destination:
```text
Local SINGLE destination: 2f6c...
TX ANNOUNCE: Amy
```
After the peer is learned, the selected initiator opens a link:
```text
RX ANNOUNCE: label=Bob hash=91a4...
TX LINKREQUEST: opening link to Bob
```
The peer reports the inbound negotiated link, and the initiator reports active status:
```text
RX LINK: inbound link established
LINK ACTIVE: initiator link established
```
Once active, both nodes send and receive packets over the negotiated link:
```text
TX LINK: Amy -> Bob iter=0
RX LINK: Amy -> Bob iter=0 | RSSI=-42.0 SNR=9.5
```
If both nodes only print announces, verify Exercise 202 first. If announces work but no link becomes active, the failure is in the `LINKREQUEST` / `LRPROOF` handshake layer rather than raw LoRa or destination discovery.
## Notes
- The link is established on top of the announced `SINGLE` destination from Exercise 202.
- The identity is generated on each boot, so destination hashes can change after reset.
- Once the link is active, the sketch stops periodic announces. This keeps the console focused on link traffic and avoids repeatedly refreshing a path that both peers already know.
- If microReticulum reports `Path table already has destination ...; keeping existing path entry`, that means a later announce referenced a destination already present in the path table. A real storage failure is still reported as `Failed to add destination ... to path table!`.
- This is the first exercise in the sequence that proves Reticulum negotiated link establishment over the LoRa interface.

View file

@ -0,0 +1,86 @@
; Exercise 203: microReticulum negotiated Link ping-pong
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
upload_speed = 460800
board_build.partitions = huge_app.csv
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
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D LORA_FREQ_MHZ=915.0
-D LORA_BW_KHZ=125.0
-D LORA_SF=7
-D LORA_CR=5
-D LORA_SYNC_WORD=0x12
-D LORA_TX_POWER_DBM=14
lib_deps =
ArduinoJson@^7.4.2
MsgPack@^0.4.2
jgromes/RadioLib@^7.0.0
https://github.com/attermann/Crypto.git
https://github.com/attermann/microStore.git
microReticulum=symlink:///usr/local/src/microreticulum/microReticulum
[env:amy]
extends = env
upload_port = /dev/ttytAMY
monitor_port = /dev/ttytAMY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Amy\"
[env:bob]
extends = env
upload_port = /dev/ttytBOB
monitor_port = /dev/ttytBOB
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Bob\"
[env:cy]
extends = env
upload_port = /dev/ttytCY
monitor_port = /dev/ttytCY
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Cy\"
[env:dan]
extends = env
upload_port = /dev/ttytDAN
monitor_port = /dev/ttytDAN
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Dan\"
[env:ed]
extends = env
upload_port = /dev/ttytED
monitor_port = /dev/ttytED
build_flags =
${env.build_flags}
-D NODE_LABEL=\"Ed\"

View file

@ -0,0 +1,166 @@
#include "TBeamSupremeLoRaInterface.h"
#include <Cryptography/Random.h>
#include <Log.h>
#include <string.h>
#ifndef LORA_CS
#error "LORA_CS not defined"
#endif
#ifndef LORA_DIO1
#error "LORA_DIO1 not defined"
#endif
#ifndef LORA_RESET
#error "LORA_RESET not defined"
#endif
#ifndef LORA_BUSY
#error "LORA_BUSY not defined"
#endif
using namespace RNS;
TBeamSupremeLoRaInterface::TBeamSupremeLoRaInterface(const char* name) : InterfaceImpl(name) {
_IN = true;
_OUT = true;
_bitrate = (double)LORA_SF * ((4.0 / LORA_CR) / (pow(2, LORA_SF) / LORA_BW_KHZ)) * 1000.0;
_HW_MTU = 508;
}
TBeamSupremeLoRaInterface::~TBeamSupremeLoRaInterface() {
stop();
delete _radio;
delete _module;
}
bool TBeamSupremeLoRaInterface::start() {
_online = false;
INFO("LoRa initializing for T-Beam Supreme...");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
_module = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY, SPI);
_radio = new SX1262(_module);
int state = _radio->begin(
LORA_FREQ_MHZ,
LORA_BW_KHZ,
LORA_SF,
LORA_CR,
LORA_SYNC_WORD,
LORA_TX_POWER_DBM);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa init failed, code %d", state);
return false;
}
state = _radio->startReceive();
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa startReceive failed, code %d", state);
return false;
}
_online = true;
INFO("LoRa init succeeded.");
return true;
}
void TBeamSupremeLoRaInterface::stop() {
if (_radio) {
_radio->standby();
}
_online = false;
}
void TBeamSupremeLoRaInterface::loop() {
if (!_online || !_radio) {
return;
}
if (!_radio->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
return;
}
int len = _radio->getPacketLength();
uint8_t rx_buf[255];
int state = _radio->readData(rx_buf, len);
if (state == RADIOLIB_ERR_NONE && len > 1) {
_last_rssi = _radio->getRSSI();
_last_snr = _radio->getSNR();
uint8_t header = rx_buf[0];
uint8_t seq = packet_sequence(header);
if (is_split_packet(header)) {
if (_rx_seq == SEQ_UNSET || _rx_seq != seq) {
_rx_seq = seq;
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
} else {
_rx_buffer.append(rx_buf + 1, len - 1);
_rx_seq = SEQ_UNSET;
on_incoming(_rx_buffer);
}
} else {
if (_rx_seq != SEQ_UNSET) {
_rx_buffer.clear();
_rx_seq = SEQ_UNSET;
}
_rx_buffer.clear();
_rx_buffer.append(rx_buf + 1, len - 1);
on_incoming(_rx_buffer);
}
} else if (state != RADIOLIB_ERR_NONE) {
DEBUGF("LoRa readData failed, code %d", state);
}
_radio->startReceive();
}
void TBeamSupremeLoRaInterface::send_outgoing(const Bytes& data) {
if (!_online || !_radio) {
return;
}
try {
uint8_t tx_buf[255];
uint8_t rand_nibble = (uint8_t)(Cryptography::randomnum(256)) & 0xF0;
if ((int)data.size() <= LORA_MAX_PAYLOAD) {
tx_buf[0] = rand_nibble;
memcpy(tx_buf + 1, data.data(), data.size());
int state = _radio->transmit(tx_buf, 1 + data.size());
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit failed, code %d", state);
}
} else {
uint8_t seq = (_tx_seq_ctr++) & HEADER_SEQ_MASK;
uint8_t split_header = rand_nibble | HEADER_SPLIT | seq;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data(), LORA_MAX_PAYLOAD);
int state = _radio->transmit(tx_buf, 1 + LORA_MAX_PAYLOAD);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 1 failed, code %d", state);
}
size_t remainder = data.size() - LORA_MAX_PAYLOAD;
tx_buf[0] = split_header;
memcpy(tx_buf + 1, data.data() + LORA_MAX_PAYLOAD, remainder);
state = _radio->transmit(tx_buf, 1 + remainder);
if (state != RADIOLIB_ERR_NONE) {
ERRORF("LoRa transmit part 2 failed, code %d", state);
}
}
_radio->startReceive();
InterfaceImpl::handle_outgoing(data);
} catch (const std::exception& e) {
ERRORF("LoRa transmit exception: %s", e.what());
}
}
void TBeamSupremeLoRaInterface::on_incoming(const Bytes& data) {
InterfaceImpl::handle_incoming(data);
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <Bytes.h>
#include <Interface.h>
#include <Arduino.h>
#include <RadioLib.h>
#include <SPI.h>
class TBeamSupremeLoRaInterface : public RNS::InterfaceImpl {
public:
explicit TBeamSupremeLoRaInterface(const char* name = "TBeamSupremeLoRa");
~TBeamSupremeLoRaInterface() override;
bool start() override;
void stop() override;
void loop() override;
float last_rssi() const { return _last_rssi; }
float last_snr() const { return _last_snr; }
private:
void send_outgoing(const RNS::Bytes& data) override;
void on_incoming(const RNS::Bytes& data);
static constexpr uint8_t HEADER_SPLIT = 0x08;
static constexpr uint8_t HEADER_SEQ_MASK = 0x07;
static constexpr uint8_t SEQ_UNSET = 0xFF;
static constexpr int LORA_MAX_PAYLOAD = 254;
static bool is_split_packet(uint8_t header) { return (header & HEADER_SPLIT) != 0; }
static uint8_t packet_sequence(uint8_t header) { return header & HEADER_SEQ_MASK; }
RNS::Bytes _rx_buffer;
uint8_t _rx_seq = SEQ_UNSET;
uint8_t _tx_seq_ctr = 0;
float _last_rssi = 0.0f;
float _last_snr = 0.0f;
Module* _module = nullptr;
SX1262* _radio = nullptr;
};

View file

@ -0,0 +1,228 @@
#include "TBeamSupremeLoRaInterface.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>
#ifndef NODE_LABEL
#define NODE_LABEL "?"
#endif
static constexpr const char* APP_NAME = "microreticulum";
static constexpr const char* APP_ASPECT = "linkping";
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.linkping";
static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface lora_interface({RNS::Type::NONE});
static RNS::Identity local_identity({RNS::Type::NONE});
static RNS::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 TBeamSupremeLoRaInterface* lora_impl = nullptr;
static bool should_initiate_link_to(const String& label) {
return strcmp(NODE_LABEL, label.c_str()) < 0;
}
static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
(void)packet;
float rssi = lora_impl ? lora_impl->last_rssi() : 0.0f;
float snr = lora_impl ? lora_impl->last_snr() : 0.0f;
Serial.printf("RX LINK: %s | RSSI=%.1f SNR=%.1f\r\n",
data.toString().c_str(), rssi, snr);
}
static void on_link_closed(RNS::Link& link) {
(void)link;
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.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.println("RX LINK: inbound link established");
}
class LinkAnnounceHandler : public RNS::AnnounceHandler {
public:
LinkAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}
void received_announce(const RNS::Bytes& destination_hash,
const RNS::Identity& announced_identity,
const RNS::Bytes& app_data) override {
String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
if (label == NODE_LABEL) {
return;
}
if (!announced_identity) {
Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
destination_hash.toHex().c_str());
return;
}
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("RX ANNOUNCE: label=%s hash=%s\r\n",
peer_label.c_str(), peer_hash.toHex().c_str());
}
};
static RNS::HAnnounceHandler announce_handler(new LinkAnnounceHandler());
static void print_config() {
Serial.printf("Node=%s\r\n", NODE_LABEL);
Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n",
(int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY,
(int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI);
Serial.printf("LoRa: freq=%.1f BW=%.1f SF=%d CR=%d sync=0x%02x txp=%d\r\n",
(double)LORA_FREQ_MHZ, (double)LORA_BW_KHZ, (int)LORA_SF,
(int)LORA_CR, (int)LORA_SYNC_WORD, (int)LORA_TX_POWER_DBM);
}
static void send_announce() {
if (!inbound_destination) {
return;
}
Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL);
inbound_destination.announce(RNS::bytesFromString(NODE_LABEL));
}
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 setup_reticulum() {
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
filesystem.init();
RNS::Utilities::OS::register_filesystem(filesystem);
lora_impl = new TBeamSupremeLoRaInterface();
lora_interface = lora_impl;
lora_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(lora_interface);
lora_interface.start();
reticulum = RNS::Reticulum();
reticulum.transport_enabled(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();
Serial.println("Exercise 203: microReticulum negotiated Link ping-pong");
print_config();
RNS::loglevel(RNS::LOG_NOTICE);
setup_reticulum();
Serial.println("microReticulum ready");
}
void loop() {
reticulum.loop();
static uint32_t next_announce_ms = 0;
static uint32_t next_link_tx_ms = 0;
static uint32_t iter = 0;
uint32_t now = millis();
if (next_announce_ms == 0) {
uint32_t offset = (NODE_LABEL[0] == 'A') ? 700 : 1700;
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_link_tx_ms == 0) {
uint32_t offset = should_initiate_link_to(peer_label) ? 1200 : 2400;
next_link_tx_ms = now + offset;
}
if (link_active && (int32_t)(now - next_link_tx_ms) >= 0) {
next_link_tx_ms = now + 3000;
String message = String(NODE_LABEL) + " -> " + peer_label + " iter=" + String(iter++);
Serial.printf("TX LINK: %s\r\n", message.c_str());
RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
}
delay(5);
}
int _write(int file, char* ptr, int len) {
(void)file;
int wrote = Serial.write(ptr, len);
Serial.flush();
return wrote;
}

View file

@ -0,0 +1,41 @@
## Measurement
There are two measurements systems used interchangeably: 1) Gauss and 2) microTelsas. The equivalency is:
1 G = 104 T = 100 μT (microtesla).
Example:
Strength of Earth's magnetic field at 0° latitude, 0° longitude: 3.2×105 T (31.869 μT)
Strength of a typical refrigerator magnet: 5×103 T (5 mT)
### Gauss (deprecated)
The gauss (symbol: G, sometimes Gs) is a unit of measurement of magnetic flux density, B (also known as magnetic induction or magnetic field). This system of measurement has been superceded by the metric system (see below), but it remain used in various fields and documents today, so it is important to realize Gauss is the older system replaced by the metric system.
Real world examples: the Earth's magnetic field at its surface 0.250.60 G
### microTeslas (current metric)
The tesla (symbol: T) is the unit of magnetic flux density (also called magnetic B-field) in the International System of Units (SI). The abbreviation SI (from French *Système international d'unités*), is the modern form of the **metric system** and the world's most widely used system of measurement. https://en.wikipedia.org/wiki/International_System_of_Units.
Source: https://en.wikipedia.org/wiki/Tesla_(unit)
The microtesla is 1/1,000,000, or a millionth, of a tesla.
1 tesla is a very strong force. Real world examples: a coil gap of a typical loudspeaker magnet is 1 T to 2.4 T or the strength of medical magnetic resonance imaging (aka "MRI") systems is 1.5 T to 3 T.
Note different units which can appear similar: "m" vs. "μ"
T tesla
mT millitesla [thousandth] = 103 T or 1/1000th of a tesla
μT microtesla [millionth] = 106 T or 1/1000th of a millitesla
## QMIC6310U
The Magnetometer installed of the T-Beam SUPREME is the QMC6310U manufactured by QST Corporation (Shanghai, China). QMC6310 is a three-axis magnetic sensor combined with an Application-Specific Integrated Circuit ("ASIC") that:
amplifies extremely small sensor signals
filters noise
converts analog signals → digital (ADC)
applies internal scaling and conditioning
formats data for I²C output
Not only does it measure raw magnetic fields for X,Y & Z, it then produces a 16 bit value that can be transmitted over the I²C line. All of this is performed in one silicon chip inside a "package" of 1.2mm (Length)*1.2mm (Width)*0.53mm (Height). 1°to 2°compass heading accuracy can be achieved by this chip with calibration.
### Modes
The chip can be configured into a variety of resolution modes. It will always produce a 16 signed integer and by setting the MagFullScaleRange, you can specify full-scale measurement range you are sampling. For example, FS_8G samples over a range of -8 G → +8 G, whereas FS_2G samples over a range of -2 G → +2 G. So the smaller the range, the higher the precision. If you were in a room with an MRI, FS_2G will clipp because there probably would be values in excess of +/-2 Gauss. But, if you are outside and free of metal and potential distortions, the FS_2G would give you the most accurate result.

View file

@ -0,0 +1,14 @@
# Design for Exercise 23 Magnetometer
directory exercises/23_magnetometer contains code which will take calibration readings and store the calibration samples on the SD card drive as file magnet_cal_YYYYMMDD_HHMISS.log.
The log should have a provenance header starting with hash tags such as:
# this file name:
# magnetometer mode:
# sample rate:
# Software name & build date:
# GPS coordinates
#
Location services via GPS
Disciplined time with update if 24 hours since last disciplining. See Excercise 11
Web services, including download and erase shall be included.

BIN
img/20260519_115429_Tue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

BIN
img/20260519_123829_Tue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB