- try both packet alignments: real RNode transmissions arrive with a 1-byte
leading offset vs a raw RNS packet, so parse at offset 0 and 1 and take the
alignment whose dest-hash validates
- accept on validated dest-hash (SHA256(name_hash||SHA256(pub)[:16])[:16]) and
treat the Ed25519 signature as a bonus: announces with large app_data get
truncated at the LoRa 255B MTU, which breaks the signature but not the
pubkey<->destination binding; report sig ok vs unverified(truncated)
- verified end-to-end on real hardware: decoded a live RNS RNode's (.116)
announces over the air with sig=ok, dest + display name extracted
All three networks now confirmed on real hardware: meshTastic, MeshCore, RNS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- "LOCK:<T|C|R>" serial command pins the radio to one PHY for debugging /
focused RX testing; "LOCK:" resumes the normal RTC scan
- clear rxFlag after a Reticulum announce TX so the TX-done interrupt no longer
makes the bridge log a phantom "reception" of its own packet
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- handleReticulumPacket: parse RNS packet header (HEADER_1/2, flags), extract
ANNOUNCE payload, reconstruct signed_data, verify Ed25519 signature with the
announced signing key, and confirm dest_hash = SHA256(name_hash ||
SHA256(pubkey)[:16])[:16]; register the destination (pubkey/app_data table)
- "RXR:<hex>" serial hook injects raw packets for radio-free validation
- verified against real RNS-generated announces: valid one decodes with correct
dest + app_data; tampered signature is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- RNS identity: X25519 (encryption, rweather Curve25519) + Ed25519 (signing,
orlp) keypairs, persisted in NVS; public_key = X25519pub || Ed25519pub
- dest hash = SHA256(name_hash || SHA256(pubkey)[:16])[:16] for "archy.messh"
- ANNOUNCE packet: [flags 0x01][hops 0][dest_hash 16][context 0][pubkey 64 +
name_hash 10 + random_hash 10 + Ed25519 sig 64 + app_data]; sig covers
dest_hash||pubkey||name_hash||random_hash||app_data (no ratchet)
- periodic auto-announce on the R window
- VERIFIED byte-valid against the real RNS 1.3.5 library: Identity.validate
accepts the signature and the dest hash matches (cross-validated offline;
no RNode hardware available to test on-air)
All three networks now transmit: meshTastic text, MeshCore advert+DM, RNS announce.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 869.525 MHz / 125 kHz / SF8 / CR4:5 (was a placeholder 867.2 MHz)
- matches the reticulum-daemon defaults + user's stated RNode interface
- NOT yet verified on air: no RNode transmitting to receive from, so RX
decode is still unbuilt/untestable. Sync word 0x12 assumed (RNode default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- sendMeshCoreText: plaintext (timestamp + flags + text) -> encryptThenMAC
(AES-128-ECB zero-padded + 2-byte HMAC-SHA256 prefix), wrapped as
[header route=flood|type=TXT_MSG][path_len 0][dest_hash][src_hash][cipher]
- serial "C:msg" now DMs the text to every heard MeshCore contact
- verified end-to-end: bridge DM decrypts to plaintext on a real node
Completes bidirectional MeshCore messaging (advert TX + DM RX + DM TX).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- vendor orlp/ed25519 (lib/ed25519, MIT — the exact lib MeshCore uses) for
Curve25519 ECDH key exchange; switch identity/advert signing to it (same
seed -> same pubkey, so peers still recognise us; sigs are byte-identical)
- store pubkeys from heard MeshCore adverts (contacts table) so a DM's
1-byte src_hash can be resolved to a full sender key
- decode PAYLOAD_TYPE_TXT_MSG: ECDH shared secret, HMAC-SHA256 MAC check,
AES-128-ECB decrypt, parse timestamp+text, drive the message modal
- verified end-to-end: real MeshCore node -> "hello from HP Pro Desk" decoded
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MeshCore: fix PHY to live narrowband config (869.618/62.5/SF8/CR5),
read off the test node's SELF_INFO — old 869.525/250/SF11 heard nothing
- MeshCore: on-chip Ed25519 identity (rweather/Crypto, NVS-persisted) and
signed ADVERT TX (verified valid + received by a real node) — the bridge
now shows up as a MeshCore contact
- Meshtastic: send text messages (portnum 1) reusing the announce path
- serial TX console with per-network targeting ("T:hi" / bare = all)
- longer dwell (3000ms) to sit through flood-rebroadcast bursts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device now transmits a NodeInfo (User protobuf: id/long_name/short_name)
on the LongFast primary channel every 60s, so it appears in other Meshtastic
clients as 'Reticutasticore' / 'RTC'. NodeNum derived from the MAC
(!4359d2d0). No public key advertised, so DMs to us fall back to channel
encryption — which the existing RX path already decodes and displays.
Confirmed on hardware: announce tx=0 (success) on the first T-window.
Also spec the planned message-received animation (upside-down spinning
smiley + lightning bolts -> Meshtastic-style modal) in the README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rebrand to RTC (Reticulum / meshTastic / meshCore); Meshtastic row = T.
- On-chip Meshtastic decode (mbedtls AES-128-CTR + protobuf) surfacing text
and node short-names, mirroring firmware/tools/meshtastic_decode.py.
- Device identity 'Reticutasticore' / 'RTC' (DEVICE_NAME) baked in for all
three networks; boot splash shows it.
- Trippy OLED: acid smiley with a periodic spinning-striped freak-out
(googly eyes + gaping mouth), punk 'bubble' RTC header, three live network
rows (counts + RX blips), last-decoded line.
- Hardware-confirmed: Meshtastic + MeshCore both received (0x12 MeshCore
sync word guess validated); Reticulum still placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AES-128-CTR (public LongFast key) + minimal protobuf walk. Verified against
packets our scanner captured on-air: decoded a live NodeInfo (longName
'Arch Optiplex', shortName 'ARCH') and POSITION packets. Serves as the
reference for the on-chip C++ port in src/main.cpp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add SSD1306 UI to the Phase 1 scanner: an animated acid smiley inside
expanding psychedelic rings, an 'RMC' (Reticulum/Meshtastic/meshCore)
strobe header, and three live network rows with per-network packet counts
and RX activity blips. Scan loop made non-blocking (millis-based dwell) so
the display animates smoothly (~20fps) while the radio rotates.
Honest note in-code: the single SX1262 is time-sliced across the three
PHYs (ARCHITECTURE.md §2); the UI shows all three armed but true concurrent
RX needs added radios (Phase 2). Pins U8g2 2.36.18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First Phase 1 increment toward the single-device 3-in-1: one Heltec V3
(ESP32-S3 + single SX1262) rotating the radio through the Meshtastic /
MeshCore / Reticulum PHY presets, logging every raw packet it demodulates
per dwell window. No framing/crypto/bridge yet — proves the RX loop first.
Validated on hardware: radio.begin() clean, all three presets cycle, and
the Meshtastic window received live broadcast packets (-36..-41 dBm). The
single-radio time-multiplex premise (ARCHITECTURE.md §2) holds.
MeshCore sync word and Reticulum PHY are flagged placeholders in CONFIGS[].
Deps pinned (espressif32 7.0.1, RadioLib 7.7.1). Adds venv/pycache/storage
to .gitignore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dispatcher deduped on inbound text but relayed a prefixed copy
("[net/sender] text"), so a re-heard relayed message hashed differently
and was re-relayed, stacking another prefix each hop — an unbounded loop,
exactly what the dedup was meant to prevent.
Add MessageBus.mark_seen() and record the on-air relayed string before
sending, so the bridge recognises and drops its own relayed copies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
close() stopped the asyncio loop without disconnecting first, leaving the
EventDispatcher._process_events() task and the serial transport pending
("Task was destroyed but it is pending!" / "Event loop is closed").
Now await MeshCore.disconnect() and let the transport's connection_lost
callback settle before stopping the loop, then join the thread.
Found by running the adapter against a live MeshCore-flashed Heltec V3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>