Initial commit: Heltec V3 MeshCore repeater
Standalone PlatformIO project using MeshCore as a library. Features: - Heltec LoRa32 V3 support (ESP32-S3 + SX1262) - OLED display integration - OTA firmware updates via WiFi - Serial CLI for configuration Uses symlinked MeshCore library from ../MeshCore
This commit is contained in:
commit
dbed318124
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# PlatformIO
|
||||
.pio/
|
||||
.pioenvs/
|
||||
.piolibdeps/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
73
docs/README.md
Normal file
73
docs/README.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Heltec V3 MeshCore Repeater
|
||||
|
||||
A standalone MeshCore repeater project for the Heltec LoRa32 V3 board.
|
||||
|
||||
## Overview
|
||||
|
||||
This project builds a LoRa mesh repeater using the [MeshCore](https://github.com/ryanmalloy/MeshCore) library. The repeater:
|
||||
|
||||
- Forwards mesh packets between nodes
|
||||
- Displays status on the onboard OLED
|
||||
- Supports OTA (Over-The-Air) firmware updates
|
||||
- Provides a serial CLI for configuration
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
repeater/
|
||||
├── platformio.ini # Build configuration
|
||||
├── src/
|
||||
│ ├── main.cpp # Entry point
|
||||
│ ├── MyMesh.cpp # Repeater mesh logic
|
||||
│ ├── MyMesh.h
|
||||
│ ├── UITask.cpp # Display handling
|
||||
│ ├── UITask.h
|
||||
│ └── RateLimiter.h
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
pio run
|
||||
|
||||
# Flash via USB
|
||||
pio run -t upload
|
||||
|
||||
# Monitor serial output
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
See [Building](building.md) and [Flashing](flashing.md) for details.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `platformio.ini` to customize your repeater:
|
||||
|
||||
```ini
|
||||
; ===== CUSTOMIZE THESE =====
|
||||
-D ADVERT_NAME='"RPM Repeater"' ; Name shown to other nodes
|
||||
-D ADVERT_LAT=0.0 ; GPS latitude
|
||||
-D ADVERT_LON=0.0 ; GPS longitude
|
||||
-D ADMIN_PASSWORD='"password"' ; Admin password for CLI
|
||||
-D MAX_NEIGHBOURS=50 ; Max tracked neighbors
|
||||
```
|
||||
|
||||
## Serial Commands
|
||||
|
||||
Connect at 115200 baud to access the CLI. See [Serial Commands](serial-commands.md).
|
||||
|
||||
## OTA Updates
|
||||
|
||||
The repeater supports wireless firmware updates. See [Flashing](flashing.md#ota-updates).
|
||||
|
||||
## Hardware
|
||||
|
||||
- **Board:** Heltec WiFi LoRa 32 V3 (ESP32-S3 + SX1262)
|
||||
- **Display:** 128x64 SSD1306 OLED
|
||||
- **Radio:** SX1262 LoRa @ 910.525 MHz (US default)
|
||||
|
||||
## Dependencies
|
||||
|
||||
This project uses MeshCore as a symlinked library from `../MeshCore`. Ensure that directory exists.
|
||||
80
docs/building.md
Normal file
80
docs/building.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Building the Firmware
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [PlatformIO](https://platformio.org/) (CLI or IDE)
|
||||
- MeshCore library at `../MeshCore`
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Full build
|
||||
pio run
|
||||
|
||||
# Clean build (removes all artifacts)
|
||||
pio run -t clean && pio run
|
||||
|
||||
# Verbose build (shows compiler commands)
|
||||
pio run -v
|
||||
```
|
||||
|
||||
## Build Output
|
||||
|
||||
After a successful build:
|
||||
|
||||
```
|
||||
RAM: 17.6% (57KB / 320KB)
|
||||
Flash: 32.0% (1.0MB / 3.3MB)
|
||||
```
|
||||
|
||||
**Output files in `.pio/build/heltec_v3_repeater/`:**
|
||||
|
||||
| File | Size | Purpose |
|
||||
|------|------|---------|
|
||||
| `firmware.bin` | ~1.1 MB | OTA updates |
|
||||
| `firmware.elf` | ~2.5 MB | Debugging |
|
||||
| `firmware.factory.bin` | ~1.2 MB | Initial flash (includes bootloader) |
|
||||
|
||||
## Build Configuration
|
||||
|
||||
Key settings in `platformio.ini`:
|
||||
|
||||
### LoRa Parameters
|
||||
|
||||
```ini
|
||||
-D LORA_FREQ=910.525 # Frequency in MHz
|
||||
-D LORA_BW=62.5 # Bandwidth in kHz
|
||||
-D LORA_SF=7 # Spreading factor (7-12)
|
||||
-D LORA_CR=5 # Coding rate (5-8)
|
||||
-D LORA_TX_POWER=22 # TX power in dBm
|
||||
```
|
||||
|
||||
### Debug Options
|
||||
|
||||
Uncomment to enable:
|
||||
|
||||
```ini
|
||||
; -D MESH_PACKET_LOGGING=1 # Log all mesh packets
|
||||
; -D MESH_DEBUG=1 # Verbose debug output
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing MeshCore
|
||||
|
||||
```
|
||||
Error: symlink://../MeshCore not found
|
||||
```
|
||||
|
||||
Ensure MeshCore exists at the expected path:
|
||||
```bash
|
||||
ls ../MeshCore/library.json
|
||||
```
|
||||
|
||||
### Library Dependency Errors
|
||||
|
||||
Clear the library cache:
|
||||
```bash
|
||||
rm -rf .pio/libdeps
|
||||
pio run
|
||||
```
|
||||
121
docs/flashing.md
Normal file
121
docs/flashing.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Flashing the Firmware
|
||||
|
||||
## USB Serial Flashing
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- USB cable connected to Heltec board
|
||||
- User in `uucp` or `dialout` group (for `/dev/ttyUSB*` access)
|
||||
|
||||
### Flash Command
|
||||
|
||||
```bash
|
||||
# Build and flash
|
||||
pio run -t upload
|
||||
|
||||
# Flash to specific port
|
||||
pio run -t upload --upload-port /dev/ttyUSB0
|
||||
```
|
||||
|
||||
### Using the Stable Device Path
|
||||
|
||||
USB device numbers can change. Use the stable path instead:
|
||||
|
||||
```bash
|
||||
pio run -t upload --upload-port /dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0
|
||||
```
|
||||
|
||||
### Boot Mode Issues
|
||||
|
||||
If flashing fails, manually enter bootloader mode:
|
||||
|
||||
1. Hold **BOOT** button
|
||||
2. Press **RST** button
|
||||
3. Release **BOOT** button
|
||||
4. Run `pio run -t upload`
|
||||
|
||||
---
|
||||
|
||||
## OTA Updates
|
||||
|
||||
Over-The-Air updates let you flash wirelessly after initial USB flash.
|
||||
|
||||
### Step 1: Enter OTA Mode
|
||||
|
||||
Connect to serial console:
|
||||
```bash
|
||||
screen /dev/ttyUSB0 115200
|
||||
```
|
||||
|
||||
Type:
|
||||
```
|
||||
start ota
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
Started: http://192.168.4.1/update
|
||||
```
|
||||
|
||||
### Step 2: Connect to WiFi
|
||||
|
||||
The repeater creates an open access point:
|
||||
|
||||
- **SSID:** `MeshCore-OTA`
|
||||
- **Password:** (none)
|
||||
|
||||
Connect your computer/phone to this network.
|
||||
|
||||
### Step 3: Upload Firmware
|
||||
|
||||
1. Open browser to `http://192.168.4.1/update`
|
||||
2. Click "Choose File"
|
||||
3. Select `.pio/build/heltec_v3_repeater/firmware.bin`
|
||||
4. Click "Update"
|
||||
5. Wait for upload and reboot (~30 seconds)
|
||||
|
||||
### OTA Endpoints
|
||||
|
||||
| URL | Purpose |
|
||||
|-----|---------|
|
||||
| `http://192.168.4.1/` | Device info page |
|
||||
| `http://192.168.4.1/update` | ElegantOTA firmware upload |
|
||||
| `http://192.168.4.1/log` | View packet log (if enabled) |
|
||||
|
||||
### Exiting OTA Mode
|
||||
|
||||
OTA mode stays active until reboot. To exit without updating:
|
||||
|
||||
- Press the **RST** button, or
|
||||
- Power cycle the device
|
||||
|
||||
---
|
||||
|
||||
## Verifying the Flash
|
||||
|
||||
After flashing, connect to serial:
|
||||
|
||||
```bash
|
||||
screen /dev/ttyUSB0 115200
|
||||
```
|
||||
|
||||
Press **RST** to see boot messages:
|
||||
|
||||
```
|
||||
Repeater ID: AABBCCDD...
|
||||
```
|
||||
|
||||
Type `ver` to check firmware version:
|
||||
```
|
||||
ver
|
||||
-> MeshCore v1.10.0 (Jan 25 2025)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exit Screen
|
||||
|
||||
To exit the `screen` session:
|
||||
|
||||
- `Ctrl+a` then `k` then `y` (kill session)
|
||||
- Or `Ctrl+a` then `d` (detach, keeps session running)
|
||||
127
docs/serial-commands.md
Normal file
127
docs/serial-commands.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Serial Commands
|
||||
|
||||
Connect at **115200 baud**:
|
||||
|
||||
```bash
|
||||
screen /dev/ttyUSB0 115200
|
||||
```
|
||||
|
||||
Commands are entered without a prompt. Type and press Enter.
|
||||
|
||||
---
|
||||
|
||||
## System Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ver` | Show firmware version |
|
||||
| `board` | Show board/hardware info |
|
||||
| `reboot` | Restart the device |
|
||||
| `erase` | Factory reset (serial only, not remote) |
|
||||
|
||||
---
|
||||
|
||||
## Network Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `advert` | Send advertisement to mesh |
|
||||
| `neighbors` | List known neighbor nodes |
|
||||
| `neighbor.remove <id>` | Remove a neighbor by ID |
|
||||
|
||||
---
|
||||
|
||||
## Time/Clock Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `clock` | Show current RTC time |
|
||||
| `clock sync` | Sync clock from mesh |
|
||||
| `time <epoch>` | Set time (Unix epoch seconds) |
|
||||
|
||||
---
|
||||
|
||||
## GPS Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `gps` | Show GPS status |
|
||||
| `gps on` | Enable GPS module |
|
||||
| `gps off` | Disable GPS module |
|
||||
| `gps sync` | Sync time from GPS |
|
||||
| `gps setloc` | Set location from GPS fix |
|
||||
| `gps advert none` | Don't include GPS in adverts |
|
||||
| `gps advert share` | Share GPS location in adverts |
|
||||
| `gps advert prefs` | Use preference setting |
|
||||
|
||||
---
|
||||
|
||||
## Configuration Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `get af` | Get airtime fairness setting |
|
||||
| `set af <0-100>` | Set airtime fairness (0=off, 100=max) |
|
||||
| `password <old> <new>` | Change admin password |
|
||||
|
||||
---
|
||||
|
||||
## Sensor Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `sensor list` | List detected sensors |
|
||||
| `sensor get <name>` | Read a sensor value |
|
||||
| `sensor set <name> <value>` | Configure a sensor |
|
||||
|
||||
---
|
||||
|
||||
## Radio Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tempradio <params>` | Temporarily change radio settings |
|
||||
|
||||
---
|
||||
|
||||
## OTA Update
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `start ota` | Start WiFi AP and OTA server |
|
||||
|
||||
After running, connect to `MeshCore-OTA` WiFi and browse to `http://192.168.4.1/update`.
|
||||
|
||||
---
|
||||
|
||||
## Logging Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `log start` | Start packet logging to SPIFFS |
|
||||
| `log stop` | Stop packet logging |
|
||||
| `clear stats` | Clear statistics counters |
|
||||
|
||||
---
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
ver
|
||||
-> MeshCore v1.10.0
|
||||
|
||||
neighbors
|
||||
-> 3 neighbors:
|
||||
1. AABBCCDD (-65 dBm, 2 hops)
|
||||
2. EEFF0011 (-78 dBm, 1 hop)
|
||||
3. 22334455 (-82 dBm, 3 hops)
|
||||
|
||||
clock
|
||||
-> 2025-01-25 11:45:32 UTC
|
||||
|
||||
advert
|
||||
-> Advertisement sent
|
||||
|
||||
start ota
|
||||
-> Started: http://192.168.4.1/update
|
||||
```
|
||||
116
platformio.ini
Normal file
116
platformio.ini
Normal file
@ -0,0 +1,116 @@
|
||||
; RPM's Heltec V3 Repeater
|
||||
; Uses MeshCore as a library
|
||||
|
||||
[platformio]
|
||||
default_envs = heltec_v3_repeater
|
||||
|
||||
[env:heltec_v3_repeater]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
framework = arduino
|
||||
board = esp32-s3-devkitc-1
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
lib_ldf_mode = deep+
|
||||
|
||||
; MeshCore as local library + its dependencies
|
||||
lib_deps =
|
||||
symlink://../MeshCore
|
||||
symlink://../MeshCore/arch/esp32/AsyncElegantOTA
|
||||
symlink://../MeshCore/lib/ed25519
|
||||
SPI
|
||||
Wire
|
||||
WiFi
|
||||
WebServer
|
||||
FS
|
||||
Update
|
||||
SPIFFS
|
||||
LittleFS
|
||||
jgromes/RadioLib @ ^7.3.0
|
||||
rweather/Crypto @ ^0.4.0
|
||||
adafruit/RTClib @ ^2.1.3
|
||||
melopero/Melopero RV3028 @ ^1.1.0
|
||||
electroniccats/CayenneLPP @ 1.6.1
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
me-no-dev/ESPAsyncWebServer @ ^3.6.0
|
||||
adafruit/Adafruit GFX Library @ ^1.12.4
|
||||
adafruit/Adafruit SSD1306 @ ^2.5.16
|
||||
adafruit/Adafruit Unified Sensor @ ^1.1.14
|
||||
adafruit/Adafruit AHTX0 @ ^2.0.5
|
||||
adafruit/Adafruit BME280 Library @ ^2.3.0
|
||||
adafruit/Adafruit BMP280 Library @ ^2.6.8
|
||||
adafruit/Adafruit SHTC3 Library @ ^1.0.2
|
||||
sensirion/Sensirion I2C SHT4x @ ^1.1.2
|
||||
|
||||
build_flags =
|
||||
-w
|
||||
-DNDEBUG
|
||||
; Include paths for MeshCore
|
||||
-I ../MeshCore/variants/heltec_v3
|
||||
-I ../MeshCore/src
|
||||
-I ../MeshCore/lib/ed25519
|
||||
-DRADIOLIB_STATIC_ONLY=1
|
||||
-DRADIOLIB_GODMODE=1
|
||||
; LoRa parameters
|
||||
-D LORA_FREQ=910.525
|
||||
-D LORA_BW=62.5
|
||||
-D LORA_SF=7
|
||||
-D LORA_CR=5
|
||||
; Security
|
||||
-D ENABLE_PRIVATE_KEY_IMPORT=1
|
||||
-D ENABLE_PRIVATE_KEY_EXPORT=1
|
||||
; RadioLib excludes (reduce binary size)
|
||||
-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
|
||||
; Tell MeshCore library what to build
|
||||
-D ESP32
|
||||
-D MC_VARIANT=heltec_v3
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
; Heltec V3 hardware pins
|
||||
-D HELTEC_LORA_V3
|
||||
-D ESP32_CPU_FREQ=80
|
||||
-D P_LORA_DIO_1=14
|
||||
-D P_LORA_NSS=8
|
||||
-D P_LORA_RESET=RADIOLIB_NC
|
||||
-D P_LORA_BUSY=13
|
||||
-D P_LORA_SCLK=9
|
||||
-D P_LORA_MISO=11
|
||||
-D P_LORA_MOSI=10
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D P_LORA_TX_LED=35
|
||||
-D PIN_BOARD_SDA=17
|
||||
-D PIN_BOARD_SCL=18
|
||||
-D PIN_USER_BTN=0
|
||||
-D PIN_VEXT_EN=36
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D PIN_GPS_RX=47
|
||||
-D PIN_GPS_TX=48
|
||||
-D PIN_GPS_EN=26
|
||||
; ===== CUSTOMIZE THESE =====
|
||||
-D ADVERT_NAME='"RPM Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; Debug (uncomment as needed)
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
upload_port = /dev/ttyUSB0
|
||||
monitor_port = /dev/ttyUSB0
|
||||
1111
src/MyMesh.cpp
Normal file
1111
src/MyMesh.cpp
Normal file
File diff suppressed because it is too large
Load Diff
228
src/MyMesh.h
Normal file
228
src/MyMesh.h
Normal file
@ -0,0 +1,228 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
#include "helpers/bridges/RS232Bridge.h"
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
#include "helpers/bridges/ESPNowBridge.h"
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include "RateLimiter.h"
|
||||
|
||||
#ifdef WITH_BRIDGE
|
||||
extern AbstractBridge* bridge;
|
||||
#endif
|
||||
|
||||
struct RepeaterStats {
|
||||
uint16_t batt_milli_volts;
|
||||
uint16_t curr_tx_queue_len;
|
||||
int16_t noise_floor;
|
||||
int16_t last_rssi;
|
||||
uint32_t n_packets_recv;
|
||||
uint32_t n_packets_sent;
|
||||
uint32_t total_air_time_secs;
|
||||
uint32_t total_up_time_secs;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint16_t err_events; // was 'n_full_events'
|
||||
int16_t last_snr; // x 4
|
||||
uint16_t n_direct_dups, n_flood_dups;
|
||||
uint32_t total_rx_air_time_secs;
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
struct NeighbourInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t advert_timestamp;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4, user should divide to get float value
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "30 Nov 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.11.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
FILESYSTEM* _fs;
|
||||
uint32_t last_millis;
|
||||
uint64_t uptime_millis;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
ClientACL acl;
|
||||
TransportKeyStore key_store;
|
||||
RegionMap region_map, temp_map;
|
||||
RegionEntry* load_stack[8];
|
||||
RegionEntry* recv_pkt_region;
|
||||
RateLimiter discover_limiter;
|
||||
bool region_load_active;
|
||||
unsigned long dirty_contacts_expiry;
|
||||
#if MAX_NEIGHBOURS
|
||||
NeighbourInfo neighbours[MAX_NEIGHBOURS];
|
||||
#endif
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
#if defined(WITH_RS232_BRIDGE)
|
||||
RS232Bridge bridge;
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge bridge;
|
||||
#endif
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
|
||||
File openAppend(const char* fname);
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
return _prefs.airtime_factor;
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
const char* getLogDateTime() override;
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override;
|
||||
void logTx(mesh::Packet* pkt, int len) override;
|
||||
void logTxFail(mesh::Packet* pkt, int len) override;
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
||||
|
||||
int getInterferenceThreshold() const override {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
int getAGCResetInterval() const override {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
uint8_t getExtraAckTransmitCount() const override {
|
||||
return _prefs.multi_acks;
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsPrefs() {
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
|
||||
}
|
||||
#endif
|
||||
|
||||
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
|
||||
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||
int searchPeersByHash(const uint8_t* hash) override;
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onControlDataRecv(mesh::Packet* packet) override;
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
|
||||
void begin(FILESYSTEM* fs);
|
||||
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
}
|
||||
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
bool formatFileSystem() override;
|
||||
void sendSelfAdvertisement(int delay_millis) override;
|
||||
void updateAdvertTimer() override;
|
||||
void updateFloodAdvertTimer() override;
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
|
||||
void eraseLogFile() override {
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
}
|
||||
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(uint8_t power_dbm) override;
|
||||
void formatNeighborsReply(char *reply) override;
|
||||
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
|
||||
void formatStatsReply(char *reply) override;
|
||||
void formatRadioStatsReply(char *reply) override;
|
||||
void formatPacketStatsReply(char *reply) override;
|
||||
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void loop();
|
||||
|
||||
#if defined(WITH_BRIDGE)
|
||||
void setBridgeState(bool enable) override {
|
||||
if (enable == bridge.isRunning()) return;
|
||||
if (enable)
|
||||
{
|
||||
bridge.begin();
|
||||
}
|
||||
else
|
||||
{
|
||||
bridge.end();
|
||||
}
|
||||
}
|
||||
|
||||
void restartBridge() override {
|
||||
if (!bridge.isRunning()) return;
|
||||
bridge.end();
|
||||
bridge.begin();
|
||||
}
|
||||
#endif
|
||||
};
|
||||
23
src/RateLimiter.h
Normal file
23
src/RateLimiter.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class RateLimiter {
|
||||
uint32_t _start_timestamp;
|
||||
uint32_t _secs;
|
||||
uint16_t _maximum, _count;
|
||||
|
||||
public:
|
||||
RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
|
||||
|
||||
bool allow(uint32_t now) {
|
||||
if (now < _start_timestamp + _secs) {
|
||||
_count++;
|
||||
if (_count > _maximum) return false; // deny
|
||||
} else { // time window now expired
|
||||
_start_timestamp = now;
|
||||
_count = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
114
src/UITask.cpp
Normal file
114
src/UITask.cpp
Normal file
@ -0,0 +1,114 @@
|
||||
#include "UITask.h"
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
// 'meshcore', 128x13px
|
||||
static const uint8_t meshcore_logo [] PROGMEM = {
|
||||
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
|
||||
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
|
||||
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
|
||||
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
|
||||
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
|
||||
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
|
||||
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
|
||||
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
|
||||
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
};
|
||||
|
||||
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
|
||||
_prevBtnState = HIGH;
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_node_prefs = node_prefs;
|
||||
_display->turnOn();
|
||||
|
||||
// strip off dash and commit hash by changing dash to null terminator
|
||||
// e.g: v1.2.3-abcdef -> v1.2.3
|
||||
char *version = strdup(firmware_version);
|
||||
char *dash = strchr(version, '-');
|
||||
if(dash){
|
||||
*dash = 0;
|
||||
}
|
||||
|
||||
// v1.2.3 (1 Jan 2025)
|
||||
sprintf(_version_info, "%s (%s)", version, build_date);
|
||||
}
|
||||
|
||||
void UITask::renderCurrScreen() {
|
||||
char tmp[80];
|
||||
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
|
||||
// meshcore logo
|
||||
_display->setColor(DisplayDriver::BLUE);
|
||||
int logoWidth = 128;
|
||||
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// version info
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 22);
|
||||
_display->print(_version_info);
|
||||
|
||||
// node type
|
||||
const char* node_type = "< Repeater >";
|
||||
uint16_t typeWidth = _display->getTextWidth(node_type);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 35);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
// node name
|
||||
_display->setCursor(0, 0);
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->print(_node_prefs->node_name);
|
||||
|
||||
// freq / sf
|
||||
_display->setCursor(0, 20);
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
|
||||
_display->print(tmp);
|
||||
|
||||
// bw / cr
|
||||
_display->setCursor(0, 30);
|
||||
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
||||
_display->print(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
#ifdef PIN_USER_BTN
|
||||
if (millis() >= _next_read) {
|
||||
int btnState = digitalRead(PIN_USER_BTN);
|
||||
if (btnState != _prevBtnState) {
|
||||
if (btnState == LOW) { // pressed?
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
}
|
||||
_prevBtnState = btnState;
|
||||
}
|
||||
_next_read = millis() + 200; // 5 reads per second
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_display->isOn()) {
|
||||
if (millis() >= _next_refresh) {
|
||||
_display->startFrame();
|
||||
renderCurrScreen();
|
||||
_display->endFrame();
|
||||
|
||||
_next_refresh = millis() + 1000; // refresh every second
|
||||
}
|
||||
if (millis() > _auto_off) {
|
||||
_display->turnOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/UITask.h
Normal file
19
src/UITask.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
class UITask {
|
||||
DisplayDriver* _display;
|
||||
unsigned long _next_read, _next_refresh, _auto_off;
|
||||
int _prevBtnState;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
|
||||
void renderCurrScreen();
|
||||
public:
|
||||
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
|
||||
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
|
||||
|
||||
void loop();
|
||||
};
|
||||
120
src/main.cpp
Normal file
120
src/main.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
#include "MyMesh.h"
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
static UITask ui_task(display);
|
||||
#endif
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
|
||||
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
|
||||
|
||||
void halt() {
|
||||
while (1) ;
|
||||
}
|
||||
|
||||
static char command[160];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
board.begin();
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display.begin()) {
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!radio_init()) {
|
||||
halt();
|
||||
}
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
fs = &InternalFS;
|
||||
IdentityStore store(InternalFS, "");
|
||||
#elif defined(ESP32)
|
||||
SPIFFS.begin(true);
|
||||
fs = &SPIFFS;
|
||||
IdentityStore store(SPIFFS, "/identity");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
LittleFS.begin();
|
||||
fs = &LittleFS;
|
||||
IdentityStore store(LittleFS, "/identity");
|
||||
store.begin();
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
if (!store.load("_main", the_mesh.self_id)) {
|
||||
MESH_DEBUG_PRINTLN("Generating new keypair");
|
||||
the_mesh.self_id = radio_new_identity(); // create new random identity
|
||||
int count = 0;
|
||||
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes
|
||||
the_mesh.self_id = radio_new_identity(); count++;
|
||||
}
|
||||
store.save("_main", the_mesh.self_id);
|
||||
}
|
||||
|
||||
Serial.print("Repeater ID: ");
|
||||
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
|
||||
|
||||
command[0] = 0;
|
||||
|
||||
sensors.begin();
|
||||
|
||||
the_mesh.begin(fs);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||
#endif
|
||||
|
||||
// send out initial Advertisement to the mesh
|
||||
the_mesh.sendSelfAdvertisement(16000);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int len = strlen(command);
|
||||
while (Serial.available() && len < sizeof(command)-1) {
|
||||
char c = Serial.read();
|
||||
if (c != '\n') {
|
||||
command[len++] = c;
|
||||
command[len] = 0;
|
||||
Serial.print(c);
|
||||
}
|
||||
if (c == '\r') break;
|
||||
}
|
||||
if (len == sizeof(command)-1) { // command buffer full
|
||||
command[sizeof(command)-1] = '\r';
|
||||
}
|
||||
|
||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||
Serial.print('\n');
|
||||
command[len - 1] = 0; // replace newline with C string null terminator
|
||||
char reply[160];
|
||||
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
|
||||
if (reply[0]) {
|
||||
Serial.print(" -> "); Serial.println(reply);
|
||||
}
|
||||
|
||||
command[0] = 0; // reset command buffer
|
||||
}
|
||||
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.loop();
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user