TiggyOpenMesh

T-Deck Plus • LoRa32 • Heltec V3 • Heltec V4 • XIAO ESP32S3
v4.1 — MeshCore Architecture

Complete technical documentation — hardware, protocol, menus, code architecture & API

1. Quick Start

If you just want to get two devices talking, start here. You do not need to understand the protocol, gateway, or code structure first.

Fastest path: flash two nodes, give them different node IDs, make sure they share the same AES key, connect from the Android app or T-Deck, and send a test message.

What You Need

ItemMinimumRecommended
LoRa nodes22 nodes plus 1 repeater for extra range
Phone1 Android phoneAndroid phone with the app installed
USB cablesEnough to flash each boardKnown good data cables, not charge-only
BrowserChrome or EdgeLatest Chrome or Edge for web flashing

Recommended Starter Setups

GoalBest OptionWhy
Cheapest repeaterXIAO ESP32S3 + Wio-SX1262Very low cost, compact, easy to deploy
Best value repeaterHeltec V3OLED status, SX1262 radio, good general-purpose choice
Best handheld communicatorT-Deck PlusKeyboard, screen, GPS, and direct field use without a phone

10-Minute Messaging Setup

  1. Flash your first device from the web flasher.
  2. Flash your second device.
  3. On first boot, note the randomly generated AES key or set your own shared key.
  4. Make sure both devices use the same AES key.
  5. Make sure each device has a different node ID.
  6. Open the Android app or use the T-Deck UI.
  7. Connect to a node over BLE.
  8. Send a short test message to the other node.

Important: matching AES key, matching radio settings, and unique node IDs are the three things that must be correct before anything else will work.

Add a Repeater

If you want more range, flash a third node as a repeater and place it between the two endpoints. The mesh will forward traffic automatically when it hears packets that are not for itself.

Node A
Repeater
Node B

Add Alerts or Remote Control

Once basic messaging works, connect a relay or sensor to a repeater node. From the Android app or T-Deck you can then read inputs, trigger outputs, and optionally forward group alerts to Telegram when a phone has internet access.

First Things To Configure

If It Does Not Work

SymptomMost likely cause
Nodes do not see each otherDifferent AES key, mismatched radio settings, bad antenna, or same node ID
BLE app cannot connectWrong firmware type, BLE disabled, or board is in solar mode
Message sends but never deliversTarget unreachable, wrong target ID, weak link, or ACK lost
Gateway does not appear on mapNo location configured or hub connection not active

Then keep reading: the rest of this page is the full reference manual covering boards, UI, protocol, gateway setup, GPIO control, power modes, and troubleshooting in more depth.

2. System Overview

TiggyOpenMesh is an off-grid messaging, alerting, and remote-control system built on cheap ESP32 LoRa boards. It is designed for small private networks where you want messages, SOS alerts, sensor inputs, and relay outputs to keep working even when there is no internet or mobile coverage.

Best fit: farms, estates, expeditions, cabins, remote sites, event control, and hobby emergency networks.

Core strengths: directed routing, message delivery receipts, relay and sensor control, Android + BLE control, T-Deck handheld UI, WiFi and PC gateway options, SOS workflows, solar repeater mode, and a shared MeshCore library so all radio protocol logic is maintained in one place.

What it does in practice:

Send short messages between field nodes, trigger relays remotely, read sensor inputs, repeat traffic across solar-powered nodes, and forward alerts to Telegram when any connected phone regains internet access.

System Roles

RoleHardwareFirmwareDescription
Full Communicator T-Deck Plus main.cpp Keyboard, 2.8" colour TFT, GPS, trackball, chat UI, SOS mode, mesh map, phrase picker, remote control panel
Repeater / Relay Node LoRa32, Heltec V3, or Heltec V4 repeater.cpp Headless mesh repeater with 0.96" OLED stats display, BLE for phone control, GPIO relay/sensor control, serial config, gateway bridge mode
WiFi Gateway Heltec V2/V3/V4 or XIAO gateway_wifi.cpp Standalone LoRa-to-WiFi bridge — no PC needed. Connects to hub, supports BLE config, location for map
Phone Controller Android phone Android app (Kotlin) BLE connection to any repeater node — chat, relay control, settings, node discovery, delivery tracking

Key Specs

ParameterValue
Frequency868.0 MHz (EU ISM band)
ModulationLoRa — SF9, BW 125 kHz, CR 4/5
TX Power20 dBm (28 dBm with Heltec V4 PA)
EncryptionAES-128-GCM (authenticated encryption)
Max Hops (TTL)5
Max Nodes Tracked50
Max Message Size200 bytes (plaintext)
Heartbeat Interval30 seconds (±2s jitter)
Route Stale Timeout120 seconds
Duty Cycle Limit10% per hour (EU legal)

2. Hardware & Pin Maps

~£70

LilyGO T-Deck Plus

MCU: ESP32-S3 • Radio: SX1262 • Display: ST7789 320×240 IPS

Extras: QWERTY keyboard, trackball, GPS (MIA-M10Q), SD card slot

FunctionPin
SPI MOSI / MISO / SCK41 / 38 / 40
Radio CS / RST / DIO1 / BUSY9 / 17 / 45 / 13
TFT CS / DC / BL12 / 11 / 42
GPS TX / RX43 / 44
I2C SDA / SCL18 / 8
Keyboard INT (I2C 0x55)46
Trackball U/D/L/R/Click3 / 15 / 1 / 2 / 0
SD Card CS39
Battery ADC4
Power ON10
~£15

TTGO LoRa32 T3 V1.6.1

MCU: ESP32 • Radio: SX1276 • Display: SSD1306 128×64 OLED

The cheap repeater. Lots of free GPIO for relays & sensors.

FunctionPin
SPI MOSI / MISO / SCK27 / 19 / 5
Radio CS / RST / DIO018 / 23 / 26
I2C SDA / SCL / OLED RST21 / 22 / 16
Battery ADC35
LED25
Button0
Default Relay Pins2, 4, 12, 15, 17, 33
Default Sensor Pins34, 36, 39 (input only)
~£20

Heltec WiFi LoRa 32 V3

MCU: ESP32-S3 • Radio: SX1262 • Display: SSD1306 128×64 OLED

Modern repeater. Better range than SX1276, more GPIO, USB-C.

FunctionPin
SPI MOSI / MISO / SCK10 / 11 / 9
Radio CS / RST / DIO1 / BUSY8 / 12 / 14 / 13
I2C SDA / SCL / OLED RST17 / 18 / 21
Vext Power Control36 (LOW = OLED on)
Battery ADC1
LED35
Default Relay Pins2, 3, 4, 5, 6, 7
Default Sensor Pins19, 20, 33
~£10

Seeed XIAO ESP32S3 + Wio-SX1262

MCU: ESP32-S3 • Radio: SX1262 • Display: None (optional I2C SSD1306)

Cheapest repeater. Plug-together kit, no soldering. Tiny 10×21mm. Available from The Pi Hut UK.

Pins verified from Seeed schematic: Wio-SX1262 for XIAO V1.0.kicad_sch

FunctionGPIOHeader Pin
SPI SCK8D8
SPI MISO9D9
SPI MOSI10D10
Radio CS (NSS)44D7
Radio RST3D2
Radio DIO133internal
Radio BUSY34internal
RF Switch (TX/RX)1D0 — HIGH=RX, LOW=TX
Optional OLED SDA / SCL5 / 6D4 / D5
Default Relay Pins2, 4, 5, 6D1, D3, D4, D5
        [USB-C]
D0/GPIO1  ●  ● 5V      RF_SW1 (TX/RX control)
D1/GPIO2  ●  ● GND     Free relay/GPIO
D2/GPIO3  ●  ● 3V3     RST
D3/GPIO4  ●  ● D10/GPIO10  MOSI
D4/GPIO5  ●  ● D9 /GPIO9   MISO  (optional OLED SDA)
D5/GPIO6  ●  ● D8 /GPIO8   SCK   (optional OLED SCL)
D6/GPIO43 ●  ● D7 /GPIO44  CS/NSS
~£25

Heltec WiFi LoRa 32 V4

MCU: ESP32-S3 • Radio: SX1262 + GC1109 PA (28 dBm!) • Display: SSD1306 128×64 OLED

Best repeater. Highest TX power, 16MB flash, native USB-C, solar input, GNSS connector.

FEM Note: The GC1109 front-end module requires GPIO 2 (FEM enable) and GPIO 46 (PA TX enable) permanently HIGH. These pins are reserved by the firmware and cannot be used as relay pins. GPIO 7 is the PA power pin (set to ANALOG mode). Misconfiguring these pins disables the power amplifier and reduces TX range to ~1/10th.

FunctionPin
SPI MOSI / MISO / SCK10 / 11 / 9
Radio CS / RST / DIO1 / BUSY8 / 12 / 14 / 13
I2C SDA / SCL / OLED RST17 / 18 / 21
FEM Enable / PA TX Enable / PA Power2 / 46 / 7 (reserved, do not touch)
Vext Power Control36 (LOW = OLED on)
ADC Control37 (HIGH = enable battery divider)
Battery ADC1
LED35
Default Relay Pins3, 4, 5, 6
Default Sensor Pins33, 34, 15

3. Code Architecture

MeshCore Shared Library

All mesh protocol logic lives in lib/MeshCore/. Both firmwares link against it — no duplicate code.

MeshCore Library
CRC, Crypto, Routing, Dedup
Packet Build, Forward, Heartbeat
←→
main.cpp
T-Deck Plus UI
Chat, GPS, Menus
←→
repeater.cpp
Repeater firmware
BLE, OLED, GPIO

Callback Architecture

Each firmware registers hardware-specific callbacks with MeshCore at startup:

// In setup():
mesh.onTransmitRaw    = radioTransmit;      // Send raw bytes via SX1262/SX1276
mesh.onChannelFree    = radioChannelFree;    // Check RSSI for listen-before-talk
mesh.onMessage        = handleMessage;       // Decrypted message arrived for us
mesh.onCmd            = handleCmd;           // CMD (GPIO control) arrived
mesh.onAck            = handleAck;           // ACK received
mesh.onNodeDiscovered = handleNodeDiscovered;// New node heard for first time
mesh.onIdConflict     = handleIdConflict;    // Another node using our ID detected
mesh.onCfg            = handleCfg;           // Config change request (SF change)
mesh.onCfgAck         = handleCfgAck;        // Config change acknowledged by a node
mesh.onCfgGo          = handleCfgGo;         // Config change committed — apply now

Main Loop Flow (both firmwares)

loop() {
    receiveCheck();                  // Check radio ISR flag, read packet, call mesh.processPacket()
    handleAckRetry();                // Retry unacknowledged messages
    mesh.processPendingForwards();   // Send jitter-delayed forwards

    // Heartbeat (every ~30s with ±2s jitter)
    if (millis() > nextHeartbeatTime)
        mesh.sendHeartbeat();

    // Route cleanup (every 10s)
    mesh.pruneStale();

    // ... GPS, UI, BLE handling ...
}

File Map

FilePurposeLines
lib/MeshCore/MeshCore.hShared library header — constants, structs, class definition, callbacks~234
lib/MeshCore/MeshCore.cppAll shared mesh logic — CRC, crypto, routing, dedup, forwarding, CFG auth~575
src/main.cppT-Deck Plus full communicator — TFT UI, keyboard, GPS, menus, display sleep, delivery tracking~2300
src/repeater.cppHeadless repeater — OLED stats, BLE, GPIO, serial config, solar mode, pulse counters~1800
src/gateway_wifi.cppWiFi ESP gateway — LoRa + WiFi bridge, BLE config, GWHEIGHT, hub WebSocket~900
include/Pins.hBoard-specific pin definitions (5 boards + custom template)~550
include/phrases.hQuick phrase library (40 phrases, 4 categories)~40
android-app/Kotlin + Jetpack Compose BLE controller app~3700
gateway/Python gateway client, hub server (headless + GUI), web map~3300

4. Packet Format & Protocol

Raw Packet Structure (on the air)

Destination
0xFFFF
Bytes 0-1
Source
0x0001
Bytes 2-3
Sequence
0x002A
Bytes 4-5
TTL
5
Byte 6
Payload (ASCII)
0001,FFFF,ABCD,5,0001,<encrypted_hex>
Bytes 7+

Payload Formats

Data Message Payload

<from>,<to>,<mid>,<ttl>,<route>,<encrypted_hex>

Example: "0001,0002,abcxyz,5,0001,48656C6C6F..."

Fields:
  from          4-char hex sender ID
  to            4-char hex destination (FFFF = broadcast)
  mid           6-char message ID (A-Z random)
  ttl           Remaining hop count
  route         Comma-separated path (e.g. "0001,0003,0005")
  encrypted_hex AES-128-GCM encrypted payload (hex encoded, nonce || ciphertext || tag)

After Decryption — Message Types

PrefixFormatDescription
MSG,MSG,Hello WorldText message
MSG,#MSG,#1/3|Part one textChunked message (part 1 of 3)
POS,POS,51.507222,-0.127500GPS position broadcast
SOS,SOS,51.507222,-0.127500 or SOS,NOFIXEmergency distress signal
CMD,CMD,SET,2,1Remote GPIO command

Special Packets (unencrypted)

TypeFormatDescription
HeartbeatHB,0001Periodic presence announcement (every ~30s)
ACKACK,0001,XYZWDelivery acknowledgement (to node 0001, for message XYZW)
CFGCFG,SF,7,A1B2,C3D4Config change proposal (type, value, changeId, authTag)
CFGACKCFGACK,SF,7,A1B2,0010Config change accepted by node 0010
CFGGOCFGGO,SF,7,A1B2,C3D4Config change committed — all nodes apply now

CFG Auth Tags: CFG and CFGGO packets include an 8-hex-char auth tag derived from the current AES key and the change ID. Nodes verify this before accepting a spreading-factor change, which stops casual or accidental reconfiguration from nodes outside the key group.

5. Encryption Details

Plaintext
MSG,Hello
Generate 12-byte
random nonce
AES-128-GCM
encrypt(key, nonce)
Hex Encode
nonce || ciphertext || tag

AES Key

AES-128-GCM (Authenticated Encryption)

Each message uses a 12-byte random nonce generated via esp_random(). GCM provides both confidentiality and authentication — no separate CRC needed.

