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:
Ryan Malloy 2026-01-25 12:15:15 -07:00
commit dbed318124
12 changed files with 2146 additions and 0 deletions

14
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

228
src/MyMesh.h Normal file
View 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
View 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
View 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
View 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
View 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();
}