Compare commits
1 Commits
main
..
psychichttp
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ff9b1987 |
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: "esp32-debug-dongle"
|
|
||||||
source: pka-assess
|
|
||||||
---
|
|
||||||
|
|
||||||
# esp32-debug-dongle
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
ESP32 dev-kit firmware turning the chip into a WiFi + Bluetooth serial-terminal dongle. Browser-based terminal uses xterm.js served from the ESP's LittleFS; also exposes a classic Bluetooth SPP port for desktop/mobile terminal apps. Multi-port (internal debug / USB / external Serial1) with on-the-fly baud-rate switching.
|
|
||||||
|
|
||||||
## Type
|
|
||||||
|
|
||||||
ESP32 PlatformIO firmware. `#embedded`, `#firmware`, `#iot`, `#service`.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **External:** PlatformIO `espressif32` + Arduino framework, xterm.js (bundled in LittleFS), Bluetooth SPP stack (classic BT — needs a WROOM-style ESP32, NOT S2/S3/C3).
|
|
||||||
- **Internal:** complementary tool to ESP32 Sh3d projects — [[Sh3dNb]], [[Sh3dController]], [[Sh3dStick]], [[Doxy]] — when serial access is awkward.
|
|
||||||
|
|
||||||
## Notable
|
|
||||||
|
|
||||||
- Uses a `huge_app.csv` partition (3 MB app, 1 MB FS, no OTA) — swap to `min_spiffs.csv` if OTA is needed.
|
|
||||||
- Classic Bluetooth requirement is a hard constraint on target hardware.
|
|
||||||
@@ -1,254 +1,275 @@
|
|||||||
# ESP32 Debug Dongle
|
# ESP32 Debug Dongle
|
||||||
|
|
||||||
A remote serial-debug bridge for ESP32 targets. It bridges a target device's UART to a
|
A WiFi/Bluetooth serial debugging tool for ESP32. Access serial ports via web browser or Bluetooth terminal.
|
||||||
**web terminal**, **telnet**, and (on the original ESP32) **Bluetooth SPP** — and adds GPIO
|
|
||||||
reset/button control, NTP-dated SD logging, an OLED status page, and a **MeshCore LoRa** comms
|
|
||||||
panel for sniffing/sending channel traffic while you debug.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*Web UI: serial terminal (left), SD log files (top), and the MeshCore comms panel (right).*
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Web terminal** — browser serial terminal (xterm.js) over WebSocket
|
- **Web Terminal**: Browser-based serial terminal using xterm.js
|
||||||
- **Telnet** (port 23) — primary remote path for `nc`/minicom or an agent
|
- **Bluetooth SPP**: Classic Bluetooth serial port for desktop/mobile apps
|
||||||
- **Bluetooth SPP** — Classic Bluetooth serial, original ESP32 only
|
- **Multi-Port**: Switch between internal debug, USB serial, and external serial
|
||||||
- **Multi-port** — switch between internal-debug loopback, USB serial, and the external target UART
|
- **Virtual Serial**: Internal loopback for ESP32's own debug output
|
||||||
- **Target control** — pulse reset, pulse/latch a button (wake / force-on) from web, telnet, and REST
|
- **Configurable**: Change baud rates on the fly
|
||||||
- **SD logging** (T3-S3) — NTP-dated logs of UART *and* MeshCore traffic, with a self-describing header
|
- **Robust Server**: Uses PsychicHttp - stable under load (unlike ESPAsyncWebServer)
|
||||||
- **MeshCore panel** (T3-S3 + LoRa) — program a channel PSK at runtime, watch received messages, send messages
|
|
||||||
- **OLED status** (T3-S3) — IP, WiFi, UART, byte counts, log/SD/NTP state
|
|
||||||
|
|
||||||
## Build variants
|
|
||||||
|
|
||||||
The board and optional features are selected by PlatformIO environment (`platformio.ini`):
|
|
||||||
|
|
||||||
| Env | Board | SD | OLED | MeshCore LoRa | Bluetooth |
|
|
||||||
|-----|-------|----|------|---------------|-----------|
|
|
||||||
| `esp32dev` | generic ESP32 | – | – | – | ✅ Classic SPP |
|
|
||||||
| `t3s3` | LilyGo T3-S3 (ESP32-S3) | ✅ | ✅ | – | – |
|
|
||||||
| `t3s3_mesh` | LilyGo T3-S3 (ESP32-S3 + SX1276) | ✅ | ✅ | ✅ | – |
|
|
||||||
|
|
||||||
ESP32-S3 has no Classic Bluetooth — telnet + the web terminal replace SerialBT there.
|
|
||||||
`t3s3_mesh` extends `t3s3` and just adds the radio (`-DUSE_MESHCORE=1`); everything mesh-related
|
|
||||||
compiles to no-ops in the other builds, so the firmware and web UI are identical across all three.
|
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
### Pin connections — LilyGo T3-S3 (`t3s3` / `t3s3_mesh`)
|
### Requirements
|
||||||
|
- ESP32 DevKit v1 (or compatible ESP32 board with Classic Bluetooth)
|
||||||
|
- **Note**: ESP32-S2, S3, C3 do NOT support Classic Bluetooth SPP
|
||||||
|
|
||||||
Wire the target to the **right-hand header** — reset, TX, RX, and button are the top pins, with
|
### Pin Connections for External Serial (Serial1)
|
||||||
GND a few pins down:
|
|
||||||
|
|
||||||
| T3-S3 GPIO | Function | Connect to target |
|
| ESP32 Pin | Function | Connect To |
|
||||||
|------------|----------|-------------------|
|
|-----------|----------|------------|
|
||||||
| GPIO38 | Reset out (active-low pulse) | target RST |
|
| GPIO16 | RX1 | External device TX |
|
||||||
| GPIO43 | TX | target RX |
|
| GPIO17 | TX1 | External device RX |
|
||||||
| GPIO44 | RX | target TX |
|
| GND | Ground | External device GND |
|
||||||
| GPIO39 | Button out (active-low pulse/latch) | target button / wake |
|
|
||||||
| GND | Ground | target GND |
|
|
||||||
|
|
||||||
Onboard peripherals (already wired on the board, listed for reference): SD card on HSPI
|
|
||||||
(SCK 14, MISO 2, MOSI 11, CS 13); OLED on I2C (SDA 18, SCL 17); and in `t3s3_mesh` the SX1276
|
|
||||||
on FSPI (NSS 7, RST 8, DIO0 9, DIO1 33, SCLK 5, MISO 3, MOSI 6, RXEN 21, TXEN 10). The reset/
|
|
||||||
button polarity is active-low (`GPIO_CTRL_ACTIVE_LOW=1`); change it in `platformio.ini` if your
|
|
||||||
target is active-high.
|
|
||||||
|
|
||||||
### Pin connections — generic ESP32 (`esp32dev`)
|
|
||||||
|
|
||||||
External serial defaults to RX=GPIO16, TX=GPIO17, plus a common GND. No SD/OLED/LoRa.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Install PlatformIO
|
### 1. Install PlatformIO
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install platformio # or use the VS Code PlatformIO IDE extension
|
# Install PlatformIO CLI (if not already installed)
|
||||||
|
pip install platformio
|
||||||
|
|
||||||
|
# Or use VS Code with PlatformIO IDE extension
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Build & upload
|
### 2. Build and Upload
|
||||||
|
|
||||||
Pick your environment with `-e`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Clone/copy this project
|
||||||
cd esp32-debug-dongle
|
cd esp32-debug-dongle
|
||||||
|
|
||||||
# Firmware
|
# Build the firmware
|
||||||
pio run -e t3s3_mesh -t upload # or: -e t3s3 / -e esp32dev
|
pio run
|
||||||
|
|
||||||
# Web files (LittleFS) -- needed on first flash and after any data/ change
|
# Upload firmware to ESP32
|
||||||
pio run -e t3s3_mesh -t uploadfs
|
pio run -t upload
|
||||||
|
|
||||||
# Serial monitor
|
# Upload web files to LittleFS
|
||||||
pio device monitor
|
pio run -t uploadfs
|
||||||
```
|
```
|
||||||
|
|
||||||
> **MeshCore note:** `t3s3_mesh` pulls the MeshCore library from a local checkout
|
|
||||||
> (`symlink:///home/scottp/github/MeshCore`) plus RadioLib / Crypto / RTClib / base64 / ed25519.
|
|
||||||
> Adjust that path in `platformio.ini` to wherever your MeshCore checkout lives.
|
|
||||||
|
|
||||||
### 3. Connect
|
### 3. Connect
|
||||||
|
|
||||||
The dongle joins your WiFi in station mode (SSID/password are build flags in `platformio.ini`).
|
#### Via WiFi (Station Mode - Default)
|
||||||
If it can't join, it falls back to an access point:
|
|
||||||
|
|
||||||
- **AP SSID:** `ESP32-DebugDongle` **Password:** `debug1234` → open `http://192.168.4.1`
|
1. Edit `src/main.cpp` and set your WiFi credentials:
|
||||||
|
```cpp
|
||||||
|
const char* STA_SSID = "YourNetworkSSID";
|
||||||
|
const char* STA_PASSWORD = "YourPassword";
|
||||||
|
```
|
||||||
|
2. Upload and check serial monitor for the assigned IP address
|
||||||
|
3. Open browser: `http://<ip-address>`
|
||||||
|
|
||||||
On a successful station join, the device prints its IP on the USB serial monitor and on the OLED:
|
#### Via WiFi (AP Mode Fallback)
|
||||||
|
|
||||||
```
|
If station mode fails, or you set `WIFI_STATION_MODE false`:
|
||||||
[Ready] http://10.0.1.241 telnet 10.0.1.241 23
|
1. Connect to WiFi network: `ESP32-DebugDongle`
|
||||||
```
|
2. Password: `debug1234`
|
||||||
|
3. Open browser: `http://192.168.4.1`
|
||||||
|
|
||||||
#### Bluetooth (`esp32dev` only)
|
#### Via Bluetooth
|
||||||
|
|
||||||
Pair with `ESP32-Debug`, then use any BT serial terminal (Android "Serial Bluetooth Terminal",
|
1. Pair with device: `ESP32-Debug`
|
||||||
Windows PuTTY on the COM port, Linux `rfcomm`, macOS `/dev/tty.ESP32-Debug`).
|
2. Use any Bluetooth serial terminal app:
|
||||||
|
- **Android**: "Serial Bluetooth Terminal" by Kai Morich
|
||||||
|
- **Windows**: PuTTY (use assigned COM port after pairing)
|
||||||
|
- **Linux**: `rfcomm connect 0 XX:XX:XX:XX:XX:XX` then use `/dev/rfcomm0`
|
||||||
|
- **macOS**: Pair in System Preferences, use `/dev/tty.ESP32-Debug`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Web interface
|
### Web Interface
|
||||||
|
|
||||||
- **Port** — Internal (debug loopback) / USB Serial / External (target UART)
|
The web terminal provides:
|
||||||
- **Baud** — 9600…921600
|
- **Port Selection**: Choose between Internal, USB Serial, or External
|
||||||
- **Reset / Button** — momentary pulse of the target reset / button lines
|
- **Baud Rate**: Configure serial speed (9600 - 921600)
|
||||||
- **Hold** — latch the button line held active (force-on) until released
|
- **Clear**: Clear terminal screen
|
||||||
- **Clear / Reconnect** — terminal + WebSocket
|
- **Reconnect**: Re-establish WebSocket connection
|
||||||
- **Log / Files** — toggle SD logging and browse/download/delete log files (T3-S3)
|
|
||||||
- **MeshCore panel** — program a channel + PSK, view RX/TX messages, send a message
|
|
||||||
|
|
||||||
### Serial ports
|
### Serial Ports
|
||||||
|
|
||||||
| Port | Description | Use case |
|
| Port | Description | Use Case |
|
||||||
|------|-------------|----------|
|
|------|-------------|----------|
|
||||||
| Internal | Virtual loopback buffer | the dongle's own debug output |
|
| Internal | Virtual loopback buffer | ESP32's own debug output |
|
||||||
| USB Serial | UART0 (shared with USB) | console |
|
| USB Serial | UART0 (USB connection) | Shared with programming |
|
||||||
| External | Serial1 (target UART pins) | the device under test |
|
| External | Serial1 (GPIO16/17) | External device debugging |
|
||||||
|
|
||||||
### Telnet (port 23)
|
### Using Internal Debug Output
|
||||||
|
|
||||||
Any line is forwarded verbatim to the target UART **unless** it starts with `~`, in which case
|
In your ESP32 code, use the provided helper functions:
|
||||||
it's a dongle command:
|
|
||||||
|
|
||||||
```
|
```cpp
|
||||||
~help list commands
|
// Write to internal virtual serial
|
||||||
~status port/baud/counters/log/ntp/heap
|
debugPrint("Sensor value: %d", sensorValue);
|
||||||
~reset [ms] pulse the reset line (default 200 ms)
|
debugPrintln("Status: OK");
|
||||||
~button [ms|on|off] pulse the button line, or latch it on/off (force-on)
|
|
||||||
~baud <n> set target baud
|
// Or write directly to the loopback stream
|
||||||
~port <int|usb|ext> select the active port
|
internalSerial.println("Debug message");
|
||||||
~log on|off SD logging (T3-S3)
|
|
||||||
~gpio <pin> <0|1> drive an arbitrary GPIO
|
|
||||||
~mesh [on|off] mesh status / toggle echo of mesh msgs to telnet
|
|
||||||
~psk <base64key> reprogram the user channel's PSK (16- or 32-byte key)
|
|
||||||
~chan <name> <base64key> set channel name + PSK together
|
|
||||||
~msg <text> send a message on the user channel
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### REST API
|
These messages appear when "Internal" port is selected.
|
||||||
|
|
||||||
Mirrors the telnet commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
/api/status /api/reset?ms= /api/button?ms= | ?latch=on|off
|
|
||||||
/api/baud?baud= /api/port?port=int|usb|ext
|
|
||||||
/api/log?on=0|1 /api/gpio?pin=&val= /api/send?data=
|
|
||||||
/api/logs /api/logfile?name= /api/logdelete?name= (T3-S3)
|
|
||||||
```
|
|
||||||
|
|
||||||
## MeshCore comms panel (`t3s3_mesh`)
|
|
||||||
|
|
||||||
The right-hand panel talks to a MeshCore `BaseChatMesh` node on the SX1276 radio. It listens on
|
|
||||||
the well-known `Public` channel plus one **user channel** (default `SensorsTest`). You can:
|
|
||||||
|
|
||||||
- **Program** a new channel name + PSK at runtime (base64-encoded 16- or 32-byte key). The PSK is
|
|
||||||
persisted to NVS, so it survives reboots, and the change is recorded in the SD log.
|
|
||||||
- **Watch** received channel messages (sender, text, RSSI/SNR).
|
|
||||||
- **Send** a message on the user channel.
|
|
||||||
|
|
||||||
The same actions are available over telnet (`~psk`, `~chan`, `~msg`, `~mesh`). LoRa PHY defaults
|
|
||||||
are Australia-narrow (916.575 MHz, BW 62.5, SF7, CR8, 20 dBm) — change them in `platformio.ini`.
|
|
||||||
|
|
||||||
## SD logging (T3-S3)
|
|
||||||
|
|
||||||
Logs are written to NTP-dated files under `/logs/` on the SD card. Each file opens with a
|
|
||||||
self-describing header and captures both the target UART stream and MeshCore RX/TX, timestamped
|
|
||||||
per line:
|
|
||||||
|
|
||||||
```
|
|
||||||
# debug-dongle log opened /logs/20260616-142348.log
|
|
||||||
# uart: port=external baud=115200 rx=44 tx=43
|
|
||||||
# mesh: up node=dongle channel=SensorsPH psk=<base64> freq=916.575 bw=62.5 sf=7 cr=8 tx=20
|
|
||||||
[14:24:15] [mesh rx batcave] device=ScottTrailer batt=13.11 ...
|
|
||||||
[14:26:36] sensor boot #1 ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This is a low-level debugger, so the channel PSK is logged in plaintext on purpose — the card
|
|
||||||
carries the key. Browse/download/delete logs from the **Files** panel or the `/api/logs*`
|
|
||||||
endpoints. Toggle with the **Log** button or `~log on|off`.
|
|
||||||
|
|
||||||
## WebSocket protocol
|
|
||||||
|
|
||||||
Endpoint: `ws://<device-ip>/ws`
|
|
||||||
|
|
||||||
- **Binary frames** — raw target serial data (both directions).
|
|
||||||
- **Text frames** — control/event JSON, prefixed with a `0x00` byte. Events carry a `type`:
|
|
||||||
`status`, `mesh` (a received/sent message), or `meshcfg` (channel/PSK state).
|
|
||||||
|
|
||||||
Browser → device commands (sent as `0x00` + JSON):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{ "cmd": "setPort", "port": 2 } // 0=Internal, 1=USB, 2=External
|
|
||||||
{ "cmd": "setBaud", "baud": 115200 }
|
|
||||||
{ "cmd": "getStatus" }
|
|
||||||
{ "cmd": "meshSend", "text": "hello" }
|
|
||||||
{ "cmd": "meshPsk", "name": "SensorsTest", "psk": "PNtgMxiq9R7eQ3IleHoL3g==" }
|
|
||||||
{ "cmd": "meshGet" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Most settings are **build flags** in `platformio.ini` (per environment): WiFi `STA_SSID` /
|
Edit `src/main.cpp` to change defaults:
|
||||||
`STA_PASSWORD`, target UART pins (`TARGET_RX_PIN` / `TARGET_TX_PIN`), control pins
|
|
||||||
(`GPIO_RESET_PIN` / `GPIO_WAKE_PIN` / `GPIO_CTRL_ACTIVE_LOW`), SD/OLED/LoRa pins, the LoRa PHY,
|
|
||||||
and the default mesh channel (`SENSORS_CHANNEL_NAME` / `SENSORS_CHANNEL_PSK_B64`). The AP SSID,
|
|
||||||
Bluetooth name, and default bauds live near the top of `src/main.cpp`.
|
|
||||||
|
|
||||||
## Project structure
|
```cpp
|
||||||
|
// WiFi Mode: true = connect to existing network, false = create AP
|
||||||
|
#define WIFI_STATION_MODE true
|
||||||
|
|
||||||
|
// Your WiFi network credentials (station mode)
|
||||||
|
const char* STA_SSID = "YourNetworkSSID";
|
||||||
|
const char* STA_PASSWORD = "YourPassword";
|
||||||
|
|
||||||
|
// Fallback Access Point settings
|
||||||
|
const char* AP_SSID = "ESP32-DebugDongle";
|
||||||
|
const char* AP_PASSWORD = "debug1234";
|
||||||
|
|
||||||
|
// Connection timeout before falling back to AP mode
|
||||||
|
#define WIFI_CONNECT_TIMEOUT 15000
|
||||||
|
|
||||||
|
// Bluetooth name
|
||||||
|
const char* BT_NAME = "ESP32-Debug";
|
||||||
|
|
||||||
|
// Serial1 pins
|
||||||
|
#define SERIAL1_RX_PIN 16
|
||||||
|
#define SERIAL1_TX_PIN 17
|
||||||
|
|
||||||
|
// Default baud rates
|
||||||
|
#define DEFAULT_BAUD_SERIAL 115200
|
||||||
|
#define DEFAULT_BAUD_SERIAL1 115200
|
||||||
|
```
|
||||||
|
|
||||||
|
### WiFi Modes
|
||||||
|
|
||||||
|
**Station Mode** (`WIFI_STATION_MODE true`):
|
||||||
|
- Connects to your existing WiFi network
|
||||||
|
- Access the dongle from any device on the same network
|
||||||
|
- Falls back to AP mode if connection fails
|
||||||
|
|
||||||
|
**Access Point Mode** (`WIFI_STATION_MODE false`):
|
||||||
|
- Creates its own WiFi network
|
||||||
|
- Connect directly to the ESP32's network
|
||||||
|
- IP address: 192.168.4.1
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
esp32-debug-dongle/
|
esp32-debug-dongle/
|
||||||
├── platformio.ini # build envs: esp32dev / t3s3 / t3s3_mesh
|
├── platformio.ini # PlatformIO configuration
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.cpp # bridge: UART <-> web/telnet/BT, control, SD log, OLED
|
│ └── main.cpp # Main ESP32 firmware
|
||||||
│ ├── meshcore_link.{h,cpp} # MeshCore node facade (no-op unless USE_MESHCORE)
|
|
||||||
│ └── LoopbackStream.{h,cpp} # internal virtual serial
|
|
||||||
├── data/
|
├── data/
|
||||||
│ └── index.html # web UI (uploaded to LittleFS)
|
│ └── index.html # Web interface (uploaded to LittleFS)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── download_xterm.py # optional: host xterm.js locally instead of CDN
|
│ └── download_xterm.py # Optional: download xterm.js locally
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## xterm.js Setup
|
||||||
|
|
||||||
|
The web interface uses xterm.js loaded from CDN. If you need offline operation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download files locally
|
||||||
|
python scripts/download_xterm.py --local
|
||||||
|
|
||||||
|
# Then edit data/index.html to use local paths:
|
||||||
|
# <link rel="stylesheet" href="/css/xterm.min.css">
|
||||||
|
# <script src="/js/xterm.min.js"></script>
|
||||||
|
# etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Protocol
|
||||||
|
|
||||||
|
The WebSocket endpoint is `ws://192.168.4.1/ws`
|
||||||
|
|
||||||
|
### Data Format
|
||||||
|
|
||||||
|
- **Regular serial data**: Raw bytes sent/received directly
|
||||||
|
- **Commands**: JSON prefixed with `0x00` byte
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Switch serial port
|
||||||
|
{ "cmd": "setPort", "port": 0 } // 0=Internal, 1=USB, 2=External
|
||||||
|
|
||||||
|
// Set baud rate
|
||||||
|
{ "cmd": "setBaud", "port": 2, "baud": 115200 }
|
||||||
|
|
||||||
|
// Get status
|
||||||
|
{ "cmd": "getStatus" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://192.168.4.1/ws');
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
// Send serial data
|
||||||
|
ws.send(new TextEncoder().encode('Hello\r\n'));
|
||||||
|
|
||||||
|
// Send command
|
||||||
|
function sendCommand(cmd) {
|
||||||
|
const json = JSON.stringify(cmd);
|
||||||
|
const data = new Uint8Array(json.length + 1);
|
||||||
|
data[0] = 0x00;
|
||||||
|
new TextEncoder().encodeInto(json, data.subarray(1));
|
||||||
|
ws.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive data
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
const data = new Uint8Array(e.data);
|
||||||
|
if (data[0] === 0x00) {
|
||||||
|
// Command response
|
||||||
|
const json = JSON.parse(new TextDecoder().decode(data.slice(1)));
|
||||||
|
console.log('Response:', json);
|
||||||
|
} else {
|
||||||
|
// Serial data
|
||||||
|
console.log('Serial:', new TextDecoder().decode(data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **Web page won't load** — upload the filesystem (`pio run -e <env> -t uploadfs`); use `http://`, not `https`.
|
### Can't connect to WiFi
|
||||||
- **No serial data** — check baud, swap TX/RX, ensure a common ground.
|
- Ensure you're connecting to `ESP32-DebugDongle` network
|
||||||
- **SD "not found" / CRC errors** — confirm the SD pins, and on `t3s3_mesh` that the radio is on
|
- Password is `debug1234` (case-sensitive)
|
||||||
FSPI and SD on HSPI (they must be different SPI peripherals).
|
- Try resetting the ESP32
|
||||||
- **MeshCore: nothing received** — confirm the channel PSK matches the sender's, and the LoRa PHY
|
|
||||||
(freq/BW/SF/CR) matches the fleet.
|
### Web page won't load
|
||||||
- **Terminal lines "staircase"** — already handled (`convertEol`); the raw SD log keeps bare `\n`.
|
- Make sure you uploaded the filesystem: `pio run -t uploadfs`
|
||||||
- **Bluetooth won't pair** — original ESP32 only (`esp32dev`); not on S3.
|
- Check serial monitor for errors
|
||||||
|
- Try `http://192.168.4.1` (not https)
|
||||||
|
|
||||||
|
### Bluetooth won't pair
|
||||||
|
- Only works on original ESP32 (not S2, S3, C3)
|
||||||
|
- Delete existing pairing and try again
|
||||||
|
- Check that Bluetooth is enabled in build flags
|
||||||
|
|
||||||
|
### No serial data
|
||||||
|
- Verify baud rate matches your device
|
||||||
|
- Check TX/RX connections (try swapping them)
|
||||||
|
- Ensure common ground connection
|
||||||
|
|
||||||
|
### Build errors
|
||||||
|
- Ensure you have the ESP32 board package installed in PlatformIO
|
||||||
|
- Library dependencies should auto-install on first build
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License — feel free to use and modify.
|
MIT License - Feel free to use and modify.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [xterm.js](https://xtermjs.org/) — terminal emulator
|
- [xterm.js](https://xtermjs.org/) - Terminal emulator
|
||||||
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) — async web server
|
- [PsychicHttp](https://github.com/hoeken/PsychicHttp) - Robust HTTP/WebSocket server for ESP32
|
||||||
- [ArduinoJson](https://arduinojson.org/) — JSON library by Benoît Blanchon
|
- [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon
|
||||||
- [MeshCore](https://github.com/meshcore-dev/MeshCore) — LoRa mesh + RadioLib radio drivers
|
|
||||||
|
|||||||
-263
@@ -109,85 +109,16 @@
|
|||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-row {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container {
|
.terminal-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal {
|
#terminal {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right-side comms panel (MeshCore today, more later) */
|
|
||||||
.comms-panel {
|
|
||||||
flex: 0 0 340px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #16213e;
|
|
||||||
border-left: 1px solid #0f3460;
|
|
||||||
padding: 10px 12px;
|
|
||||||
gap: 10px;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.comms-panel h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
color: #e94560;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.comms-cfg {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
.comms-cfg input {
|
|
||||||
background: #0f3460;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #1a4a7a;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.comms-cfg .row { display: flex; gap: 6px; }
|
|
||||||
.comms-cfg .row input { flex: 1; }
|
|
||||||
#meshMessages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: #0f1a30;
|
|
||||||
border: 1px solid #0f3460;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
||||||
font-size: 0.8em;
|
|
||||||
line-height: 1.4;
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
.mc-msg { margin-bottom: 4px; word-break: break-word; }
|
|
||||||
.mc-msg .who { color: #60a5fa; }
|
|
||||||
.mc-msg.tx .who { color: #4ade80; }
|
|
||||||
.mc-msg .meta { color: #666; font-size: 0.85em; }
|
|
||||||
.mc-empty { color: #666; }
|
|
||||||
.comms-send { display: flex; gap: 6px; }
|
|
||||||
.comms-send input { flex: 1; }
|
|
||||||
.mesh-dot {
|
|
||||||
width: 8px; height: 8px; border-radius: 50%;
|
|
||||||
background: #666; display: inline-block;
|
|
||||||
}
|
|
||||||
.mesh-dot.up { background: #4ade80; }
|
|
||||||
.mesh-dot.down { background: #ef4444; }
|
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
@@ -207,14 +138,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 900px) {
|
|
||||||
.main-row { flex-direction: column; }
|
|
||||||
.comms-panel {
|
|
||||||
flex: 0 0 45vh;
|
|
||||||
border-left: none;
|
|
||||||
border-top: 1px solid #0f3460;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -242,13 +165,8 @@
|
|||||||
<option value="921600">921600</option>
|
<option value="921600">921600</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="danger" onclick="doReset()" title="Pulse the target reset line">Reset</button>
|
|
||||||
<button onclick="doButton()" title="Momentary press of the target button / wake line">Button</button>
|
|
||||||
<button id="holdBtn" onclick="toggleHold()" title="Latch the button line held down (force-on) / release">Hold: off</button>
|
|
||||||
<button onclick="clearTerminal()">Clear</button>
|
<button onclick="clearTerminal()">Clear</button>
|
||||||
<button onclick="reconnect()">Reconnect</button>
|
<button onclick="reconnect()">Reconnect</button>
|
||||||
<button id="logBtn" onclick="toggleLog()">Log: --</button>
|
|
||||||
<button onclick="toggleFiles()">Files</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
@@ -266,39 +184,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="filesPanel" style="display:none; background:#16213e; border-bottom:1px solid #0f3460; padding:10px 20px; max-height:40vh; overflow:auto;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
|
||||||
<strong>SD log files</strong>
|
|
||||||
<span><span id="logState" style="color:#aaa; margin-right:10px;"></span><button onclick="refreshLogs()">Refresh</button></span>
|
|
||||||
</div>
|
|
||||||
<table id="filesTable" style="width:100%; border-collapse:collapse; font-size:0.85em;"></table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-row">
|
|
||||||
<div class="terminal-container">
|
<div class="terminal-container">
|
||||||
<div id="terminal"></div>
|
<div id="terminal"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comms-panel" id="commsPanel">
|
|
||||||
<h2>
|
|
||||||
<span>📡 MeshCore</span>
|
|
||||||
<span><span class="mesh-dot" id="meshDot"></span> <span id="meshState" style="font-size:0.75em;color:#aaa;">…</span></span>
|
|
||||||
</h2>
|
|
||||||
<div class="comms-cfg">
|
|
||||||
<input type="text" id="meshChan" placeholder="Channel name (e.g. SensorsTest)">
|
|
||||||
<div class="row">
|
|
||||||
<input type="text" id="meshPsk" placeholder="PSK (base64, 16 or 32-byte key)">
|
|
||||||
<button onclick="programChannel()">Program</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="meshMessages"><div class="mc-empty">No messages yet.</div></div>
|
|
||||||
<div class="comms-send">
|
|
||||||
<input type="text" id="meshInput" placeholder="Message…" onkeydown="if(event.key==='Enter')sendMesh()">
|
|
||||||
<button onclick="sendMesh()">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<span id="rxCount">RX: 0 bytes</span>
|
<span id="rxCount">RX: 0 bytes</span>
|
||||||
<span id="txCount">TX: 0 bytes</span>
|
<span id="txCount">TX: 0 bytes</span>
|
||||||
@@ -309,7 +198,6 @@
|
|||||||
// Terminal setup
|
// Terminal setup
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
convertEol: true, // target sends bare '\n'; treat it as CRLF so lines don't staircase
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||||
theme: {
|
theme: {
|
||||||
@@ -374,7 +262,6 @@
|
|||||||
|
|
||||||
// Request initial status
|
// Request initial status
|
||||||
sendCommand({ cmd: 'getStatus' });
|
sendCommand({ cmd: 'getStatus' });
|
||||||
sendCommand({ cmd: 'meshGet' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
@@ -396,13 +283,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
// Control/mesh events arrive as text frames (0x00-prefixed JSON).
|
|
||||||
if (typeof event.data === 'string') {
|
|
||||||
let s = event.data;
|
|
||||||
if (s.charCodeAt(0) === 0) s = s.slice(1);
|
|
||||||
try { handleResponse(JSON.parse(s)); } catch (e) {}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
const data = new Uint8Array(event.data);
|
const data = new Uint8Array(event.data);
|
||||||
|
|
||||||
@@ -435,8 +315,6 @@
|
|||||||
document.getElementById('baudSelect').value = msg.baudSerial1;
|
document.getElementById('baudSelect').value = msg.baudSerial1;
|
||||||
updateStatus('btStatus', msg.btConnected);
|
updateStatus('btStatus', msg.btConnected);
|
||||||
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
||||||
updateLogUi(msg);
|
|
||||||
updateButtonUi(msg);
|
|
||||||
|
|
||||||
// Show WiFi info
|
// Show WiFi info
|
||||||
const wifiInfo = document.getElementById('wifiInfo');
|
const wifiInfo = document.getElementById('wifiInfo');
|
||||||
@@ -455,76 +333,9 @@
|
|||||||
const portNames = ['Internal', 'USB Serial', 'External'];
|
const portNames = ['Internal', 'USB Serial', 'External'];
|
||||||
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
|
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mesh':
|
|
||||||
appendMeshMsg(msg);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'meshcfg':
|
|
||||||
updateMeshCfg(msg);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- MeshCore comms panel ----
|
|
||||||
let meshEnabled = false;
|
|
||||||
function updateMeshCfg(c) {
|
|
||||||
meshEnabled = !!c.enabled;
|
|
||||||
const dot = document.getElementById('meshDot');
|
|
||||||
const state = document.getElementById('meshState');
|
|
||||||
const chan = document.getElementById('meshChan');
|
|
||||||
const psk = document.getElementById('meshPsk');
|
|
||||||
if (!meshEnabled) {
|
|
||||||
dot.className = 'mesh-dot';
|
|
||||||
state.textContent = 'not in build';
|
|
||||||
} else if (c.up) {
|
|
||||||
dot.className = 'mesh-dot up';
|
|
||||||
state.textContent = `up · rx ${c.rx} tx ${c.tx}`;
|
|
||||||
} else {
|
|
||||||
dot.className = 'mesh-dot down';
|
|
||||||
state.textContent = 'radio down';
|
|
||||||
}
|
|
||||||
// Don't clobber a field the user is editing.
|
|
||||||
if (document.activeElement !== chan && typeof c.channel !== 'undefined') chan.value = c.channel;
|
|
||||||
if (document.activeElement !== psk && typeof c.psk !== 'undefined') psk.value = c.psk;
|
|
||||||
const disabled = !meshEnabled;
|
|
||||||
document.getElementById('meshInput').disabled = disabled;
|
|
||||||
}
|
|
||||||
function appendMeshMsg(m) {
|
|
||||||
const box = document.getElementById('meshMessages');
|
|
||||||
const empty = box.querySelector('.mc-empty');
|
|
||||||
if (empty) empty.remove();
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'mc-msg' + (m.dir === 'tx' ? ' tx' : '');
|
|
||||||
const t = new Date().toLocaleTimeString();
|
|
||||||
let meta = m.dir === 'tx' ? '↑' : '↓';
|
|
||||||
if (m.channel) meta += ' ' + m.channel;
|
|
||||||
if (typeof m.rssi !== 'undefined' && m.rssi) meta += ` ${m.rssi}dBm`;
|
|
||||||
div.innerHTML = `<span class="meta">[${t}] ${escapeHtml(meta)}</span> ` +
|
|
||||||
`<span class="who">${escapeHtml(m.sender || '?')}:</span> ` +
|
|
||||||
escapeHtml(m.text || '');
|
|
||||||
box.appendChild(div);
|
|
||||||
box.scrollTop = box.scrollHeight;
|
|
||||||
}
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/[&<>"']/g, c =>
|
|
||||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
||||||
}
|
|
||||||
function programChannel() {
|
|
||||||
const name = document.getElementById('meshChan').value.trim();
|
|
||||||
const psk = document.getElementById('meshPsk').value.trim();
|
|
||||||
if (!psk) { alert('Enter a PSK (base64 16- or 32-byte key)'); return; }
|
|
||||||
sendCommand({ cmd: 'meshPsk', name: name, psk: psk });
|
|
||||||
setTimeout(() => sendCommand({ cmd: 'meshGet' }), 300);
|
|
||||||
}
|
|
||||||
function sendMesh() {
|
|
||||||
const inp = document.getElementById('meshInput');
|
|
||||||
const text = inp.value;
|
|
||||||
if (!text) return;
|
|
||||||
sendCommand({ cmd: 'meshSend', text: text });
|
|
||||||
inp.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendCommand(cmd) {
|
function sendCommand(cmd) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
const json = JSON.stringify(cmd);
|
const json = JSON.stringify(cmd);
|
||||||
@@ -596,80 +407,6 @@
|
|||||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Target control lines (reset / button) ----
|
|
||||||
function pulseLine(path, label) {
|
|
||||||
fetch(path + '?ms=200').then(r => r.text())
|
|
||||||
.then(t => term.writeln(`\r\n\x1b[33m[${label}] ${t.trim()}\x1b[0m`))
|
|
||||||
.catch(() => term.writeln(`\r\n\x1b[31m[${label} failed]\x1b[0m`));
|
|
||||||
}
|
|
||||||
function doReset() { pulseLine('/api/reset', 'reset'); }
|
|
||||||
function doButton() {
|
|
||||||
fetch('/api/button?ms=200').then(r => r.json()).then(s => {
|
|
||||||
updateButtonUi(s);
|
|
||||||
term.writeln('\r\n\x1b[33m[button] pulsed\x1b[0m');
|
|
||||||
}).catch(() => term.writeln('\r\n\x1b[31m[button failed]\x1b[0m'));
|
|
||||||
}
|
|
||||||
let holdOn = false;
|
|
||||||
function toggleHold() {
|
|
||||||
fetch('/api/button?latch=' + (holdOn ? 'off' : 'on')).then(r => r.json()).then(s => {
|
|
||||||
updateButtonUi(s);
|
|
||||||
term.writeln(`\r\n\x1b[33m[button] hold ${holdOn ? 'ON' : 'off'}\x1b[0m`);
|
|
||||||
}).catch(() => term.writeln('\r\n\x1b[31m[hold failed]\x1b[0m'));
|
|
||||||
}
|
|
||||||
function updateButtonUi(s) {
|
|
||||||
if (typeof s.buttonLatch === 'undefined') return;
|
|
||||||
holdOn = !!s.buttonLatch;
|
|
||||||
const b = document.getElementById('holdBtn');
|
|
||||||
b.textContent = 'Hold: ' + (holdOn ? 'ON' : 'off');
|
|
||||||
b.classList.toggle('danger', holdOn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- SD logging controls ----
|
|
||||||
let logOn = false;
|
|
||||||
function toggleLog() {
|
|
||||||
fetch('/api/log?on=' + (logOn ? 0 : 1))
|
|
||||||
.then(r => r.json()).then(updateLogUi)
|
|
||||||
.catch(() => term.writeln('\r\n\x1b[31m[log toggle failed]\x1b[0m'));
|
|
||||||
}
|
|
||||||
function updateLogUi(s) {
|
|
||||||
if (typeof s.log === 'undefined') return;
|
|
||||||
logOn = !!s.log;
|
|
||||||
const b = document.getElementById('logBtn');
|
|
||||||
b.textContent = 'Log: ' + (logOn ? 'ON' : 'off');
|
|
||||||
b.classList.toggle('danger', logOn);
|
|
||||||
const st = document.getElementById('logState');
|
|
||||||
if (st) {
|
|
||||||
st.textContent = !s.sd ? 'no SD card'
|
|
||||||
: s.logfile ? ('file: ' + String(s.logfile).split('/').pop())
|
|
||||||
: (s.ntp ? 'idle' : 'waiting for NTP date');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function toggleFiles() {
|
|
||||||
const p = document.getElementById('filesPanel');
|
|
||||||
const show = p.style.display === 'none';
|
|
||||||
p.style.display = show ? 'block' : 'none';
|
|
||||||
if (show) refreshLogs();
|
|
||||||
}
|
|
||||||
function refreshLogs() {
|
|
||||||
fetch('/api/logs').then(r => r.json()).then(list => {
|
|
||||||
const t = document.getElementById('filesTable');
|
|
||||||
if (!list.length) { t.innerHTML = '<tr><td style="color:#666;">(no log files yet)</td></tr>'; return; }
|
|
||||||
t.innerHTML = list.map(f =>
|
|
||||||
`<tr style="border-bottom:1px solid #0f3460;">
|
|
||||||
<td style="padding:4px 0;">${f.name}${f.active ? ' <span style="color:#4ade80;">(active)</span>' : ''}</td>
|
|
||||||
<td style="text-align:right; color:#aaa;">${formatBytes(f.size)}</td>
|
|
||||||
<td style="text-align:right; padding-left:12px;"><a href="/api/logfile?name=${encodeURIComponent(f.name)}" style="color:#60a5fa;">download</a></td>
|
|
||||||
<td style="text-align:right; padding-left:12px;">${f.active ? '' : `<a href="#" onclick="deleteLog('${f.name}');return false;" style="color:#e94560;">delete</a>`}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
}).catch(() => {
|
|
||||||
document.getElementById('filesTable').innerHTML = '<tr><td style="color:#e94560;">(failed -- SD only on T3-S3 build)</td></tr>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function deleteLog(name) {
|
|
||||||
if (!confirm('Delete ' + name + '?')) return;
|
|
||||||
fetch('/api/logdelete?name=' + encodeURIComponent(name)).then(() => refreshLogs());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial connection
|
// Initial connection
|
||||||
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
|
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
|
||||||
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
|
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
+10
-153
@@ -14,172 +14,29 @@
|
|||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
|
; Serial monitor settings
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = esp32_exception_decoder
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
board_build.filesystem = littlefs
|
; Build flags
|
||||||
|
|
||||||
; Use a larger app partition (pick ONE):
|
|
||||||
board_build.partitions = huge_app.csv ; 3MB app, 1MB FS, no OTA
|
|
||||||
; board_build.partitions = no_ota.csv ; 2MB app, 2MB FS, no OTA
|
|
||||||
; board_build.partitions = min_spiffs.csv ; 1.9MB app + OTA, 190KB FS
|
|
||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DCORE_DEBUG_LEVEL=3
|
-DCORE_DEBUG_LEVEL=3
|
||||||
-DCONFIG_BT_ENABLED=1
|
-DCONFIG_BT_ENABLED=1
|
||||||
-DCONFIG_BLUEDROID_ENABLED=1
|
-DCONFIG_BLUEDROID_ENABLED=1
|
||||||
-DHAS_BT_CLASSIC=1 ; classic SPP exists on the original ESP32
|
|
||||||
-DSTA_SSID="${sysenv.STA_SSID}"
|
; Partition scheme with space for LittleFS
|
||||||
-DSTA_PASSWORD="${sysenv.STA_PASSWORD}"
|
board_build.partitions = default.csv
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
; Async Web Server and dependencies
|
; PsychicHttp - robust HTTP server with WebSocket support
|
||||||
https://github.com/ESP32Async/ESPAsyncWebServer
|
hoeken/PsychicHttp @ ^2.1.0
|
||||||
https://github.com/ESP32Async/AsyncTCP
|
|
||||||
|
|
||||||
; ArduinoJson for configuration/commands
|
; ArduinoJson for configuration/commands
|
||||||
ArduinoJson
|
ArduinoJson @ ^7.0.0
|
||||||
|
|
||||||
; Upload settings (adjust port as needed)
|
; Upload settings (adjust port as needed)
|
||||||
; upload_port = /dev/ttyUSB0
|
; upload_port = /dev/ttyUSB0
|
||||||
; upload_speed = 921600
|
; upload_speed = 921600
|
||||||
|
|
||||||
; Extra scripts for LittleFS
|
|
||||||
extra_scripts =
|
|
||||||
pre:scripts/download_xterm.py
|
|
||||||
|
|
||||||
; ============================================================================
|
|
||||||
; LilyGo T3-S3 -- ESP32-S3 + SX127x LoRa + SSD1306 128x64 OLED + microSD.
|
|
||||||
; The remote debug bridge: UART <-> telnet(:23)/WebSocket, GPIO reset/wake,
|
|
||||||
; SD logging (NTP-dated), OLED status. NOTE: ESP32-S3 has NO Bluetooth Classic,
|
|
||||||
; so HAS_BT_CLASSIC is NOT set here (telnet + WebSocket replace SerialBT).
|
|
||||||
; Pins marked CONFIRM must be checked against your actual board wiring.
|
|
||||||
; ============================================================================
|
|
||||||
[env:t3s3]
|
|
||||||
platform = espressif32
|
|
||||||
board = esp32-s3-devkitc-1
|
|
||||||
framework = arduino
|
|
||||||
monitor_speed = 115200
|
|
||||||
monitor_filters = esp32_exception_decoder
|
|
||||||
board_build.mcu = esp32s3
|
|
||||||
board_build.flash_size = 4MB
|
|
||||||
board_upload.flash_size = 4MB
|
|
||||||
board_build.filesystem = littlefs
|
|
||||||
board_build.partitions = huge_app.csv
|
|
||||||
build_flags =
|
|
||||||
-DCORE_DEBUG_LEVEL=3
|
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1 ; console on USB CDC -> frees UART0 (43/44)
|
|
||||||
;-DSTA_SSID="${sysenv.STA_SSID}"
|
|
||||||
;-DSTA_PASSWORD="${sysenv.STA_PASSWORD}"
|
|
||||||
-DSTA_SSID="MeridenRainbow5G"
|
|
||||||
-DSTA_PASSWORD="4z8bcw5vfrs3n7dm"
|
|
||||||
-DBOARD_T3S3=1
|
|
||||||
; --- OLED (SSD1306 128x64) ---
|
|
||||||
-DT3S3_OLED_SDA=18
|
|
||||||
-DT3S3_OLED_SCL=17
|
|
||||||
-DT3S3_OLED_RST=21
|
|
||||||
-DT3S3_OLED_ADDR=0x3C
|
|
||||||
; --- target UART bridge (wire to the sensor's debug UART) ---
|
|
||||||
-DTARGET_RX_PIN=44 ; CONFIRM: dongle RX <- sensor TX
|
|
||||||
-DTARGET_TX_PIN=43 ; CONFIRM: dongle TX -> sensor RX
|
|
||||||
; --- GPIO control lines to the target (reset out / button out) ---
|
|
||||||
; GPIO38 + GPIO39 are free pins on the T3-S3 RIGHT header, right next to the
|
|
||||||
; UART pins TXD=43/RXD=44 -- so reset/button/TX/RX/GND all come off one row.
|
|
||||||
; (GPIO2=SD MISO and GPIO1=battery ADC; GPIO4/12 are not broken out at all.)
|
|
||||||
-DGPIO_RESET_PIN=38 ; -> sensor RST (active-low pulse)
|
|
||||||
-DGPIO_WAKE_PIN=39 ; -> sensor button/wake (active-low pulse)
|
|
||||||
-DGPIO_CTRL_ACTIVE_LOW=1
|
|
||||||
; --- microSD on FSPI/SPI2 (separate bus from LoRa, which uses HSPI/SPI3) ---
|
|
||||||
-DT3S3_SD_SCK=14
|
|
||||||
-DT3S3_SD_MISO=2
|
|
||||||
-DT3S3_SD_MOSI=11
|
|
||||||
-DT3S3_SD_CS=13
|
|
||||||
-DT3S3_SD_FREQ_HZ=20000000UL
|
|
||||||
; --- timezone for NTP-dated logs (Australia/Victoria) ---
|
|
||||||
-DNTP_TZ='"AEST-10AEDT,M10.1.0,M4.1.0/3"'
|
|
||||||
lib_deps =
|
|
||||||
https://github.com/ESP32Async/ESPAsyncWebServer
|
|
||||||
https://github.com/ESP32Async/AsyncTCP
|
|
||||||
ArduinoJson
|
|
||||||
adafruit/Adafruit SSD1306
|
|
||||||
adafruit/Adafruit GFX Library
|
|
||||||
extra_scripts =
|
|
||||||
pre:scripts/download_xterm.py
|
|
||||||
|
|
||||||
; ============================================================================
|
|
||||||
; LilyGo T3-S3 + MeshCore radio. Extends env:t3s3 (OLED + SD logging) and
|
|
||||||
; adds the SX1276 LoRa link so the web "MeshCore" panel and telnet ~psk/~msg
|
|
||||||
; commands work. Builds on the same hardware; pins match MeshCore's
|
|
||||||
; experiment/lilygot3s3logger (the proven T3-S3 SX1276 reference).
|
|
||||||
;
|
|
||||||
; pio run -e t3s3_mesh -t upload && pio run -e t3s3_mesh -t uploadfs
|
|
||||||
; ============================================================================
|
|
||||||
[env:t3s3_mesh]
|
|
||||||
extends = env:t3s3
|
|
||||||
; ed25519 (MeshCore's bundled identity crypto) lives in MeshCore/lib.
|
|
||||||
lib_extra_dirs =
|
|
||||||
/home/scottp/github/MeshCore/lib
|
|
||||||
lib_ldf_mode = deep+
|
|
||||||
build_unflags =
|
|
||||||
-DBOARD_HAS_PSRAM
|
|
||||||
lib_deps =
|
|
||||||
${env:t3s3.lib_deps}
|
|
||||||
symlink:///home/scottp/github/MeshCore
|
|
||||||
jgromes/RadioLib @ ^7.6.0
|
|
||||||
rweather/Crypto @ ^0.4.0
|
|
||||||
adafruit/RTClib @ ^2.1.3
|
|
||||||
densaugeo/base64 @ ~1.4.0
|
|
||||||
Preferences
|
|
||||||
ed25519
|
|
||||||
build_flags =
|
|
||||||
${env:t3s3.build_flags}
|
|
||||||
-w
|
|
||||||
-DUSE_MESHCORE=1
|
|
||||||
-D ESP32_PLATFORM
|
|
||||||
; --- pin 21 is the SX1276 RX-enable on the T3-S3, so the OLED can't use it
|
|
||||||
; as a reset line here; SSD1306 over I2C runs fine with no reset pin. ---
|
|
||||||
-DT3S3_OLED_RST=-1
|
|
||||||
; --- RadioLib: lock to the SX127x family ---
|
|
||||||
-D RADIOLIB_STATIC_ONLY=1
|
|
||||||
-D RADIOLIB_GODMODE=1
|
|
||||||
-D RADIOLIB_EXCLUDE_CC1101=1
|
|
||||||
-D RADIOLIB_EXCLUDE_RF69=1
|
|
||||||
-D RADIOLIB_EXCLUDE_SX1231=1
|
|
||||||
-D RADIOLIB_EXCLUDE_SI443X=1
|
|
||||||
-D RADIOLIB_EXCLUDE_RFM2X=1
|
|
||||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
|
||||||
-D RADIOLIB_EXCLUDE_AFSK=1
|
|
||||||
-D RADIOLIB_EXCLUDE_AX25=1
|
|
||||||
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
|
|
||||||
-D RADIOLIB_EXCLUDE_MORSE=1
|
|
||||||
-D RADIOLIB_EXCLUDE_APRS=1
|
|
||||||
-D RADIOLIB_EXCLUDE_BELL=1
|
|
||||||
-D RADIOLIB_EXCLUDE_RTTY=1
|
|
||||||
-D RADIOLIB_EXCLUDE_SSTV=1
|
|
||||||
; --- T3-S3 v1.2 SX1276 pin map (MeshCore variants/lilygo_t3s3_sx1276) ---
|
|
||||||
-D USE_SX1276
|
|
||||||
-D P_LORA_NSS=7
|
|
||||||
-D P_LORA_RESET=8
|
|
||||||
-D P_LORA_DIO_0=9
|
|
||||||
-D P_LORA_DIO_1=33
|
|
||||||
-D P_LORA_SCLK=5
|
|
||||||
-D P_LORA_MISO=3
|
|
||||||
-D P_LORA_MOSI=6
|
|
||||||
-D P_LORA_TX_LED=37
|
|
||||||
-D SX127X_CURRENT_LIMIT=120
|
|
||||||
-D SX176X_RXEN=21
|
|
||||||
-D SX176X_TXEN=10
|
|
||||||
; --- LoRa PHY: Australia Narrow (match the sensor fleet) ---
|
|
||||||
-D LORA_FREQ=916.575
|
|
||||||
-D LORA_BW=62.5
|
|
||||||
-D LORA_SF=7
|
|
||||||
-D LORA_CR=8
|
|
||||||
-D LORA_TX_POWER=20
|
|
||||||
-D MAX_GROUP_CHANNELS=4
|
|
||||||
-D MAX_CONTACTS=8
|
|
||||||
; --- channel defaults (runtime-overridable + persisted in NVS) ---
|
|
||||||
-D NODE_NAME='"dongle"'
|
|
||||||
-D SENSORS_CHANNEL_NAME='"SensorsTest"'
|
|
||||||
-D SENSORS_CHANNEL_PSK_B64='"PNtgMxiq9R7eQ3IleHoL3g=="'
|
|
||||||
-D PUBLIC_CHANNEL_PSK_B64='"izOH6cXN6mrJ5e26oRXNcg=="'
|
|
||||||
|
|||||||
+284
-684
File diff suppressed because it is too large
Load Diff
@@ -1,296 +0,0 @@
|
|||||||
/**
|
|
||||||
* meshcore_link.cpp -- see meshcore_link.h.
|
|
||||||
*
|
|
||||||
* Pattern mirrors MeshCore/experiment/lilygot3s3logger (same board: LilyGo
|
|
||||||
* T3-S3, ESP32-S3 + SX1276), trimmed to a single reprogrammable channel.
|
|
||||||
*/
|
|
||||||
#include "meshcore_link.h"
|
|
||||||
|
|
||||||
#if USE_MESHCORE
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <LittleFS.h>
|
|
||||||
#include <Preferences.h>
|
|
||||||
#include <mbedtls/base64.h> // ESP-IDF builtin; avoids clashing with MeshCore's base64.hpp
|
|
||||||
|
|
||||||
#include <Mesh.h>
|
|
||||||
#include <helpers/BaseChatMesh.h>
|
|
||||||
#include <helpers/ArduinoHelpers.h>
|
|
||||||
#include <helpers/StaticPoolPacketManager.h>
|
|
||||||
#include <helpers/SimpleMeshTables.h>
|
|
||||||
#include <helpers/IdentityStore.h>
|
|
||||||
#include <helpers/ESP32Board.h>
|
|
||||||
#include <helpers/AutoDiscoverRTCClock.h>
|
|
||||||
#include <helpers/radiolib/CustomSX1276.h>
|
|
||||||
#include <helpers/radiolib/CustomSX1276Wrapper.h>
|
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/queue.h"
|
|
||||||
|
|
||||||
// ---- compile-time defaults (override via build flags) ----------------------
|
|
||||||
#ifndef NODE_NAME
|
|
||||||
#define NODE_NAME "dongle"
|
|
||||||
#endif
|
|
||||||
#ifndef SENSORS_CHANNEL_NAME
|
|
||||||
#define SENSORS_CHANNEL_NAME "SensorsTest"
|
|
||||||
#endif
|
|
||||||
#ifndef SENSORS_CHANNEL_PSK_B64
|
|
||||||
#define SENSORS_CHANNEL_PSK_B64 "PNtgMxiq9R7eQ3IleHoL3g=="
|
|
||||||
#endif
|
|
||||||
// Standard MeshCore "Public" channel (same key the companion examples use).
|
|
||||||
#ifndef PUBLIC_CHANNEL_PSK_B64
|
|
||||||
#define PUBLIC_CHANNEL_PSK_B64 "izOH6cXN6mrJ5e26oRXNcg=="
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// ---- radio + mesh support objects (file-scope, mirrors the logger) ---------
|
|
||||||
ESP32Board board;
|
|
||||||
// Radio on FSPI (SPI2). The SD card is on HSPI (SPI3, see main.cpp). They MUST
|
|
||||||
// be different peripherals -- SPIClass() defaults to HSPI, which would collide
|
|
||||||
// with the SD bus and corrupt it the moment the radio re-inits the pins.
|
|
||||||
SPIClass lora_spi(FSPI);
|
|
||||||
CustomSX1276 lora(new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, lora_spi));
|
|
||||||
CustomSX1276Wrapper radio_driver(lora, board);
|
|
||||||
ESP32RTCClock fallback_clock;
|
|
||||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
|
||||||
StdRNG fast_rng;
|
|
||||||
SimpleMeshTables tables;
|
|
||||||
|
|
||||||
mc::RxHandler s_on_rx = nullptr;
|
|
||||||
bool s_up = false;
|
|
||||||
volatile bool s_cfg_dirty = false;
|
|
||||||
QueueHandle_t s_queue = nullptr;
|
|
||||||
|
|
||||||
// User-channel state (index 1; "Public" sits at index 0).
|
|
||||||
const int USER_IDX = 1;
|
|
||||||
char s_channel[32] = SENSORS_CHANNEL_NAME;
|
|
||||||
char s_psk[48] = SENSORS_CHANNEL_PSK_B64;
|
|
||||||
|
|
||||||
// ---- queued action (producer = any task, consumer = loop()) ----------------
|
|
||||||
struct McAction {
|
|
||||||
uint8_t kind; // 0 = send text, 1 = reprogram channel
|
|
||||||
char name[32];
|
|
||||||
char text[176];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Split "<sender>: <payload>" in place; returns sender (or "?") + payload.
|
|
||||||
void split_sender(char* buf, const char** sender, const char** payload) {
|
|
||||||
*sender = "?";
|
|
||||||
*payload = buf;
|
|
||||||
char* sep = strstr(buf, ": ");
|
|
||||||
if (sep) { *sep = 0; *sender = buf; *payload = sep + 2; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- the mesh node ---------------------------------------------------------
|
|
||||||
class DongleMesh : public BaseChatMesh {
|
|
||||||
ChannelDetails* _public = nullptr;
|
|
||||||
ChannelDetails* _user = nullptr;
|
|
||||||
ChannelDetails _user_view; // kept in sync for sending
|
|
||||||
uint32_t _rx = 0, _tx = 0;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// Unused contact-protocol hooks -- this node only does channel chat.
|
|
||||||
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {}
|
|
||||||
void onContactPathUpdated(const ContactInfo&) override {}
|
|
||||||
ContactInfo* processAck(const uint8_t*) override { return nullptr; }
|
|
||||||
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
|
||||||
void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
|
||||||
void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {}
|
|
||||||
uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { return 0; }
|
|
||||||
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {}
|
|
||||||
void onSendTimeout() override {}
|
|
||||||
uint32_t calcFloodTimeoutMillisFor(uint32_t air) const override { return 500 + 16 * air; }
|
|
||||||
uint32_t calcDirectTimeoutMillisFor(uint32_t air, uint8_t) const override { return 500 + 16 * air; }
|
|
||||||
// Listener only -- do not re-flood other nodes' traffic.
|
|
||||||
bool allowPacketForward(const mesh::Packet*) override { return false; }
|
|
||||||
|
|
||||||
const char* channelName(const mesh::GroupChannel& ch) {
|
|
||||||
int idx = findChannelIdx(ch);
|
|
||||||
if (idx < 0) return "unknown";
|
|
||||||
ChannelDetails cd;
|
|
||||||
if (!getChannel(idx, cd)) return "unknown";
|
|
||||||
return cd.name[0] ? cd.name : "unnamed";
|
|
||||||
}
|
|
||||||
|
|
||||||
void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt,
|
|
||||||
uint32_t, const char* text) override {
|
|
||||||
_rx++;
|
|
||||||
int rssi = (int)lroundf(radio_driver.getLastRSSI());
|
|
||||||
float snr = radio_driver.getLastSNR();
|
|
||||||
const char* chan = channelName(channel);
|
|
||||||
|
|
||||||
char buf[200];
|
|
||||||
strlcpy(buf, text, sizeof(buf));
|
|
||||||
const char *sender, *payload;
|
|
||||||
split_sender(buf, &sender, &payload);
|
|
||||||
|
|
||||||
if (s_on_rx) s_on_rx(chan, sender, payload, rssi, snr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
DongleMesh()
|
|
||||||
: BaseChatMesh(radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock,
|
|
||||||
*new StaticPoolPacketManager(16), tables) {}
|
|
||||||
|
|
||||||
void begin() {
|
|
||||||
BaseChatMesh::begin();
|
|
||||||
|
|
||||||
IdentityStore store(LittleFS, "/identity");
|
|
||||||
if (!store.load("_main", self_id)) {
|
|
||||||
self_id = mesh::LocalIdentity(getRNG());
|
|
||||||
store.save("_main", self_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_public = addChannel("Public", PUBLIC_CHANNEL_PSK_B64);
|
|
||||||
_user = addChannel(s_channel, s_psk); // -> index USER_IDX
|
|
||||||
if (_user) getChannel(USER_IDX, _user_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite the user channel's key (and name) at runtime. Recomputes the
|
|
||||||
// channel hash from the decoded secret via setChannel().
|
|
||||||
bool reprogram(const char* name, const char* psk_b64) {
|
|
||||||
ChannelDetails cd;
|
|
||||||
memset(&cd, 0, sizeof(cd));
|
|
||||||
unsigned char key[32];
|
|
||||||
size_t len = 0;
|
|
||||||
if (mbedtls_base64_decode(key, sizeof(key), &len,
|
|
||||||
(const unsigned char*)psk_b64, strlen(psk_b64)) != 0)
|
|
||||||
return false;
|
|
||||||
if (len != 16 && len != 32) return false; // PSK must be a 16- or 32-byte key
|
|
||||||
memcpy(cd.channel.secret, key, len);
|
|
||||||
strlcpy(cd.name, name, sizeof(cd.name));
|
|
||||||
if (!setChannel(USER_IDX, cd)) return false;
|
|
||||||
getChannel(USER_IDX, _user_view);
|
|
||||||
_user = &_user_view;
|
|
||||||
strlcpy(s_channel, name, sizeof(s_channel));
|
|
||||||
strlcpy(s_psk, psk_b64, sizeof(s_psk));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool send(const char* text) {
|
|
||||||
if (!_user) return false;
|
|
||||||
bool ok = sendGroupMessage(getRTCClock()->getCurrentTime(),
|
|
||||||
_user_view.channel, NODE_NAME, text, strlen(text));
|
|
||||||
if (ok) _tx++;
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t rxCount() const { return _rx; }
|
|
||||||
uint32_t txCount() const { return _tx; }
|
|
||||||
};
|
|
||||||
|
|
||||||
DongleMesh the_mesh;
|
|
||||||
|
|
||||||
void persist_cfg() {
|
|
||||||
Preferences p;
|
|
||||||
if (p.begin("mcfg", false)) {
|
|
||||||
p.putString("name", s_channel);
|
|
||||||
p.putString("psk", s_psk);
|
|
||||||
p.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void load_cfg() {
|
|
||||||
Preferences p;
|
|
||||||
if (p.begin("mcfg", true)) {
|
|
||||||
String n = p.getString("name", "");
|
|
||||||
String k = p.getString("psk", "");
|
|
||||||
if (n.length()) strlcpy(s_channel, n.c_str(), sizeof(s_channel));
|
|
||||||
if (k.length()) strlcpy(s_psk, k.c_str(), sizeof(s_psk));
|
|
||||||
p.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace mc {
|
|
||||||
|
|
||||||
bool enabled() { return true; }
|
|
||||||
|
|
||||||
void begin(RxHandler on_rx) {
|
|
||||||
s_on_rx = on_rx;
|
|
||||||
s_queue = xQueueCreate(8, sizeof(McAction));
|
|
||||||
|
|
||||||
load_cfg(); // NVS overrides of channel/psk, if any
|
|
||||||
|
|
||||||
board.begin();
|
|
||||||
fallback_clock.begin();
|
|
||||||
if (!lora.std_init(&lora_spi)) {
|
|
||||||
Serial.println("[mesh] radio init FAILED -- mesh disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fast_rng.begin(lora.random(0x7FFFFFFF));
|
|
||||||
the_mesh.begin();
|
|
||||||
lora.startReceive();
|
|
||||||
s_up = true;
|
|
||||||
Serial.printf("[mesh] up: freq=%.3f bw=%.1f sf=%d cr=%d channel='%s'\n",
|
|
||||||
(float)LORA_FREQ, (float)LORA_BW, (int)LORA_SF, (int)LORA_CR, s_channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
if (s_queue) {
|
|
||||||
McAction a;
|
|
||||||
while (xQueueReceive(s_queue, &a, 0) == pdTRUE) {
|
|
||||||
if (a.kind == 0) {
|
|
||||||
the_mesh.send(a.text);
|
|
||||||
} else {
|
|
||||||
if (the_mesh.reprogram(a.name, a.text)) persist_cfg();
|
|
||||||
s_cfg_dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (s_up) the_mesh.loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestSend(const char* text) {
|
|
||||||
if (!s_queue || !text) return;
|
|
||||||
McAction a; a.kind = 0; a.name[0] = 0;
|
|
||||||
strlcpy(a.text, text, sizeof(a.text));
|
|
||||||
xQueueSend(s_queue, &a, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestPsk(const char* name, const char* psk_b64) {
|
|
||||||
if (!s_queue || !psk_b64) return;
|
|
||||||
McAction a; a.kind = 1;
|
|
||||||
strlcpy(a.name, (name && *name) ? name : s_channel, sizeof(a.name));
|
|
||||||
strlcpy(a.text, psk_b64, sizeof(a.text));
|
|
||||||
xQueueSend(s_queue, &a, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool up() { return s_up; }
|
|
||||||
bool consumeCfgChanged() { bool d = s_cfg_dirty; s_cfg_dirty = false; return d; }
|
|
||||||
const char* nodeName() { return NODE_NAME; }
|
|
||||||
const char* channelName() { return s_channel; }
|
|
||||||
const char* pskB64() { return s_psk; }
|
|
||||||
const char* radioConfig() {
|
|
||||||
static char buf[80];
|
|
||||||
snprintf(buf, sizeof(buf), "freq=%.3f bw=%.1f sf=%d cr=%d tx=%d",
|
|
||||||
(float)LORA_FREQ, (float)LORA_BW, (int)LORA_SF, (int)LORA_CR, (int)LORA_TX_POWER);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
uint32_t rxCount() { return the_mesh.rxCount(); }
|
|
||||||
uint32_t txCount() { return the_mesh.txCount(); }
|
|
||||||
|
|
||||||
} // namespace mc
|
|
||||||
|
|
||||||
#else // ---- USE_MESHCORE not set: cheap no-op build ----------------------
|
|
||||||
|
|
||||||
namespace mc {
|
|
||||||
bool enabled() { return false; }
|
|
||||||
void begin(RxHandler) {}
|
|
||||||
void loop() {}
|
|
||||||
void requestSend(const char*) {}
|
|
||||||
void requestPsk(const char*, const char*) {}
|
|
||||||
bool up() { return false; }
|
|
||||||
bool consumeCfgChanged() { return false; }
|
|
||||||
const char* nodeName() { return "me"; }
|
|
||||||
const char* channelName() { return ""; }
|
|
||||||
const char* pskB64() { return ""; }
|
|
||||||
const char* radioConfig() { return ""; }
|
|
||||||
uint32_t rxCount() { return 0; }
|
|
||||||
uint32_t txCount() { return 0; }
|
|
||||||
} // namespace mc
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* meshcore_link -- thin facade over a MeshCore BaseChatMesh node.
|
|
||||||
*
|
|
||||||
* Lets the debug dongle send/receive text on a single PSK-protected group
|
|
||||||
* channel (plus the well-known "Public" channel), and reprogram that channel's
|
|
||||||
* PSK at runtime. All MeshCore + RadioLib headers stay inside meshcore_link.cpp
|
|
||||||
* so the rest of the firmware compiles identically with or without the radio.
|
|
||||||
*
|
|
||||||
* Enabled by the USE_MESHCORE build flag (see env:t3s3_mesh in platformio.ini).
|
|
||||||
* When the flag is absent every function below is a cheap no-op, so callers
|
|
||||||
* need no #if guards.
|
|
||||||
*
|
|
||||||
* Threading: requestSend()/requestPsk() are safe to call from any task (e.g.
|
|
||||||
* the async web-server callback). They enqueue work that is applied inside
|
|
||||||
* loop(), which must be called from the same task that owns the radio.
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
namespace mc {
|
|
||||||
|
|
||||||
// Fired from loop() (radio task context) for each received channel message.
|
|
||||||
typedef void (*RxHandler)(const char* channel, const char* sender,
|
|
||||||
const char* text, int rssi, float snr);
|
|
||||||
|
|
||||||
bool enabled(); // compile-time: USE_MESHCORE set?
|
|
||||||
void begin(RxHandler on_rx); // bring up radio + mesh
|
|
||||||
void loop(); // pump radio, drain action queue
|
|
||||||
|
|
||||||
void requestSend(const char* text); // queue a TX on the user channel
|
|
||||||
void requestPsk(const char* name, const char* psk_b64); // queue a channel reprogram
|
|
||||||
|
|
||||||
bool up(); // radio initialised OK
|
|
||||||
bool consumeCfgChanged(); // true once after a reprogram
|
|
||||||
const char* nodeName(); // our sender name
|
|
||||||
const char* channelName(); // current user-channel name
|
|
||||||
const char* pskB64(); // current user-channel PSK (base64)
|
|
||||||
const char* radioConfig(); // "freq=.. bw=.. sf=.. cr=.. tx=.." ("" if disabled)
|
|
||||||
uint32_t rxCount();
|
|
||||||
uint32_t txCount();
|
|
||||||
|
|
||||||
} // namespace mc
|
|
||||||
Reference in New Issue
Block a user