// Encrypted blob format (hex-encoded):
nonce(12 bytes) || ciphertext(N bytes) || tag(16 bytes)

// Decryption: extract nonce, decrypt, verify GCM tag
// If tag check fails → message rejected (tampered or wrong key)

Message ID

6-character random (A-Z), used for deduplication and ACK matching. It is not used as the encryption nonce.

6. Routing & Scalability

Directed Routing Algorithm

This system maintains a routing table and prefers directed forwarding when a route is known. If no route exists, it falls back to a controlled broadcast with random jitter to reduce collision storms.

// Route cost = (hop_count × 10) - RSSI
// Lower is better. Closer nodes with fewer hops win.

Example:
  Route via 0003: cost = (2 hops × 10) - (-85 RSSI) = 105
  Route via 0005: cost = (1 hop  × 10) - (-60 RSSI) = 70winner

Smart Forward Decision

Packet arrives
not for me
Check dedup
(hash ring, 128 slots)
Route exists?
& doesn't loop?
Directed send
to next hop
No route
Broadcast
(dest 0xFFFF)
Jitter delay
50-500ms random
Listen-before-talk
RSSI < -120 dBm?
Transmit

Collision Avoidance

MechanismDetail
Jitter DelayRandom 50–500ms wait before forwarding. Prevents multiple nodes forwarding simultaneously.
Listen-Before-TalkScans channel RSSI before transmitting. If > -120 dBm, waits up to 200ms for channel to clear.
Duty CycleTracks total airtime. The 10% budget (EU 868 MHz legal limit) is split: local traffic (messages, heartbeats, setpoint actions) is capped at 7%, reserving 3% exclusively for forwarding. This guarantees that a node acting as a repeater will never refuse to forward other nodes' packets because its own automation commands used up the budget.
Heartbeat JitterHeartbeats sent every 30s ±2s random offset to prevent periodic collisions.

Deduplication

// FNV-1a hash of the 6-char Message ID
hash = 2166136261;    // FNV offset basis
for each char c in mid:
    hash ^= c;
    hash *= 16777619; // FNV prime
hash |= 1;           // Never zero (zero = empty slot)

// Stored in 128-slot ring buffer. O(1) lookup, O(1) insert.

7. T-Deck Plus User Interface

The T-Deck Plus has a full 320×240 colour TFT display with a hardware QWERTY keyboard and trackball. All screens share a consistent layout: header bar (28px), content area, and status bar (18px).

Main Menu

TiggyOpenMesh v4.1 ONLINE
0001 > FFFF GPS N:3 87%

Menu Items Explained

ItemActionKey
New MessageOpens text input — type a message with the keyboard, press Enter to sendTrackball click / Enter
Quick PhrasesPick up to 5 pre-defined phrases from 4 categories. Phrases are compressed to hex IDs for compact transmission.
View MessagesChat bubble view showing message history with scroll
Remote ControlSend GPIO commands (SET/GET/PULSE) to the target node
SOS EmergencyFlashing red screen, broadcasts SOS + GPS every 2 minutes. Hold Esc 3s to cancel.
SettingsOpens settings submenu
Known NodesList of discovered nodes with RSSI, signal bars, age, and GPS indicator
Mesh MapLive view of mesh topology — nodes, signal strength, and neighbour links. Auto-refreshes every 5s.
Broadcast PosSends your GPS coordinates to all nodes
Find NodeCompass arrow and distance to the target node (requires both nodes to have GPS)

Chat View

Messages > 0002
0002
Hello, are you at camp?
-82dBm   14:32
You
Yes, arrived 10 mins ago
✓✓ 14:33
0002
Great, heading your way now
-78dBm   14:35
[r] Reply   [n] New   [Esc] Back   [Up/Dn] Scroll
0001 > 0002 GPS N:3 TX.. 87%

Text Input

New Message
To: 0002 12/160
Hello world_
[Enter] Send    [Bksp] Delete    [Esc] Cancel
0001 > 0002 87%

Settings Submenu

TiggyOpenMesh v4.1
0001 87%

Settings Actions

SettingInputNotes
Set Local ID4 hex characters (0-9, A-F)Your node's unique address on the mesh
Set Target ID4 hex charactersDefault recipient. FFFF = broadcast to all
Edit AES KeyExactly 16 ASCII charactersAll nodes must share the same key!
BrightnessCycles: 100% → 50% → 25%Display backlight PWM
Display TimeoutCycles: Off → 30s → 60s → 120s → 300sAuto-sleep after idle. See Display Sleep.
Clear NodesImmediateWipes all known node IDs from memory & EEPROM

SOS Mode

!! EMERGENCY !!
!! EMERGENCY !!
51.50722, -0.12750
Next broadcast: 118s
Hold Esc 3s to cancel
0001 SOS ACTIVE

Remote Control Panel

Remote Control > 0010
Target: 0010
0001 87%

First-Boot Wizard

On first power-up (EEPROM magic byte 0xA5 not found), a 3-step wizard guides setup:

  1. Step 1: A random 4-hex-char Node ID is auto-generated (e.g. 3FA7) and a 10-second conflict scan runs in the background. A progress bar and countdown are shown on screen. If no conflict is detected, the suggested ID is accepted. If a collision is found, the ID auto-regenerates and re-scans. You can override the ID at any time by typing a custom one.
  2. Step 2: Set or accept the AES encryption key (16 chars, pre-filled with default)
  3. Step 3: Confirmation screen showing all settings, press Enter to start

Repeater first-boot: Repeater nodes also auto-generate a random ID on first boot and run a 10-second conflict scan with an OLED countdown. If a collision is found, the ID is regenerated and the scan restarts automatically. Config is only saved to EEPROM after a clear scan completes.

Keyboard Controls

ContextKeyAction
MenuTrackball Up/DownNavigate items
MenuTrackball Click / EnterSelect item
MenuEsc / Trackball LeftBack to parent menu
ChatnNew message
ChatrQuick reply to last sender
ChatTrackball Up/DownScroll messages
ChatEscBack to menu
Text InputEnterSend / confirm
Text InputBackspaceDelete last char
Text InputEscCancel
Tracking/Mapq / EscBack to menu
SOSHold Esc 3sCancel SOS mode

Boot Screen

TiggyOpenMesh
T-Deck Plus Edition
v4.1
Freq: 868.0 MHz    SF: 9
BW: 125 kHz    Power: 20dBm
Node: 0001    AES: Active
Chip: ESP32-S3 + SX1262

8. Repeater OLED Display

The repeater boards (LoRa32 & Heltec V3) show a 128×64 OLED stats screen that refreshes every 5 seconds.

MESH RPT 0010
________________________
RX:1247  FWD:583
N:5 R:12 BLE
RSSI:-78dBm
Relay:101000
Up: 14h23m

Display Fields

FieldMeaning
MESH RPT 0010Device role + node ID
RX:1247Total packets received since boot
FWD:583Total packets forwarded (relayed)
N:5Number of known nodes
R:12Active routes in routing table
BLEShown when a phone is connected via BLE
RSSI:-78dBmSignal strength of last received packet
Relay:101000State of each relay pin (1=ON, 0=OFF)
Up: 14h23mUptime since last reset

9. Android App

The companion Android app connects to any repeater node via BLE (Nordic UART Service). It turns any headless board into a full communicator with chat, relay control, sensor monitoring, and remote node management. A foreground BLE service keeps the connection alive when the app is in the background.

App Screens

Scan for Devices
LoRa32_0010 -52 dBm
HeltecV3_0020 -88 dBm
Unknown_BLE -105 dBm
SCANNING...
To: FFFF Broadcast
0002
Anyone at base camp?
-82dBm   14:32
You
Yes, just arrived!
14:33
Type a message...
Chat
Nodes
Control
Settings
Relay Control
Relay 1
GPIO 2 - ON
Pulse
Relay 2
GPIO 4 - OFF
Pulse
Sensors
Sensor 1
GPIO 34
2847
Chat
Nodes
Control
Settings

App Architecture

ComponentFileResponsibility
BLE ManagerBleManager.ktScan, connect, disconnect, send/receive via Nordic UART. Remote relay/sensor control functions.
BLE ServiceBleConnectionService.ktAndroid foreground service — keeps BLE connection alive when app is in background.
App SingletonLoRaMeshApp.ktApplication-level BleManager instance shared across all components.
ViewModelMeshViewModel.ktParse incoming data, manage UI state. Auto-saves key/ID to EEPROM after changes.
Scan ScreenScanScreen.ktBLE device discovery with RSSI colour coding
Chat ScreenChatScreen.ktSend/receive messages, broadcast/direct toggle
Control ScreenControlScreen.ktRelay toggles, pulse buttons, sensor sparklines. Node picker for remote node control via mesh.
Nodes ScreenNodesScreen.ktDiscovered mesh nodes list with online/offline status and signal strength
Settings ScreenSettingsScreen.ktNode ID, AES key (auto-saved), GPS toggle, solar mode toggle, Telegram config
ThemeTheme.ktDark theme matching firmware cyan/green palette

Remote Node Control

The Control screen includes a node picker dropdown at the top. Select any discovered mesh node to control its relays and read its sensors remotely. Commands are encrypted and sent via MSG,<nodeId>,CMD,... over the mesh. The local node processes commands directly via BLE.

Background BLE

The app uses an Android foreground service (BleConnectionService) to maintain the BLE connection when the app is backgrounded or the screen is off. A persistent notification shows the connection status. This prevents Android from killing the BLE connection during battery optimization.

10. BLE Protocol (Phone ↔ Node)

BLE Service: Nordic UART Service (NUS)

Service UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E

TX (phone → node): 6E400002-...   RX (node → phone): 6E400003-...

Data is chunked to 20-byte BLE MTU packets with 30ms delays between chunks.

Commands: Phone → Node

CommandFormatExampleResponse
Send MessageMSG,<target>,<text>MSG,0001,HelloOK,SENT,0001
Set RelayCMD,SET,<pin>,<0|1>CMD,SET,2,1CMD,RSP,2,1
Read PinCMD,GET,<pin>CMD,GET,34CMD,RSP,34,2847
PulseCMD,PULSE,<pin>,<ms>CMD,PULSE,2,1000CMD,RSP,2,0
List PinsCMD,LISTPINS,R:2,4,12|S:34,36
Set Node IDID <4hex>ID 0002OK,ID,0002
Set AES KeyKEY <16chars>KEY MySecretKey123OK,KEY
Get StatusSTATUSSTATUS,ID:0010,BOARD:...
Save ConfigSAVEOK,SAVED
Set SFSF,<7-12>SF,7OK,SF,7
Commit SFSFGO,<7-12>SFGO,7OK,SFGO,7
Solar Mode OnPOWER,SOLAROK,POWER,SOLAR (BLE deinits, connection drops)
Solar Mode OffPOWER,NORMALOK,POWER,NORMAL
Poll Local SensorsPOLLSDATA,0010,34:2048,36:1200
Poll RemotePOLL,<nodeId>POLL,0002Remote node replies with SDATA over mesh
Set TimerTIMER,<pin>,ON|OFF,<sec>TIMER,4,ON,300OK,TIMER,4,ON,300
Pulse TimerTIMER,<pin>,PULSE,<on>,<off>,<rpt>TIMER,4,PULSE,60,120,5OK,TIMER,4,PULSE
List TimersTIMER,LISTTIMERS,<count>,...
Clear TimersTIMER,CLEAROK,TIMER,CLEAR
Set SetpointSETPOINT,<sPin>,<op>,<thr>,<node>,<rPin>,<act>SETPOINT,34,GT,2000,0010,4,1OK,SETPOINT
List SetpointsSETPOINT,LISTSETPOINTS,<count>,...
Clear SetpointsSETPOINT,CLEAROK,SETPOINT,CLEAR
Start Auto-PollAUTOPOLL,<nodeId>,<sec>AUTOPOLL,FFFF,300OK,AUTOPOLL,FFFF,300
Stop Auto-PollAUTOPOLL,OFFOK,AUTOPOLL,OFF
Set Pin ModePINMODE,<pin>,PULSE|AUTOPINMODE,34,PULSEOK,PINMODE,34,PULSE

POWER,SOLAR warning: Enabling solar mode via BLE deinitialises BLE after sending the response — the connection drops. The app should warn the user before sending. To restore: connect via USB serial and send SOLAR OFF, or press the PRG button.

Notifications: Node → Phone

EventFormatExample
Incoming MessageRX,<from>,<text>,<rssi>RX,0001,Hello,-85
Message SentSENT,<mid>SENT,abcxyz
Delivery ACKACK,<from>,<mid>ACK,0002,abcxyz
Command ResponseCMD,RSP,<pin>,<value>CMD,RSP,2,1
Pin ListPINS,R:<pins>|S:<pins>PINS,R:2,4,12|S:34,36
StatusSTATUS,<key:val,...>STATUS,ID:0010,BOARD:LoRa32...,POWER:NORMAL
Node FoundNODE,<id>,<rssi>,<hops>NODE,0002,-95,1
ID ConflictCONFLICT,<id>,<rssi>CONFLICT,0010,-65
CFG ProposedCFGSTART,<type>,<value>,<changeId>CFGSTART,SF,7,A1B2
CFG Node ACKCFGACK,<nodeId>,<type>,<value>,<changeId>CFGACK,0010,SF,7,A1B2
CFG AppliedCFGGO,<type>,<value>,<changeId>CFGGO,SF,7,A1B2
Sensor DataSDATA,<nodeId>,<pin>:<val>,...SDATA,0010,34:2048,36:1200
Timer FiredTIMER,FIRED,<pin>,ON|OFFTIMER,FIRED,4,ON
Timer DoneTIMER,DONE,<pin>TIMER,DONE,4
Setpoint FiredSETPOINT,FIRED,<pin>,<value>SETPOINT,FIRED,34,2100

11. Serial Configuration (Repeater)

Connect to the repeater via USB serial at 115200 baud. Same commands work via BLE.

