updating docs to reflect v1.4.2

This commit is contained in:
dude.eth
2026-03-07 17:38:45 -07:00
parent a32182140d
commit b48c9064b9
8 changed files with 883 additions and 1036 deletions

348
README.md
View File

@@ -1,296 +1,156 @@
# Ratdeck
Standalone [Reticulum](https://reticulum.network/) transport node + [LXMF](https://github.com/markqvist/LXMF) encrypted messenger, built for the [LilyGo T-Deck Plus](https://www.lilygo.cc/products/t-deck-plus) with integrated SX1262 LoRa radio.
**v1.4.2** | [Ratspeak.org](https://ratspeak.org)
Not an RNode. Not a gateway. A fully self-contained mesh node with a keyboard, touchscreen, trackball, and LoRa radio — no host computer required.
Standalone [Reticulum](https://reticulum.network/) mesh node + [LXMF](https://github.com/markqvist/LXMF) encrypted messenger for the [LilyGo T-Deck Plus](https://www.lilygo.cc/products/t-deck-plus).
## What This Does
Not an RNode. Not a gateway. A fully self-contained LoRa mesh communicator with a keyboard, trackball, and LVGL UI — no host computer required.
Ratdeck turns a T-Deck Plus into a Reticulum mesh node that can:
- **Send and receive encrypted messages** over LoRa (LXMF protocol, Ed25519 signatures)
- **Discover other nodes** automatically via Reticulum announces
- **Bridge LoRa to WiFi** so desktop Reticulum instances can reach the mesh
- **Connect to remote Reticulum nodes** over TCP (e.g., `rns.beleth.net`)
- **Store messages and contacts** on flash and SD card with automatic backup
- **Configure everything on-device** — no config files, no host tools
The device runs [microReticulum](https://github.com/attermann/microReticulum) (a C++ port of the Reticulum stack) directly on the ESP32-S3, with a register-level SX1262 LoRa driver.
```
+----------------------------------------------+
| [|||] Ratspeak [87%] |
+----------------------------------------------+
| |
| CONTENT AREA |
| 320x240, LVGL v8.4 |
+----------------------------------------------+
| Home Friends Msgs Peers Setup |
+----------------------------------------------+
```
## Features
| Category | Details |
|----------|---------|
| **Networking** | Reticulum transport node, path discovery, announce propagation, auto-announce every 5 min |
| **Messaging** | LXMF encrypted messages, Ed25519 signatures, delivery tracking, per-conversation storage |
| **LoRa Radio** | SX1262 at 915 MHz, configurable SF (5-12), BW (7.8-500 kHz), CR (4/5-4/8), TX power (2-22 dBm) |
| **WiFi** | AP mode (TCP server on :4242) or STA mode (TCP client to remote nodes) — not concurrent |
| **BLE** | NimBLE Sideband interface for Reticulum over Bluetooth |
| **Storage** | Dual-backend: LittleFS (7.8 MB on flash) + FAT32 microSD, atomic writes, identity backup |
| **Display** | 320x240 IPS TFT via LovyanGFX, signal green on black, double-buffered sprite rendering |
| **Input** | Full QWERTY keyboard (ESP32-C3 I2C), GT911 capacitive touchscreen, optical trackball |
| **Audio** | I2S codec, notification sounds for messages, announces, errors, boot chime |
| **GPS** | UBlox MIA-M10Q GNSS (pins defined, v1.1) |
| **Power** | Screen dim/off/wake on input, configurable timeouts, battery % in status bar |
| **Reliability** | Boot loop recovery (NVS counter, forces WiFi OFF after 3 failures) |
| **Diagnostics** | Ctrl+D full dump, Ctrl+T radio test packet, Ctrl+R 5-second RSSI monitor |
- **Encrypted LoRa messaging** — LXMF protocol, Ed25519 signatures, per-conversation storage
- **Mesh networking** — Reticulum endpoint or transport node, automatic path discovery
- **Node discovery** — see who's online, save contacts, manage friends
- **WiFi bridging** — TCP client to remote Reticulum nodes, or AP mode to bridge your desktop to LoRa
- **BLE transport** — NimBLE Sideband interface
- **On-device config** — 7-category settings, radio presets, multi-slot identity management
- **Dual storage** — LittleFS flash + SD card with atomic writes and automatic backup
- **OTA-ready** — check for firmware updates directly from the device
Built on [microReticulum](https://github.com/attermann/microReticulum) with a register-level SX1262 driver and LVGL v8.4.
## Hardware
| Component | Part | Notes |
|-----------|------|-------|
| **Board** | LilyGo T-Deck Plus | ESP32-S3, 16MB flash, PSRAM, 320x240 IPS TFT, QWERTY keyboard, trackball, touchscreen |
| **Radio** | Integrated SX1262 | 915 MHz ISM, TCXO 1.8V, DIO2 RF switch, shared SPI bus |
| **Board** | LilyGo T-Deck Plus | ESP32-S3, 16MB flash, 8MB PSRAM, 320x240 IPS, QWERTY keyboard, trackball |
| **Radio** | Integrated SX1262 | 915 MHz ISM, TCXO 1.8V, DIO2 RF switch |
| **Storage** | microSD card | Optional but recommended. FAT32, any size |
| **USB** | USB-C | USB-Serial/JTAG. Port: `/dev/cu.usbmodem*` (macOS), `/dev/ttyACM*` (Linux) |
See [docs/PINMAP.md](docs/PINMAP.md) for the full GPIO pin map.
## Prerequisites
| Requirement | Version | Install |
|-------------|---------|---------|
| **Python** | 3.12+ | [python.org](https://www.python.org/downloads/) or your package manager |
| **PlatformIO Core** | 6.x | `pip install platformio` |
| **Git** | any | Your package manager |
| **USB driver** | — | None needed on macOS/Linux (ESP32-S3 USB-Serial/JTAG is built-in) |
PlatformIO automatically downloads the ESP32-S3 toolchain, Arduino framework, and all library dependencies on first build.
## Build and Flash
## Quick Start
```bash
# Clone
git clone https://github.com/defidude/Ratdeck.git
cd Ratdeck
# Build (first build takes ~2 min to download toolchain + deps)
python3 -m platformio run -e ratdeck_915
# Flash (plug in T-Deck Plus via USB-C)
python3 -m platformio run -e ratdeck_915 -t upload --upload-port /dev/cu.usbmodem*
python3 -m platformio run # build
python3 -m platformio run --target upload # flash
```
> If `pio` is not on your PATH after install, use `python3 -m platformio` everywhere.
First build pulls the ESP32-S3 toolchain and all dependencies automatically.
### Alternative: esptool
### Web Flash
PlatformIO's default baud sometimes fails over USB-Serial/JTAG. esptool at 460800 is more reliable:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* --baud 460800 \
--before default-reset --after hard-reset \
write_flash -z 0x10000 .pio/build/ratdeck_915/firmware.bin
```
See [docs/BUILDING.md](docs/BUILDING.md) for merged binaries, build flags, partition table, and CI/CD details.
No build tools? Visit **[ratspeak.org/flash](https://ratspeak.org/flash)** to flash from your browser.
## First Boot
1. Plug in or power on the T-Deck Plus
2. Boot animation with progress bar (~3 seconds)
3. SX1262 radio initializes at 915 MHz
4. SD card checked, `/ratdeck/` directories auto-created
5. Reticulum identity generated (Ed25519 keypair, persisted to flash + SD)
6. WiFi AP starts: `ratdeck-XXXX` (password: `ratspeak`)
7. Initial announce broadcast to the mesh
8. Home screen: identity hash, transport status, radio info, uptime
1. Boot animation with progress bar
2. SX1262 radio initializes (915 MHz, Balanced preset)
3. SD card checked, `/ratputer/` directories auto-created
4. Reticulum identity generated (Ed25519 keypair, triple-redundant backup)
5. Name input screen (optional — auto-generates if skipped)
6. Home screen — you're on the mesh
## UI Layout
```
+------------------------------------------+
| [87%] [Ratspeak.org] [LoRa] | Status bar: battery, transport mode, radio
+------------------------------------------+
| |
| CONTENT AREA | Screens: Home, Messages, Nodes, Map, Settings
| 320 x 240 |
| |
+------------------------------------------+
| [Home] [Msgs] [Nodes] [Map] [Setup] | Tab bar with unread badges
+------------------------------------------+
```
**Theme**: Signal green (#00FF41) on black. 320x240 pixels. LovyanGFX double-buffered sprite rendering.
## Keyboard and Hotkeys
### Hotkeys (Ctrl+key)
| Shortcut | Action |
|----------|--------|
| Ctrl+H | Toggle help overlay (shows all hotkeys on screen) |
| Ctrl+M | Jump to Messages tab |
| Ctrl+N | Compose new message |
| Ctrl+S | Jump to Settings tab |
| Ctrl+A | Force announce to network (immediate) |
| Ctrl+D | Dump full diagnostics to serial |
| Ctrl+T | Send radio test packet |
| Ctrl+R | RSSI monitor (5 seconds continuous sampling) |
## Usage
### Navigation
| Key | Action |
|-----|--------|
| Trackball | Scroll / navigate |
| Touch | Tap to select UI elements |
| Enter | Select / confirm / send message |
| Esc | Back / cancel |
| Backspace | Delete character in text input |
| Input | Action |
|-------|--------|
| Trackball up/down | Scroll lists |
| Trackball left/right | Cycle tabs |
| Trackball click | Select / confirm |
| Long-press (1.2s) | Context menu |
| `,` / `/` | Previous / next tab |
| Enter | Select / send |
| Esc / Backspace | Back / cancel |
| Ctrl+H | Hotkey help overlay |
## WiFi and Networking
### Tabs
Three WiFi modes, configured in Settings:
| Tab | What It Shows |
|-----|---------------|
| **Home** | Name, LXMF address, connection status, online nodes |
| **Friends** | Saved contacts with display names |
| **Msgs** | Conversations — sorted by most recent, preview, unread dots |
| **Peers** | All discovered nodes with RSSI/SNR |
| **Setup** | Device, Display, Radio, Network, Audio, Info, System |
### OFF Mode
### Sending a Message
No WiFi. Saves power and heap. LoRa-only operation.
### AP Mode (default)
Creates a WiFi hotspot named `ratdeck-XXXX` (password: `ratspeak`). Runs a TCP server on port 4242 with HDLC framing.
**Bridge to desktop Reticulum**: Connect your laptop to the `ratdeck-XXXX` network, then add to your Reticulum config (`~/.reticulum/config`):
```ini
[[ratdeck]]
type = TCPClientInterface
target_host = 192.168.4.1
target_port = 4242
```
Now your desktop `rnsd` can reach the LoRa mesh through Ratdeck.
### STA Mode
Connects to an existing WiFi network. Establishes outbound TCP connections to configured Reticulum endpoints.
**Setup**:
1. Ctrl+S, WiFi, Mode: **STA**
2. Enter your WiFi SSID and password
3. Add TCP endpoints: e.g., `rns.beleth.net` port `4242`, auto-connect enabled
4. Save — Ratdeck connects to your WiFi, then opens TCP links to the configured hosts
1. Another node appears in **Peers** (or connect via WiFi)
2. Select the node, press Enter to open chat
3. Type your message, press Enter to send
4. Status: yellow (sending) → green (delivered) → red (failed)
## LoRa Radio
### Default Configuration
Three presets in Settings → Radio:
| Parameter | Default | Range |
|-----------|---------|-------|
| Frequency | 915 MHz | Hardware-dependent |
| Spreading Factor | SF9 | SF5-SF12 |
| Bandwidth | 125 kHz | 7.8 kHz - 500 kHz |
| Coding Rate | 4/5 | 4/5 - 4/8 |
| TX Power | 22 dBm | 2-22 dBm |
| Preamble | 18 symbols | Configurable |
| Sync Word | 0x1424 | Reticulum standard |
| Max Packet | 255 bytes | SX1262 hardware limit |
| Preset | SF | BW | CR | TX | Use Case |
|--------|----|----|----|----|----------|
| **Balanced** | 9 | 250 kHz | 4/5 | 14 dBm | General use |
| Long Range | 12 | 125 kHz | 4/8 | 17 dBm | Maximum range |
| Fast | 7 | 500 kHz | 4/5 | 10 dBm | Short range, fast |
All parameters configurable via Settings. Changes take effect immediately and persist across reboots.
All parameters individually configurable. Changes apply immediately.
## SD Card
## WiFi & Networking
Optional microSD card (FAT32, any size). Provides backup storage and extended capacity beyond the LittleFS partition.
| Mode | Description |
|------|-------------|
| **STA** (default) | Connect to your WiFi, TCP client to remote Reticulum nodes |
| **AP** | Creates `ratdeck-XXXX` hotspot, TCP server on port 4242 |
| **OFF** | LoRa only, saves power |
### Directory Structure (auto-created on first boot)
**Connect to the mesh over WiFi:** Settings → Network → enter WiFi creds → add TCP endpoint (e.g., `rns.beleth.net:4242`).
```
/ratdeck/
config/
user.json Runtime settings (radio, WiFi, display, audio)
messages/
<peer_hex>/ Per-conversation message history (JSON)
contacts/ Discovered Reticulum nodes
identity/
identity.key Ed25519 keypair backup
```
**Bridge your desktop to LoRa:** Set AP mode → connect laptop to `ratdeck-XXXX` (password: `ratspeak`) → add `TCPClientInterface` to `~/.reticulum/config` pointing at `192.168.4.1:4242`.
## Dependencies
## Transport Mode
All automatically managed by PlatformIO — no manual installation needed:
Default: **endpoint** (handles own traffic). Enable **Transport Node** in Settings → Network to relay packets and maintain routing tables for the mesh.
| Library | Version | Purpose |
|---------|---------|---------|
| [microReticulum](https://github.com/attermann/microReticulum) | git HEAD | Reticulum protocol stack (C++ port) |
| [Crypto](https://github.com/attermann/Crypto) | git HEAD | Ed25519, X25519, AES-128, SHA-256, HMAC |
| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | ^7.4.2 | JSON serialization for config and message storage |
| [LovyanGFX](https://github.com/lovyan03/LovyanGFX) | ^1.1.16 | Display driver — SPI, DMA, sprite double-buffering |
| [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) | ^2.1 | BLE stack for Sideband interface |
### Build Toolchain
| Component | Version | Notes |
|-----------|---------|-------|
| PlatformIO | espressif32@6.7.0 | ESP-IDF + Arduino framework |
| Board | esp32-s3-devkitc-1 | Generic ESP32-S3, 16MB flash, PSRAM |
| Arduino Core | ESP32 Arduino 2.x | C++17, exceptions enabled |
## Flash Memory Layout
16MB flash, partitioned for OTA support:
| Partition | Offset | Size | Purpose |
|-----------|--------|------|---------|
| nvs | 0x9000 | 20 KB | Boot counter, WiFi credentials |
| otadata | 0xE000 | 8 KB | OTA boot selection |
| app0 | 0x10000 | 4 MB | Active firmware |
| app1 | 0x410000 | 4 MB | OTA update slot (reserved) |
| littlefs | 0x810000 | 7.8 MB | Identity, config, messages, transport paths |
| coredump | 0xFF0000 | 64 KB | ESP-IDF crash dump |
## Project Structure
```
Ratdeck/
src/
main.cpp Entry point: setup() + main loop
config/ BoardConfig.h (pins), Config.h (compile-time), UserConfig.* (runtime JSON)
radio/ SX1262.* (register-level driver), RadioConstants.h
hal/ Display (LovyanGFX), TouchInput (GT911), Trackball, Keyboard, Power, GPS, Audio
input/ InputManager (unified input), HotkeyManager (Ctrl+key dispatch)
ui/ UIManager, StatusBar, TabBar, Theme
screens/ Boot, Home, Messages, MessageView, Nodes, Map, Settings, HelpOverlay
reticulum/ ReticulumManager, AnnounceManager, LXMFManager, LXMFMessage
transport/ LoRaInterface, WiFiInterface, TCPClientInterface, BLEInterface, BLESideband
storage/ FlashStore (LittleFS), SDStore (FAT32), MessageStore (dual)
power/ PowerManager (dim/off/wake)
audio/ AudioNotify (boot, message, announce, error sounds)
docs/ BUILDING, PINMAP, TROUBLESHOOTING, DEVELOPMENT, ARCHITECTURE, QUICKSTART, HOTKEYS
platformio.ini Build configuration (single env: ratdeck_915)
partitions_16MB.csv Flash partition table
.github/workflows/ CI: build on push, release merged binary on tag
```
## Documentation
| Document | Contents |
|----------|----------|
| [Quick Start](docs/QUICKSTART.md) | First build, first boot, navigation, WiFi setup, SD card |
| [Building](docs/BUILDING.md) | Build commands, flashing, merged binaries, CI/CD, build flags |
| [Pin Map](docs/PINMAP.md) | Full GPIO assignments for all peripherals |
| [Hotkeys](docs/HOTKEYS.md) | Complete keyboard reference |
| [Architecture](docs/ARCHITECTURE.md) | Layer diagram, directory tree, design decisions |
| [Development](docs/DEVELOPMENT.md) | How to add screens, hotkeys, settings, transports |
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Radio, build, boot loop, storage, WiFi issues |
## Current Status
**v1.0.0** — Working on hardware.
## Status
| Subsystem | Status |
|-----------|--------|
| LoRa radio | Working — TX/RX verified |
| WiFi AP | Working — TCP server, HDLC framing, bridges to desktop rnsd |
| WiFi STA + TCP | Working — connects to remote Reticulum nodes |
| LXMF messaging | Working — send/receive/store with Ed25519 signatures |
| Node discovery | Working — automatic announce processing |
| SD card storage | Working — dual-backend with atomic writes |
| Settings | Working — full on-device configuration |
| Touchscreen | Working — GT911 capacitive touch |
| Trackball | Working — optical navigation |
| BLE Sideband | Working — NimBLE interface |
| GPS | Pins defined — v1.1 |
| OTA updates | Partition reserved — not implemented |
| LoRa (SX1262) | Working — async TX/RX, configurable presets |
| LXMF messaging | Working — send/receive/store, Ed25519, delivery tracking |
| LVGL UI | Working — 5 tabs, chat bubbles, settings, contacts |
| WiFi STA + TCP | Working — auto-reconnect, live server switching |
| WiFi AP | Working — TCP server, HDLC framing, desktop bridge |
| BLE Sideband | Working — NimBLE transport |
| Node discovery | Working — announces, friend management |
| SD + Flash storage | Working — dual-backend, atomic writes |
| Identity management | Working — multi-slot, triple-redundant |
| Transport mode | Working — endpoint or relay |
| Touchscreen | Disabled — GT911 needs calibration |
| GPS | Pins defined — not yet implemented |
## Documentation
| Doc | Contents |
|-----|----------|
| **[Quick Start](docs/QUICKSTART.md)** | Build, flash, first boot, first message |
| [Building](docs/BUILDING.md) | Build flags, flashing, CI/CD, partition table |
| [Architecture](docs/ARCHITECTURE.md) | Layer diagram, data flow, design decisions |
| [Development](docs/DEVELOPMENT.md) | Adding screens, settings, transports, debugging |
| [Pin Map](docs/PINMAP.md) | Full T-Deck Plus GPIO assignments |
| [Hotkeys](docs/HOTKEYS.md) | Keyboard shortcuts and navigation |
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Radio, build, boot, storage issues |
## License

View File

@@ -1,83 +1,99 @@
# Ratputer — Architecture
# Ratdeck — Architecture
## Overview
Ratputer is a standalone Reticulum transport node with LXMF messaging, built for the M5Stack Cardputer Adv with Cap LoRa-1262 radio module.
Ratdeck is a standalone Reticulum mesh node with LXMF encrypted messaging, built for the LilyGo T-Deck Plus (ESP32-S3). It runs the full Reticulum protocol stack on-device — no host computer, no gateway, no KISS protocol. LoRa, WiFi, and BLE transports operate independently or bridged.
## Layer Diagram
```
┌─────────────────────────────────────┐
UI Layer (M5Canvas)
Screens: Home, Msgs, Nodes, Setup
Widgets: ScrollList, TextInput
StatusBar, TabBar, HelpOverlay
├─────────────────────────────────────┤
Application Layer
LXMFManager AnnounceManager
UserConfig AudioNotify
│ PowerManager MessageStore │
├─────────────────────────────────────┤
Reticulum Layer
ReticulumManager (microReticulum) │
Identity, Destination, Transport
├─────────────────────────────────────┤
Transport Layer
LoRaInterface WiFiInterface
TCPClientInterface BLEStub
├─────────────────────────────────────┤
Storage Layer
FlashStore (LittleFS)
SDStore (FAT32 microSD)
├─────────────────────────────────────┤
│ Hardware Layer │
SX1262 Radio M5Cardputer │
LittleFS ESP32-S3
└─────────────────────────────────────┘
+---------------------------------------------+
| UI Layer (LVGL v8.4) |
| Screens: Home, Friends, Msgs, Peers, Setup |
| LvStatusBar, LvTabBar, LvInput |
| Theme: Matrix green (#00FF41) on black |
+---------------------------------------------+
| Application Layer |
| LXMFManager AnnounceManager |
| IdentityManager UserConfig |
| MessageStore AudioNotify |
+---------------------------------------------+
| Reticulum Layer |
| ReticulumManager (microReticulum C++) |
| Identity, Destination, Transport |
+---------------------------------------------+
| Transport Layer |
| LoRaInterface WiFiInterface |
| TCPClientInterface BLESideband |
+---------------------------------------------+
| Storage Layer |
| FlashStore (LittleFS, 7.875 MB) |
| SDStore (FAT32 microSD) |
| NVS (ESP32 Preferences) |
+---------------------------------------------+
| Hardware Layer |
| SX1262 Radio ST7789V Display |
| I2C Keyboard GPIO Trackball |
| ESP32-S3 (8MB PSRAM, 16MB Flash) |
+---------------------------------------------+
```
## Directory Structure
```
src/
├── main.cpp Main entry point (setup + loop)
├── config/
├── BoardConfig.h Pin definitions, hardware constants
├── Config.h Compile-time settings, feature flags, paths
└── UserConfig.* Runtime settings (JSON, dual SD+flash backend)
├── radio/
├── SX1262.* SX1262 LoRa driver (register-level)
└── RadioConstants.h Register definitions
├── input/
├── Keyboard.* M5Cardputer keyboard wrapper
└── HotkeyManager.* Ctrl+key dispatch
├── ui/
├── Theme.h Color palette, layout metrics
├── UIManager.* Canvas rendering, screen stack
│ ├── Screen.h Abstract base class
├── StatusBar.* Top bar (battery, transport, LoRa)
├── TabBar.* Bottom tab navigation
│ ├── screens/ Per-tab screen implementations
├── widgets/ Reusable UI components
└── assets/ Boot logo
├── reticulum/
├── ReticulumManager.* microReticulum integration
├── AnnounceManager.* Node discovery, contact persistence
├── LXMFManager.* LXMF messaging protocol
└── LXMFMessage.* Message format (MsgPack wire, JSON storage)
├── transport/
├── LoRaInterface.* SX1262 ↔ Reticulum bridge (1-byte header)
│ ├── WiFiInterface.* WiFi AP transport, TCP server on :4242
│ ├── TCPClientInterface.* WiFi STA transport, TCP client to remote nodes
└── BLEStub.* BLE advertising placeholder (disabled)
├── storage/
│ ├── FlashStore.* LittleFS with atomic writes
├── SDStore.* SD card (FAT32) with atomic writes + wipe
│ └── MessageStore.* Per-conversation storage (dual: flash + SD)
├── power/
└── PowerManager.* Screen dim/off/wake
└── audio/
└── AudioNotify.* Notification sounds
+-- main.cpp Setup + main loop (single-threaded, core 1)
+-- config/
| +-- BoardConfig.h GPIO pins, SPI config, hardware constants
| +-- Config.h Version, feature flags, storage paths, limits
| +-- UserConfig.* Runtime settings (JSON, dual SD+flash backend)
+-- radio/
| +-- SX1262.* SX1262 LoRa driver (register-level, async TX)
| +-- RadioConstants.h SX1262 register addresses and command bytes
+-- hal/
| +-- Display.* LovyanGFX ST7789V driver + LVGL flush callback
| +-- Power.* Screen dim/off/wake, battery ADC, brightness
| +-- Keyboard.* I2C keyboard (ESP32-C3 at 0x55)
| +-- Trackball.* GPIO trackball (ISR-based, 5 pins)
| +-- TouchInput.* GT911 capacitive touch (disabled)
+-- input/
| +-- InputManager.* Unified keyboard + trackball + touch polling
| +-- HotkeyManager.* Ctrl+key dispatch table
+-- ui/
| +-- Theme.h Color palette, layout constants
| +-- LvTheme.* LVGL theme application
| +-- LvStatusBar.* Signal bars + brand + battery %
| +-- LvTabBar.* 5-tab bar with unread badges
| +-- LvInput.* Key event routing to active screen
| +-- UIManager.* Screen lifecycle, boot/normal modes
| +-- screens/
| +-- LvBootScreen.* Boot animation with progress bar
| +-- LvHomeScreen.* Name, LXMF address, status, online nodes
| +-- LvContactsScreen.* Saved friends (display name only)
| +-- LvMessagesScreen.* Conversation list (sorted, previews, unread dots)
| +-- LvMessageView.* Chat bubbles + status colors + text input
| +-- LvNodesScreen.* Node browser (contacts + online sections)
| +-- LvSettingsScreen.* 7-category settings editor
| +-- LvHelpOverlay.* Hotkey reference modal
| +-- LvNameInputScreen.* First-boot name entry
+-- reticulum/
| +-- ReticulumManager.* microReticulum lifecycle, transport loop
| +-- AnnounceManager.* Node discovery, friend persistence
| +-- LXMFManager.* LXMF send/receive, queue, delivery tracking
| +-- LXMFMessage.* Wire format + JSON storage format
| +-- IdentityManager.* Multi-slot identity management
+-- transport/
| +-- LoRaInterface.* SX1262 <-> Reticulum (1-byte header)
| +-- WiFiInterface.* WiFi AP, TCP server on port 4242
| +-- TCPClientInterface.* WiFi STA, TCP client to remote nodes
| +-- BLEInterface.* BLE transport
| +-- BLESideband.* NimBLE Sideband interface
+-- storage/
| +-- FlashStore.* LittleFS with atomic writes
| +-- SDStore.* SD card (FAT32) with atomic writes
| +-- MessageStore.* Per-conversation dual-backend storage
+-- audio/
+-- AudioNotify.* Notification sounds
```
## Data Flow
@@ -85,67 +101,99 @@ src/
### Incoming LoRa Packet
```
SX1262 IRQ (DIO1) → SX1262::receive() reads FIFO
LoRaInterface::loop() strips 1-byte header
RNS::InterfaceImpl::receive_incoming()
RNS::Transport processes packet
├── Announce AnnounceManager callback UI update
├── LXMF data LXMFManager MessageStore (flash + SD) UI notification
└── Path/link Transport table update persist to flash
SX1262 IRQ (DIO1, GPIO 45) -> SX1262::receive() reads FIFO
-> LoRaInterface::loop() strips 1-byte header
-> RNS::InterfaceImpl::receive_incoming()
-> RNS::Transport processes packet
+-- Announce -> AnnounceManager callback -> UI update
+-- LXMF data -> LXMFManager -> MessageStore (flash + SD) -> UI notification
+-- Path/link -> Transport table update -> persist to flash + SD backup
```
### Outgoing LXMF Message
```
User types message MessageView LXMFManager::send()
→ Pack: source_hash(16) + msgpack([ts, content, title, fields]) + Ed25519 sig(64)
→ RNS::Packet → RNS::Transport selects interface
├── LoRaInterface → prepend 1-byte header → SX1262::beginPacket/endPacket
└── WiFi/TCPClient → HDLC frame (0x7E delimit, 0x7D escape) → TCP socket
User types message -> LvMessageView -> LXMFManager::send()
-> Save to MessageStore (QUEUED status)
-> Pack: dest_hash(16) + src_hash(16) + signature(64) + msgpack([ts, title, content, fields])
-> RNS::Packet -> RNS::Transport selects interface
+-- LoRaInterface -> prepend 1-byte header -> SX1262::beginPacket/endPacket (async)
+-- TCPClient -> HDLC frame (0x7E delimit, 0x7D escape) -> TCP socket
-> StatusCallback fires -> re-save with SENT/DELIVERED/FAILED status
```
### Config Save
```
SettingsScreen UserConfig::save(sd, flash)
├── serialize to JSON string
├── SDStore::writeAtomic("/ratputer/config/user.json") .tmp verify .bak rename
└── FlashStore::writeAtomic("/config/user.json") .tmp verify .bak rename
LvSettingsScreen -> UserConfig::save(sd, flash)
+-- serialize to JSON string
+-- SDStore::writeAtomic("/ratputer/config/user.json") -> .tmp -> verify -> .bak -> rename
+-- FlashStore::writeAtomic("/config/user.json") -> .tmp -> verify -> .bak -> rename
```
## Key Design Decisions
### Radio Driver
Extracted from RNode_Firmware_CE, stripped of multi-interface and CSMA/CA. Custom SPI (HSPI) with TCXO 3.0V configuration. IRQ stale latch fix applied to prevent DCD lockup after first TX.
Register-level SX1262 driver with no library abstraction. Async TX mode (`endPacket(true)`) returns immediately — `isTxBusy()` polls completion in `LoRaInterface::loop()`. `waitOnBusy()` calls a yield callback so LVGL stays responsive during SPI waits. TCXO 1.8V, DIO2 as RF switch, 8 MHz SPI clock.
### Display
Double-buffered M5Canvas sprite (240×135 RGB565). All rendering goes through UIManager which handles status bar, content area clipping, tab bar, and overlay.
LovyanGFX drives the ST7789V over SPI at 15 MHz. LVGL v8.4 renders to a flush buffer with DMA transfer. 320x240 landscape, 20px status bar top, 20px tab bar bottom, 200px content area.
### Reticulum Integration
microReticulum C++ library with LittleFS-backed filesystem. Device runs as a Transport Node with LoRa and WiFi/TCP interfaces registered.
### LXMF Messages
Wire format: `source_hash(16) + msgpack([timestamp, content, title, fields]) + signature(64)`. Direct packet delivery for messages under MDU. Stored as JSON per-conversation in flash and SD.
microReticulum C++ library with LittleFS-backed filesystem. Device runs as either an Endpoint (default) or Transport Node (configurable in Settings). All interfaces (LoRa, WiFi, TCP, BLE) register with `RNS::Transport`.
### LXMF Wire Format
```
[dest_hash:16][src_hash:16][signature:64][msgpack_payload]
```
MsgPack payload: `fixarray(4): [timestamp(float64), title(str), content(str), fields(map)]`
Signature covers: `dest_hash || src_hash || msgpack_payload`
Messages under MDU (~254 bytes for LoRa) are sent as single direct packets. Stored as JSON per-conversation in flash and SD.
### Shared SPI Bus
Display, SX1262 radio, and SD card all share SPI2_HOST (FSPI on ESP32-S3). Single-threaded access from `loop()` — no mutex needed. Arduino FSPI=0 maps to SPI2 hardware; do NOT use `SPI2_HOST` IDF constant directly.
### WiFi Transport
Three separate modes (not concurrent):
- **OFF**: No WiFi — saves power and ~20KB heap
- **AP**: Creates hotspot, TCP server on port 4242 with HDLC framing (0x7E delimiters, 0x7D escape)
Three mutually exclusive modes:
- **OFF**: No WiFi — saves power and ~20 KB heap
- **AP**: Creates `ratdeck-XXXX` hotspot, TCP server on port 4242, HDLC framing
- **STA**: Connects to existing network, TCP client connections to configured endpoints
AP+STA concurrent mode was removed — it consumed too much heap and caused instability.
AP+STA concurrent mode was removed — consumed too much heap and caused instability.
### TCP Client Transport
Outbound TCP connections to remote Reticulum nodes. Created on first WiFi STA connection, auto-reconnect on disconnect. Uses same HDLC framing as WiFi AP.
Outbound TCP connections to remote Reticulum nodes. Created on WiFi STA connection, auto-reconnect on disconnect (15s interval). Settings changes apply live via `_tcpChangeCb` callback — `reloadTCPClients()` stops old clients and creates new ones. Switching servers clears transient nodes.
### Dual-Backend Storage
FlashStore (LittleFS) is the primary store. SDStore (FAT32 microSD) provides backup and extended capacity. Both use atomic writes (.tmp → verify → .bak → rename) to prevent corruption on power loss. UserConfig, MessageStore, and AnnounceManager write to both backends.
### Boot Loop Recovery
NVS counter tracks consecutive boot failures. After 3 failures, WiFi is forced OFF on next boot (WiFi init is the most common crash source). Counter resets to 0 at end of successful setup().
FlashStore (LittleFS, 7.875 MB partition) is the primary store. SDStore (FAT32 microSD) provides backup and extended capacity. Both use atomic writes (`.tmp` -> verify -> `.bak` -> rename) to prevent corruption on power loss. UserConfig, MessageStore, and AnnounceManager write to both backends.
### Identity System
Triple-redundant storage: Flash (LittleFS), SD card, and NVS (ESP32 Preferences). Load order: Flash -> SD -> NVS -> generate new. On any successful load or creation, saves to all three tiers. Multi-slot identity management with per-slot display names.
### Transport Reference Stability
`RNS::Transport::_interfaces` stores `Interface&` references (not copies). All `RNS::Interface` wrappers must outlive the transport — stored in `std::list` (not vector, which would invalidate references on reallocation).
### Power Management
Three states: Active → Dimmed (25% brightness) → Screen Off. Wakes on any keypress. Configurable timeouts via Settings.
Three states: ACTIVE -> DIMMED -> SCREEN_OFF. Strong activity (keyboard, trackball nav/click) wakes from any state. Weak activity (trackball raw movement) wakes from DIMMED only. Configurable timeouts via Settings.
### Boot Loop Recovery
NVS counter tracks consecutive boot failures. After 3 failures, WiFi is forced OFF on next boot. Counter resets to 0 at end of successful `setup()`.
### Input System
Trackball click uses deferred release with 80ms GPIO debounce. Long press fires at 1200ms hold, suppresses click on release. Nav events are rate-limited to 200ms with threshold of 3 accumulated deltas. I2C keyboard at address 0x55 with interrupt on GPIO 46.

View File

@@ -1,60 +1,74 @@
# Ratputer — Build & Flash Reference
# Ratdeck — Build & Flash Reference
## Prerequisites
- **Python 3.12+** (for PlatformIO and esptool)
- **PlatformIO Core** (CLI): `pip install platformio`
- **esptool** (usually installed with PlatformIO, or `pip install esptool`)
- **Python 3.x** with PlatformIO: `pip install platformio`
- **Git**
- **USB-C cable** (data-capable)
No USB drivers needed on macOS or Linux — the ESP32-S3's USB-Serial/JTAG interface is natively supported.
> **Note**: PlatformIO may not be on your PATH after pip install. Use `python3 -m platformio` if `pio` is not found. This applies to all `pio` commands throughout this document.
> If `pio` is not on your PATH after install, use `python3 -m platformio` for all commands in this document.
## Build
```bash
python3 -m platformio run -e ratputer_915
git clone https://github.com/defidude/Ratdeck.git
cd Ratdeck
python3 -m platformio run
```
Output binary: `.pio/build/ratputer_915/firmware.bin`
Output: `.pio/build/ratdeck_915/firmware.bin`
First build downloads all dependencies automatically (M5Unified, M5GFX, M5Cardputer, microReticulum, Crypto, ArduinoJson).
First build downloads all dependencies automatically (microReticulum, Crypto, ArduinoJson, LovyanGFX, NimBLE-Arduino, LVGL).
## Flash
### Via PlatformIO (simple)
### Via PlatformIO
```bash
python3 -m platformio run -e ratputer_915 -t upload --upload-port /dev/cu.usbmodem*
python3 -m platformio run --target upload
```
### Via esptool (more reliable)
PlatformIO defaults to 921600 baud which sometimes fails. esptool at 460800 is more reliable:
PlatformIO defaults to the upload speed in `platformio.ini` (460800 baud). If flashing fails, use esptool directly:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* --baud 460800 \
--before default-reset --after hard-reset \
write-flash -z 0x10000 .pio/build/ratputer_915/firmware.bin
write-flash -z 0x10000 .pio/build/ratdeck_915/firmware.bin
```
### Download Mode
If the device doesn't respond to flash commands, enter download mode:
1. Hold the trackball (BOOT/GPIO 0) button
2. While holding, press the reset button
3. Release both — device enters bootloader mode
4. Flash as normal
### Web Flash
Visit [ratspeak.org/flash](https://ratspeak.org/flash) to flash directly from your browser using WebSerial — no build tools required.
### Creating a Merged Binary
A merged binary includes bootloader + partition table + app in one file for clean flashing:
A merged binary includes bootloader + partition table + app in one file:
```bash
python3 -m esptool --chip esp32s3 merge-bin \
--output ratputer_merged.bin \
--flash-mode dio --flash-size 8MB \
--output ratdeck_merged.bin \
--flash-mode dio --flash-size 16MB \
0x0 ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32s3/bin/bootloader_dio_80m.bin \
0x8000 .pio/build/ratputer_915/partitions.bin \
0x8000 .pio/build/ratdeck_915/partitions.bin \
0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
0x10000 .pio/build/ratputer_915/firmware.bin
0x10000 .pio/build/ratdeck_915/firmware.bin
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* --baud 460800 \
--before default-reset --after hard-reset \
write-flash 0x0 ratputer_merged.bin
write-flash 0x0 ratdeck_merged.bin
```
## Serial Monitor
@@ -63,22 +77,18 @@ python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* --baud 460800 \
python3 -m platformio device monitor -b 115200
```
Or with any serial terminal at 115200 baud.
### Serial WIPE Command
Within the first 500ms of boot, send `WIPE` over serial to wipe the SD card's `/ratputer/` directory. Useful for factory reset of stored messages, contacts, and config.
Or any serial terminal at 115200 baud.
## USB Port Identification
The ESP32-S3 on Cardputer Adv uses USB-Serial/JTAG (not a separate UART chip):
The T-Deck Plus ESP32-S3 uses USB-Serial/JTAG (not a separate UART chip):
| State | Port Name | Notes |
|-------|-----------|-------|
| Firmware mode | `/dev/cu.usbmodem<ID>` | Normal operation, serial + flashing |
| Bootloader mode | `/dev/cu.usbmodem5101` | Hold G0 during boot, esptool only |
| OS | Port | Notes |
|----|------|-------|
| macOS | `/dev/cu.usbmodem*` | Use glob to match |
| Linux | `/dev/ttyACM0` | May need `dialout` group |
The firmware-mode port name includes the chip's unique ID (e.g., `/dev/cu.usbmodem3C0F02E81B4C1`). Use `/dev/cu.usbmodem*` glob to match either.
On Linux: `sudo usermod -aG dialout $USER` (log out and back in).
## Build Flags
@@ -86,35 +96,55 @@ From `platformio.ini`:
| Flag | Purpose |
|------|---------|
| `-fexceptions` | Enable C++ exceptions (required by microReticulum) |
| `-DRATPUTER=1` | Main feature flag — guards all Ratputer-specific code |
| `-DARDUINO_USB_CDC_ON_BOOT=1` | USB CDC serial on boot (USBMode=default) |
| `-DARDUINO_USB_MODE=1` | USB mode 1 = USB-Serial/JTAG (not native CDC) |
| `-std=gnu++17` | C++17 standard |
| `-fexceptions` | C++ exceptions (required by microReticulum) |
| `-DRATDECK=1` | Main feature flag |
| `-DARDUINO_USB_CDC_ON_BOOT=1` | USB CDC serial on boot |
| `-DARDUINO_USB_MODE=1` | USB-Serial/JTAG mode (not native CDC) |
| `-DRNS_USE_FS` | microReticulum: use filesystem for persistence |
| `-DRNS_PERSIST_PATHS` | microReticulum: persist transport paths to flash |
| `-DRNS_PERSIST_PATHS` | microReticulum: persist transport paths |
| `-DMSGPACK_USE_BOOST=OFF` | Disable Boost dependency in MsgPack |
| `-DBOARD_HAS_PSRAM` | Enable 8MB PSRAM |
| `-DDISPLAY_WIDTH=320` | Display width |
| `-DDISPLAY_HEIGHT=240` | Display height |
| `-DLV_CONF_INCLUDE_SIMPLE` | LVGL config via root `lv_conf.h` |
| `"-I${PROJECT_DIR}"` | Required for LVGL to find root `lv_conf.h` |
`build_unflags = -fno-exceptions` removes the Arduino default no-exceptions flag.
`build_unflags = -fno-exceptions -std=gnu++11` removes Arduino defaults.
## Partition Table
`partitions_8MB_ota.csv`8MB flash layout with OTA support:
`partitions_16MB.csv`16MB flash layout with OTA support:
| Name | Type | Offset | Size | Purpose |
|------|------|--------|------|---------|
| nvs | data/nvs | 0x9000 | 20 KB | Non-volatile storage (boot counter, WiFi creds) |
| otadata | data/ota | 0xE000 | 8 KB | OTA boot selection |
| app0 | app/ota_0 | 0x10000 | 3 MB | Primary firmware |
| app1 | app/ota_1 | 0x310000 | 3 MB | OTA update slot (reserved) |
| littlefs | data/spiffs | 0x610000 | 1.875 MB | LittleFS — identity, config, messages, paths |
| coredump | data/coredump | 0x7F0000 | 64 KB | ESP-IDF core dump on crash |
| app0 | app/ota_0 | 0x10000 | 4 MB | Primary firmware |
| app1 | app/ota_1 | 0x410000 | 4 MB | OTA update slot |
| littlefs | data/spiffs | 0x810000 | 7.875 MB | LittleFS — identity, config, messages, paths |
| coredump | data/coredump | 0xFF0000 | 64 KB | ESP-IDF core dump on crash |
## CI/CD
GitHub Actions workflow (`.github/workflows/build.yml`):
- **Build**: Triggers on push to `main` and PRs. Runs `pio run`, uploads `firmware.bin` as artifact.
- **Release**: Triggers on `v*` tags. Builds firmware and creates a GitHub Release with the binary attached.
- **Release**: Triggers on `v*` tags. Builds firmware, creates a GitHub Release with the binary ZIP attached. Powers the web flasher at [ratspeak.org/flash](https://ratspeak.org/flash).
### Release Process
```bash
# 1. Bump version in src/config/Config.h (all 4 defines)
# 2. Commit and push
git add -A && git commit -m "v1.4.2: description"
git push origin main
# 3. Tag and push tag
git tag v1.4.2
git push origin v1.4.2
# CI creates GitHub release automatically
```
## Dependencies
@@ -122,39 +152,29 @@ All managed by PlatformIO's `lib_deps`:
| Library | Source | Purpose |
|---------|--------|---------|
| microReticulum | github.com/attermann/microReticulum | Reticulum protocol (C++) |
| Crypto | github.com/attermann/Crypto | Ed25519, X25519, AES, SHA256 |
| ArduinoJson | bblanchon/ArduinoJson ^7.4.2 | Config serialization |
| M5Unified | m5stack/M5Unified | Hardware abstraction |
| M5GFX | m5stack/M5GFX | Display + canvas rendering |
| M5Cardputer | m5stack/M5Cardputer | Keyboard (TCA8418) |
| microReticulum | `attermann/microReticulum#392363c` | Reticulum protocol stack (C++) |
| Crypto | `attermann/Crypto` | Ed25519, X25519, AES, SHA-256 |
| ArduinoJson | `bblanchon/ArduinoJson ^7.4.2` | Config and message serialization |
| LovyanGFX | `lovyan03/LovyanGFX ^1.1.16` | SPI display driver |
| NimBLE-Arduino | `h2zero/NimBLE-Arduino ^2.1` | BLE Sideband transport |
| LVGL | `lvgl/lvgl ^8.3.4` | UI framework (v8.4) |
## Erasing Flash
To completely erase the ESP32-S3 flash (useful if LittleFS is corrupted or you want a clean start):
To completely erase the ESP32-S3 flash (useful if LittleFS is corrupted):
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* erase-flash
```
After erasing, you must reflash the firmware. LittleFS will auto-format on first boot, and a new identity will be generated.
After erasing, reflash the firmware. LittleFS will auto-format on first boot and a new identity will be generated.
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `A]Fatal error occurred: Could not open port` | Device not connected or wrong port | Check USB cable, try `/dev/cu.usbmodem*` glob |
| `Timed out waiting for packet header` | Baud rate too high for USB-Serial/JTAG | Use `--baud 460800` with esptool |
| `No such option: --upload-port` | Old PlatformIO version | `pip install -U platformio` |
| `ImportError: No module named platformio` | PlatformIO not installed for this Python | `pip install platformio` or use the correct `python3` |
| `pio: command not found` | PlatformIO not on PATH | Use `python3 -m platformio` instead |
| `Error: Bootloader binary size ... exceeds` | Partition mismatch | Ensure `partitions_8MB_ota.csv` is present in repo root |
## macOS vs Linux Ports
| OS | Firmware Port | Bootloader Port |
|----|---------------|-----------------|
| macOS | `/dev/cu.usbmodem<chipID>` | `/dev/cu.usbmodem5101` |
| Linux | `/dev/ttyACM0` (typical) | `/dev/ttyACM0` |
On Linux, you may need to add your user to the `dialout` group: `sudo usermod -aG dialout $USER` (then log out and back in).
| `Could not open port` | Device not connected or wrong port | Check USB cable, try `/dev/cu.usbmodem*` glob |
| `Timed out waiting for packet header` | Baud rate too high | Use `--baud 460800` with esptool |
| Serial disconnects mid-flash | Unstable USB connection | Enter download mode (hold trackball + reset) |
| `No module named platformio` | PlatformIO not installed | `pip install platformio` |
| `pio: command not found` | Not on PATH | Use `python3 -m platformio` instead |

View File

@@ -1,105 +1,27 @@
# Ratputer — Developer Guide
# Ratdeck — Developer Guide
## Project Overview
## Overview
Ratputer is a **standalone Reticulum transport node** with LXMF messaging, built for the M5Stack Cardputer Adv with Cap LoRa-1262 radio. It is **NOT an RNode** — it does not speak KISS protocol. It runs its own Reticulum stack (microReticulum) directly on the device.
Ratdeck is a standalone Reticulum mesh node with LXMF encrypted messaging for the LilyGo T-Deck Plus (ESP32-S3). It is NOT an RNode — it does not speak KISS protocol. It runs the full Reticulum stack (microReticulum) directly on the device.
Key characteristics:
- Standalone operation — no host computer required
- LoRa transport with 1-byte header framing (RNode-compatible on-air format)
- LoRa transport with 1-byte header framing
- WiFi transport — AP mode (TCP server) or STA mode (TCP client)
- BLE transport — NimBLE Sideband interface
- LXMF encrypted messaging with Ed25519 signatures
- Cyberpunk terminal UI with tabbed navigation
- LVGL v8.4 UI with 5-tab navigation
- JSON-based runtime configuration with SD card + flash dual-backend
## Source Tree
```
src/
├── main.cpp Setup (24-step init) + main loop (10 steps, 20 FPS)
├── config/
│ ├── BoardConfig.h GPIO pins, SPI config, hardware constants
│ ├── Config.h Compile-time: version, feature flags, storage paths, limits
│ └── UserConfig.* Runtime settings: dual-backend JSON (SD primary, flash fallback)
├── radio/
│ ├── SX1262.* Full SX1262 driver (register-level, extracted from RNode CE)
│ └── RadioConstants.h SX1262 register addresses and command bytes
├── input/
│ ├── Keyboard.* M5Cardputer TCA8418 keyboard wrapper, key event generation
│ └── HotkeyManager.* Ctrl+key dispatch table, tab cycle callback
├── ui/
│ ├── Theme.h Signal green (#00FF41) on black, layout metrics
│ ├── UIManager.* Canvas rendering loop, screen stack, boot/normal modes
│ ├── Screen.h Abstract base: handleKey(), render(), update()
│ ├── StatusBar.* Battery %, transport mode, LoRa indicator, announce flash
│ ├── TabBar.* Home/Msgs/Nodes/Setup tabs with unread badges
│ ├── screens/
│ │ ├── BootScreen.* Animated boot with progress bar
│ │ ├── HomeScreen.* Identity hash, transport status, radio info, uptime
│ │ ├── MessagesScreen.* Conversation list with unread counts
│ │ ├── MessageView.* Single conversation view with text input
│ │ ├── NodesScreen.* Discovered Reticulum nodes with RSSI/SNR
│ │ ├── SettingsScreen.* Radio, WiFi, Display, Audio, About, Factory Reset
│ │ └── HelpOverlay.* Hotkey reference overlay (Ctrl+H toggle)
│ ├── widgets/
│ │ ├── ScrollList.* Scrollable list with selection highlight
│ │ ├── TextInput.* Single-line text input with cursor
│ │ └── ProgressBar.* Boot progress and general-purpose bars
│ └── assets/
│ └── BootLogo.h Embedded boot screen graphic
├── reticulum/
│ ├── ReticulumManager.* microReticulum lifecycle, identity, announce, transport loop
│ ├── AnnounceManager.* Node discovery, contact persistence (SD + flash)
│ ├── LXMFManager.* LXMF send/receive, outgoing queue, delivery tracking
│ └── LXMFMessage.* Wire format: source(16) + msgpack + sig(64)
├── transport/
│ ├── LoRaInterface.* SX1262 ↔ Reticulum bridge (InterfaceImpl), 1-byte header
│ ├── WiFiInterface.* WiFi AP, TCP server on port 4242, HDLC framing
│ ├── TCPClientInterface.* WiFi STA, TCP client to remote endpoints, HDLC framing
│ └── BLEStub.* BLE advertising placeholder (disabled, v1.1)
├── storage/
│ ├── FlashStore.* LittleFS wrapper with atomic writes (.tmp→verify→.bak→rename)
│ ├── SDStore.* SD card (FAT32) with atomic writes, wipe, directory management
│ └── MessageStore.* Per-conversation message storage (dual: flash + SD backup)
├── power/
│ └── PowerManager.* Screen dim/off/wake state machine, brightness control
└── audio/
└── AudioNotify.* Notification sounds (boot, message, announce, error)
```
## Configuration System
### Compile-Time (`Config.h`)
Feature flags (`HAS_LORA`, `HAS_WIFI`, etc.), storage paths, protocol limits, power defaults. Changed only by editing source and recompiling.
Feature flags (`HAS_LORA`, `HAS_WIFI`, `HAS_BLE`, etc.), storage paths, protocol limits, power defaults. Changed only by editing source and recompiling.
### Runtime (`UserConfig`)
JSON-based settings persisted to storage. Schema defined by `UserSettings` struct in `UserConfig.h`:
```
{
"loraFrequency": 915000000,
"loraSF": 7,
"loraBW": 500000,
"loraCR": 5,
"loraTxPower": 10,
"wifiMode": 1, // 0=OFF, 1=AP, 2=STA
"wifiAPSSID": "ratputer-XXXX",
"wifiAPPassword": "ratspeak",
"wifiSTASSID": "",
"wifiSTAPassword": "",
"tcpConnections": [{"host": "rns.beleth.net", "port": 4242, "autoConnect": true}],
"screenDimTimeout": 30,
"screenOffTimeout": 60,
"brightness": 255,
"audioEnabled": true,
"audioVolume": 80,
"displayName": ""
}
```
**Dual-backend persistence**: `UserConfig::load(SDStore&, FlashStore&)` reads from SD first (`/ratputer/config/user.json`), falls back to flash (`/config/user.json`). `save()` writes to both.
JSON-based settings persisted to storage. Schema defined by `UserSettings` struct in `UserConfig.h`. Dual-backend persistence: `UserConfig::load(SDStore&, FlashStore&)` reads from SD first (`/ratputer/config/user.json`), falls back to flash (`/config/user.json`). `save()` writes to both.
## Transport Architecture
@@ -122,109 +44,65 @@ Any `0x7E` or `0x7D` in payload is escaped as `0x7D (byte ^ 0x20)`.
### LoRa 1-Byte Header
Every LoRa packet has a 1-byte header prepended:
- Upper nibble: random sequence number (for future split-packet tracking)
- Upper nibble: random sequence number
- Lower nibble: flags (`0x01` = split, not currently implemented)
This matches the RNode on-air format, so Ratputer packets are structurally compatible with RNodes on the same frequency/modulation.
## Screen System (LVGL v8.4)
## Reticulum Integration
All screens extend `LvScreen` base class:
- `createUI(lv_obj_t* parent)` — build LVGL widgets
- `onEnter()` — called when screen becomes active
- `handleKey(const KeyEvent&)` — keyboard/trackball input
- `handleLongPress()` — trackball long-press (1200ms)
### microReticulum Library
### Active Screens
C++ port of the Python Reticulum stack. Provides `Identity`, `Destination`, `Transport`, `Packet`, and `Link` classes.
| Screen | Class | Tab | Purpose |
|--------|-------|-----|---------|
| Boot | LvBootScreen | — | Boot animation with progress bar |
| Home | LvHomeScreen | 1 | Name, LXMF address, status, online nodes |
| Friends | LvContactsScreen | 2 | Saved contacts (display name only) |
| Msgs | LvMessagesScreen | 3 | Conversation list, sorted by recent, previews |
| Peers | LvNodesScreen | 4 | All discovered nodes (contacts + online) |
| Setup | LvSettingsScreen | 5 | 7-category settings editor |
| Help | LvHelpOverlay | — | Hotkey reference modal (Ctrl+H) |
| Name | LvNameInputScreen | — | First-boot name entry |
Key integration points in `ReticulumManager`:
- `RNS::Reticulum::start()` — initialize the stack
- `RNS::Transport::register_interface()` — add LoRa, WiFi, TCP interfaces
- `RNS::Transport::register_announce_handler()` — node discovery callback
- `RNS::Reticulum::loop()` — process incoming/outgoing in main loop
### Theme
### Identity Persistence
Matrix green (#00FF41) on black. Layout: 320x240, status bar 20px top, tab bar 20px bottom, content 200px.
Device identity (Ed25519 keypair) is stored at `/identity/identity.key` in LittleFS with a backup copy on SD at `/ratputer/identity/identity.key`. If flash identity is lost (e.g., LittleFS format), the SD backup is restored automatically.
### Path Persistence
Transport paths are serialized to `/transport/paths.msgpack` in LittleFS periodically (every 60 seconds, configurable via `PATH_PERSIST_INTERVAL_MS`).
## LXMF Protocol
Wire format for direct LoRa delivery:
```
source_hash(16 bytes) + msgpack([timestamp, content, title, fields]) + signature(64 bytes)
```
- `source_hash` — 16-byte truncated SHA-256 of sender's public key
- MsgPack array: `[double timestamp, string content, string title, map fields]`
- `signature` — Ed25519 signature over `source_hash + msgpack_content`
Messages under the MDU (Maximum Data Unit, ~254 bytes for LoRa) are sent as single direct packets. Larger messages would require link-based transfer (not yet implemented).
Messages are stored as JSON per-conversation in both flash (`/messages/<peer_hex>/`) and SD (`/ratputer/messages/<peer_hex>/`).
## Storage Architecture
### FlashStore (LittleFS)
Primary storage for all persistent data. 1.875 MB partition at offset 0x610000.
**Atomic write pattern**: Write to `.tmp` → verify read-back → rename existing to `.bak` → rename `.tmp` to final path. Prevents corruption on power loss.
### SDStore (FAT32)
Secondary/backup storage on microSD card. Shares HSPI bus with LoRa radio.
Directory structure:
```
/ratputer/
├── config/
│ └── user.json Runtime settings backup
├── messages/
│ └── <peer_hex>/ Per-conversation message history
├── contacts/ Discovered node info
└── identity/
└── identity.key Identity key backup
```
### MessageStore (Dual Backend)
Wraps FlashStore and SDStore to provide unified message access. Writes go to both backends; reads prefer SD (larger capacity), fall back to flash.
## WiFi State Machine
Three modes, selected in Settings:
```
RAT_WIFI_OFF (0) ──→ No WiFi, saves power + heap
RAT_WIFI_AP (1) ──→ Creates AP "ratputer-XXXX", TCP server on :4242
RAT_WIFI_STA (2) ──→ Connects to configured network, TCP client connections
```
In STA mode, WiFi connection is non-blocking. TCP client interfaces are created on first successful connection and auto-reconnect if WiFi drops.
Boot loop recovery forces WiFi to OFF if 3 consecutive boots fail.
Key colors:
- `PRIMARY` (0x00FF41) — main text, active elements
- `ACCENT` (0x00FFFF) — cyan highlights, incoming messages
- `MUTED` (0x559955) — secondary text
- `SELECTION_BG` (0x004400) — selected row
- `ERROR_CLR` (0xFF3333) — errors, disconnected indicators
## How To: Add a New Screen
1. Create `src/ui/screens/MyScreen.h` and `MyScreen.cpp`
2. Inherit from `Screen` — implement `handleKey()`, `render()`, optionally `update()`
3. In `render()`, use `m5canvas` to draw within the content area (y: 14 to 119)
1. Create `src/ui/screens/LvMyScreen.h` and `LvMyScreen.cpp`
2. Inherit from `LvScreen` — implement `createUI()`, `handleKey()`, optionally `onEnter()`
3. In `createUI()`, build LVGL widgets on the provided parent container
4. Add a global instance in `main.cpp`
5. Wire it up: either add to `tabScreens[]` array or navigate to it from a hotkey/callback
5. Wire it up: add to `tabScreens[]` array for tab navigation, or navigate to it from a hotkey/callback via `ui.showScreen()`
## How To: Add a New Hotkey
1. In `main.cpp`, create a callback function: `void onHotkeyX() { ... }`
2. Register in `setup()`: `hotkeys.registerHotkey('x', "Description", onHotkeyX);`
3. Update `docs/HOTKEYS.md` and the help overlay text in `HelpOverlay.cpp`
3. Update `docs/HOTKEYS.md` and the help overlay text in `LvHelpOverlay.cpp`
## How To: Add a Settings Submenu
## How To: Add a Settings Category
1. In `SettingsScreen.h`, add an enum value to the menu state
2. In `SettingsScreen.cpp`, add menu item text and handler
3. Add `render*()` and `handleKey*()` methods for the new submenu
4. Use `userConfig->save(sdStore, flash)` to persist changes
Settings uses a category/item system in `LvSettingsScreen`:
1. Add your category to the categories array
2. Define items with types: `READONLY`, `TOGGLE`, `INTEGER`, `ENUM_CHOICE`, `TEXT_INPUT`, `ACTION`
3. Add value getter/setter logic in the category handler
4. Changes that need reboot: set flag and show toast
5. Live changes: apply immediately in the setter callback
6. Persist with `userConfig->save(sdStore, flash)`
## How To: Add a New Transport Interface
@@ -232,117 +110,121 @@ Boot loop recovery forces WiFi to OFF if 3 consecutive boots fail.
2. Implement `start()`, `stop()`, `loop()`, `send_outgoing()`
3. In `loop()`, call `receive_incoming(data)` when data arrives
4. In `main.cpp`, construct the impl, wrap in `RNS::Interface`, register with `RNS::Transport`
5. Store the `RNS::Interface` wrapper in a persistent container (e.g., `std::list`) — Transport holds references
5. Store the `RNS::Interface` wrapper in a `std::list` (not vector) — Transport holds references, and vector reallocation invalidates them
## Initialization Sequence
`setup()` runs these steps in order:
1. M5Cardputer.begin() — display, keyboard, battery ADC
2. UI init + boot screen
3. Keyboard init
4. Register hotkeys (Ctrl+H/M/N/S/A/D/T/R)
5. Mount LittleFS (FlashStore)
6. Boot loop detection (NVS counter)
7. Radio init — SX1262 begin, configure modulation, enter RX
8. SD card init (shares HSPI, must be after radio)
9. Serial WIPE window (500ms)
10. Reticulum init — identity load/generate, transport start
11. MessageStore init (dual backend)
12. LXMF init + message callback
13. AnnounceManager init + contact load
14. Register announce handler with Transport
15. Load UserConfig (SD → flash fallback)
16. Boot loop recovery check (force WiFi OFF if triggered)
17. Apply saved radio settings
18. WiFi start (AP, STA, or OFF based on config)
19. BLE skip (disabled)
20. Power manager init + apply saved brightness/timeouts
21. Audio init + apply saved volume
22. Boot complete — switch to Home screen
23. Initial announce broadcast
24. Clear boot loop counter (NVS reset to 0)
1. GPIO 10 HIGH — enables all T-Deck Plus peripherals
2. Serial begin (115200 baud)
3. Shared SPI bus init (SCK=40, MISO=38, MOSI=41)
4. I2C bus init (SDA=18, SCL=8)
5. Display init (LovyanGFX + LVGL)
6. Boot screen shown
7. Keyboard init (I2C 0x55)
8. Trackball init (GPIO ISRs on pins 0/1/2/3/15)
9. Register hotkeys (Ctrl+H/M/N/S/A/D/T/R)
10. Mount LittleFS (FlashStore)
11. Boot loop detection (NVS counter)
12. Radio init — SX1262 TCXO, calibrate, configure, enter RX
13. SD card init (shares SPI, must be after radio)
14. Reticulum init — identity load/generate (triple-redundant), transport start
15. MessageStore init (dual backend)
16. LXMF init + message callback
17. AnnounceManager init + contact load
18. Register announce handler with Transport
19. Load UserConfig (SD -> flash fallback)
20. Boot loop recovery check (force WiFi OFF if triggered)
21. Apply saved radio settings
22. WiFi start (AP, STA, or OFF based on config)
23. BLE init (NimBLE Sideband)
24. Power manager init + apply brightness/timeouts
25. Audio init
26. Name input screen (first boot only)
27. Switch to Home screen
28. Initial announce broadcast
29. Clear boot loop counter (NVS reset to 0)
## Main Loop
Runs at 20 FPS (50ms interval):
Single-threaded on core 1:
1. `M5Cardputer.update()` — poll M5 hardware
2. Keyboard poll → hotkey dispatch → screen key handler → tab cycling
3. `rns.loop()` — Reticulum transport + radio RX processing
4. Auto-announce (every 5 minutes)
5. `lxmf.loop()` — outgoing message queue
6. WiFi STA connection handler + TCP client creation
7. `wifiImpl->loop()` — WiFi transport (AP server accepts, processes clients)
8. TCP client loops — reconnection, frame processing
9. `power.loop()` — dim/off state machine
10. Canvas render (if screen is on)
```
loop() {
1. inputManager.update() -- keyboard, trackball, touch polling
2. Long-press dispatch -- ui.handleLongPress() at 1200ms
3. Key event dispatch -- hotkeys -> LvInput::feedKey() -> screen handleKey()
4. lv_timer_handler() -- LVGL rendering (skipped when screen off)
5. rns.loop() -- Reticulum + LoRa RX (throttled to 5ms)
6. Auto-announce (5 min interval)
7. lxmf.loop() + announce loop -- message queue + deferred saves
8. WiFi STA handler -- connect/disconnect, TCP client creation
9. WiFi/TCP/BLE loops -- transport processing
10. powerMgr.loop() -- ACTIVE -> DIMMED -> SCREEN_OFF
11. Status bar update (1Hz) -- battery, signal indicators
12. Heartbeat (5s serial)
}
```
## Memory Budget
The ESP32-S3 has 512 KB SRAM. Typical free heap at runtime:
ESP32-S3 with 8MB PSRAM. PSRAM is used for large allocations; SRAM (512 KB) for stack and DMA:
| State | Free Heap | Notes |
|-------|-----------|-------|
| Boot complete (WiFi OFF) | ~170 KB | Baseline |
| Boot complete (WiFi AP) | ~150 KB | WiFi stack + TCP server |
| Boot complete (WiFi STA) | ~140 KB | WiFi stack + TCP clients |
| With BLE enabled | -50 KB | BLE disabled in v1.0 to save this |
| With BLE enabled | ~120 KB | NimBLE stack |
Key consumers:
- microReticulum transport tables: ~2040 KB (scales with paths/links)
- M5Canvas sprite buffer: 240×135×2 = 64.8 KB (RGB565 double-buffer)
- ArduinoJson documents: ~4 KB per config parse
- SX1262 TX/RX buffers: 255 bytes each
- TCP RX buffer: 600 bytes per connection
Monitor with `Ctrl+D``Free heap` or `ESP.getFreeHeap()` in code.
## Debugging Tips
### Serial output
All subsystems log with `[TAG]` prefixes. Connect at 115200 baud. Key tags: `[BOOT]`, `[RADIO]`, `[LORA_IF]`, `[WIFI]`, `[LXMF]`, `[SD]`.
### Radio debugging
- `Ctrl+D` dumps all SX1262 registers — compare sync word, IQ polarity, LNA, and OCP with a known-working device
- `Ctrl+T` sends a test packet and reads back the FIFO — confirms the TX path end-to-end
- `Ctrl+R` samples RSSI for 5 seconds — if readings stay at -110 to -120 dBm while another device transmits, the RX front-end isn't receiving RF
- `DevErrors: 0x0040` = PLL lock failure → check TCXO voltage (must be 3.0V / 0x06)
### Crash debugging
ESP-IDF stores a core dump in the `coredump` partition (64 KB at 0x7F0000). To read it:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* read-flash 0x7F0000 0x10000 coredump.bin
python3 -m esp_coredump info_corefile -t raw -c coredump.bin .pio/build/ratputer_915/firmware.elf
```
### Common crash causes
| Crash | Cause | Fix |
|-------|-------|-----|
| `LoadProhibited` at transport loop | Dangling `Interface&` reference | Store `RNS::Interface` in `std::list` (not vector, not local scope) |
| `Stack overflow` in task | Deep call chain in ISR or recursive render | Increase stack size or reduce nesting |
| `Guru Meditation` on WiFi init | Heap exhaustion | Disable BLE, reduce TCP connections, check for leaks |
| Boot loop (3+ failures) | WiFi or TCP init crash | Boot loop recovery auto-disables WiFi; fix root cause in Settings |
Monitor with `Ctrl+D` -> `Free heap` in serial output.
## Compile-Time Limits
These are defined in `Config.h` and can be adjusted:
Defined in `Config.h`:
| Constant | Default | Purpose |
|----------|---------|---------|
| `RATPUTER_MAX_NODES` | 50 | Max discovered nodes in AnnounceManager |
| `RATPUTER_MAX_MESSAGES_PER_CONV` | 100 | Max messages stored per conversation |
| `FLASH_MSG_CACHE_LIMIT` | 20 | Keep only N most recent messages per conv in flash (SD has full history) |
| `RATPUTER_MAX_OUTQUEUE` | 20 | Max pending outgoing LXMF messages |
| `RATDECK_MAX_NODES` | 200 | Max discovered nodes (PSRAM allows more) |
| `RATDECK_MAX_MESSAGES_PER_CONV` | 100 | Max messages stored per conversation |
| `FLASH_MSG_CACHE_LIMIT` | 20 | Recent messages per conv in flash (SD has full history) |
| `RATDECK_MAX_OUTQUEUE` | 20 | Max pending outgoing LXMF messages |
| `MAX_TCP_CONNECTIONS` | 4 | Max simultaneous TCP client connections |
| `TCP_RECONNECT_INTERVAL_MS` | 10000 | Retry interval for dropped TCP connections |
| `TCP_CONNECT_TIMEOUT_MS` | 5000 | Timeout for TCP connect() |
| `PATH_PERSIST_INTERVAL_MS` | 60000 | How often transport paths are saved to flash |
| `TCP_RECONNECT_INTERVAL_MS` | 15000 | Retry interval for dropped TCP connections |
| `TCP_CONNECT_TIMEOUT_MS` | 500 | Timeout for TCP connect() |
| `PATH_PERSIST_INTERVAL_MS` | 60000 | Transport path save interval |
| `SCREEN_DIM_TIMEOUT_MS` | 30000 | Default screen dim timeout |
| `SCREEN_OFF_TIMEOUT_MS` | 60000 | Default screen off timeout |
| `ANNOUNCE_INTERVAL_MS` | 300000 | Auto-announce period (5 minutes, defined in main.cpp) |
## Debugging
### Serial Output
All subsystems log with `[TAG]` prefixes at 115200 baud. Key tags: `[BOOT]`, `[RADIO]`, `[LORA_IF]`, `[WIFI]`, `[TCP]`, `[LXMF]`, `[SD]`, `[BLE]`.
### Radio Debugging
- `Ctrl+D` — dumps SX1262 registers, identity, transport status, heap, PSRAM, uptime
- `Ctrl+T` — sends test packet with FIFO readback verification
- `Ctrl+R` — 5-second continuous RSSI sampling
### Core Dump
ESP-IDF stores a core dump in the `coredump` partition (64 KB at 0xFF0000):
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* \
read-flash 0xFF0000 0x10000 coredump.bin
python3 -m esp_coredump info_corefile -t raw -c coredump.bin \
.pio/build/ratdeck_915/firmware.elf
```
### Common Crashes
| Crash | Cause | Fix |
|-------|-------|-----|
| `LoadProhibited` at transport loop | Dangling `Interface&` reference | Store in `std::list` (not vector, not local scope) |
| `Stack overflow` | Deep call chain or recursive render | Increase stack size or reduce nesting |
| `Guru Meditation` on WiFi init | Heap exhaustion | Reduce TCP connections, check for leaks |
| Boot loop (3+ failures) | WiFi or TCP init crash | Boot loop recovery auto-disables WiFi |

View File

@@ -1,69 +1,92 @@
# Ratputer — Hotkey Reference
# Ratdeck — Hotkey Reference
All hotkeys use **Ctrl+key** combinations.
## Hotkeys (Ctrl+key)
| Shortcut | Action |
|----------|--------|
| Ctrl+H | Toggle help overlay |
| Ctrl+H | Toggle help overlay (shows all hotkeys on screen) |
| Ctrl+M | Jump to Messages tab |
| Ctrl+N | New message |
| Ctrl+N | Compose new message |
| Ctrl+S | Jump to Settings tab |
| Ctrl+A | Force announce to network |
| Ctrl+D | Dump diagnostics to serial |
| Ctrl+T | Send radio test packet (FIFO verification) |
| Ctrl+A | Force announce to all interfaces (LoRa + TCP) |
| Ctrl+D | Dump full diagnostics to serial |
| Ctrl+T | Send radio test packet |
| Ctrl+R | RSSI monitor (5-second continuous sampling) |
## Navigation
These keys match the physical arrow key positions on the Cardputer Adv keyboard:
| Key | Action |
|-----|--------|
| `;` (semicolon) | Scroll up / previous item |
| `.` (period) | Scroll down / next item |
| Input | Action |
|-------|--------|
| Trackball up/down | Scroll / navigate lists |
| Trackball left/right | Cycle tabs |
| Trackball click | Select / confirm |
| Trackball long-press (1.2s) | Context menu |
| `,` (comma) | Previous tab |
| `/` (slash) | Next tab |
| Enter | Select / confirm / send |
| Enter | Select / confirm / send message |
| Esc | Back / cancel |
| Backspace | Delete character / go back (if input empty) |
## Text Input
When a text input field is active:
When a text field is active (message compose, WiFi password, etc.):
- Type normally to enter characters
- **Backspace** to delete
- **Enter** to submit
- **Esc** to cancel
- Double-tap **Aa** for caps lock
- **Enter** to submit / send
- **Esc** to cancel and go back
## Tabs
| Tab | Contents |
|-----|----------|
| Home | Identity, transport status, radio info, uptime |
| Msgs | Conversation list with unread badges |
| Nodes | Discovered Reticulum nodes |
| Setup | Settings, about, factory reset |
| Tab | Name | Contents |
|-----|------|----------|
| 1 | Home | Name, LXMF address, connection status, online node count |
| 2 | Friends | Saved contacts (display name only) |
| 3 | Msgs | Conversation list — sorted by most recent, with preview and unread dots |
| 4 | Peers | All discovered nodes — contacts section + online section |
| 5 | Setup | 7-category settings (Device, Display, Radio, Network, Audio, Info, System) |
## Home Screen
- **Enter / trackball click**: Announce to all connected interfaces (LoRa + TCP)
## Messages Screen
- **Up/Down**: Navigate conversations
- **Enter**: Open conversation
- **Long-press**: Context menu (Add Friend / Delete Chat / Cancel)
- Navigate menu with Up/Down, confirm with Enter
## Message View (Chat)
- **Type**: Compose message
- **Enter**: Send message
- **Up/Down**: Scroll message history
- **Esc / Backspace** (empty input): Go back to conversation list
## Peers Screen
- **Up/Down**: Navigate node list
- **Enter**: Open chat with selected node
- **'s' key**: Toggle saved/friend status
- **Long-press**: Unsaved node → add as friend; saved friend → delete prompt
## Settings Screen
- **Up/Down**: Navigate settings items
- **Enter**: Toggle / edit selected setting
- **Left/Right**: Adjust numeric values, cycle enum choices
- Categories: Device, Display & Input, Radio, Network, Audio, Info, System
## Serial Diagnostics
**Ctrl+D** prints to serial (115200 baud):
- Identity hash, transport status, path/link counts
- Identity hash, destination hash, transport status
- Path/link counts
- Radio parameters (freq, SF, BW, CR, TX power, preamble)
- SX1262 register dump (sync word, IQ, LNA, OCP, TX clamp)
- Device errors, current RSSI
- Free heap, flash usage, uptime
- Free heap, PSRAM, flash usage, uptime
**Ctrl+T** sends a test packet with header `0xA0` and payload `RATPUTER_TEST_1234567890`, then reads back the FIFO buffer to verify the TX path.
**Ctrl+T** sends a test packet and verifies FIFO readback.
**Ctrl+R** samples RSSI continuously for 5 seconds, printing each reading. Transmit from another device during sampling to verify the RX front-end is hearing RF.
## Settings Submenus (Ctrl+S)
| Menu Item | Contents |
|-----------|----------|
| Radio | Frequency (Hz), spreading factor (512), bandwidth (7.8k500k), coding rate (4/54/8), TX power (222 dBm) |
| WiFi | Mode (OFF/AP/STA), AP SSID + password, STA SSID + password, TCP endpoints list |
| Display | Brightness (0255), dim timeout (seconds), off timeout (seconds) |
| Audio | Enable/disable notifications, volume (0100%) |
| About | Firmware version, Reticulum identity hash, uptime, free heap, flash usage |
| Factory Reset | Clears config from flash + SD, reboots with defaults (identity and messages preserved) |
**Ctrl+R** samples RSSI continuously for 5 seconds.

View File

@@ -1,100 +1,137 @@
# Ratputer — Hardware Pin Map
# Ratdeck — Hardware Pin Map
M5Stack Cardputer Adv (ESP32-S3) + Cap LoRa-1262
All pin definitions are in `src/config/BoardConfig.h`.
LilyGo T-Deck Plus (ESP32-S3) — all pin definitions in `src/config/BoardConfig.h`.
## Bus Overview
```
ESP32-S3
├── HSPI (shared bus) ──┬── SX1262 LoRa (CS=5)
│ SCK=40 └── SD Card (CS=12)
│ MISO=39
│ MOSI=14
├── SPI2_HOST (shared bus) ──┬── ST7789V Display (CS=12, DC=11)
│ SCK=40 ├── SX1262 LoRa (CS=9)
│ MISO=38 └── SD Card (CS=39)
│ MOSI=41
├── I2C ── TCA8418 Keyboard (SDA=8, SCL=9, INT=11)
├── I2C (SDA=18, SCL=8) ────┬── Keyboard ESP32-C3 (0x55)
│ └── GT911 Touch (0x5D)
├── UART (reserved) ── GNSS module (RX=15, TX=13)
├── GPIO ── Trackball (UP=3, DOWN=2, LEFT=1, RIGHT=15, CLICK=0)
├── USB-Serial/JTAG ── USB-C port (firmware + debug)
├── UART ── UBlox MIA-M10Q GPS (TX=43, RX=44)
── M5Unified managed ──┬── ST7789V2 Display (SPI)
├── ES8311 Audio Codec (I2S)
└── Battery ADC
── I2S ── ES7210 Audio Codec
└── USB-C ── USB-Serial/JTAG
```
## SX1262 LoRa Radio
Uses **HSPI** (custom SPI bus, not the default VSPI):
## Power Control
| Signal | GPIO | Notes |
|--------|------|-------|
| SCK | 40 | SPI clock |
| MISO | 39 | SPI data out (radio → ESP) |
| MOSI | 14 | SPI data in (ESP → radio) |
| CS | 5 | Chip select (active low) |
| IRQ | 4 | DIO1 interrupt (falling edge) |
| RST | 3 | Reset (active low, 100μs pulse) |
| BUSY | 6 | Poll before SPI transactions |
| BOARD_POWER_PIN | 10 | **Must be set HIGH at boot** to enable all peripherals |
## SX1262 LoRa Radio
Shares SPI2_HOST bus with display and SD card:
| Signal | GPIO | Notes |
|--------|------|-------|
| CS | 9 | Chip select (active low) |
| IRQ | 45 | DIO1 interrupt |
| RST | 17 | Reset (active low) |
| BUSY | 13 | Poll before SPI transactions |
| RXEN | -1 | Not connected (DIO2 used as RF switch) |
| TXEN | -1 | Not connected |
**Radio configuration:**
- TCXO voltage: 3.0V (`MODE_TCXO_3_0V_6X` = 0x06)
- TCXO voltage: 1.8V (`MODE_TCXO_1_8V_6X` = 0x02)
- DIO2 as RF switch: enabled
- SPI clock: 8 MHz
## Display (ST7789V)
| Signal | GPIO | Notes |
|--------|------|-------|
| CS | 12 | Chip select |
| DC | 11 | Data/command |
| BL | 42 | Backlight PWM |
320x240 pixels, landscape (rotation=1), SPI clock up to 15 MHz (30 MHz overclockable).
## Shared SPI Bus
| Signal | GPIO |
|--------|------|
| SCK | 40 |
| MISO | 38 |
| MOSI | 41 |
All three SPI devices (display, radio, SD) share SPI2_HOST. Single-threaded access from `loop()`, no mutex needed.
## Keyboard (ESP32-C3)
| Signal | GPIO | Notes |
|--------|------|-------|
| I2C addr | 0x55 | Fixed address |
| INT | 46 | Interrupt pin |
| SDA | 18 | Shared I2C bus |
| SCL | 8 | Shared I2C bus |
## Trackball
| Signal | GPIO | Notes |
|--------|------|-------|
| UP | 3 | ISR-based, GPIO interrupt |
| DOWN | 2 | ISR-based, GPIO interrupt |
| LEFT | 1 | ISR-based, GPIO interrupt |
| RIGHT | 15 | ISR-based, GPIO interrupt |
| CLICK | 0 | Shared with BOOT button |
Click uses deferred release with 80ms debounce. Long press: 1200ms hold threshold.
## Touchscreen (GT911)
| Signal | GPIO | Notes |
|--------|------|-------|
| INT | 16 | Interrupt |
| I2C addr | 0x5D | Depends on INT state at boot |
| SDA | 18 | Shared I2C bus |
| SCL | 8 | Shared I2C bus |
Currently disabled — coordinates uncalibrated.
## SD Card
| Signal | GPIO | Notes |
|--------|------|-------|
| CS | 12 | Separate from LoRa CS (5) |
| CS | 39 | Shared SPI bus |
Shares HSPI bus with LoRa radio (SCK=40, MISO=39, MOSI=14). Only one device active at a time — SD must be initialized **after** radio.
FAT32, shares SPI2_HOST with display and radio.
## Keyboard (TCA8418)
## GPS (UBlox MIA-M10Q)
| Signal | GPIO | Notes |
|--------|------|-------|
| SDA | 8 | I2C data |
| SCL | 9 | I2C clock |
| INT | 11 | Active low, falling edge |
| TX | 43 | ESP TX → GPS RX |
| RX | 44 | GPS TX → ESP RX |
Managed by the M5Cardputer library. The TCA8418 is a dedicated keyboard controller IC with built-in key matrix scanning and FIFO.
## GNSS (Reserved — v1.1)
| Signal | GPIO | Notes |
|--------|------|-------|
| RX | 15 | GPS TX → ESP RX |
| TX | 13 | ESP TX → GPS RX |
UART at 115200 baud. Pins defined but no code path yet.
## Display
**ST7789V2** — 240×135 pixels, RGB565, SPI interface.
Fully managed by M5Unified. No GPIO definitions needed in firmware — the M5Unified library auto-configures display pins based on board detection.
## Audio
**ES8311** codec + **NS4150B** amplifier.
Fully managed by M5Unified. No GPIO definitions needed.
UART at 115200 baud. Pins defined, not yet implemented.
## Battery
ADC via M5Unified. 1750mAh LiPo, TP4057 charger IC.
| Signal | GPIO | Notes |
|--------|------|-------|
| ADC | 4 | Battery voltage measurement |
## SPI Bus Sharing
## Audio (ES7210 I2S)
The HSPI bus is shared between the SX1262 radio (CS=5) and the SD card (CS=12). Key constraints:
1. **Initialize radio first** — SD card init must come after `radio.begin()` since the SPI bus is configured during radio init
2. **One active at a time** — pull CS high on the inactive device before talking to the other
3. **Radio has priority** — if a packet arrives during SD access, there may be a brief delay before the ISR fires
| Signal | GPIO |
|--------|------|
| WS (LRCK) | 5 |
| DOUT | 6 |
| BCK | 7 |
| DIN | 14 |
| SCK | 47 |
| MCLK | 48 |
## Hardware Constants
@@ -102,6 +139,6 @@ The HSPI bus is shared between the SX1262 radio (CS=5) and the SD card (CS=12).
|----------|-------|-------|
| `MAX_PACKET_SIZE` | 255 | SX1262 maximum single packet |
| `SPI_FREQUENCY` | 8 MHz | SPI clock for SX1262 |
| `DISPLAY_WIDTH` | 240 | Pixels |
| `DISPLAY_HEIGHT` | 135 | Pixels |
| `GPS_BAUD` | 115200 | GNSS UART speed |
| `TFT_WIDTH` | 320 | Display pixels (landscape) |
| `TFT_HEIGHT` | 240 | Display pixels (landscape) |
| `TFT_SPI_FREQ` | 15 MHz | Display SPI clock |

View File

@@ -1,10 +1,10 @@
# Ratputer — Quick Start
# Ratdeck — Quick Start
## Hardware Required
- M5Stack Cardputer Adv (ESP32-S3, 8MB flash)
- Cap LoRa-1262 module (SX1262, 915 MHz)
- microSD card (optional, recommended)
- **LilyGo T-Deck Plus** (ESP32-S3, 16MB flash, 8MB PSRAM, integrated SX1262 LoRa)
- **microSD card** (optional, recommended — FAT32, any size)
- **USB-C cable** (data-capable)
## Build & Flash
@@ -13,140 +13,149 @@
pip install platformio
# Clone and build
git clone https://github.com/defidude/Ratputer.git
cd Ratputer
python3 -m platformio run -e ratputer_915
git clone https://github.com/defidude/Ratdeck.git
cd Ratdeck
python3 -m platformio run
# Flash to device (use glob to match USB port)
python3 -m platformio run -e ratputer_915 -t upload --upload-port /dev/cu.usbmodem*
# Flash
python3 -m platformio run --target upload
```
> **Note**: If `pio` is not on your PATH, use `python3 -m platformio` instead. See [BUILDING.md](BUILDING.md) for esptool flashing and merged binary instructions.
> If `pio` is not on your PATH, use `python3 -m platformio` instead. See [BUILDING.md](BUILDING.md) for esptool instructions and troubleshooting.
### Web Flash (No Build Required)
Visit [ratspeak.org/flash](https://ratspeak.org/flash) to flash directly from your browser using WebSerial.
### USB Port
The Cardputer Adv uses USB-Serial/JTAG — the port appears as `/dev/cu.usbmodem<chipID>` in firmware mode. Use the `*` glob to match it.
The T-Deck Plus uses USB-Serial/JTAG — port appears as `/dev/cu.usbmodem*` (macOS) or `/dev/ttyACM*` (Linux).
## First Boot
1. Power on the Cardputer Adv
1. Power on the T-Deck Plus
2. Boot animation plays with progress bar
3. Radio initializes at 915 MHz
4. SD card checked (auto-creates `/ratputer/` directories)
5. Reticulum transport node starts, identity generated
6. WiFi AP starts: `ratputer-XXXX` (password: `ratspeak`)
7. Home screen shows identity hash and status
3. SX1262 radio initializes (915 MHz, Balanced preset)
4. SD card checked auto-creates `/ratputer/` directories
5. Reticulum identity generated (Ed25519 keypair, saved to flash + SD + NVS)
6. Name input screen — type a display name or press Enter to auto-generate one
7. Home screen shows your name, LXMF address, connection status, and online node count
## Navigation
Keys match the physical arrow positions on the Cardputer Adv keyboard:
The T-Deck Plus has a QWERTY keyboard, trackball, and touchscreen (touch currently disabled):
- **`;`** / **`.`**: Scroll up/down in lists
- **`,`** / **`/`**: Cycle between tabs (left/right)
- **Enter**: Select/confirm
- **Esc**: Back/cancel
- **Ctrl+key**: Hotkeys (press Ctrl+H for help)
| Input | Action |
|-------|--------|
| Trackball up/down | Scroll lists |
| Trackball left/right | Cycle tabs |
| Trackball click | Select / confirm |
| Trackball long-press (1.2s) | Context menu |
| `,` / `/` | Previous / next tab |
| Enter | Select / send message |
| Esc or Backspace | Back / cancel |
| Ctrl+H | Show hotkey help overlay |
## Sending a Message
### Tabs
1. Wait for another node to appear in the **Nodes** tab
2. Press **Ctrl+M** to go to Messages
3. Select a conversation or use **Ctrl+N** for new
4. Type your message and press **Enter**
| Tab | Name | Contents |
|-----|------|----------|
| 1 | Home | Your name, LXMF address, connection status, online nodes |
| 2 | Friends | Saved contacts with display names |
| 3 | Msgs | Conversations sorted by most recent, with preview and unread dots |
| 4 | Peers | All discovered nodes — contacts section + online section |
| 5 | Setup | Device, Display, Radio, Network, Audio, Info, System |
## Sending Your First Message
1. Wait for another node to appear in the **Peers** tab (or connect via WiFi)
2. Navigate to the node and press **Enter** to open a chat
3. Type your message on the keyboard
4. Press **Enter** to send
5. Message status: yellow (sending) -> green (delivered) or red (failed)
## Connecting Two Ratdecks Over LoRa
1. Power on both devices
2. Ensure both use the same radio settings (defaults work out of the box)
3. Wait ~30 seconds for announces to propagate
4. The other device appears in the **Peers** tab with RSSI and SNR
5. Select the node and press Enter to start chatting
Both devices must use the same frequency, spreading factor, bandwidth, and coding rate. Default: 915 MHz, SF9, BW 250 kHz, CR 4/5 (Balanced preset).
## WiFi Setup
Default: AP mode with SSID `ratputer-XXXX`.
Default WiFi mode is **STA** (client). Change in Settings -> Network.
### AP Mode (default)
### STA Mode (Connect to Your WiFi)
Connect a laptop to the `ratputer-XXXX` WiFi network, then configure `rnsd` with a TCPClientInterface pointing at `192.168.4.1:4242`.
### STA Mode
To connect Ratputer to your WiFi network:
1. Press **Ctrl+S** → WiFi → Mode → STA
1. Go to Setup -> Network
2. Enter your WiFi SSID and password
3. Save — device reconnects in STA mode
4. Add TCP endpoints (e.g., `rns.beleth.net:4242`) in WiFi → TCP Connections
3. Add a TCP endpoint (e.g., `rns.beleth.net:4242`)
4. Ratdeck connects and bridges LoRa <-> TCP automatically
### AP Mode (Ratdeck as Hotspot)
1. Set WiFi Mode to AP in Settings -> Network
2. Connect your laptop to `ratdeck-XXXX` (password: `ratspeak`)
3. Add to `~/.reticulum/config`:
```ini
[[ratdeck]]
type = TCPClientInterface
target_host = 192.168.4.1
target_port = 4242
```
4. Your desktop is now bridged to the LoRa mesh
### WiFi OFF
Select OFF in WiFi settings to disable WiFi entirely (saves power and ~20KB heap).
Select OFF in Settings -> Network to disable WiFi entirely (saves power).
## SD Card
Insert a microSD card before powering on. The firmware auto-creates:
Insert a microSD card (FAT32) before powering on. The firmware auto-creates:
```
/ratputer/config/ Settings backup
/ratputer/messages/ Message archives
/ratputer/contacts/ Discovered nodes
/ratputer/identity/ Identity key backup
/ratputer/
config/ Settings backup
messages/ Message archives (up to 100 per conversation)
contacts/ Saved friends
identity/ Identity key backup
transport/ Routing table backup
```
To wipe SD data: connect serial at 115200 baud, send `WIPE` within 500ms of boot.
## Managing Friends
## Serial Monitor
- **Peers tab**: Navigate to a node, press `s` to toggle friend status, or long-press to add/delete
- **Messages tab**: Long-press a conversation for context menu (Add Friend / Delete Chat / Cancel)
- Friends are saved to flash + SD and persist across reboots
- Friends are never auto-evicted from the node list
## Radio Presets
Three presets available in Setup -> Radio:
| Preset | SF | BW | CR | TX | Use Case |
|--------|----|----|----|----|----------|
| **Balanced** (default) | 9 | 250 kHz | 4/5 | 14 dBm | General use |
| Long Range | 12 | 125 kHz | 4/8 | 17 dBm | Maximum range, slow |
| Fast | 7 | 500 kHz | 4/5 | 10 dBm | Short range, fast |
All parameters are individually configurable. Changes apply immediately.
## Serial Diagnostics
Connect at 115200 baud for debug output:
```bash
python3 -m platformio device monitor -b 115200
```
Useful serial hotkeys:
- **Ctrl+D** on device: dump full diagnostics
- **Ctrl+D** on device: full diagnostics dump (identity, radio, heap, uptime)
- **Ctrl+T** on device: send radio test packet
- **Ctrl+R** on device: 5-second RSSI sampling
## Settings
## Transport Mode
Press **Ctrl+S** to access settings:
- **Radio**: frequency, spreading factor, bandwidth, coding rate, TX power
- **WiFi**: mode (OFF/AP/STA), AP SSID + password, STA SSID + password, TCP endpoints
- **Display**: brightness (0255), dim timeout, off timeout
- **Audio**: enable/disable, volume (0100)
- **About**: version, identity hash, uptime, factory reset
Changes take effect immediately and persist to both flash and SD.
## Connecting Two Ratputers
Two Ratputers on the same LoRa settings will discover each other automatically:
1. Power on both devices
2. Wait ~30 seconds for announces to propagate
3. Check the **Nodes** tab — the other device should appear with its identity hash, RSSI, and SNR
4. Select the node → opens a conversation in Messages
5. Type a message and press Enter
Both devices must use the same frequency, spreading factor, bandwidth, and coding rate. The defaults (915 MHz, SF7, BW 500kHz, CR 4/5) work out of the box.
## Connecting to a Desktop Reticulum Instance
### Option A: AP Mode Bridge (Ratputer as hotspot)
1. Leave Ratputer in AP mode (default)
2. On your laptop, connect to `ratputer-XXXX` (password: `ratspeak`)
3. Add to `~/.reticulum/config`:
```ini
[[ratputer]]
type = TCPClientInterface
target_host = 192.168.4.1
target_port = 4242
```
4. Restart `rnsd` — your desktop is now on the LoRa mesh
### Option B: STA Mode (Ratputer joins your WiFi)
1. Switch Ratputer to STA mode (Ctrl+S → WiFi → Mode → STA)
2. Enter your WiFi SSID and password
3. On your laptop (same network), configure Ratputer as a TCP server interface in `~/.reticulum/config`:
```ini
[[ratputer]]
type = TCPServerInterface
listen_ip = 0.0.0.0
listen_port = 4242
```
4. On Ratputer, add a TCP endpoint pointing to your laptop's IP and port 4242
5. Both sides can now exchange Reticulum traffic over WiFi
By default, Ratdeck runs as an **Endpoint** (handles its own traffic only). Enable **Transport Node** in Setup -> Network to relay packets for other nodes in the mesh. Requires reboot.

View File

@@ -1,40 +1,44 @@
# Ratputer — Troubleshooting
# Ratdeck — Troubleshooting
Collected hardware and software gotchas, organized by category.
Collected hardware and software gotchas for the LilyGo T-Deck Plus with SX1262 LoRa.
---
## Radio Issues
### TCXO voltage must be 3.0V
### TCXO voltage must be 1.8V
The Cap LoRa-1262 uses a TCXO (temperature-compensated crystal oscillator) that requires exactly 3.0V. This is configured as `MODE_TCXO_3_0V_6X` (0x06) in `BoardConfig.h`.
The T-Deck Plus SX1262 uses a TCXO that requires 1.8V. This is configured as `MODE_TCXO_1_8V_6X` (0x02) in `BoardConfig.h`.
**Symptom**: Radio reports online but can't synthesize frequencies. PLL lock fails, no TX or RX.
**Diagnosis**: Check `Ctrl+D` diagnostics — if `DevErrors` shows `0x0040`, that's PLL lock failure.
**Diagnosis**: `Ctrl+D` diagnostics — if `DevErrors` shows `0x0040`, that's PLL lock failure.
**Fix**: Verify `LORA_TCXO_VOLTAGE` is `0x06` in `BoardConfig.h`.
**Fix**: Verify `LORA_TCXO_VOLTAGE` is `0x02` in `BoardConfig.h`.
### IRQ stale latch fix
### SetModulationParams must be called from STDBY mode
The SX1262's IRQ flags can become latched from previous operations. If stale flags persist, DCD (detect channel activity) gets stuck in "channel busy" state, CSMA blocks, and TX never fires.
The SX1262 silently rejects `SetModulationParams` (0x8B) when issued from RX or TX mode. The command appears to succeed but the hardware ignores the new SF/BW/CR values.
**Symptom**: First packet sends fine, then all subsequent TX attempts hang. DCD reports channel busy even with nothing transmitting.
**Symptom**: Software logs show correct SF/BW/CR but actual TX airtime is wrong. Two devices see each other's RF (RSSI visible) but every packet fails CRC.
**Fix** (applied in `SX1262.cpp`):
- Clear all IRQ flags at the top of `receive()` before entering RX mode
- In `dcd()`, clear stale header error flags when preamble is not detected
**Fix**: `setModulationParams()` now calls `standby()` internally before issuing the SPI command.
### `_txp` is a base class member
### Calibration must run after TCXO is enabled
The `_txp` (TX power) field is declared in `RadioInterface` base class (inherited from RNode firmware lineage). It cannot be initialized in the `sx126x` constructor initializer list — must set `_txp = 14` in the constructor body.
Per SX1262 datasheet Section 13.1.12, if a TCXO is used, it must be enabled before calling `calibrate()`. Calibration locks to whichever oscillator is active.
**Symptom**: TX power reads as 0, which may cause silent failures or very weak transmission.
**Symptom**: TX completes successfully on both devices, RSSI shows real signals, but neither device ever decodes the other's packets.
### PA ramp time
**Fix**: Init order must be: `enableTCXO()` -> `delay(5ms)` -> `setRegulatorMode(DC-DC)` -> `loraMode()` -> `standby(XOSC)` -> `calibrate()` -> `calibrate_image()`
Use 40μs PA ramp time for the Cap LoRa-1262. This is set during `setTxParams()` in the SX1262 driver. Faster ramp times may cause spectral splatter; slower wastes time.
### IRQ stale latch
The SX1262's IRQ flags can become latched from previous operations. If stale flags persist, DCD gets stuck in "channel busy" and TX never fires.
**Symptom**: First packet sends fine, then all subsequent TX attempts hang.
**Fix**: Clear all IRQ flags at the top of `receive()` before entering RX mode. In `dcd()`, clear stale header error flags when preamble is not detected.
---
@@ -42,45 +46,32 @@ Use 40μs PA ramp time for the Cap LoRa-1262. This is set during `setTxParams()`
### PlatformIO not on PATH
After `pip install platformio`, the `pio` binary may not be in your shell's PATH.
**Fix**: Use `python3 -m platformio` instead of `pio`:
```bash
python3 -m platformio run -e ratputer_915
python3 -m platformio run -e ratputer_915 -t upload
python3 -m platformio run
python3 -m platformio run --target upload
python3 -m platformio device monitor -b 115200
```
### esptool baud rate
### Flash fails or disconnects mid-upload
PlatformIO defaults to 921600 baud for flashing, which sometimes fails with USB-Serial/JTAG.
The T-Deck Plus USB-Serial/JTAG can be sensitive to baud rates.
**Fix**: Use 460800 baud with esptool directly:
**Fix**: Enter download mode (hold trackball/BOOT button while pressing reset), then flash:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* --baud 460800 \
write-flash -z 0x10000 .pio/build/ratputer_915/firmware.bin
write-flash -z 0x10000 .pio/build/ratdeck_915/firmware.bin
```
### esptool hyphenated flags
esptool deprecated underscored command names. Use hyphens:
| Correct | Deprecated |
|---------|-----------|
| `merge-bin` | `merge_bin` |
| `write-flash` | `write_flash` |
| `--flash-mode` | `--flash_mode` |
| `--flash-size` | `--flash_size` |
### USBMode must be `default`
The build flag `ARDUINO_USB_MODE=1` selects USB-Serial/JTAG mode (not native CDC).
**Symptom**: With `hwcdc` (USB_MODE=0), the native USB CDC peripheral doesn't enumerate on macOS in firmware mode. The port never appears.
**Symptom**: With `hwcdc` (USB_MODE=0), the port never appears on macOS.
**Fix**: Keep `ARDUINO_USB_MODE=1` in `platformio.ini`. The port appears as `/dev/cu.usbmodem<chipID>` (not `usbmodem5101`, which is bootloader-only).
**Fix**: Keep `ARDUINO_USB_MODE=1` in `platformio.ini`.
---
@@ -88,24 +79,28 @@ The build flag `ARDUINO_USB_MODE=1` selects USB-Serial/JTAG mode (not native CDC
### Boot loop detection and recovery
Ratputer tracks consecutive boot failures in NVS (non-volatile storage, separate from LittleFS). If 3 consecutive boots fail to reach the end of `setup()`, WiFi is forced OFF on the next boot.
Ratdeck tracks consecutive boot failures in NVS. If 3 consecutive boots fail to reach the end of `setup()`, WiFi is forced OFF on the next boot.
**How it works**:
1. On each boot, NVS counter `bootc` increments
2. If `bootc >= 3`, `bootLoopRecovery = true` WiFi forced to `RAT_WIFI_OFF`
3. At the end of successful `setup()`, counter resets to 0
2. If `bootc >= 3`, WiFi forced to OFF
3. At end of successful `setup()`, counter resets to 0
**Manual recovery**: If the device is boot-looping, connect serial at 115200 baud and watch for the `[BOOT] Boot loop detected` message. The device should stabilize with WiFi off, then you can change WiFi settings.
**Manual recovery**: Connect serial at 115200 baud and watch for `[BOOT] Boot loop detected`. The device should stabilize with WiFi off, then change WiFi settings in Setup.
### Root cause: Transport reference stability
### Transport reference crash
The original boot loop was caused by `RNS::Transport::_interfaces` storing `Interface&` (references, NOT copies). A local `RNS::Interface iface(tcp)` declared inside a loop or function would go out of scope, creating a dangling reference. When Reticulum's transport loop tried to access it: `LoadProhibited` crash.
`RNS::Transport::_interfaces` stores `Interface&` (references, not copies). If a `RNS::Interface` goes out of scope, it creates a dangling reference `LoadProhibited` crash.
**Fix**: Store TCP Interface wrappers in `std::list<RNS::Interface> tcpIfaces` at global scope. Must use `std::list`, not `std::vector` — vector reallocation would move objects in memory, invalidating references held by Transport.
**Fix**: Store TCP Interface wrappers in `std::list<RNS::Interface>` at global scope. Must use `std::list`, not `std::vector` — vector reallocation invalidates references.
### "auto detect board:24" in serial output
### GPIO 10 must be HIGH
This is the M5Unified library auto-detecting the board type. It's informational, not an error. The number 24 is M5's internal board ID for Cardputer Adv.
The T-Deck Plus requires GPIO 10 to be set HIGH at boot to enable all peripherals (display, radio, keyboard, etc.).
**Symptom**: Blank screen, no serial output, device appears dead.
**Fix**: `setup()` sets `pinMode(10, OUTPUT); digitalWrite(10, HIGH);` as the very first step.
---
@@ -113,67 +108,27 @@ This is the M5Unified library auto-detecting the board type. It's informational,
### LittleFS not mounting
**Symptom**: `[E][vfs_api.cpp:24] open(): File system is not mounted` during boot.
**Symptom**: `[E][vfs_api.cpp:24] open(): File system is not mounted`
**Possible causes**:
- First boot after flash erase — LittleFS partition needs formatting
- Partition table mismatch — verify `partitions_8MB_ota.csv` matches flash layout
- `flash.begin()` may fail silently
**Causes**: First boot after flash erase, or partition table mismatch.
**Workaround**: FlashStore attempts `LittleFS.begin(true)` which auto-formats on first use. If it persists, erase the LittleFS partition and reflash.
**Fix**: FlashStore calls `LittleFS.begin(true)` which auto-formats on first use. If it persists, erase flash and reflash:
### SD card directories missing
On first boot with a new SD card, the `/ratputer/` directory tree doesn't exist.
**Fix**: `setup()` calls `sdStore.ensureDir()` for all required paths after SD init. If directories are still missing, check that SD CS (GPIO 12) is not conflicting with radio SPI.
---
## Interop & RF Issues
### Ratputer TX/RX verification (QA Round 9)
- **Ratputer RX confirmed**: Received Heltec V3 RNode announce at -38 dBm, SNR 13.0
- **Ratputer TX confirmed**: All SX1262 registers verified correct (SF7, BW 500kHz, CR 4/5, sync 0x1424, CRC on)
- **Heltec V3 RNode receive path**: Has never decoded a single LoRa packet from any source. Shows Ratputer RF as interference (-50 to -81 dBm) but can't decode. This is a Heltec issue, not Ratputer.
### SetModulationParams must be called from STDBY mode
The SX1262 silently rejects `SetModulationParams` (0x8B) when issued from RX or TX mode. Only STDBY_RC and STDBY_XOSC are valid. The command appears to succeed (no error, BUSY goes low), but the hardware ignores the new SF/BW/CR values.
**Symptom**: Radio logs show correct SF/BW/CR (read from software variables), but actual TX airtime is wrong. For example, software says SF9 (expected ~900ms for 168 bytes) but actual TX completes in ~280ms (SF7). Both devices see each other's RF (RSSI visible) but every packet fails CRC. SNR is consistently -17 to -20 dB despite short range.
**Diagnosis**: Add timing instrumentation to `endPacket()` — measure `millis()` from `OP_TX_6X` to `TX_DONE` IRQ. Compare against expected airtime for the configured SF. If actual << expected, the SF didn't apply.
**Root cause**: `setSpreadingFactor()` / `setSignalBandwidth()` / `setCodingRate4()` were called from `main.cpp` after `begin()` had already entered RX mode via `receive()`. The SX1262 silently dropped the `SetModulationParams` command.
**Fix**: `setModulationParams()` now calls `standby()` before issuing the SPI command, ensuring the radio is in a valid mode to accept configuration changes.
### SX1262 calibration must run after TCXO is enabled
Per SX1262 datasheet Section 13.1.12, if a TCXO is used, it **must** be enabled before calling `calibrate()` or `calibrate_image()`. Calibration locks to whichever oscillator is active. If TCXO isn't enabled yet, calibration uses the internal RC oscillator (~13MHz, ±3% tolerance). Each chip's RC has a different offset, so two devices end up synthesizing slightly different actual frequencies. The combined error can exceed the LoRa demodulation window.
**Symptom**: TX completes successfully on both devices (TX_DONE fires, no errors). RSSI shows real signals (not noise floor). But neither device ever decodes the other's packets — no RX_DONE, no CRC errors, just silence. Each device works fine individually (e.g., over TCP/LXMF).
**Diagnosis**: If two TCXO-equipped SX1262 devices can't hear each other despite confirmed TX and visible RSSI, suspect calibration ordering.
**Fix**: In `SX1262::begin()`, the init order must be:
```
enableTCXO() → delay(5ms) → setRegulatorMode(DC-DC) → loraMode() → standby() → calibrate() → calibrate_image()
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* erase-flash
```
Also set:
- **Regulator mode** to DC-DC (0x01) — default is LDO, wastes power on boards with DC-DC inductors
- **RX/TX fallback mode** to STDBY_XOSC (0x30) — default STDBY_RC (0x20) shuts off TCXO between TX/RX transitions
### SD card not detected
### Debugging RF with RSSI Monitor
**Check**: SD card must be FAT32. The SD card shares the SPI bus with the radio and display (CS=GPIO 39).
Press **Ctrl+R** to sample RSSI continuously for 5 seconds. Transmit from another device during sampling. If RSSI stays at the noise floor (around -110 to -120 dBm), the RX front-end isn't hearing RF.
**Fix**: Ensure SD card is inserted before power-on. Check serial output for `[SD]` messages. The device works without an SD card — flash is used as fallback.
### Radio test packet
### Messages not persisting
Press **Ctrl+T** to send a test packet with a fixed header (0xA0) and payload `RATPUTER_TEST_1234567890`. Includes FIFO readback verification. Use this to confirm the TX path is working without involving Reticulum.
**Check**: Verify SD card directories exist. Boot should auto-create `/ratputer/messages/`, `/ratputer/contacts/`, etc.
**Fix**: If directories are missing, reboot with SD card inserted. `SDStore::formatForRatputer()` creates the full directory tree on boot.
---
@@ -181,17 +136,94 @@ Press **Ctrl+T** to send a test packet with a fixed header (0xA0) and payload `R
### AP and STA are separate modes
Ratputer uses **three WiFi modes**: OFF, AP, STA. These are NOT concurrent — `WIFI_AP_STA` was removed because it consumed ~20KB extra heap and caused instability.
Ratdeck uses three WiFi modes: OFF, AP, STA. These are NOT concurrent — `WIFI_AP_STA` was removed because it consumed too much heap and caused instability.
- **AP mode**: Creates `ratputer-XXXX` hotspot, runs TCP server on port 4242
- **STA mode**: Connects to an existing network, creates TCP client connections to configured endpoints
- **OFF**: No WiFi (saves power and heap)
- **AP mode**: Creates `ratdeck-XXXX` hotspot, TCP server on port 4242
- **STA mode**: Connects to an existing network, TCP client connections
- **OFF**: No WiFi
Switch modes in Settings → WiFi.
Switch modes in Setup -> Network. WiFi mode changes require reboot.
### TCP client connection lifecycle
### TCP client won't connect
In STA mode, TCP client connections to configured endpoints are created **once** on first WiFi connection. If WiFi drops and reconnects, existing TCP clients attempt reconnection automatically (every 10 seconds).
**Check**: WiFi must be in STA mode and connected to a network first. TCP clients are created after WiFi connection succeeds.
**Fix**: Verify the TCP endpoint address and port. Check serial for `[TCP]` messages. Auto-reconnect runs every 15 seconds.
### WiFi causes crashes
WiFi initialization is the most common crash source. The boot loop recovery system (3 consecutive failures -> WiFi OFF) handles this automatically.
**Manual fix**: If stuck, erase flash to reset NVS, reflash, and reconfigure WiFi settings.
---
## RF Debugging
### RSSI Monitor
Press **Ctrl+R** to sample RSSI continuously for 5 seconds. Transmit from another device during sampling. If RSSI stays at the noise floor (~-110 to -120 dBm), the RX front-end isn't hearing RF.
### Test Packet
Press **Ctrl+T** to send a test packet with FIFO readback verification. Confirms the TX path without involving Reticulum.
### Full Diagnostics
Press **Ctrl+D** for a complete diagnostic dump to serial:
| Field | Meaning |
|-------|---------|
| Identity hash | 16-byte Reticulum identity hash |
| Destination hash | LXMF destination address |
| Transport | ACTIVE or OFFLINE, endpoint or transport node |
| Paths / Links | Known Reticulum paths and active links |
| Freq / SF / BW / CR / TXP | Current radio parameters |
| SyncWord regs | Raw 0x0740/0x0741 values (should be 0x14/0x24) |
| DevErrors | SX1262 error register (0x0040 = PLL lock failure) |
| Current RSSI | Instantaneous RSSI in dBm |
| Free heap / PSRAM | Available memory |
| Flash | LittleFS used/total |
| Uptime | Seconds since boot |
---
## Factory Reset
### Settings-only reset
In Setup -> System -> Factory Reset: clears user config from flash and SD, then reboots. Radio, WiFi, display, and audio revert to defaults. Identity and messages are preserved.
### SD card wipe
Connect serial at 115200 baud. Power cycle the device and send `WIPE` within 500ms of boot. Deletes `/ratputer/*` on the SD card and recreates clean directories.
### Full flash erase
Erases everything — LittleFS, NVS, firmware:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* erase-flash
```
Reflash firmware after erasing. A new identity will be generated. SD card data is preserved.
---
## Serial Log Tags
| Tag | Subsystem |
|-----|-----------|
| `[BOOT]` | Setup sequence |
| `[RADIO]` | SX1262 driver |
| `[LORA_IF]` | LoRa <-> Reticulum interface |
| `[WIFI]` | WiFi AP/STA |
| `[TCP]` | TCP client connections |
| `[LXMF]` | LXMF message protocol |
| `[SD]` | SD card storage |
| `[BLE]` | BLE Sideband transport |
| `[HOTKEY]` | Keyboard hotkey dispatch |
| `[ID]` | Identity management |
---
@@ -199,71 +231,7 @@ In STA mode, TCP client connections to configured endpoints are created **once**
| Feature | Status | Notes |
|---------|--------|-------|
| BLE | Disabled (stub) | Saves ~50KB heap. Planned for v1.1 Sideband protocol |
| GNSS | Pins defined, no code | v1.1 — GPS RX=15, TX=13 |
| OTA updates | Partition exists, not implemented | `app1` partition at 0x310000 is reserved |
| LittleFS | Occasional mount failures | Auto-formats on first use; rare failures after that |
| WiFi AP+STA | Removed | Uses too much heap; separate AP/STA modes instead |
| Split packets | Header flag defined, not implemented | Single-frame LoRa only (max 254 bytes payload) |
---
## Factory Reset
### SD card wipe (preserves flash data)
Connect serial at 115200 baud. Power cycle the device and send `WIPE` within 500ms of boot. This recursively deletes `/ratputer/*` on the SD card and recreates clean directories.
### Flash erase (full reset)
Erase all flash contents including LittleFS, NVS, and firmware:
```bash
python3 -m esptool --chip esp32s3 --port /dev/cu.usbmodem* erase-flash
```
Then reflash the firmware. A new Reticulum identity will be generated on first boot. All settings, messages, and contacts stored in flash are lost. SD card data is preserved.
### Settings-only reset
In Settings → About → Factory Reset: clears the user config JSON from flash and SD, then reboots. Radio, WiFi, display, and audio revert to compile-time defaults. Identity and messages are preserved.
---
## Diagnostic Reference
### Serial log tags
Every subsystem logs with a tag prefix for easy filtering:
| Tag | Subsystem |
|-----|-----------|
| `[BOOT]` | Setup sequence |
| `[RADIO]` | SX1262 driver |
| `[LORA_IF]` | LoRa ↔ Reticulum interface |
| `[WIFI]` | WiFi AP/STA |
| `[TCP]` | TCP client connections |
| `[LXMF]` | LXMF message protocol |
| `[SD]` | SD card storage |
| `[HOTKEY]` | Keyboard hotkey dispatch |
| `[TEST]` | Radio test packet (Ctrl+T) |
| `[RSSI]` | RSSI monitor (Ctrl+R) |
| `[AUTO]` | Periodic auto-announce |
| `[BLE]` | BLE stub status |
### Ctrl+D diagnostic fields
| Field | Meaning |
|-------|---------|
| Identity | 16-byte Reticulum destination hash (hex) |
| Transport | ACTIVE or OFFLINE |
| Paths / Links | Number of known Reticulum paths and active links |
| Freq / SF / BW / CR / TXP | Current radio parameters |
| Preamble | Preamble length in symbols |
| SyncWord regs | Raw 0x0740/0x0741 register values (should be 0x14/0x24 for Reticulum) |
| DevErrors | SX1262 error register (0x0040 = PLL lock failure) |
| Status | SX1262 chip mode and command status |
| Current RSSI | Instantaneous RSSI in dBm (noise floor ~-110 to -120 dBm) |
| Free heap | Available RAM in bytes (typical: ~120150 KB) |
| Flash | LittleFS used/total bytes |
| Uptime | Seconds since boot |
| Touchscreen | Disabled | GT911 needs calibration |
| GPS | Pins defined | Not yet implemented |
| Split packets | Header flag defined | Single-frame LoRa only (max ~254 bytes) |
| TCP client leak | Minor | Stopped clients not freed (Transport holds refs) |