mirror of
https://github.com/markqvist/RNode_Firmware.git
synced 2026-04-28 09:43:08 +00:00
Standalone GPS beacon mode: when no KISS host is connected for 15s, the RNode transmits position and battery telemetry over LoRa. Two beacon paths: - LXMF (recommended): encrypted per-packet messages with announces, compatible with Sideband and any LXMF application. Supports IFAC network authentication. - Legacy JSON: plaintext or encrypted raw packets for simple collectors. Key changes: - GPS support for T-Beam Supreme S3 (L76K) and Heltec V4 (external) - SX1262 radio fixes: IQ polarity, DCD preamble lockup, RX reliability - LXMF identity management with NVS-backed Ed25519/X25519 keys - IFAC authentication (CMD_IFAC_KEY 0x89) for private networks - Per-channel serial isolation (USB, BLE, WiFi) - GPS status page in OLED display rotation - Provisioning via rnlog: provision-lxmf, provision-ifac - Documentation in Documentation/BEACON.md
209 lines
6.8 KiB
C
209 lines
6.8 KiB
C
// Copyright (C) 2026, GPS beacon support contributed by GlassOnTin
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
#ifndef BEACON_H
|
|
#define BEACON_H
|
|
|
|
#if HAS_GPS == true
|
|
|
|
// Beacon interval and timing
|
|
#define BEACON_INTERVAL_MS 30000 // 30 seconds between beacons
|
|
#define BEACON_STARTUP_DELAY_MS 10000 // Wait 10s after boot before first beacon
|
|
// BEACON_NO_HOST_TIMEOUT_MS and last_host_activity defined in GPS.h
|
|
|
|
// Beacon radio parameters — must match the router's LoRa interface
|
|
#define BEACON_FREQ 868000000
|
|
#define BEACON_BW 125000
|
|
#define BEACON_SF 7
|
|
#define BEACON_CR 5
|
|
#define BEACON_TXP 17
|
|
|
|
// Pre-computed RNS destination hash for PLAIN destination "rnlog.beacon"
|
|
// Computed as: SHA256(SHA256("rnlog.beacon")[:10])[:16]
|
|
const uint8_t RNS_DEST_HASH[16] = {
|
|
0x18, 0xbc, 0xd8, 0xa3, 0xde, 0xa1, 0x6e, 0xf6,
|
|
0x76, 0x5c, 0x6b, 0x27, 0xd0, 0x08, 0xd2, 0x20
|
|
};
|
|
|
|
// RNS packet header constants (PLAIN destination, HEADER_1, DATA)
|
|
// FLAGS: header_type=0 (HEADER_1), propagation=0 (BROADCAST),
|
|
// destination=0 (PLAIN), packet_type=2 (DATA), transport=0
|
|
#define RNS_FLAGS 0x08
|
|
#define RNS_CONTEXT 0x00
|
|
|
|
// Beacon state
|
|
bool beacon_mode_active = false;
|
|
uint32_t last_beacon_tx = 0;
|
|
|
|
// Forward declarations from main firmware
|
|
void lora_receive();
|
|
bool startRadio();
|
|
void setTXPower();
|
|
void setBandwidth();
|
|
void setSpreadingFactor();
|
|
void setCodingRate();
|
|
void beacon_transmit(uint16_t size);
|
|
|
|
// Diagnostic: track which gate blocks beacon_update()
|
|
// 0=not called, 1=host active, 2=startup delay, 3=radio offline,
|
|
// 4=no gps fix, 5=interval wait, 6=beacon sent
|
|
uint8_t beacon_gate = 0;
|
|
|
|
void beacon_check_host_activity() {
|
|
last_host_activity = millis();
|
|
if (beacon_mode_active) {
|
|
beacon_mode_active = false;
|
|
}
|
|
}
|
|
|
|
void beacon_update() {
|
|
// Don't beacon if host has been active recently
|
|
if (last_host_activity > 0 &&
|
|
(millis() - last_host_activity < BEACON_NO_HOST_TIMEOUT_MS)) {
|
|
beacon_gate = 1;
|
|
return;
|
|
}
|
|
|
|
// Wait for startup delay after boot
|
|
if (millis() < BEACON_STARTUP_DELAY_MS) {
|
|
beacon_gate = 2;
|
|
return;
|
|
}
|
|
|
|
// No point beaconing without a GPS fix — check BEFORE touching
|
|
// radio params to avoid putting the radio into standby needlessly
|
|
if (!gps_has_fix) {
|
|
beacon_gate = 4;
|
|
return;
|
|
}
|
|
|
|
// Radio must be online — restart if needed
|
|
if (!radio_online) {
|
|
lora_freq = (uint32_t)868000000;
|
|
lora_bw = (uint32_t)125000;
|
|
lora_sf = 7;
|
|
lora_cr = 5;
|
|
lora_txp = 17;
|
|
if (!startRadio()) {
|
|
beacon_gate = 3;
|
|
return;
|
|
}
|
|
} else {
|
|
// Radio is online but may have host/EEPROM params — force beacon settings
|
|
lora_freq = (uint32_t)868000000;
|
|
lora_bw = (uint32_t)125000;
|
|
lora_sf = 7;
|
|
lora_cr = 5;
|
|
lora_txp = 17;
|
|
setTXPower();
|
|
setBandwidth();
|
|
setSpreadingFactor();
|
|
setCodingRate();
|
|
}
|
|
|
|
// Respect beacon interval
|
|
if (last_beacon_tx > 0 &&
|
|
(millis() - last_beacon_tx < BEACON_INTERVAL_MS)) {
|
|
beacon_gate = 5;
|
|
return;
|
|
}
|
|
|
|
beacon_mode_active = true;
|
|
beacon_gate = 6;
|
|
|
|
// LXMF path: send proper LXMF message with FIELD_TELEMETRY directly to Sideband
|
|
if (lxmf_identity_configured && beacon_crypto_configured) {
|
|
// Periodic LXMF announce (every 10 minutes)
|
|
lxmf_announce_if_needed("RNode GPS Tracker");
|
|
|
|
// Get Unix timestamp from GPS directly
|
|
uint32_t timestamp = (uint32_t)(millis() / 1000);
|
|
#if HAS_GPS == true
|
|
{
|
|
extern TinyGPSPlus gps_parser;
|
|
if (gps_parser.date.isValid() && gps_parser.time.isValid() && gps_parser.date.year() >= 2024) {
|
|
uint32_t days = 0;
|
|
uint16_t yr = gps_parser.date.year();
|
|
uint8_t mo = gps_parser.date.month();
|
|
for (uint16_t y = 1970; y < yr; y++)
|
|
days += (y % 4 == 0) ? 366 : 365;
|
|
static const uint16_t mdays[] = {0,31,59,90,120,151,181,212,243,273,304,334};
|
|
if (mo >= 1 && mo <= 12) {
|
|
days += mdays[mo - 1];
|
|
if (mo > 2 && (yr % 4 == 0)) days++;
|
|
}
|
|
days += gps_parser.date.day() - 1;
|
|
timestamp = days * 86400UL + gps_parser.time.hour() * 3600UL
|
|
+ gps_parser.time.minute() * 60UL + gps_parser.time.second();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
lxmf_beacon_send(gps_lat, gps_lon, gps_alt,
|
|
gps_speed, gps_hdop,
|
|
timestamp, (int)battery_percent);
|
|
last_beacon_tx = millis();
|
|
return;
|
|
}
|
|
|
|
// Legacy path: JSON payload for rnlog collector (no LXMF identity)
|
|
char json_buf[256];
|
|
int json_len = snprintf(json_buf, sizeof(json_buf),
|
|
"{\"lat\":%.6f,\"lon\":%.6f,\"alt\":%.1f,"
|
|
"\"sat\":%d,\"spd\":%.1f,\"hdop\":%.1f,"
|
|
"\"bat\":%d,\"fix\":%s}",
|
|
gps_lat, gps_lon, gps_alt,
|
|
gps_sats, gps_speed, gps_hdop,
|
|
(int)battery_percent,
|
|
gps_has_fix ? "true" : "false");
|
|
|
|
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) return;
|
|
|
|
if (beacon_crypto_configured) {
|
|
// Encrypted SINGLE packet (legacy JSON to rnlog.collector)
|
|
tbuf[0] = 0x00; // FLAGS: HEADER_1, BROADCAST, SINGLE, DATA
|
|
tbuf[1] = 0x00; // HOPS
|
|
memcpy(&tbuf[2], collector_dest_hash, 16);
|
|
tbuf[18] = 0x00; // CONTEXT_NONE
|
|
|
|
int crypto_len = beacon_crypto_encrypt(
|
|
(uint8_t*)json_buf, json_len,
|
|
collector_pub_key, collector_identity_hash,
|
|
&tbuf[19]
|
|
);
|
|
|
|
if (crypto_len > 0 && (19 + crypto_len) <= (int)MTU) {
|
|
beacon_transmit(19 + crypto_len);
|
|
lora_receive();
|
|
last_beacon_tx = millis();
|
|
}
|
|
} else {
|
|
// Fallback: PLAIN beacon (unencrypted, original behavior)
|
|
tbuf[0] = RNS_FLAGS; // 0x08 = PLAIN DATA
|
|
tbuf[1] = 0x00;
|
|
memcpy(&tbuf[2], RNS_DEST_HASH, 16);
|
|
tbuf[18] = RNS_CONTEXT;
|
|
|
|
memcpy(&tbuf[19], json_buf, json_len);
|
|
if (19 + json_len <= (int)MTU) {
|
|
beacon_transmit(19 + json_len);
|
|
lora_receive();
|
|
last_beacon_tx = millis();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
#endif
|