ID <4hex>              Set node ID          e.g. ID 0002
KEY <16chars>          Set AES key          e.g. KEY MySecretKey12345
RELAY <pins>           Set relay GPIO pins  e.g. RELAY 2,4,12,15
SENSOR <pins>          Set sensor GPIO pins e.g. SENSOR 34,36
STATUS                 Show device info
SAVE                   Write config to EEPROM
RESET                  Restore factory defaults
GATEWAY ON             Enable gateway mode (raw packet forwarding)
GATEWAY OFF            Disable gateway mode
PKT,<hex>              Inject raw packet into mesh (gateway mode only)
AUTOPOLL <id> <sec>    Start auto-polling   e.g. AUTOPOLL FFFF 300
AUTOPOLL OFF           Stop auto-polling
PINMODE <pin> <mode>   Set sensor mode      e.g. PINMODE 34 PULSE (or AUTO)
PINMODE <pin> PULSE <ms> Pulse mode + sample window  e.g. PINMODE 5 PULSE 3000
EXPAND ON              Enable IO expansion board on UART2
EXPAND OFF             Disable IO expansion board
SOLAR ON               Enable solar/low-power mode (SX1262 boards only)
SOLAR OFF              Disable solar mode, restore normal operation
BEACON,SCAN            Scan for nearby BLE beacons (2 seconds)
BEACON,ADD,...         Add a beacon detection rule (see iBeacon Scanner)
BEACON,LIST            List all active beacon rules
BEACON,CLEAR           Delete all beacon rules
SETPOINT,...           Add a cross-node setpoint rule (see Setpoints)
SETPOINT,LIST          List all active setpoint rules
SETPOINT,CLEAR         Delete all setpoint rules
REBOOT                 Restart the node (settings preserved)
EEPROM,RESET           Factory reset — wipe all EEPROM and restart
GPSPOS                 Query current GPS position
GPS,<tx>,<rx>          Set GPS UART pins e.g. GPS,38,39

# STATUS response includes:
#   ID, BOARD, POWER:NORMAL|SOLAR, SF:9, RELAY pins, SENSOR pins

Pin safety: The firmware blocks writes to SPI, radio, I2C, and boot pins. Only user-configurable GPIO pins are allowed for relay/sensor control. This prevents accidental damage to the radio or display.

12. Quick Phrase System

The T-Deck's phrase picker lets you compose messages from pre-defined phrases without typing. Phrases are sent as hex IDs (e.g. $0A|$1E) and expanded on receipt — extremely bandwidth-efficient.

STATUS (0-9)REQUEST (10-19)LOCATION (20-29)INTERACTION (30-39)
I AM OKSEND HELPAT BASEWHERE ARE YOU
I AM LOSTNEED WATERNEAR RIVERI SEE YOU
I AM INJUREDNEED FOODAT CAMPCOME TO ME
I AM RESTINGNEED BATTERYNEAR ROADGO TO BASE
I AM MOVINGNEED WARMTHNEAR TREEFOLLOW ME
I AM SAFEBRING SUPPLIESNEAR BUILDINGSTAY THERE
I AM WAITINGRADIO CHECKON TRAILTURN BACK
I AM COLDGPS HELPINSIDE SHELTERJOIN ME
I AM SCAREDMEDIC NEEDEDAT CHECKPOINTMEET AT CAMP
I AM WETSEND TRANSPORTLOST POSITIONCONFIRM SIGNAL

Wire format example: Selecting "I AM OK" + "AT CAMP" + "NEED WATER" sends: $00|$16|$0B (6 bytes instead of 30)

13. GPIO Remote Control Protocol

GPIO commands are encrypted and routed through the mesh like regular messages. A T-Deck or phone app can control relays on any reachable node.

Command Format (inside encrypted payload)

CommandFormatDescription
SETCMD,SET,<pin>,<0|1>Set pin HIGH (1) or LOW (0). Pin is configured as OUTPUT.
GETCMD,GET,<pin>Read pin value. Analog sensor pins return 0-4095. Digital pins return 0/1. (ESP32: ADC on GPIO 32-39. ESP32-S3: ADC on GPIO 1-20.)
PULSECMD,PULSE,<pin>,<ms>Set pin HIGH for N milliseconds, then LOW. Max 30 seconds.
LISTCMD,LISTReturns configured relay and sensor pins.
RSPCMD,RSP,<pin>,<value>Response containing pin value (sent back to requester).

Forbidden Pins: The firmware blocks access to these pins regardless of configuration:

SPI bus (MOSI, MISO, SCK) • Radio (CS, RST, DIO, BUSY) • Display (CS, DC) • Boot (GPIO 0)

14. Relay Timers

Up to 4 concurrent relay timers can be set via BLE, serial, or mesh command. Timers are ephemeral (not saved to EEPROM) and lost on reboot.

Timer Commands

CommandFormatDescription
Delay ON/OFFTIMER,<pin>,ON|OFF,<seconds>Set pin HIGH or LOW after a delay (max 86400s / 24 hours).
PulseTIMER,<pin>,PULSE,<onSec>,<offSec>,<repeats>Alternating ON/OFF cycle. repeats=0 = forever. Max 3600s per phase.
ListTIMER,LISTReturns active timers and remaining time.
ClearTIMER,CLEARCancels all active timers.

Behaviour

Example: Irrigation Pump

# Turn on pump (GPIO 4) for 30 minutes, then off
TIMER,4,ON,0             # ON immediately
TIMER,4,OFF,1800          # OFF after 30 min

# Pulse: 5 min on, 10 min off, repeat 3 times
TIMER,4,PULSE,300,600,3

15. Cross-Node Setpoint Rules

Setpoints are conditional automation rules that run on the node itself — no gateway needed. The node reads its own sensor, evaluates the condition locally, and only uses the radio when the condition triggers. This is far more efficient than gateway-polled rules.

Why on-node matters: A gateway rule polls the sensor every 5 seconds (3 radio transmissions per cycle even when nothing changes). An on-node setpoint uses zero radio until the condition triggers — then sends one packet. For battery/solar nodes this is critical.

Two Action Types

TypeFormatDescription
Relay SETPOINT,<pin>,<op>,<threshold>,<target>,<relayPin>,<action> When condition triggers, send CMD,SET to target node. If target is self, the relay is set directly via digitalWrite() with zero radio usage.
Message SETPOINT,<pin>,<op>,<threshold>,MSG,<msgTrue>[,<msgFalse>] Broadcasts a text message when condition becomes true (e.g., “Gate open”) and optionally a different message when it clears (e.g., “Gate closed”).
ListSETPOINT,LISTReturns all active setpoint rules.
ClearSETPOINT,CLEARRemoves all setpoint rules.

Operators

OpMeaningExample
GTGreater thanTemperature above threshold → turn on fan
LTLess thanSoil moisture below threshold → turn on pump
EQEqual toDigital input = 1 → broadcast “Gate open”
GEGreater or equalWind speed ≥ 15 m/s → sound alarm
LELess or equalBattery ≤ 3.3V → send low-battery alert
NENot equalValue changed from expected → alert

Optional Suffixes: Scale & Debounce

Append these to any setpoint command to add signal processing on the node:

SuffixFormatWhat It Does
SCALE ,SCALE,<factor>,<offset> Converts raw sensor reading to engineering units before comparison: scaled = raw × factor + offset. This means the threshold is in real units (m/s, °C, etc.) not raw ADC counts.
DEBOUNCE ,DEBOUNCE,<ms> Condition must stay true for this many milliseconds before the action fires. Filters out noise and brief spikes. If the condition goes false before the timer expires, it resets.

Safety Features

Examples

# Gate monitor: broadcast "Gate open" / "Gate closed" on digital input change
SETPOINT,5,EQ,1,MSG,Gate open,Gate closed

# Wind alarm: Scale raw pulse rate to m/s, alarm if >= 15 m/s, debounce 3s
SETPOINT,5,GE,15.0,0010,2,1,SCALE,0.4524,0,DEBOUNCE,3000

# Frost protection: Scale ADC to °C, trigger heater when < 3°C
SETPOINT,34,LT,3.0,0010,4,1,SCALE,0.0244,-10.0

# Local relay: target is self (same node), no radio needed
SETPOINT,34,GT,2000,0001,4,1
# (where 0001 is this node's own ID)

16. Sensor Monitoring & Auto-Poll (SDATA)

Nodes can read local analog/digital sensors and broadcast bundled sensor data (SDATA) across the mesh for remote monitoring.

SDATA Format

SDATA,<nodeID>,<pin1>:<value1>,<pin2>:<value2>,...

# Example: Node 0010 reporting 3 sensors
SDATA,0010,34:2048,36:1200,39:4095

Reading Sensors

CommandFormatDescription
Local POLLPOLL (BLE/serial)Read all local sensor pins, return SDATA over BLE/serial.
Remote POLLPOLL,<targetNodeId> (BLE)Send CMD,POLL to a remote node; it replies with SDATA over the mesh.
Single PinCMD,GET,<pin>Read one specific pin on a remote node.

Auto-Poll (Periodic Broadcasting)

Auto-Poll sends SDATA to a target node at a regular interval. Ideal for unattended sensor stations that periodically report readings.

CommandFormatDescription
StartAUTOPOLL,<targetNodeId>,<intervalSec>Begin periodic SDATA broadcasts. Minimum interval: 30 seconds.
StopAUTOPOLL,OFFStop auto-polling.

Persistence: Auto-poll configuration is saved to EEPROM (address 433). It resumes automatically 10 seconds after reboot.

Sensor Pin Types

Pin RangeRead MethodValue RangeUse Case
ESP32: GPIO 32-39analogRead()0-4095 (12-bit ADC)Temperature, soil moisture, light, wind speed
ESP32-S3: GPIO 1-20analogRead()0-4095 (12-bit ADC)Same — S3 has ADC on most GPIOs
Any GPIO (digital)digitalRead()0 or 1Door switches, reed sensors, rain gauges

Default Sensor Pins per Board

BoardDefault Sensor PinsNotes
LoRa32 T334, 36, 39Input-only, ADC1. Best for analog sensors.
Heltec V319, 20, 33All ADC-capable on ESP32-S3.
Heltec V433, 34, 15GPIO 19/20 are USB D-/D+, GPIO 2/46/7 are GC1109 FEM, GPIO 38 is GPS header — do not use any of these as GPIO.
XIAO ESP32S343, 44Digital-only (UART pins). Use SENSOR 2,4,5 for analog.
Heltec V236, 39Input-only, ADC1. Used as WiFi gateway — sensors optional.
T-Deck PlusNoneNo free GPIO — all pins used by display/keyboard/GPS.

Reconfigure pins: Use SENSOR <pin1>,<pin2>,... via serial, then SAVE. This overrides the defaults.

Farming & Environmental Monitoring

The sensor system is designed for unattended outdoor monitoring. Typical setup:

Connect sensors to a solar-powered repeater node (see Section 24: Solar Mode), configure AUTOPOLL to send readings every 5 minutes to a gateway node, and monitor via the PC gateway GUI, hub GUI, or web map — all with sparkline charts.

Viewing Sensor Data

17. Pulse Counter Mode (Wind Speed, Flow Meters)

Sensor pins can be switched from analog/digital read mode to pulse counter mode, where hardware interrupts count rising-edge pulses. The firmware calculates pulses per second over a configurable sample window, so SDATA and setpoint rules work with rate values, not raw counts.

How It Works

Commands

InterfaceCommandResponse
BLEPINMODE,<pin>,PULSEOK,PINMODE,<pin>,PULSE
BLEPINMODE,<pin>,AUTOOK,PINMODE,<pin>,AUTO
SerialPINMODE <pin> PULSEOK: Pin <pin> set to PULSE mode (window=5000ms)
SerialPINMODE <pin> PULSE <windowMs>OK: Pin <pin> set to PULSE mode (window=<windowMs>ms)
SerialPINMODE <pin> AUTOOK: Pin <pin> set to AUTO mode

Rate Calculation

Every sample window (default 5 seconds), the firmware reads the pulse count, calculates the delta since last read, and divides by elapsed time to get pulses per second. The SDATA value for pulse pins is rate × 100 — so a reading of 470 means 4.70 pulses per second.

Example: Wind Speed from Anemometer

# Configure sensor pin 5 for pulse counting (3-second window)
PINMODE 5 PULSE 3000

# SDATA response shows rate × 100 (centipulses/sec):
# SDATA,0010,5:470,36:2048
#   pin 5: 470 = 4.70 pulses/sec
#   pin 36: 2048 (analog read, temperature sensor)

# On-node setpoint: scale pulse rate to m/s, alarm at 15 m/s
# For an anemometer with 7.2cm arm, 1 PPR:
#   speed_ms = pulses_per_sec × 2π × 0.072 = rate × 0.4524
# But SDATA gives rate×100, so scaleFactor = 0.4524 / 100 = 0.004524
SETPOINT,5,GE,15.0,0010,2,1,SCALE,0.004524,0

Pin requirements: Pulse counter pins must support hardware interrupts. On ESP32, most GPIO pins support interrupts. The pin is configured as INPUT_PULLUP when pulse mode is enabled. Use PINMODE <pin> AUTO to revert to normal analog/digital read. Minimum sample window is 100ms.

18. SOS Emergency Alert

The mesh network includes a built-in SOS emergency system. When triggered, an SOS alert is broadcast to all nodes on the mesh with the sender's GPS location (if available), and forwarded to Telegram.

Sending an SOS

SOS Message Format

# With GPS fix:
SOS,51.481583,-3.178500

# Without GPS:
SOS

What Happens When SOS Is Received

Emergency use only: SOS alerts are broadcast to the entire mesh and cannot be recalled. The confirmation dialog is there for a reason — only send when you genuinely need help.

19. EEPROM Memory Map

512 bytes of EEPROM store persistent configuration. Layout is identical across all boards.

AddressSizeContentDefault
05 bytesLocal Node ID (4 chars + null)"0001"
105 bytesTarget Node ID (4 chars + null)"FFFF"
20~100 bytesKnown nodes array (up to 20 × 5 bytes)Empty
4001 byteKnown node count0
4301 byteSolar mode flag (0=off, 1=on) — repeater only0
4311 byteRuntime spreading factor (7-12) — repeater only9 (LORA_SF)
4321 byteDisplay sleep timeout in seconds (0=off) — T-Deck only60
4331 byteAuto-poll enabled flag (0=off, 1=on)0
4345 bytesAuto-poll target node ID (4 chars + null)Empty
4392 bytesAuto-poll interval in seconds (uint16)300
45017 bytesAES-128 key (16 chars + null)Randomly generated on first boot
5081 byteFirst-boot magic byte0xA5 after first setup

Tip: The SAVE command (via serial or BLE) calls EEPROM.commit() which writes the buffer to flash. Without SAVE, changes are lost on reboot.

20. Building & Flashing

Prerequisites

Build Commands

# Build all 4 targets
pio run

# Build specific target
pio run -e tdeck-plus
pio run -e lora32
pio run -e heltec-v3
pio run -e heltec-v4

# Flash specific target (with board connected)
pio run -e heltec-v4 --target upload

# Open serial monitor
pio device monitor -b 115200

# Clean build
pio run -e lora32 --target clean

Build Isolation

PlatformIO uses build_src_filter to ensure each target only compiles its firmware:

TargetCompilesExcludes
tdeck-plusmain.cpp + MeshCorerepeater.cpp
lora32repeater.cpp + MeshCoremain.cpp
heltec-v3repeater.cpp + MeshCoremain.cpp
heltec-v4repeater.cpp + MeshCoremain.cpp

Android App

# Open in Android Studio:
android-app/

# Build: compileSdk 34, minSdk 26
# Dependencies: Jetpack Compose BOM 2024.01, BLE

21. Gateway Bridge — Internet-Linked Mesh Islands

The gateway feature connects geographically separated LoRa mesh networks over the internet. Each gateway node is a repeater connected via USB to a PC running the Python bridge server. Packets remain AES-128-GCM encrypted end-to-end — the servers never decrypt them.

Hub Architecture (Recommended)

The simplest way to connect multiple mesh islands is using a central hub server. Each location runs a gateway that connects to the hub. Gateways only need the hub's address — no complex peer-to-peer configuration.

  ISLAND A              CENTRAL HUB              ISLAND B
                       (cloud server or VPS)

  [Node 0001]                                         [Node 0005]
       |                                                   |
  [Repeater 0003]                                    [Repeater 0007]
       |                                                   |
  USB Serial                                          USB Serial
       |                                                   |
  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
  │ Gateway A    │────>│ Gateway Hub  │<────│ Gateway B    │
  │ "Base Camp"  │     │ (relay only) │     │ "Ridge Post" │
  └──────────────┘     └──────┬───────┘     └──────────────┘
  laptop at camp                          PC at ridge post
                              
                     ┌──────────────┐
                     │ Gateway C    │
                     │ "Town HQ"    │
                     └──────────────┘
                     office in town

End-to-end encryption: The hub and gateway servers only see opaque hex-encoded ciphertext. They never possess the AES key and cannot read message contents. All encryption/decryption happens on the LoRa nodes themselves.

System Components

ComponentFileWhat It DoesWhere It Runs
Repeater Firmware src/repeater.cpp LoRa radio + gateway serial protocol (PKT,<hex> forwarding) LoRa32 or Heltec V3 board
Gateway Client gateway/gateway.py Reads packets from repeater via USB serial, sends to hub or peers PC/laptop at each location
Gateway Hub gateway/gateway_hub.py Central relay — receives packets from any gateway, relays to all others Cloud VPS, home server, or any always-on PC

How It Works — Step by Step

  1. A node on Island A sends an encrypted message destined for a node on Island B
  2. The repeater on Island A receives the packet over LoRa radio — MeshCore processes it normally (routing, forwarding)
  3. Because gateway mode is enabled, the repeater also outputs the raw packet bytes over USB serial as PKT,<hex>
  4. The Python gateway client (gateway.py) reads this line and sends it to the central hub via WebSocket
  5. The hub (gateway_hub.py) relays the packet to all other connected gateways
  6. Gateway Client B receives the packet and injects it into Island B's mesh via PKT,<hex> over serial
  7. The repeater on Island B transmits the raw packet over LoRa radio
  8. The destination node receives and decrypts the message — end-to-end encryption preserved

Security Model

LayerProtection
Message ContentAES-128-GCM encrypted on-device. Hub and gateways never see plaintext.
Hub AccessOptional authentication key (--key) — only gateways with the correct key can join.
TransportWebSocket (ws://) by default. Use wss:// with a TLS reverse proxy (nginx/caddy) for encrypted transport.
NetworkFor maximum security, run the hub on a VPN or use SSH tunnels between sites.

Important: The hub auth key prevents unauthorized gateways from connecting, but it does not encrypt the WebSocket traffic itself. For sensitive deployments, always use wss:// (TLS) or a VPN to protect the transport layer.

Serial Protocol (Repeater ↔ Gateway Client)

DirectionFormatDescription
Repeater → ClientPKT,<hex>Raw radio packet forwarded as hex string (auto when gateway mode ON)
Client → RepeaterPKT,<hex>Inject raw packet into local mesh via radio transmission
ConfigGATEWAY ONEnable gateway mode (gateway.py sends this automatically on start)
ConfigGATEWAY OFFDisable gateway mode (sent automatically on shutdown)

WebSocket Protocol

All WebSocket messages are JSON. The hub understands three message types:

Authentication (Gateway → Hub)

// Python gateway (PC-based)
{
  "type":       "auth",
  "gateway_id": "3f8a1b2c",       // Unique ID (auto-generated)
  "name":       "Base Camp",      // Friendly name (--name flag)
  "key":        "MySecretKey"     // Hub auth key (--hub-key flag)
}

// ESP WiFi gateway (includes location for map)
{
  "type":    "auth",
  "key":     "MySecretKey",
  "id":      "AA:BB:CC:DD:EE:FF",  // WiFi MAC
  "name":    "Ridge Top",
  "lat":     51.5074,              // GPS latitude
  "lon":     -3.1791,              // GPS longitude
  "antenna": 2,                    // 0=indoor 1=external 2=rooftop
  "height":  10.0                  // Antenna height in metres
}

Auth Response (Hub → Gateway)

{
  "type":    "auth_result",
  "success": true,
  "peers":   2                   // Number of other gateways online
}

Packet Relay (bidirectional)

{
  "type":   "pkt",
  "data":   "A50012...",        // Hex-encoded raw radio packet
  "origin": "3f8a1b2c"          // Source gateway ID (prevents echo loops)
}

Peer Events (Hub → Gateway)

{
  "type": "peer_joined",        // or "peer_left"
  "name": "Ridge Post",
  "peers": 3                    // Total gateways now online
}

Deduplication

Both the hub and each gateway maintain time-expiring hash sets to prevent packets from bouncing in loops:

ComponentBuffer SizeTTL
Gateway Client256 entries30 seconds
Gateway Hub512 entries30 seconds

Additionally, the origin field in each packet prevents a gateway from re-processing packets it originally sent.

Complete Setup Guide

Step 1: Flash the Repeaters

Each location needs a repeater board (LoRa32 or Heltec V3) connected to a PC via USB.

# Flash a LoRa32 as a repeater
pio run -e lora32 --target upload

# Configure via serial (115200 baud):
ID 0003                      # Unique node ID for this repeater
KEY MySecretKey12345         # Must match ALL nodes in the mesh
SAVE

Key rule: All nodes across all mesh islands must share the same AES key. The key is what makes them one logical mesh network, even if they're physically separated.

Step 2: Set Up the Hub Server

The hub runs on any always-on machine with a public IP or port forwarding. It does not need a LoRa radio — it's purely a network relay.

# On your server/VPS:
cd gateway/
pip install -r requirements.txt

# Start hub (open, no authentication)
python gateway_hub.py --listen 9000

# Start hub WITH authentication (recommended)
python gateway_hub.py --listen 9000 --key MySecretMeshKey

Step 3: Connect Gateways to the Hub

At each location, run gateway.py pointing to the hub:

# At Location A:
python gateway.py --port COM3 --hub ws://your-server.com:9000 \
    --hub-key MySecretMeshKey --name "Base Camp"

# At Location B:
python gateway.py --port COM5 --hub ws://your-server.com:9000 \
    --hub-key MySecretMeshKey --name "Ridge Post"

# At Location C:
python gateway.py --port /dev/ttyUSB0 --hub ws://your-server.com:9000 \
    --hub-key MySecretMeshKey --name "Town HQ"

That's it. The gateway automatically:

Step 4: Network Configuration

Port forwarding: The hub server needs its WebSocket port (default 9000) accessible from the internet. If behind a router, forward port 9000 to the hub PC. For production deployments, use a TLS reverse proxy (nginx/caddy) for wss:// encrypted transport.

Advanced: Direct Peer-to-Peer Mode

For two-site setups or when you don't want a central hub, gateways can connect directly to each other:

# Site A (listens for inbound connections)
python gateway.py --port COM3 --listen 9000

# Site B (connects to Site A)
python gateway.py --port COM5 --listen 9001 --peers ws://siteA-ip:9000

In this mode, each gateway must know every other gateway's address. For 3+ sites, the hub mode is strongly recommended.

Topology Options

TopologyModeDescriptionBest For
Star (Hub)--hubAll gateways connect to one central hub3+ sites, simplest setup, recommended
Direct Pair--peersTwo gateways connect directlyTwo-site deployments
Full Mesh--peersEvery gateway connects to every otherMaximum resilience (complex config)

CLI Reference

gateway.py (runs at each site)

FlagDescriptionExample
--port, -pSerial port to repeater--port COM3
--baud, -bBaud rate (default: 115200)--baud 115200
--hubHub server WebSocket URL--hub ws://server:9000
--hub-keyAuthentication key for hub--hub-key MySecret
--name, -nFriendly name for this gateway--name "Base Camp"
--listen, -lLocal WebSocket port (peer mode)--listen 9000
--peersDirect peer URLs (peer mode)--peers ws://ip:9000
--list-portsList serial ports and exit--list-ports

gateway_hub.py (runs once, centrally)

FlagDescriptionExample
--listen, -lWebSocket listen port--listen 9000
--key, -kAuthentication key (gateways must match)--key MySecret

OLED Gateway Indicator

When gateway mode is active, the repeater OLED displays GW in the top-right status area instead of the BLE indicator. The STATUS serial command and BLE status response also include the gateway state.

Gateway File Structure

Files marked with ★ are part of TiggyOpenMesh Pro (paid). The free version includes the CLI gateway (gateway.py) and hub server.
gateway/
  gateway.py              # Gateway client — headless (Linux/Windows CLI)
  gateway_gui.py# Gateway client — Windows GUI (topology view + packet waterfall)
  gateway_hub.py          # Central hub server — headless (Linux/Windows CLI)
  gateway_hub_gui.py      # Central hub server — Windows GUI (live dashboard)
  automation_engine.py# Logic Builder evaluation engine — rule model, persistence, anti-flood
  automation_canvas.py# Logic Builder visual canvas — drag, connect, configure blocks
  telegram_bot.py# Telegram bot bridge — exposes rules as chat commands
  meshtastic_bridge.py# TiggyOpenMesh ↔ Meshtastic bidirectional bridge
  sensor_dashboard.py# Sensor gauges, line chart, CSV export (Sensors tab)
  rules.json              # Auto-generated Logic Builder rules (JSON persistence)
  requirements.txt        # Python dependencies: pyserial, websockets, aiohttp, customtkinter
  gateways.json           # Auto-generated gateway registry (location persistence)
  web/
    map.html              # Live gateway coverage map (Leaflet.js, served by hub)

Hub serves the map: The hub serves both WebSocket and HTTP on the same port. Open http://hub-address:9000/ in a browser to see the Live Gateway Map. Gateways connect via ws://hub:9000/ (same port).

Gateway GUI — Feature Overview

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

The Windows GUI client (gateway_gui.py) provides a comprehensive dashboard with four main tabs:

Tab / FeatureWhat It Shows
Mesh TopologyLive force-directed graph of all discovered nodes. Edges coloured and sized by RSSI (green=strong, red=weak). Signal quality summary bar, weak link warnings. Node cards with signal bars, hop count, route info, and right-click context menus.
Packets tabPacket Waterfall: scrolling log of all mesh traffic with timestamps, direction, source, destination, TTL, and route. Click a packet to inspect. Packet Inspector: detailed breakdown of the selected packet with AES key entry for manual decryption.
Sensors tabSensor Dashboard: circular gauges with needles and min/max tracking, matplotlib line charts, pause/resume, clear, and CSV export. See Section 41.
Logic tabLogic Builder: PLC-style visual automation canvas. Drag-and-drop blocks, wire connections, live value display on wires, rule management, deploy/undeploy. See Section 22.

Additional GUI integrations above the tabs:

22. Logic Builder — Visual Automation for the Gateway GUI

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

The Logic Builder is a PLC-style visual automation system built into the PC gateway GUI. It lets you create cross-node automation rules by dragging and connecting blocks on a canvas — like ladder logic or Scratch, but for your mesh network. Rules evaluate sensor data received from the mesh and fire actions (relay control, messages) back through the mesh.

Two levels of automation: The Logic Builder creates gateway-evaluated rules that run in the Python GUI process. For simple rules (one sensor → one compare → one relay), you can also deploy to the node as a firmware setpoint that runs autonomously even without the gateway — see Section 15: Cross-Node Setpoint Rules.

Opening the Logic Builder

Click the Logic tab at the bottom of the gateway GUI (alongside Packets and Sensors tabs). The automation canvas appears below the mesh topology map. Rules continue evaluating in the background regardless of which tab is active.

Creating a Rule

  1. Click [+ New] in the toolbar to create a new rule. Give it a descriptive name (e.g., “Wind alarm”, “Gate monitor”).
  2. Use the [+ Input], [+ Transform], [+ Condition], and [+ Action] dropdown buttons to add blocks to the canvas.
  3. Double-click any block to configure it (choose node ID, pin number, operator, etc.).
  4. Connect blocks by clicking an output port (right side, coloured dot) then clicking a compatible input port (left side) on another block.
  5. Set the Check every interval and Min action gap (see below).
  6. Tick Enabled to activate the rule.

Block Types

Input Blocks (cyan border)

Input blocks read data from the mesh or provide constant values.

BlockWhat It DoesConfigurationOutput
Sensor Read Reads the latest sensor value for a specific pin on a specific node. Uses SDATA data received by the gateway. Node ID: The 4-character hex ID of the node (e.g., A1B2). Pin: GPIO pin number. Label: Descriptive name (e.g., “Wind Sensor”). A number (the raw sensor reading, 0–4095 for analog).
Beacon Detect Detects an iBeacon tag by UUID or MAC address. Outputs true/false based on proximity (RSSI threshold). See Section 38: iBeacon Scanner. Beacon ID: UUID or MAC address. Name: Human label. RSSI Threshold: Minimum signal strength (e.g., -70 dBm). detected (bool), rssi (number).
Constant Outputs a fixed number. Use as a threshold for comparisons. Value: Any number (e.g., 2000, 15.5). A number.
Schedule Time-based trigger. Outputs true during configured hours/days. Uses the gateway PC’s clock. Supports overnight schedules (e.g., 22:00–06:00). On time: HH:MM (24hr). Off time: HH:MM. Days: Mon,Tue,Wed,Thu,Fri,Sat,Sun (comma-separated, default all). active (bool) — true during scheduled hours on selected days.

Transform Blocks (green border)

Transform blocks convert raw sensor values into meaningful units.

BlockWhat It DoesConfigurationFormula
Scale Multiplies a value by a factor and adds an offset. Use this to convert raw ADC counts into real-world units. Factor: Multiply value by this number. Offset: Add this after multiplying. Unit: Label for the result (e.g., “m/s”, “°C”). output = (input × factor) + offset
Moving Average Smooths a noisy signal by averaging the last N samples. Window: Number of samples to average (e.g., 5). output = average of last N inputs
Delta Rate Calculates the rate of change per second. Useful for converting a growing counter into a speed value. None — automatic. output = (current − previous) / elapsed_seconds

Example — Wind speed from pulse counter: A Sensor Read block reads a pulse-counter pin. The Delta Rate block converts the growing count into pulses-per-second. A Scale block then multiplies by the anemometer's calibration factor (e.g., ×0.0278) to get metres per second. The canvas shows live values on each wire so you can verify the maths.

Condition Blocks (yellow border)

Condition blocks evaluate true/false logic to decide when actions should fire.

BlockWhat It DoesConfiguration
Compare Compares two numbers and outputs true or false. Operator (choose one):
>Greater than
<Less than
=Equal to
!=Not equal to
>=Greater than or equal to
<=Less than or equal to
Input a is compared against input b. Example: if a > b, output is true.
AND Gate Outputs true only when all connected inputs are true. Has up to 4 inputs (in1–in4). None — connect 2–4 boolean inputs.
OR Gate Outputs true when any connected input is true. Has up to 4 inputs (in1–in4). None — connect 2–4 boolean inputs.
NOT Gate Inverts a boolean: true becomes false, false becomes true. None — one boolean input.
Debounce Only outputs true after the input has been true for a sustained period. Filters out brief spikes or noise. Hold (seconds): How long the input must stay true before the output becomes true (e.g., 5.0).
Monostable One-shot retriggerable timer. When the trigger input goes true, the output goes true and stays true for the configured hold time. If the trigger fires again before the timer expires, the timer restarts. Useful for beacon-based gate control: “keep the gate open while the beacon is nearby, close 60 seconds after it leaves.” Hold time (seconds): How long the output stays true after the last trigger (e.g., 60.0).
Latch (SR Flip-Flop) Once “set” goes true, the output stays true until “reset” goes true. Like a physical toggle switch. None — connect a set signal and a reset signal.
Geofence GPS boundary detection. Checks if a node’s GPS position is inside or outside a circle you define. Outputs distance in metres. Node ID: which GPS node to track. Centre Lat/Lon: fence centre coordinates. Radius: metres (default 500). inside (bool) — true if node is within the radius. distance (number) — metres from centre.

Action Blocks (red border)

Action blocks send commands to the mesh when their trigger input transitions from false to true (rising edge).

BlockWhat It DoesConfiguration
Set Relay Turns a relay (digital output) on or off on a target node. Node ID: Target node. Pin: GPIO pin number. Action: HIGH (on) or LOW (off).
Pulse Relay Briefly activates a relay for a set duration, then turns it off. Good for solenoids, buzzers, or momentary triggers. Node ID: Target node. Pin: GPIO pin number. Duration (ms): How long to hold the relay on.
Send Broadcast Sends a text message to all nodes on the mesh (address FFFF). Message (true): Text sent when trigger becomes true (e.g., “Gate open”). Message (false): Optional text sent when trigger becomes false (e.g., “Gate closed”).
Send Direct Sends a text message to a specific node. Node ID: Target node. Message (true): Text when true. Message (false): Optional text when false.
Telegram Output Stores the incoming value for Telegram bot queries. When a user sends the rule's #name command in Telegram (e.g., #getwind), the bot reads this block's last value and replies with a formatted message. Does not fire a LoRa action — this is a read-only display endpoint. Label: Human name (e.g., “Wind Speed”). Unit: Engineering unit (e.g., “m/s”). Format: Template string (default: {label}: {value} {unit}).

Connecting Blocks (Wires)

Blocks have ports — small coloured dots on the left (inputs) and right (outputs) of each block:

To connect: click an output port on one block, then click a compatible input port on another block. A smooth Bezier curve wire appears between them. Live values are displayed on wires during evaluation (e.g., “23.4”, “True”).

Toolbar Settings

SettingWhat It MeansDefault
Check every How often the rule is evaluated (reads sensor data, runs all blocks, checks conditions). Lower values mean faster response but more processing. 5 seconds
Min action gap Minimum time between action firings for the same rule. Prevents rapid relay toggling when a sensor value oscillates around a threshold (e.g., temperature bouncing between 19°C and 21°C around a 20°C threshold). 10 seconds
Enabled When ticked, the rule is actively evaluated. Untick to pause a rule without deleting it. Enabled

Canvas Interactions

ActionHow
Add a blockClick one of the category dropdown buttons: [+ Input], [+ Transform], [+ Condition], [+ Action]
Move a blockClick and drag the block body. Blocks snap to a 10-pixel grid.
Configure a blockDouble-click the block to open its settings dialog.
Connect blocksClick an output port (right side), then click an input port (left side) on another block.
Delete a blockRight-click the block → Delete from the context menu, or select and press the Delete key. All connected wires are also removed.
Pan the canvasMiddle-click and drag, or scroll.

Safety & Anti-Flood Features

With many rules across many nodes, actions could flood the LoRa channel. The Logic Builder includes multiple layers of protection:

Deploy to Node (Firmware Setpoints)

For rules that match the firmware setpoint format, you can push the rule directly to a node so it runs autonomously without the gateway. Click the [Deploy] button to attempt this. Deployed rules use zero radio for monitoring — they only transmit when the condition triggers.

A rule can be deployed as a firmware setpoint if it contains:

  1. One Sensor Read block
  2. One Compare block (any operator: >, <, =, ≥, ≤, ≠)
  3. One action: Set Relay or Broadcast Message
  4. Optionally one Scale block (factor + offset applied before comparison)
  5. Optionally one Debounce block (hold time before firing)
  6. One Constant block (the threshold value)

The gateway builds an expanded SETPOINT command with optional suffixes:

SETPOINT,<pin>,<op>,<threshold>,<target>,<relayPin>,<action>[,SCALE,<f>,<o>][,DEBOUNCE,<ms>]
SETPOINT,<pin>,<op>,<threshold>,MSG,<msgTrue>[,<msgFalse>][,SCALE,<f>,<o>][,DEBOUNCE,<ms>]

Smart Deploy

The [Deploy] button analyses your rule chain and picks the best deploy strategy automatically:

PatternDeploy ModeHow It Works
Sensor → Compare → Relay/BroadcastFirmware setpointSends a SETPOINT command to the node. Runs autonomously with zero radio until the condition triggers.
Beacon Detect → RelayFirmware beacon ruleSends a BEACON,ADD command. Node scans for BLE beacons independently.
Beacon Detect → Monostable → RelayFirmware beacon + REVERTThe Monostable's hold time becomes a REVERT timer: relay auto-reverts after the beacon leaves range for that duration.
Beacon Detect → Broadcast MsgFirmware beacon + MSGNode broadcasts a mesh message when the beacon is detected.
Anything with Telegram Output or MonostableGUI engineRuns in the Python gateway process. No firmware deploy needed — the engine evaluates the rule and the Telegram bot reads the output.

Node Management (Right-Click Menu)

Right-click any node card in the Discovered Nodes panel to access management commands:

ActionWhat It Does
Query RulesSends BEACON,LIST and SETPOINT,LIST to the node and shows deployed rules in a popup.
Set NameAssign a friendly name to the node (e.g., “Farm1”). Saved between sessions. Used by the Telegram bot for human-readable responses.
Clear RulesSends BEACON,CLEAR and SETPOINT,CLEAR to remove all deployed rules from the node.

Limitation: Rules using Moving Average, Delta Rate, AND/OR/NOT gates, Latch, Pulse Relay, or Direct Message blocks cannot deploy to node — they can only run as gateway-evaluated rules. Each node supports a maximum of 4 setpoints (RAM only, lost on reboot).

Example Rules

Example 1: Wind Speed Alarm

Sound an alarm relay on a base-station node when wind speed exceeds 15 m/s.

[Sensor Read] ──→ [Scale] ──→ [Compare] ──→ [Set Relay]
  A1B2:pin5          ×0.0278     > 15.0          C3D4:pin2=HIGH
  "Anemometer"       m/s         ↑
                              [Constant: 15]

Sensor Read gets the raw pulse count from node A1B2 pin 5. Scale converts pulses to m/s. Compare checks if the speed exceeds the constant 15. Set Relay activates pin 2 on node C3D4.

Example 2: Gate Monitor with Messages

Broadcast “Gate open” or “Gate closed” based on a digital input.

[Sensor Read] ──→ [Compare] ──→ [Send Broadcast]
  F1E2:pin5           = 1           "Gate open" / "Gate closed"
  "Gate Sensor"       ↑
                  [Constant: 1]

Sensor Read gets the digital value (0 or 1) from node F1E2 pin 5. Compare checks if it equals 1. Send Broadcast sends “Gate open” when true, “Gate closed” when the trigger goes false.

Example 3: Frost Protection with Debounce

Only activate a heater if temperature stays below threshold for 30 seconds (avoids reacting to brief cold gusts).

[Sensor Read] ──→ [Scale] ──→ [Compare] ──→ [Debounce] ──→ [Set Relay]
  0010:pin34         ×0.1-40      < 3.0           30 sec          0010:pin4=HIGH
  "Temp Sensor"      °C           ↑               hold            "Heater"
                              [Constant: 3.0]

Scale converts raw ADC (0–4095) to degrees Celsius. Compare checks if temp is below 3°C. Debounce waits 30 seconds of continuous “true” before allowing the Set Relay to fire.

Persistence

All rules are automatically saved to gateway/rules.json whenever you make changes. Rules are reloaded when you restart the gateway GUI. The file stores block positions, configurations, wire connections, and rule settings.

Event Log

The bottom of the Logic Builder panel shows a one-line event log with timestamped status messages:

23. WiFi ESP Gateway — No PC Required

The WiFi gateway firmware turns any ESP32 board into a standalone internet bridge. Unlike the PC-based gateway, it needs no computer — just power. Flash it, configure once via the Android app over BLE, mount it outside with a solar panel, and it runs forever.

Old method:  LoRa nodes → ESP repeater → USB cable → PC → WebSocket → Hub
New method:  LoRa nodes → ESP gateway → WiFi → WebSocket → Hub

Supported Boards

Heltec V2 (~£15)
ESP32 + SX1276 + OLED
Classic board, widely available
Heltec V3 (~£18)
ESP32-S3 + SX1262 + OLED
Better radio sensitivity
Heltec V4 (~£20) ★ Best
ESP32-S3 + SX1262 + 28dBm PA
Solar input, 16MB flash
XIAO ESP32S3 (~£10) ★ Cheapest
+ Wio-SX1262 kit, tiny 10×21mm
Perfect for solar tree node

Step 1 — Flash the Gateway Firmware

Use the web flasher, select your board under WiFi Gateway, and click Flash.

Step 2 — Configure via Android App

Open the Android app, tap Connect to pair with the gateway over BLE, then send these commands from the terminal/config screen:

WIFI,YourSSID,YourPassword       Set WiFi network
HUBURL,ws://hub.example.com:9000 Hub WebSocket address
HUBKEY,your-secret-key           Hub auth key (must match hub)
GWNAME,Ridge Top Gateway         Friendly name shown on hub
GWLOC,51.5074,-3.1791            Lat/lon (optional, for map)
GWANTENNA,1                      0=indoor  1=external  2=rooftop
GWHEIGHT,10                       Antenna height in metres (0-500)
SAVE                              Write to flash and reboot

The gateway reboots, connects to WiFi, and begins bridging. The OLED shows the WiFi IP and hub connection status.

GWSTATUS — Check Connection

GWSTATUS   Returns: name, WiFi IP, hub connected/disconnected, RX/TX counts, HEIGHT

Gateway auth includes location: When the gateway connects to the hub, the auth message includes lat, lon, antenna type, and height. This enables the Live Gateway Map on the hub.

Solar Deployment Guide

Total BOM for a self-powered tree-mounted gateway:

PartCostNotes
XIAO ESP32S3 + Wio-SX1262~£10From The Pi Hut
6V 2W mini solar panel~£5Point south, 30° tilt
TP4056 LiPo charger + 18650 cell~£4Powers overnight
IP65 weatherproof box (75×58mm)~£3Cable gland for antenna
868MHz fiberglass antenna (3dBi)~£6SMA-female pigtail
Total~£28Zero running cost

Power draw: ESP32S3 active ~160mA @ 3.3V = 0.53W. A 2W panel fully charges a 2000mAh cell in ~4 hours of sun. Runs 15+ hours overnight on the cell alone.

Hub Setup (Linux / Tailscale)

The hub runs on any always-on Linux machine (Debian, Ubuntu, Raspberry Pi, etc). The automated setup script handles everything — Python, systemd service, and Tailscale Funnel:

# Download and run the setup script (Debian/Ubuntu)
curl -fsSL https://raw.githubusercontent.com/MichaelGaylor/Tiggy_Lora_Mesh/main/gateway/setup-hub-linux.sh -o setup.sh
bash setup.sh

The script will:

  1. Install Python and aiohttp (the hub's HTTP/WebSocket server library)
  2. Optionally install Tailscale and connect to your account
  3. Create a systemd service that auto-starts on boot (tiggyopenmesh-hub.service)
  4. Optionally enable Tailscale Funnel for a public wss:// URL

Manual Setup (without the script)

If you prefer to set things up manually or the script doesn't suit your distro:

# 1. Install dependencies
sudo apt-get install -y python3 python3-pip
pip3 install --break-system-packages aiohttp

# 2. Copy gateway files to your home directory
#    You need: gateway_hub.py, gateway_hub_gui.py, gui_common.py
#    And: web/map.html (create a web/ folder next to gateway_hub.py)
cp gateway_hub.py gateway_hub_gui.py gui_common.py ~/
mkdir -p ~/web && cp web/map.html ~/web/

# 3. Test it works
python3 ~/gateway_hub.py --listen 8443
# Open http://localhost:8443/ in a browser — you should see the map

# 4. Create systemd service for auto-start on boot
sudo tee /etc/systemd/system/tiggyopenmesh-hub.service > /dev/null <<EOF
[Unit]
Description=TiggyOpenMesh Gateway Hub
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$USER
WorkingDirectory=$HOME
ExecStart=/usr/bin/python3 $HOME/gateway_hub.py --listen 8443
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable tiggyopenmesh-hub
sudo systemctl start tiggyopenmesh-hub

Tailscale Funnel port: Funnel only supports ports 443, 8443, and 10000. The hub runs on port 8443 by default. Do not change this to 9000 or Funnel will not work.

To enable Funnel manually after setup:

# Enable Tailscale Funnel on port 8443 (runs persistently in background)
sudo tailscale funnel --bg 8443

# Check the public URL assigned to your machine
tailscale funnel status
# → https://your-machine.tail1234.ts.net

Configure each gateway via BLE with:

HUBURL,wss://your-machine.tail1234.ts.net
SAVE

No port forwarding, no dynamic DNS, free SSL certificate. The hub service and Funnel both survive reboots automatically.

Accessing the Map and GUI

The headless hub serves the Live Gateway Map as a web page on the same port as the WebSocket relay. Multiple users can open it simultaneously — it's a read-only page that refreshes every 10 seconds:

From whereURL
On the hub machinehttp://localhost:8443/
Same LAN / Tailscale networkhttp://<hub-ip>:8443/
Anywhere (via Tailscale Funnel)https://your-machine.tail1234.ts.net/

If your Linux box has a desktop environment (XFCE, GNOME, KDE, etc), you can create desktop shortcuts for quick access:

Desktop shortcut: Open Map in browser

cat > ~/Desktop/TiggyOpenMesh-Map.desktop <<'EOF'
[Desktop Entry]
Version=1.0
Type=Application
Name=TiggyOpenMesh Map
Comment=Open the live gateway coverage map in your browser
Exec=xdg-open http://localhost:8443/
Icon=applications-internet
Terminal=false
Categories=Network;
EOF
chmod +x ~/Desktop/TiggyOpenMesh-Map.desktop

Desktop shortcut: GUI Dashboard

The GUI version (gateway_hub_gui.py) shows a full desktop dashboard with live packet flow visualisation, connected gateway table, and statistics. It requires customtkinter:

pip3 install --break-system-packages customtkinter

Since the GUI and headless hub both use the same port, the launcher script stops the headless service before starting the GUI, then restarts it when the GUI window is closed:

# Create launcher script
cat > ~/start_hub_gui.sh <<'EOF'
#!/bin/bash
sudo systemctl stop tiggyopenmesh-hub 2>/dev/null
sleep 1
cd ~/
python3 ~/gateway_hub_gui.py
sudo systemctl start tiggyopenmesh-hub 2>/dev/null
EOF
chmod +x ~/start_hub_gui.sh

# Create desktop shortcut
cat > ~/Desktop/TiggyOpenMesh-Hub.desktop <<'EOF'
[Desktop Entry]
Version=1.0
Type=Application
Name=TiggyOpenMesh Hub
Comment=Gateway Hub Dashboard - stops headless service, opens GUI
Exec=bash ~/start_hub_gui.sh
Icon=network-workgroup
Terminal=false
Categories=Network;
EOF
chmod +x ~/Desktop/TiggyOpenMesh-Hub.desktop

Headless vs GUI: The headless service (gateway_hub.py) runs 24/7 via systemd, relaying packets and serving the browser map. The GUI (gateway_hub_gui.py) is optional — use it when you want a desktop dashboard. Both serve the same map and relay the same packets. You don't need a display to run the hub.

Hub Management

# View live logs
sudo journalctl -u tiggyopenmesh-hub -f

# Restart hub (e.g. after updating gateway_hub.py)
sudo systemctl restart tiggyopenmesh-hub

# Check status
sudo systemctl status tiggyopenmesh-hub

# Update hub files from the repo
cd ~/
curl -fsSL https://raw.githubusercontent.com/MichaelGaylor/Tiggy_Lora_Mesh/main/gateway/gateway_hub.py -o gateway_hub.py
curl -fsSL https://raw.githubusercontent.com/MichaelGaylor/Tiggy_Lora_Mesh/main/gateway/web/map.html -o web/map.html
sudo systemctl restart tiggyopenmesh-hub

24. Solar / Low-Power Repeater Mode

Remote repeater nodes on hilltops or rooftops often need to run indefinitely from a small solar panel and an 18650 battery. Solar mode dramatically reduces power consumption by putting the ESP32-S3 into light sleep between radio events, while keeping the radio in continuous RX so no packets are lost.

SX1262 only: Solar mode is available on Heltec V3 and Heltec V4 boards (ESP32-S3 + SX1262 radio). The LoRa32 (SX1276 / classic ESP32) does not support this mode.

Power Budget

ComponentNormal ModeSolar Mode
ESP32-S3 (CPU)~80 mA (active)~0.8 mA (light sleep, wakes on DIO1)
SX1262 (Radio)~10 mA (continuous RX)~10 mA (continuous RX — no data loss)
OLED Display~15 mA0 mA (powered off via Vext)
BLE~15 mA0 mA (deinitialised)
Total average~120 mA~11 mA

With a 3000 mAh 18650: ~11 days on battery alone.
With a 1W solar panel: indefinite operation.

How It Works

Activation

Via Serial (USB)

# Enable solar mode (persists across reboots)
SOLAR ON

# Disable solar mode and restore normal operation
SOLAR OFF

# Check status (works in solar mode)
STATUS

Via BLE (Android App)

# Enable solar mode (SX1262 boards only)
POWER,SOLAR     → OK,POWER,SOLAR (BLE then disconnects!)

# Disable solar mode
POWER,NORMAL    → OK,POWER,NORMAL

BLE disabled in solar mode: Sending POWER,SOLAR via BLE immediately deinitialises Bluetooth after sending the response. The phone app disconnects. To exit solar mode, use serial (SOLAR OFF) or press the PRG button.

Via Android App Settings Screen

The app's Settings tab shows a Power Mode card for SX1262-based repeater boards. It displays the current mode from the STATUS response and provides a toggle with a confirmation dialog.

Solar mode is persisted to EEPROM — the node will boot directly into solar mode after a power cycle. No need to re-send the command.

Behaviour in Solar Mode

FeatureBehaviour
OLED displayOff. Press the PRG button to show status for 10 seconds.
BLE (phone app)Disabled — phone app cannot connect. Deinitialised to save ~15 mA.
LEDSingle brief blink every heartbeat (60s) as sign-of-life.
Serial commandsAll serial commands work. UART wakeup is enabled — sending any character wakes from light sleep.
Mesh routingFully functional — packets are received, forwarded, and routed normally. No data loss.
Gateway modeStill works if previously enabled — PKT, forwarding over serial continues.
GPIO relaysFully operational — CMD processing, timers, setpoints, and sensor polling all continue.

Recommended Hardware Setup

Deployment tip: Flash the node, set the ID and AES key, send SOLAR ON, then disconnect USB. The node is now a fully autonomous solar-powered mesh repeater. Reconnect USB and send SOLAR OFF when you need to reconfigure.

25. Message Delivery Tracking

Every message sent through the mesh now has end-to-end delivery confirmation. The sender knows exactly whether the message was transmitted, delivered, or failed — with visual status indicators on both the T-Deck and the Android app.

Delivery States

SENDING
Queued for TX
SENT
Radio transmitted
DELIVERED
ACK received
3s timeout, no ACK
FAILED
No delivery receipt

How It Works

  1. Sender transmits message — state set to SENDING
  2. Radio confirms transmission complete — firmware sends SENT,<mid> to BLE, state becomes SENT
  3. Recipient decrypts the message successfully and sends back a directed plain ACK,<from>,<mid> packet to the original sender
  4. Sender receives the ACK — state becomes DELIVERED
  5. If no ACK arrives within 3 seconds, state becomes FAILED (the message may still have been received — the ACK could have been lost)

Visual Indicators

StateT-Deck Chat BubbleAndroid App
SENDINGOrange clock icon / "..." next to bubbleOrange "Sending..." text
SENTSingle checkmark ()Single grey checkmark
DELIVEREDDouble checkmark (✓✓)Double green checkmarks
FAILEDRed cross ()Red "Failed" with retry option

BLE Protocol

DirectionFormatMeaning
Node → AppSENT,XYZWMessage XYZW has been transmitted over LoRa
Node → AppACK,0002,XYZWNode 0002 confirmed receipt of message XYZW

Broadcast messages: Messages sent to FFFF (broadcast) skip delivery tracking — there's no single recipient to send an ACK. The state goes directly from SENDING → SENT.

FAILED does not mean undelivered: A FAILED state means the sender didn't receive an ACK within 3 seconds. The message may have been delivered but the ACK packet was lost in transit. In lossy environments, consider re-sending important messages.

26. Spreading Factor Change Protocol

LoRa Spreading Factor (SF) controls the trade-off between range and speed. Changing SF requires all nodes to switch simultaneously — a node on SF7 cannot hear one on SF12. The two-phase CFG protocol ensures safe, coordinated changes across the entire mesh.

SF Values

SFRangeSpeedAirtime (50 bytes)Use Case
SF7ShortestFastest~56 msDense urban, many nodes close together
SF9MediumMedium~205 msDefault — balanced for most deployments
SF12LongestSlowest~1.3 sMax range, sparse rural, mountain-to-valley

Two-Phase Change Protocol

Phase 1: Propose & Collect ACKs

Initiator sends
SF,7 via BLE
Node broadcasts
CFG,SF,7,A1B2,C3D4
Peers respond
CFGACK,SF,7,A1B2,0010
Initiator collects
ACKs from mesh

Phase 2: Commit

Initiator sends
SFGO,7 via BLE
Node broadcasts
CFGGO,SF,7,A1B2,C3D4
All nodes wait
2s delay
All switch to SF7
simultaneously

Auth Tags

CFG and CFGGO packets include an 8-hex-char auth tag computed using AES-128-GCM:

authTag = truncate(GCM_tag(key, IV=SHA(changeId), AAD=changeId), 4 bytes)
// Deterministic IV derived from changeId for reproducible verification

Receiving nodes verify the tag before accepting any CFG or CFGGO packet. This prevents:

Packet Formats

PacketFormatFields
CFG (propose)CFG,SF,7,A1B2,C3D4type, value, changeId (random 4-hex), authTag
CFGACK (accept)CFGACK,SF,7,A1B2,0010type, value, changeId, respondingNodeId
CFGGO (commit)CFGGO,SF,7,A1B2,C3D4type, value, changeId, authTag

BLE Commands

CommandDescriptionResponse
SF,7Start Phase 1 — broadcast CFG proposal for SF7OK,SF,7 + CFGSTART,SF,7,A1B2 + CFGACK notifications
SFGO,7Start Phase 2 — broadcast CFGGO commit for SF7OK,SFGO,7 then radio reconfigures after 2s delay

Timing & Safety

Critical: If only some nodes receive the CFGGO, the mesh splits — nodes on different SFs cannot hear each other. Always check that CFGACK responses come back from all expected nodes before sending CFGGO.

27. Node ID Collision Detection

Node IDs are 4-hex-char values (0001–FFFE) — 65534 possible addresses. As networks grow, or when default IDs are used, collisions become likely. Two nodes with the same ID cause routing chaos — messages get misdelivered, heartbeats overwrite routes, and the mesh becomes unreliable.

Detection Mechanism

Collision detection piggybacks on the existing heartbeat system (no new packets, no extra airtime):

  1. Every node broadcasts HB,<nodeID> every ~30 seconds
  2. In processPacket(), if the heartbeat's node ID matches our own localID, a collision is detected
  3. The onIdConflict callback fires with the conflicting ID and RSSI
  4. The heartbeat is not processed as a normal peer (not added to routing table)
// In MeshCore::processPacket() — heartbeat handler
if (hbFrom == String(localID)) {
    idConflictDetected = true;
    if (onIdConflict) onIdConflict(hbFrom, lastRSSI);
    return;  // Don't add ourselves as a peer
}

Alert Behaviour

DeviceAlert
Repeater (OLED)Full-screen warning: !! ID CONFLICT !! with conflicting ID and RSSI. Stays on screen until resolved.
Repeater (BLE)Sends CONFLICT,<id>,<rssi> to connected phone
T-Deck10-second notification banner: "ID CONFLICT! XXXX in use!"
Android AppSystem chat message: "WARNING: ID collision! Node XXXX is used by another device."
!! ID
CONFLICT !!
 
ID 0010 RSSI:-65
Change via app/serial

First-Boot Conflict Scan

Both repeaters and T-Deck devices perform an automatic conflict scan on first boot:

StepRepeaterT-Deck
1. Generate IDRandom 4-hex ID (0001-FFFE)Random 4-hex ID, pre-filled in wizard
2. Scan10s blocking scan with OLED countdown10s background scan with progress bar
3. Conflict?Regenerate random ID, restart scanRegenerate, restart scan
4. ClearSave to EEPROM, continue bootShow "No conflicts!", user can accept or override

No auto-fix for running nodes: If a collision is detected on a node that's already deployed in the field, the firmware does not auto-change the ID. It alerts the user so they can manually choose a new ID via the app, serial, or keyboard. This avoids surprise routing changes on unmanned repeaters.

28. Display Auto-Sleep (T-Deck Plus)

The T-Deck's 2.8" TFT backlight draws significant power. Display auto-sleep turns off the backlight after a configurable idle period, extending battery life for portable use.

Behaviour

Idle TimeAction
< 50% of timeoutScreen at full brightness
50% – 100% of timeoutScreen dims to 25% brightness (warning)
100% of timeoutBacklight turns off completely
Any key / trackball inputInstant wake to full brightness

Configuration

Set via the T-Deck Settings menu → Display Timeout. Cycles through:

Off (0)
30s
60s (default)
120s
300s

Special Cases

Power Impact

StateBacklight Current
Full brightness (100%)~40 mA
Dimmed (25%)~10 mA
Off (sleeping)0 mA

With a 60-second timeout and typical usage patterns, display sleep can reduce average power consumption by 30–50%, significantly extending battery life.

Repeater nodes do not have display sleep — their OLED draws only ~15 mA and auto-refreshes every 5s. For repeater power savings, use Solar Mode which disables the OLED entirely.

29. Live Gateway Coverage Map

The hub server hosts a live web map showing registered gateways, their online/offline status, and estimated coverage areas. It is an optional planning and monitoring tool — the radio mesh itself works without it.

Zero internet dependency for the mesh: nodes talk directly over LoRa with no internet, hub, or cloud service required. The map is only an optional add-on that provides visibility when internet is available.

Accessing the Map

The hub serves both WebSocket and HTTP on the same port — no extra port needed:

# If hub runs on port 9000:
Map:       http://your-hub-address:9000/
API:       http://your-hub-address:9000/api/gateways
WebSocket: ws://your-hub-address:9000/       (gateways connect here)

# Tailscale Funnel exposes ONE port — everything works through it:
https://your-machine.tail1234.ts.net/

Map Features

FeatureDescription
Online markersGreen pulsing dots for connected gateways
Offline markersRed static dots for disconnected gateways (remembered from registry)
Coverage ringsSemi-transparent circles showing estimated LoRa coverage per gateway
Click popupsGateway name, antenna type, height, uptime, packet count
Auto-fitMap auto-centers to show all gateways on first load
Auto-refreshUpdates every 10 seconds via /api/gateways

Coverage Ring Calculations

Approximate coverage radius based on antenna type and height (tuned for SF9 / 868 MHz):

Antenna TypeCodeBase RadiusFormulaExample (10m height)
Indoor0800 m800 × √height~2.5 km
External12,000 m2000 × √height~6.3 km
Rooftop24,000 m4000 × √height~12.6 km

These are rough estimates. Real-world coverage depends on terrain, buildings, and antenna quality.

Gateway Configuration for the Map

Each ESP gateway must have location configured via BLE before it appears on the map:

GWLOC,51.5074,-3.1791     Latitude and longitude
GWANTENNA,2                0=indoor  1=external  2=rooftop
GWHEIGHT,10                Antenna height in metres
SAVE

Gateways with lat=0, lon=0 (not configured) are hidden from the map.

HTTP API

EndpointMethodDescription
/GETServes map.html
/api/gatewaysGETJSON array of all gateways with status

API Response Format

[{
  "id":            "AA:BB:CC:DD:EE:FF",
  "name":          "Base Camp",
  "lat":           51.5074,
  "lon":           -3.1791,
  "antenna_type":  2,
  "antenna_height":10.0,
  "online":        true,
  "uptime":        3600,
  "packets":       150
}]

Gateway Registry Persistence

The hub stores gateway locations in gateway/gateways.json. This means:

Hub Auth Message (Updated)

The gateway firmware now includes location data in the WebSocket auth message:

{
  "type":    "auth",
  "key":     "MySecretKey",
  "id":      "AA:BB:CC:DD:EE:FF",  // WiFi MAC address
  "name":    "Base Camp",
  "lat":     51.5074,
  "lon":     -3.1791,
  "antenna": 2,                  // 0=indoor, 1=external, 2=rooftop
  "height":  10.0                 // metres
}

Backward compatible: Old gateways without location fields still connect and relay packets normally. They just don't appear on the map. Old hubs ignore the new fields (Python dict.get() defaults).

30. Telegram Alerts (Android App)

The Android app can forward group mesh messages and SOS emergencies to a Telegram chat via a bot. This lets family members, a base-camp coordinator, or anyone with Telegram receive real-time alerts — even if they are miles away with no LoRa hardware.

Privacy model: decryption happens entirely on your phone. The hub server never sees plaintext. Your AES mesh key never leaves your device.

How it works

What gets forwarded

Message typeForwarded?Format in Telegram
Group broadcast✅ Yes📡 [NODE1] Hello everyone
SOS with GPS✅ Yes (priority)🚨 SOS EMERGENCY from NODE1 — maps link
SOS no GPS fix✅ Yes (priority)🚨 SOS EMERGENCY from NODE1 (no GPS fix)
Direct message❌ NoStays private between two nodes
Position ping❌ NoInternal routing packet

Step-by-step setup

Step 1 — Create a Telegram bot

  1. Open Telegram and search for @BotFather.
  2. Send /newbot and follow the prompts to name your bot.
  3. BotFather will reply with a bot token that looks like:
    123456789:ABCdefGHIjklMNOpqrSTUvwxYZ
    Copy this — you will need it in the app.

Step 2 — Get your Chat ID

  1. Add your bot to a Telegram group, or start a private chat with it and send any message.
  2. In a browser, open:
    https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
  3. Look for "chat":{"id": in the JSON response. That number is your Chat ID.
  4. For a group it will be a negative number, e.g. -1001234567890.

Step 3 — Configure in the app

  1. Open the Android app → Settings tab.
  2. Toggle Telegram Alerts on.
  3. Enter your bot token and chat ID.
  4. Tap Save then Test — a confirmation message should appear in your Telegram chat immediately.
Multiple groups, one hub: each group configures their own bot in their own copy of the app. The hub server is never involved — it only relays encrypted packets.

T-Deck and other standalone devices

Devices running the firmware directly (T-Deck, LoRa32, Heltec) are completely unaffected by the Telegram feature. They send and receive mesh messages as normal. If a group member with an Android phone happens to receive the same message and has mobile signal, the forwarding happens transparently — the T-Deck user never knows and nothing changes on their device.

31. Colour Theme Reference

Consistent colour palette used across the TFT display, OLED, and Android app.

TFT Display Colours (RGB565)

0x0000 Background (Pure Black)
0x10A2 Panel
0x0926 Header
0x07FF Accent (Cyan)
0xFFFF Text (White)
0x8410 Dim (Mid Grey)
0x4208 Faint (Dark Grey)
0x07E0 Good (Green)
0xFD20 Warn (Orange)
0xF800 Bad (Red)
0x1126 Bubble Incoming (Teal)
0x0320 Bubble Outgoing (Green)
0x8000 Bubble SOS (Dark Red)
0x2104 Selected Item
0xFFE0 Cursor (Yellow)

Android App Colours

MeshCyan Primary accent
MeshGreen Success / ON
MeshOrange Warnings
MeshRed Errors / SOS
MeshBlue Incoming messages
MeshGrey Secondary text
MeshDarkBg Background
MeshSurface Card surfaces

Signal Strength Colour Coding

RSSI RangeColourMeaning
> -80 dBmGreenExcellent signal
-80 to -100 dBmOrangeModerate signal
< -100 dBmRedWeak signal

32. IO Expansion Board (UART2 Daisy-Chain)

You can daisy-chain a second ESP32 to any repeater node as an IO expansion board, adding up to 16 additional sensor inputs and relay outputs. The expansion board connects via UART (serial) and has no LoRa radio — it is a simple IO slave controlled by the main mesh node.

Why use an expansion board? Repeater nodes have limited free GPIO (typically 3–6 pins shared between sensors and relays). An expansion board gives you 6+ extra analog inputs and 6+ relay outputs using a cheap ESP32 devkit (£3–5). The main node handles all mesh communication; the expansion board just reads sensors and sets pins.

Wiring

Connect 3 wires between the main node and the expansion ESP32:

Main NodeExpansion Board
UART2 TXRX (GPIO 3 or Serial RX)
UART2 RXTX (GPIO 1 or Serial TX)
GNDGND

UART2 Pins per Board

Main Node BoardTX PinRX Pin
Heltec V3 / V4GPIO 2GPIO 3
XIAO ESP32S3GPIO 2GPIO 4
LoRa32 T3GPIO 17GPIO 16
Heltec V2GPIO 17GPIO 13

Enabling

# Enable IO expansion (opens UART2 at 115200 baud)
EXPAND ON

# Disable IO expansion
EXPAND OFF

Virtual Pins

Once enabled, the expansion board's sensors appear as virtual pins 100–115 on the main node. You can use these in setpoint rules, auto-poll SDATA, and the Logic Builder just like regular GPIO pins.

# Setpoint using expansion board sensor on virtual pin 100
SETPOINT,100,GT,2000,0010,4,1

# The main node polls the expansion board every 2 seconds
# and caches the values. readSensorPin(100) returns the cached value.

Serial Protocol

The main node and expansion board communicate over a simple ASCII line protocol at 115200 baud:

Main SendsExpansion RespondsPurpose
?SS,34:2048,36:1024,...Read all sensors
?S,34S,34:2048Read single sensor
!R,4,1R,OKSet relay pin 4 HIGH
?PP,5:1234,...Read pulse counters
?II,ESP32,6S,6RIdentify board (6 sensors, 6 relays)
!P,5P,OK,5Enable pulse counter on pin 5

Expansion Board Firmware

The expansion board runs a separate, minimal firmware (io-expansion build target). It has no LoRa, no BLE, no mesh — just serial IO. Flash it using the web flasher or PlatformIO:

pio run -e io-expansion -t upload

Edit the pin arrays at the top of src/io_expansion.cpp to match your wiring:

const int SENSOR_PINS[] = {34, 35, 36, 39, 32, 33};  // Analog/digital inputs
const int RELAY_PINS[]  = {2, 4, 5, 12, 13, 14};      // Digital outputs

Non-blocking: The main node polls the expansion board every 2 seconds and caches the response. Sensor reads from virtual pins return the cached value instantly — there is no blocking serial wait that could delay LoRa reception.

33. Known Issues & Board-Specific Caveats

Board-Specific Issues

BoardIssueWorkaround
Heltec V2 (gateway)Only ~80KB free heap after BLE+WiFi. TLS certificate verification (beginSslWithCA) fails due to insufficient memory for cert chain processing. VERIFY_TLS_CERT is disabled for this board. Uses insecure TLS (no cert verification). Other gateway boards (V3/V4/XIAO on ESP32-S3) have cert verification enabled.
Heltec V2 (gateway)Flash is 86% full with min_spiffs.csv partition. Adding features may require removing debug output. CORE_DEBUG_LEVEL=0 is already set for this target.
XIAO ESP32S3Default sensor pins (43, 44) are UART TX/RX — digital only (0/1). No analog readings on defaults. Reconfigure: SENSOR 2,4,5,6 + SAVE. Pins 2-6 support ADC on ESP32-S3.
T-Deck PlusNo free GPIO pins for sensors or relays. All pins used by display, keyboard, GPS, and radio. Use T-Deck as communicator only. Pair with a repeater node for sensor/relay control.
LoRa32 T3 (repeater)ESP32 (not S3) has different light-sleep behaviour. Solar mode uses esp_light_sleep_start() which works on both, but power savings vary by chip. For lowest-power solar deployments, prefer Heltec V3/V4/XIAO on ESP32-S3.
Heltec V4GPIO 19/20 are USB D-/D+, GPIO 2/46/7 are GC1109 FEM. The V4 has no CP2102 — it uses ESP32-S3 native USB (HWCDC). Configuring any of these pins as sensor/relay pins will kill USB or disable the 28 dBm power amplifier. Default sensor pins are 33, 34, 15. Default relay pins are 3, 4, 5, 6. All forbidden pins are blocked in isPinSafe(). See Section 43 for full details.
Heltec V4USB serial (HWCDC) takes ~60 seconds to start outputting data after boot. The first minute of serial output may be lost. Normal behaviour for ESP32-S3 native USB. The gateway GUI will receive data once the HWCDC connection establishes. Solar mode light sleep is disabled on V4 to keep USB alive.

Protocol Notes

Gateway / Hub Notes

34. GPS Support

Nodes with a GPS receiver can broadcast their position over the mesh and share location data with the Android app. GPS is supported on any board with a spare UART — the GPS TX/RX pins are configurable per-board via BLE or serial command.

Supported Boards

BoardDefault GPS Pins (TX/RX)Notes
Heltec V438 / 39Ribbon connector for GPS module
All other boardsNone (configurable)Set via GPS,<tx>,<rx> BLE command

GPS BLE Commands

CommandResponseDescription
GPSPOSGPS,<lat>,<lng> or GPS,NOFIX or GPS,OFFQuery current GPS position
GPS,<tx>,<rx>OK,GPS,<tx>,<rx>Set GPS UART pins (saved to EEPROM)
GPS,OFFOK,GPS,OFFDisable GPS

Position Broadcasting

Android App — Map View

EEPROM Storage

GPS pins are saved at EEPROM address 441 (2 bytes: TX pin, RX pin). Value 0xFF means disabled. On boards without a default GPS, the pins remain disabled until explicitly configured via BLE. Pin values are validated against the isPinSafe() forbidden list to prevent conflicts with UART0, I2C, SPI, or USB pins.

35. Device Management (Reboot & Factory Reset)

Nodes can be rebooted or factory-reset remotely via BLE or LoRa. This is useful when a node is deployed in a hard-to-reach location or when EEPROM corruption prevents serial access.

BLE Commands

CommandResponseDescription
REBOOTOK,REBOOTRestart the node. All settings preserved.
EEPROM,RESETOK,EEPROM,RESETWipe all EEPROM settings to factory defaults and restart. Node ID, GPIO config, GPS pins, solar mode, setpoints — all reset.

LoRa Mesh Commands

CommandDescription
CMD,REBOOTRemote reboot via encrypted mesh command. The node sends a CMD,RSP,REBOOTING acknowledgement before restarting. Requires the mesh AES key — cannot be spoofed by anyone outside your mesh.

Android App

The Settings screen has a Device Management card with two buttons:

Security

36. Signal Strength & Route Visibility

All platforms show signal strength and routing information for discovered nodes, making it easy to optimise repeater placement and diagnose connectivity issues.

Gateway GUI

Android App

T-Deck Plus

38. iBeacon Scanner — BLE Proximity Triggers

Repeater nodes can scan for iBeacon BLE tags alongside the normal GATT server (dual-role BLE). When a configured beacon enters range (based on RSSI threshold), the node can trigger a relay or broadcast a mesh message — all without any phone or gateway connection.

Use cases: Livestock tracking (cow enters/leaves field), asset proximity (forklift near gate), personnel detection (worker arrives at remote site), vehicle presence (car in driveway).

How It Works

  1. The node runs a non-blocking BLE scan every 5 seconds (1-second scan duration).
  2. Each detected device is checked against up to 8 beacon rules.
  3. Matching is by iBeacon UUID (contains dashes) or MAC address (contains colons).
  4. If the beacon's RSSI is above the configured threshold, the rule's action fires.
  5. A cooldown prevents re-triggering too quickly (default 10 seconds).
  6. For relay actions, an optional auto-revert turns the relay off when the beacon has not been seen for a configurable duration.

Beacon Commands (BLE / Serial)

CommandDescription
BEACON,SCANOne-off 2-second scan — returns all visible BLE devices with MAC, RSSI, UUID (if iBeacon), and name.
BEACON,LISTList all active beacon rules with their configuration.
BEACON,ADD,<uuid_or_mac>,<name>,<rssi>,RELAY,<pin>,<state>[,<cooldown>][,REVERT,<ms>] Add a relay action rule. Fires digitalWrite(pin, state) when beacon detected. Optional cooldown (ms) and auto-revert (ms after beacon gone).
BEACON,ADD,<uuid_or_mac>,<name>,<rssi>,MSG,<text>[,<cooldown>] Add a broadcast message action. Sends an encrypted mesh broadcast when beacon detected.
BEACON,DELETE,<index>Delete a rule by its slot index (0-7).
BEACON,CLEARDelete all beacon rules.

Examples

# Scan for nearby beacons
BEACON,SCAN
# Response: BEACONSCAN,3,aa:bb:cc:dd:ee:ff:-62dBm,11:22:33:44:55:66:-88dBm:MyTag,...

# Gate opener: turn on relay pin 4 when tag is within ~3m (-65 dBm)
# Auto-revert relay after tag gone for 30 seconds
BEACON,ADD,aa:bb:cc:dd:ee:ff,Gate,-65,RELAY,4,1,10000,REVERT,30000

# Alert: broadcast "COW_OUT" when iBeacon UUID detected
BEACON,ADD,FDA50693-A4E2-4FB1-AFCF-C6EB0764...,Daisy,-80,MSG,COW_OUT,60000

# List configured rules
BEACON,LIST
# Response: BEACONS,0:Gate:aa:bb:cc:dd:ee:ff:-65dBm:RELAY4:REVERT30000ms,1:Daisy:FDA50693...:MSG

Events

When a beacon rule fires, the node sends a notification over BLE/serial:

# Relay triggered
BEACON,TRIGGERED,Gate,RELAY,4

# Message broadcast triggered
BEACON,TRIGGERED,Daisy,MSG,COW_OUT

# Relay auto-reverted (tag gone)
BEACON,REVERTED,Gate

Persistence

Beacon rules are saved to EEPROM (address 512+, 128 bytes per rule, up to 8 rules = 1024 bytes). Rules persist across reboots. The runtime state (lastSeen, triggered) is not persisted — all rules start in the untriggered state after reboot.

Logic Builder Integration

The gateway GUI Logic Builder includes a Beacon Detect input block that can trigger automation rules based on beacon proximity events received from the mesh. Beacon events (BEACON,TRIGGERED / BEACON,REVERTED) from any node in the mesh are parsed by the gateway and fed into the block evaluation engine.

Note: Beacon scanning is disabled in solar mode to conserve power. The BLE GATT server (phone app connection) continues to work alongside scanning in normal mode.

39. Meshtastic Bridge

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

The gateway GUI includes a built-in Meshtastic bridge that connects TiggyOpenMesh and Meshtastic networks via two USB serial ports. Messages, position reports, and SOS alerts are translated bidirectionally between the two mesh protocols.

Use case: Your TiggyOpenMesh sensor network can relay alerts and data to hikers carrying Meshtastic handhelds, or vice versa. No firmware changes needed on either side — the bridge runs entirely in the Python gateway GUI.

Setup (GUI)

  1. Connect your TiggyOpenMesh repeater to a USB port (the normal gateway serial connection).
  2. Connect a Meshtastic device to a second USB port.
  3. In the gateway GUI, find the Meshtastic row below the Telegram row.
  4. Click Detect to list available serial ports, select the Meshtastic device port.
  5. Click Start Bridge.

What Gets Bridged

DirectionMessage TypeTranslation
TOM → MeshText messagesSent as [TOM <nodeId>] <text> via Meshtastic text channel
TOM → MeshPosition (POS)Sent as Meshtastic position packet
TOM → MeshSOS alertsSent as Meshtastic text: SOS ALERT from TOM node <id>
Mesh → TOMText messagesBroadcast as [Mesh <name>] <text> on TOM mesh
Mesh → TOMPositionInjected as POS packet into TOM mesh
Mesh → TOMNode infoNode names discovered and mapped to virtual TOM IDs (F000+)

Standalone CLI Mode

The bridge can also run standalone without the GUI:

python meshtastic_bridge.py --tom COM18 --mesh COM4 --key DebdaleLodge2401

Deduplication

A 256-entry hash ring with 30-second TTL prevents packets from bouncing between the two networks. The origin field and [TOM ] / [Mesh ] prefixes provide additional loop detection.

Node ID Mapping

Meshtastic 32-bit node IDs are mapped to TOM 16-bit hex IDs using a persistent mapping file (bridge_nodes.json). Meshtastic nodes appear as Fxxx IDs on the TOM mesh.

40. Telegram Bot (Gateway GUI)

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

The gateway GUI includes a built-in Telegram bot that exposes your automation rules as chat commands. Unlike the Android app's Telegram forwarding (which forwards broadcast messages), this bot lets remote users query sensor values and trigger relay actions from anywhere via Telegram.

How it differs from the Android app: The Android app's Telegram feature forwards incoming mesh broadcasts to a chat. The gateway's Telegram bot lets users send commands back to the mesh. They complement each other.

Setup

  1. Create a bot via @BotFather on Telegram. Send /newbot, follow prompts, and copy the bot token.
  2. Get your Chat ID by messaging the bot and checking https://api.telegram.org/bot<TOKEN>/getUpdates.
  3. In the gateway GUI, enter the Bot Token and Chat ID in the Telegram row.
  4. Click Start Bot.

Rule-as-Command

Any automation rule whose name starts with # becomes a Telegram command. The bot matches the command to the rule and executes it:

Rule NameTelegram CommandWhat Happens
#getwind#getwind or /getwindReads the latest sensor values from the rule's Sensor Read blocks, applies any Scale transforms, and replies with formatted values.
#opendoor#opendoor or /opendoorFires the rule's action blocks (Set Relay, Pulse Relay, etc.) and confirms.

Telegram Output block: If a rule contains a Telegram Output action block, the bot reads the formatted value from that block instead of raw sensor data. This lets you chain Sensor → Scale → Telegram Output for precisely formatted responses (e.g., “Wind Speed: 12.3 m/s”).

Built-In Commands

CommandDescription
/help or #helpLists all available commands (built-in + rule commands)
/status or #statusShows all nodes with online/offline status, RSSI, and last-seen time
/nodes or #nodesLists all discovered nodes with friendly names

Security

The bot only responds to messages from the configured Chat ID. Messages from other chats or users are silently ignored. The bot token should be kept private.

Alert Sending

The automation engine can also push alerts to Telegram when rules trigger. The send_alert_threadsafe() method lets any rule fire a Telegram notification without blocking the GUI or radio threads.

41. Sensor Dashboard

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

The gateway GUI's Sensors tab provides a full sensor monitoring dashboard with real-time visualisation of all SDATA values received from the mesh.

Dashboard Widgets

WidgetDescription
Circular GaugesOne gauge per sensor key (nodeId:pin). Shows the current value with a needle, colour gradient ring (green → orange → red), and min/max tracking labels. Auto-scales to the observed data range.
Line ChartMatplotlib-based time-series chart showing sensor history. Configurable history length (default 120 samples). Multiple sensors can be plotted simultaneously.
CSV ExportClick the export button to save all sensor data to a timestamped CSV file in gateway/exports/. Useful for offline analysis in Excel or Python.

Controls

ButtonAction
Pause / ResumeFreezes the display for inspection without losing incoming data
ClearClears all historical data from the dashboard (does not affect the underlying data store)
Export CSVSaves data to gateway/exports/sensors_YYYYMMDD_HHMMSS.csv

Data Flow

Node sends
SDATA,0010,34:2048
Gateway decrypts
(if via mesh MSG)
Stored in
sensor_data dict
Dashboard
renders gauges

The dashboard shares the same sensor_data dictionary used by the Logic Builder engine. Both update in real time from the same data source.

42. Hardware Timer Watchdog

All three firmware targets (repeater, T-Deck, WiFi gateway) include a hardware timer watchdog that automatically reboots the device if the main loop hangs. This is completely independent of the RTOS task watchdog and catches low-level lockups that RTOS watchdogs miss (e.g., radio driver deadlocks, SPI bus hangs).

How It Works

ParameterValue
Timeout30 seconds
Timer typeESP32 hardware timer (independent of RTOS)
Pet locationMain loop() function — timerWrite(swWdt, 0) every iteration
Reset actionHard reboot via timer ISR (cannot be blocked by stuck tasks)

Why Not RTOS WDT?

The RTOS task watchdog (TWDT) monitors whether tasks yield to the scheduler. But if the main loop is stuck inside a blocking radio call or SPI transaction, the RTOS scheduler still runs (it just can't preempt the stuck call on the same core). The hardware timer is independent of all software — if loop() does not pet it within 30 seconds, the ESP32 reboots unconditionally.

Implications

43. Heltec WiFi LoRa 32 V4 — USB & FEM Notes

The Heltec V4 uses the ESP32-S3's native USB (HWCDC) instead of an external UART chip like the V3's CP2102. This has important implications:

Key Differences from V3

FeatureV3V4
USB chipCP2102 (external)ESP32-S3 native USB (HWCDC)
COM portAlways availableAppears after firmware boots (~2-5s)
Serial during resetWorks (CP2102 stays powered)Drops briefly during reboot
GPIO 19/20Available as GPIOUSB D-/D+ — DO NOT USE
Build flagDefaultARDUINO_USB_CDC_ON_BOOT=1

Forbidden Pins

The following pins are hardcoded into the isPinSafe() forbidden list and cannot be configured as sensor or relay pins:

Pin(s)FunctionConsequence if misconfigured
GPIO 19, 20USB D-/D+ (HWCDC)Kills USB serial connection immediately
GPIO 2GC1109 FEM enable (HIGH = PA on)Disables front-end module, TX power drops from 28 dBm to ~10 dBm
GPIO 46GC1109 PA TX enable (HIGH)Disables power amplifier, severely reduces range
GPIO 7GC1109 PA power (ANALOG mode)Disrupts PA power supply

Troubleshooting

44. Web Gauge — Live Sensor Sharing

TiggyOpenMesh Pro Feature — visit tiggyengineering.com

Share any sensor reading as a live web page. Anyone with the URL sees an auto-refreshing gauge on their phone — no app, no login, no limit on viewers.

URL Format

https://<hub-address>/gauge/<node-id>/<pin>?label=<name>&unit=<unit>&min=<min>&max=<max>

Parameters

ParameterRequiredDefaultExample
node-idYes5041
pinYes15
labelNoSensorBarn+Temperature
unitNo(none)°C
minNo00
maxNo50050

Examples

/gauge/5041/15?label=Barn+Temperature&unit=°C&min=0&max=50
/gauge/5041/19?label=Wind+Speed&unit=km/h&min=0&max=100
/gauge/263A/3?label=Water+Level&unit=%25&min=0&max=100

How It Works

  1. Gateway GUI decodes sensor data (SDATA) from nodes via LoRa.
  2. GUI forwards decoded values to the hub via WebSocket: {"type": "sensor", "node": "5041", "pin": "15", "value": 423}
  3. Hub stores latest value per node/pin in memory.
  4. /gauge/<node>/<pin> serves a live HTML gauge page with auto-refresh every 5 seconds.
  5. /api/sensor/<node>/<pin> returns JSON for programmatic access.

API Endpoint

GET /api/sensor/<node>/<pin>
Response: {"node": "5041", "pin": "15", "value": 423, "ts": 1711234567}

Requirements

Sharing

45. Licensing & Tiers

TiggyOpenMesh uses a free core + paid tiers model. The firmware, CLI gateway, Android app, and hub map are free and open-source. Premium GUI features are licensed per-seat with an offline activation key.

TierPriceWhat You Get
FreeNode firmware, CLI gateway (gateway.py), hub server (gateway_hub.py), Android app, live hub map
Hobbyist£30Gateway GUI + Sensor Dashboard + manual relay control
Pro£80Everything in Hobbyist + Logic Builder (all blocks) + Telegram Bot + Meshtastic Bridge + Web Gauges
Business£300Everything in Pro + priority support + remote setup assistance

Activation

  1. Purchase your licence key at tiggyengineering.com.
  2. Launch gateway_gui.py. On first run a licence dialog appears.
  3. Paste your key and click Activate.
  4. The key is validated offline — no internet connection is required after purchase.

Keys are perpetual: Once activated, the licence does not expire. Future updates within the same major version are included.

TiggyOpenMesh v4.1 — MeshCore Architecture

T-Deck Plus • LoRa32 T3 V1.6.1 • Heltec WiFi LoRa 32 V3 • Heltec WiFi LoRa 32 V4 • XIAO ESP32S3

45 sections — Hardware, Protocol, UI, BLE, Gateway, Logic Builder, Timers, Setpoints, Sensors, Pulse Counters, SOS, Map, Solar, Delivery, SF Change, Collision Detection, IO Expansion, GPS, Device Management, Signal Routing, iBeacon Scanner, Meshtastic Bridge, Telegram Bot (Gateway), Sensor Dashboard, HW Watchdog, Heltec V4 USB/FEM, Web Gauge, Licensing, Caveats

Documentation updated March 2026