Merge branch 'dev' into trace

# Conflicts:
#	src/Dispatcher.cpp
#	src/Mesh.cpp
#	src/helpers/BaseChatMesh.cpp
This commit is contained in:
Scott Powell 2025-03-07 12:14:26 +11:00
commit b03aac18c0
112 changed files with 23323 additions and 1359 deletions

View file

@ -0,0 +1,119 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#define AUTO_OFF_MILLIS 15000 // 15 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(const char* node_name, const char* build_date, uint32_t pin_code) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
clearMsgPreview();
_node_name = node_name;
_build_date = build_date;
_pin_code = pin_code;
_display->turnOn();
}
void UITask::clearMsgPreview() {
_origin[0] = 0;
_msg[0] = 0;
}
void UITask::showMsgPreview(uint8_t path_len, const char* from_name, const char* text) {
if (path_len == 0xFF) {
sprintf(_origin, "(F) %s", from_name);
} else {
sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name);
}
StrHelper::strncpy(_msg, text, sizeof(_msg));
if (!_display->isOn()) _display->turnOn();
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
}
void UITask::renderCurrScreen() {
char tmp[80];
if (_origin[0] && _msg[0]) {
// render message preview
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->print(_node_name);
_display->setCursor(0, 12);
_display->print(_origin);
_display->setCursor(0, 24);
_display->print(_msg);
//_display->setCursor(100, 9); TODO
//_display->setTextSize(2);
//_display->printf("%d", msgs);
} else {
// render 'home' screen
_display->drawXbm(0, 0, meshcore_logo, 128, 13);
_display->setCursor(0, 20);
_display->setTextSize(1);
_display->print(_node_name);
sprintf(tmp, "Build: %s", _build_date);
_display->setCursor(0, 32);
_display->print(tmp);
if (_connected) {
//_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);
//_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr);
} else if (_pin_code != 0) {
_display->setTextSize(2);
_display->setCursor(0, 43);
sprintf(tmp, "Pin:%d", _pin_code);
_display->print(tmp);
}
}
}
void UITask::loop() {
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (_display->isOn()) {
clearMsgPreview();
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 100; // 10 reads per second
}
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View file

@ -0,0 +1,25 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
bool _connected;
uint32_t _pin_code;
const char* _node_name;
const char* _build_date;
char _origin[62];
char _msg[80];
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; _connected = false; }
void begin(const char* node_name, const char* build_date, uint32_t pin_code);
void setHasConnection(bool connected) { _connected = connected; }
void clearMsgPreview();
void showMsgPreview(uint8_t path_len, const char* from_name, const char* text);
void loop();
};

View file

@ -72,14 +72,39 @@
#include <helpers/ESP32Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static ESP32Board board;
#elif defined(LILYGO_TLORA)
#include <helpers/LilyGoTLoraBoard.h>
#include <helpers/CustomSX1276Wrapper.h>
static LilyGoTLoraBoard board;
#elif defined(RAK_4631)
#include <helpers/nrf52/RAK4631Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static RAK4631Board board;
#elif defined(T1000_E)
#include <helpers/nrf52/T1000eBoard.h>
#include <helpers/CustomLR1110Wrapper.h>
static T1000eBoard board;
#elif defined(HELTEC_T114)
#include <helpers/nrf52/T114Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static T114Board board;
#elif defined(LILYGO_TECHO)
#include <helpers/nrf52/TechoBoard.h>
#include <helpers/CustomSX1262Wrapper.h>
static TechoBoard board;
#else
#error "need to provide a 'board' object"
#endif
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
static DISPLAY_CLASS display;
#include "UITask.h"
static UITask ui_task(display);
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@ -92,6 +117,16 @@ static uint32_t _atoi(const char* sp) {
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 2
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "3 Mar 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.0.0"
#endif
#define CMD_APP_START 1
#define CMD_SEND_TXT_MSG 2
#define CMD_SEND_CHANNEL_TXT_MSG 3
@ -113,6 +148,15 @@ static uint32_t _atoi(const char* sp) {
#define CMD_REBOOT 19
#define CMD_GET_BATTERY_VOLTAGE 20
#define CMD_SET_TUNING_PARAMS 21
#define CMD_DEVICE_QEURY 22
#define CMD_EXPORT_PRIVATE_KEY 23
#define CMD_IMPORT_PRIVATE_KEY 24
#define CMD_SEND_RAW_DATA 25
#define CMD_SEND_LOGIN 26
#define CMD_SEND_STATUS_REQ 27
#define CMD_HAS_CONNECTION 28
#define CMD_LOGOUT 29 // 'Disconnect'
#define CMD_GET_CONTACT_BY_KEY 30
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@ -127,12 +171,19 @@ static uint32_t _atoi(const char* sp) {
#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE
#define RESP_CODE_EXPORT_CONTACT 11
#define RESP_CODE_BATTERY_VOLTAGE 12 // a reply to a CMD_GET_BATTERY_VOLTAGE
#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QEURY
#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY
#define RESP_CODE_DISABLED 15
// these are _pushed_ to client app at any time
#define PUSH_CODE_ADVERT 0x80
#define PUSH_CODE_PATH_UPDATED 0x81
#define PUSH_CODE_SEND_CONFIRMED 0x82
#define PUSH_CODE_MSG_WAITING 0x83
#define PUSH_CODE_RAW_DATA 0x84
#define PUSH_CODE_LOGIN_SUCCESS 0x85
#define PUSH_CODE_LOGIN_FAIL 0x86
#define PUSH_CODE_STATUS_RESPONSE 0x87
/* -------------------------------------------------------------------------------------- */
@ -149,20 +200,26 @@ struct NodePrefs { // persisted to file
uint8_t tx_power_dbm;
uint8_t unused[3];
float rx_delay_base;
uint32_t ble_pin;
};
class MyMesh : public BaseChatMesh {
FILESYSTEM* _fs;
RADIO_CLASS* _phy;
IdentityStore* _identity_store;
NodePrefs _prefs;
uint32_t expected_ack_crc; // TODO: keep table of expected ACKs
uint32_t pending_login;
uint32_t pending_status;
mesh::GroupChannel* _public;
BaseSerialInterface* _serial;
unsigned long last_msg_sent;
ContactsIterator _iter;
uint32_t _iter_filter_since;
uint32_t _most_recent_lastmod;
uint32_t _active_ble_pin;
bool _iter_started;
uint8_t app_target_ver;
uint8_t cmd_frame[MAX_FRAME_SIZE+1];
uint8_t out_frame[MAX_FRAME_SIZE+1];
@ -173,6 +230,17 @@ class MyMesh : public BaseChatMesh {
int offline_queue_len;
Frame offline_queue[OFFLINE_QUEUE_SIZE];
void loadMainIdentity(mesh::RNG& trng) {
if (!_identity_store->load("_main", self_id)) {
self_id = mesh::LocalIdentity(&trng); // create new random identity
saveMainIdentity(self_id);
}
}
bool saveMainIdentity(const mesh::LocalIdentity& identity) {
return _identity_store->save("_main", identity);
}
void loadContacts() {
if (_fs->exists("/contacts3")) {
File file = _fs->open("/contacts3");
@ -241,11 +309,11 @@ class MyMesh : public BaseChatMesh {
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
char path[64];
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
if (_fs->exists(path)) {
File f = _fs->open(path);
if (f) {
@ -260,11 +328,11 @@ class MyMesh : public BaseChatMesh {
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override {
char path[64];
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
#if defined(NRF52_PLATFORM)
File f = _fs->open(path, FILE_O_WRITE);
if (f) { f.seek(0); f.truncate(); }
@ -275,7 +343,7 @@ class MyMesh : public BaseChatMesh {
int n = f.write(src_buf, len);
f.close();
if (n == len) return true; // success!
_fs->remove(path); // blob was only partially written!
}
return false; // error
@ -292,6 +360,12 @@ class MyMesh : public BaseChatMesh {
_serial->writeFrame(buf, 1);
}
void writeDisabledFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_DISABLED;
_serial->writeFrame(buf, 1);
}
void writeContactRespFrame(uint8_t code, const ContactInfo& contact) {
int i = 0;
out_frame[i++] = code;
@ -300,7 +374,7 @@ class MyMesh : public BaseChatMesh {
out_frame[i++] = contact.flags;
out_frame[i++] = contact.out_path_len;
memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); i += MAX_PATH_SIZE;
memcpy(&out_frame[i], contact.name, 32); i += 32;
StrHelper::strzcpy((char *) &out_frame[i], contact.name, 32); i += 32;
memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); i += 4;
memcpy(&out_frame[i], &contact.gps_lat, 4); i += 4;
memcpy(&out_frame[i], &contact.gps_lon, 4); i += 4;
@ -394,16 +468,19 @@ protected:
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return true;
}
return false;
return checkConnectionsAck(data);
}
void onMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override {
void queueMessage(const ContactInfo& from, uint8_t txt_type, uint8_t path_len, uint32_t sender_timestamp, const uint8_t* extra, int extra_len, const char *text) {
int i = 0;
out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV;
memcpy(&out_frame[i], from.id.pub_key, 6); i += 6; // just 6-byte prefix
out_frame[i++] = path_len;
out_frame[i++] = TXT_TYPE_PLAIN;
out_frame[i++] = txt_type;
memcpy(&out_frame[i], &sender_timestamp, 4); i += 4;
if (extra_len > 0) {
memcpy(&out_frame[i], extra, extra_len); i += extra_len;
}
int tlen = strlen(text); // TODO: UTF-8 ??
if (i + tlen > MAX_FRAME_SIZE) {
tlen = MAX_FRAME_SIZE - i;
@ -418,6 +495,25 @@ protected:
} else {
soundBuzzer();
}
#ifdef DISPLAY_CLASS
ui_task.showMsgPreview(path_len, from.name, text);
#endif
}
void onMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override {
markConnectionActive(from); // in case this is from a server, and we have a connection
queueMessage(from, TXT_TYPE_PLAIN, path_len, sender_timestamp, NULL, 0, text);
}
void onCommandDataRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override {
markConnectionActive(from); // in case this is from a server, and we have a connection
queueMessage(from, TXT_TYPE_CLI_DATA, path_len, sender_timestamp, NULL, 0, text);
}
void onSignedMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
markConnectionActive(from);
saveContacts(); // from.sync_since change needs to be persisted
queueMessage(from, TXT_TYPE_SIGNED_PLAIN, path_len, sender_timestamp, sender_prefix, 4, text);
}
void onChannelMessageRecv(const mesh::GroupChannel& channel, int in_path_len, uint32_t timestamp, const char *text) override {
@ -441,11 +537,62 @@ protected:
} else {
soundBuzzer();
}
#ifdef DISPLAY_CLASS
ui_task.showMsgPreview(in_path_len < 0 ? 0xFF : in_path_len, "Public", text);
#endif
}
void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override {
// TODO: check for login response
// TODO: check for Get Stats response
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4);
if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response
// yes, is response to pending sendLogin()
pending_login = 0;
int i = 0;
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
out_frame[i++] = 0; // legacy: is_admin = false
} else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
if (keep_alive_secs > 0) {
startConnection(contact, keep_alive_secs);
}
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
out_frame[i++] = data[6]; // permissions (eg. is_admin)
} else {
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
out_frame[i++] = 0; // reserved
}
memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix
_serial->writeFrame(out_frame, i);
} else if (len > 4 && pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0) { // check for status response
// yes, is response to pending sendStatusRequest()
pending_status = 0;
int i = 0;
out_frame[i++] = PUSH_CODE_STATUS_RESPONSE;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix
memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4);
_serial->writeFrame(out_frame, i);
}
}
void onRawDataRecv(mesh::Packet* packet) override {
int i = 0;
out_frame[i++] = PUSH_CODE_RAW_DATA;
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
out_frame[i++] = 0xFF; // reserved (possibly path_len in future)
memcpy(&out_frame[i], packet->payload, packet->payload_len); i += packet->payload_len;
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline");
}
}
void onContactTraceRecv(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t hash[], int8_t snr[], uint8_t path_len) override {
@ -456,7 +603,7 @@ protected:
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
}
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override {
return SEND_TIMEOUT_BASE_MILLIS +
return SEND_TIMEOUT_BASE_MILLIS +
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1));
}
@ -470,6 +617,9 @@ public:
{
_iter_started = false;
offline_queue_len = 0;
app_target_ver = 0;
_identity_store = NULL;
pending_login = pending_status = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@ -489,24 +639,63 @@ public:
BaseChatMesh::begin();
#if defined(NRF52_PLATFORM)
IdentityStore store(fs, "");
_identity_store = new IdentityStore(fs, "");
#else
IdentityStore store(fs, "/identity");
_identity_store = new IdentityStore(fs, "/identity");
#endif
if (!store.load("_main", self_id)) {
self_id = mesh::LocalIdentity(&trng); // create new random identity
store.save("_main", self_id);
}
loadMainIdentity(trng);
// load persisted prefs
if (_fs->exists("/node_prefs")) {
File file = _fs->open("/node_prefs");
if (file) {
file.read((uint8_t *) &_prefs, sizeof(_prefs));
uint8_t pad[8];
file.read((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0
file.read((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4
file.read(pad, 4); // 36
file.read((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40
file.read((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48
file.read((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61
file.read((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.read((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2)); // 63
file.read((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
file.read((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69
file.read((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.read(pad, 4); // 76
file.read((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
_prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f);
_prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 7, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
file.close();
}
}
#ifdef BLE_PIN_CODE
if (_prefs.ble_pin == 0) {
#ifdef DISPLAY_CLASS
_active_ble_pin = trng.nextInt(100000, 999999); // random pin each session
#else
_active_ble_pin = BLE_PIN_CODE; // otherwise static pin
#endif
} else {
_active_ble_pin = _prefs.ble_pin;
}
#else
_active_ble_pin = 0;
#endif
// init 'blob store' support
_fs->mkdir("/bl");
@ -521,6 +710,7 @@ public:
}
const char* getNodeName() { return _prefs.node_name; }
uint32_t getBLEPin() { return _active_ble_pin; }
void startInterface(BaseSerialInterface& serial) {
_serial = &serial;
@ -535,18 +725,48 @@ public:
File file = _fs->open("/node_prefs", "w", true);
#endif
if (file) {
file.write((const uint8_t *)&_prefs, sizeof(_prefs));
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
file.write((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0
file.write((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40
file.write((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48
file.write((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61
file.write((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.write((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2)); // 63
file.write((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
file.write((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69
file.write((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write(pad, 4); // 76
file.write((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
}
}
void handleCmdFrame(size_t len) {
if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID
uint8_t app_ver = cmd_frame[1];
// cmd_frame[2..7] reserved future
if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection
app_target_ver = cmd_frame[1]; // which version of protocol does app understand
int i = 0;
out_frame[i++] = RESP_CODE_DEVICE_INFO;
out_frame[i++] = FIRMWARE_VER_CODE;
memset(&out_frame[i], 0, 6); i += 6; // reserved
memset(&out_frame[i], 0, 12);
strcpy((char *) &out_frame[i], FIRMWARE_BUILD_DATE); i += 12;
StrHelper::strzcpy((char *) &out_frame[i], board.getManufacturerName(), 40); i += 40;
StrHelper::strzcpy((char *) &out_frame[i], FIRMWARE_VERSION, 20); i += 20;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID
// cmd_frame[1..7] reserved future
char* app_name = (char *) &cmd_frame[8];
cmd_frame[len] = 0; // make app_name null terminated
MESH_DEBUG_PRINTLN("App %s connected, ver: %d", app_name, (uint32_t)app_ver);
MESH_DEBUG_PRINTLN("App %s connected", app_name);
_iter_started = false; // stop any left-over ContactsIterator
int i = 0;
@ -562,7 +782,7 @@ public:
memcpy(&out_frame[i], &lat, 4); i += 4;
memcpy(&out_frame[i], &lon, 4); i += 4;
memcpy(&out_frame[i], &alt, 4); i += 4;
uint32_t freq = _prefs.freq * 1000;
memcpy(&out_frame[i], &freq, 4); i += 4;
uint32_t bw = _prefs.bw*1000;
@ -581,12 +801,18 @@ public:
memcpy(&msg_timestamp, &cmd_frame[i], 4); i += 4;
uint8_t* pub_key_prefix = &cmd_frame[i]; i += 6;
ContactInfo* recipient = lookupContactByPubKey(pub_key_prefix, 6);
if (recipient && attempt < 4 && txt_type == TXT_TYPE_PLAIN) {
if (recipient && attempt < 4 && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
char *text = (char *) &cmd_frame[i];
int tlen = len - i;
uint32_t est_timeout;
text[tlen] = 0; // ensure null
int result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack_crc, est_timeout);
int result;
if (txt_type == TXT_TYPE_CLI_DATA) {
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
expected_ack_crc = 0; // no Ack expected
} else {
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack_crc, est_timeout);
}
// TODO: add expected ACK to table
if (result == MSG_SEND_FAILED) {
writeErrFrame();
@ -734,6 +960,14 @@ public:
} else {
writeErrFrame(); // not found, or unable to send
}
} else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) {
uint8_t* pub_key = &cmd_frame[1];
ContactInfo* contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (contact) {
writeContactRespFrame(RESP_CODE_CONTACT, *contact);
} else {
writeErrFrame(); // not found
}
} else if (cmd_frame[0] == CMD_EXPORT_CONTACT) {
if (len < 1 + PUB_KEY_SIZE) {
// export SELF
@ -805,16 +1039,18 @@ public:
_prefs.tx_power_dbm = cmd_frame[1];
savePrefs();
_phy->setOutputPower(_prefs.tx_power_dbm);
writeOKFrame();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
int i = 1;
uint32_t rx;
uint32_t rx, af;
memcpy(&rx, &cmd_frame[i], 4); i += 4;
memcpy(&af, &cmd_frame[i], 4); i += 4;
_prefs.rx_delay_base = ((float)rx) / 1000.0f;
_prefs.airtime_factor = ((float)af) / 1000.0f;
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_REBOOT) {
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
board.reboot();
} else if (cmd_frame[0] == CMD_GET_BATTERY_VOLTAGE) {
uint8_t reply[3];
@ -822,6 +1058,96 @@ public:
uint16_t battery_millivolts = board.getBattMilliVolts();
memcpy(&reply[1], &battery_millivolts, 2);
_serial->writeFrame(reply, 3);
} else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) {
#if ENABLE_PRIVATE_KEY_EXPORT
uint8_t reply[65];
reply[0] = RESP_CODE_PRIVATE_KEY;
self_id.writeTo(&reply[1], 64);
_serial->writeFrame(reply, 65);
#else
writeDisabledFrame();
#endif
} else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) {
#if ENABLE_PRIVATE_KEY_IMPORT
mesh::LocalIdentity identity;
identity.readFrom(&cmd_frame[1], 64);
if (saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
} else {
writeErrFrame();
}
#else
writeDisabledFrame();
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
int8_t path_len = cmd_frame[i++];
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
uint8_t* path = &cmd_frame[i]; i += path_len;
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
} else {
writeErrFrame();
}
} else {
writeErrFrame(); // flood, not supported (yet)
}
} else if (cmd_frame[0] == CMD_SEND_LOGIN && len >= 1+PUB_KEY_SIZE) {
uint8_t* pub_key = &cmd_frame[1];
ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
char *password = (char *) &cmd_frame[1+PUB_KEY_SIZE];
cmd_frame[len] = 0; // ensure null terminator in password
if (recipient) {
uint32_t est_timeout;
int result = sendLogin(*recipient, password, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame();
} else {
pending_status = 0;
memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &pending_login, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1+PUB_KEY_SIZE) {
uint8_t* pub_key = &cmd_frame[1];
ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint32_t est_timeout;
int result = sendStatusRequest(*recipient, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame();
} else {
pending_login = 0;
memcpy(&pending_status, recipient->id.pub_key, 4); // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &pending_status, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(); // contact not found
}
} else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1+PUB_KEY_SIZE) {
uint8_t* pub_key = &cmd_frame[1];
if (hasConnectionTo(pub_key)) {
writeOKFrame();
} else {
writeErrFrame();
}
} else if (cmd_frame[0] == CMD_LOGOUT && len >= 1+PUB_KEY_SIZE) {
uint8_t* pub_key = &cmd_frame[1];
stopConnection(pub_key);
writeOKFrame();
} else {
writeErrFrame();
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
@ -851,12 +1177,25 @@ public:
_serial->writeFrame(out_frame, 5);
_iter_started = false;
}
} else if (!_serial->isWriteBusy()) {
checkConnections();
}
#ifdef DISPLAY_CLASS
ui_task.setHasConnection(_serial->isConnected());
ui_task.loop();
#endif
}
};
#ifdef ESP32
#ifdef BLE_PIN_CODE
#ifdef WIFI_SSID
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface serial_interface;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
@ -877,6 +1216,9 @@ public:
#if defined(NRF52_PLATFORM)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#elif defined(LILYGO_TLORA)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#elif defined(P_LORA_SCLK)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
@ -901,6 +1243,10 @@ void setup() {
float tcxo = 1.6f;
#endif
#ifdef DISPLAY_CLASS
display.begin();
#endif
#if defined(NRF52_PLATFORM)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
@ -935,7 +1281,7 @@ void setup() {
#ifdef BLE_PIN_CODE
char dev_name[32+10];
sprintf(dev_name, "MeshCore-%s", the_mesh.getNodeName());
serial_interface.begin(dev_name, BLE_PIN_CODE);
serial_interface.begin(dev_name, the_mesh.getBLEPin());
#else
pinMode(WB_IO2, OUTPUT);
serial_interface.begin(Serial);
@ -945,10 +1291,13 @@ void setup() {
SPIFFS.begin(true);
the_mesh.begin(SPIFFS, trng);
#ifdef BLE_PIN_CODE
#ifdef WIFI_SSID
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
char dev_name[32+10];
sprintf(dev_name, "MeshCore-%s", the_mesh.getNodeName());
serial_interface.begin(dev_name, BLE_PIN_CODE);
serial_interface.begin(dev_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
@ -956,6 +1305,10 @@ void setup() {
#else
#error "need to define filesystem"
#endif
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, the_mesh.getBLEPin());
#endif
}
void loop() {

View file

@ -1,161 +0,0 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <SPIFFS.h>
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/RadioLibWrappers.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
/* ------------------------------ Config -------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 125
#endif
#ifndef LORA_SF
#define LORA_SF 9
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifdef HELTEC_LORA_V3
#include <helpers/HeltecV3Board.h>
static HeltecV3Board board;
#else
#error "need to provide a 'board' object"
#endif
/* ------------------------------ Code -------------------------------- */
class MyMesh : public mesh::Mesh {
uint32_t last_advert_timestamp = 0;
mesh::Identity server_id;
uint8_t server_secret[PUB_KEY_SIZE];
int server_path_len = -1;
uint8_t server_path[MAX_PATH_SIZE];
bool got_adv = false;
protected:
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override {
if (memcmp(app_data, "PING", 4) == 0) {
Serial.println("Received advertisement from a PING server");
// check for replay attacks
if (timestamp > last_advert_timestamp) {
last_advert_timestamp = timestamp;
server_id = id;
self_id.calcSharedSecret(server_secret, id); // calc ECDH shared secret
got_adv = true;
}
}
}
int searchPeersByHash(const uint8_t* hash) override {
if (got_adv && server_id.isHashMatch(hash)) {
return 1;
}
return 0; // not found
}
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
// lookup pre-calculated shared_secret
memcpy(dest_secret, server_secret, PUB_KEY_SIZE);
}
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_RESPONSE) {
Serial.println("Received PING Reply!");
if (packet->isRouteFlood()) {
// let server know path TO here, so they can use sendDirect() for future ping responses
mesh::Packet* path = createPathReturn(server_id, secret, packet->path, packet->path_len, 0, NULL, 0);
if (path) sendFlood(path);
}
}
}
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
// must be from server_id
Serial.printf("PATH to server, path_len=%d\n", (uint32_t) path_len);
memcpy(server_path, path, server_path_len = path_len); // store a copy of path, for sendDirect()
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
Serial.println("Received PING Reply!");
}
return true; // send reciprocal path if necessary
}
public:
MyMesh(mesh::Radio& radio, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables)
{
}
void sendPingPacket() {
if (!got_adv) return; // have not received Advert yet
uint32_t now = getRTCClock()->getCurrentTime(); // important, need timestamp in packet, so that packet_hash will be unique
mesh::Packet* ping = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, server_id, server_secret, (uint8_t *) &now, sizeof(now));
if (ping) {
if (server_path_len < 0) {
sendFlood(ping);
} else {
sendDirect(ping, server_path, server_path_len);
}
}
}
};
SPIClass spi;
StdRNG fast_rng;
SimpleMeshTables tables;
SX1262 radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
MyMesh the_mesh(*new RadioLibWrapper(radio, board), fast_rng, *new VolatileRTCClock(), tables);
unsigned long nextPing;
void halt() {
while (1) ;
}
void setup() {
Serial.begin(115200);
board.begin();
#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#else
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#endif
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
halt();
}
fast_rng.begin(radio.random(0x7FFFFFFF));
the_mesh.begin();
RadioNoiseListener true_rng(radio);
the_mesh.self_id = mesh::LocalIdentity(&true_rng); // create new random identity
nextPing = 0;
}
void loop() {
if (the_mesh.millisHasNowPassed(nextPing)) {
the_mesh.sendPingPacket();
nextPing = the_mesh.futureMillis(10000); // attempt ping every 10 seconds
}
the_mesh.loop();
}

View file

@ -1,194 +0,0 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <SPIFFS.h>
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/RadioLibWrappers.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
/* ------------------------------ Config -------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 125
#endif
#ifndef LORA_SF
#define LORA_SF 9
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifdef HELTEC_LORA_V3
#include <helpers/HeltecV3Board.h>
static HeltecV3Board board;
#else
#error "need to provide a 'board' object"
#endif
/* ------------------------------ Code -------------------------------- */
struct ClientInfo {
mesh::Identity id;
uint32_t last_timestamp;
uint8_t secret[PUB_KEY_SIZE];
int out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
};
#define MAX_CLIENTS 4
class MyMesh : public mesh::Mesh {
int num_clients;
ClientInfo known_clients[MAX_CLIENTS];
ClientInfo* putClient(const mesh::Identity& id) {
for (int i = 0; i < num_clients; i++) {
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
}
if (num_clients < MAX_CLIENTS) {
auto newClient = &known_clients[num_clients++];
newClient->id = id;
newClient->out_path_len = -1; // initially out_path is unknown
newClient->last_timestamp = 0;
self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret
return newClient;
}
return NULL; // table is full
}
protected:
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_ANON_REQ) { // received a PING!
uint32_t timestamp;
memcpy(&timestamp, data, 4);
auto client = putClient(sender); // add to known clients (if not already known)
if (client == NULL || timestamp <= client->last_timestamp) {
return; // FATAL: client table is full -OR- replay attack
}
client->last_timestamp = timestamp;
uint32_t now = getRTCClock()->getCurrentTime(); // response packets always prefixed with timestamp
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the Ping response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, (uint8_t *) &now, sizeof(now));
if (path) sendFlood(path);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, (uint8_t *) &now, sizeof(now));
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
} else {
sendFlood(reply);
}
}
}
}
}
int matching_peer_indexes[MAX_CLIENTS];
int searchPeersByHash(const uint8_t* hash) override {
int n = 0;
for (int i = 0; i < num_clients; i++) {
if (known_clients[i].id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
return n;
}
// not needed for this example, but for sake of 'completeness' of Mesh impl
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
if (peer_idx >= 0 && peer_idx < MAX_CLIENTS) {
// lookup pre-calculated shared_secret
int i = matching_peer_indexes[peer_idx];
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("Invalid peer_idx: %d", peer_idx);
}
}
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
if (sender_idx >= 0 && sender_idx < MAX_CLIENTS) {
Serial.printf("PATH to client, path_len=%d\n", (uint32_t) path_len);
// TODO: prevent replay attacks
int i = matching_peer_indexes[sender_idx];
if (i >= 0 && i < num_clients) {
auto client = &known_clients[i]; // get from our known_clients table (sender SHOULD already be known in this context)
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
}
} else {
MESH_DEBUG_PRINTLN("Invalid sender_idx: %d", sender_idx);
}
// NOTE: no reciprocal path send!!
return false;
}
public:
MyMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(16), tables)
{
num_clients = 0;
}
};
SPIClass spi;
StdRNG fast_rng;
SimpleMeshTables tables;
SX1262 radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
MyMesh the_mesh(*new RadioLibWrapper(radio, board), *new ArduinoMillis(), fast_rng, *new VolatileRTCClock(), tables);
unsigned long nextAnnounce;
void halt() {
while (1) ;
}
void setup() {
Serial.begin(115200);
board.begin();
#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#else
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#endif
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
halt();
}
fast_rng.begin(radio.random(0x7FFFFFFF));
the_mesh.begin();
RadioNoiseListener true_rng(radio);
the_mesh.self_id = mesh::LocalIdentity(&true_rng); // create new random identity
nextAnnounce = 0;
}
void loop() {
if (the_mesh.millisHasNowPassed(nextAnnounce)) {
mesh::Packet* pkt = the_mesh.createAdvert(the_mesh.self_id, (const uint8_t *)"PING", 4);
if (pkt) the_mesh.sendFlood(pkt);
nextAnnounce = the_mesh.futureMillis(30000); // announce every 30 seconds (test only, don't do in production!)
}
the_mesh.loop();
// TODO: periodically check for OLD entries in known_clients[], and evict
}

View file

@ -0,0 +1,79 @@
#include "UITask.h"
#include <Arduino.h>
#define AUTO_OFF_MILLIS 20000 // 20 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(const char* node_name, const char* build_date) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_name = node_name;
_build_date = build_date;
_display->turnOn();
}
void UITask::renderCurrScreen() {
char tmp[80];
// render 'home' screen
_display->drawXbm(0, 0, meshcore_logo, 128, 13);
_display->setCursor(0, 20);
_display->setTextSize(1);
_display->print(_node_name);
sprintf(tmp, "Build: %s", _build_date);
_display->setCursor(0, 32);
_display->print(tmp);
_display->setCursor(0, 43);
_display->print("< Repeater >");
//_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);
//_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr);
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
const char* _node_name;
const char* _build_date;
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(const char* node_name, const char* build_date);
void loop();
};

View file

@ -13,13 +13,21 @@
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <RTClib.h>
/* ------------------------------ Config -------------------------------- */
#define FIRMWARE_VER_TEXT "v4 (build: 17 Feb 2025)"
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "3 Mar 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.0.0"
#endif
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
@ -51,8 +59,6 @@
#define ADMIN_PASSWORD "password"
#endif
#define MIN_LOCAL_ADVERT_INTERVAL 60
#if defined(HELTEC_LORA_V3)
#include <helpers/HeltecV3Board.h>
#include <helpers/CustomSX1262Wrapper.h>
@ -70,27 +76,42 @@
#include <helpers/ESP32Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static ESP32Board board;
#elif defined(LILYGO_TLORA)
#include <helpers/LilyGoTLoraBoard.h>
#include <helpers/CustomSX1276Wrapper.h>
static LilyGoTLoraBoard board;
#elif defined(RAK_4631)
#include <helpers/nrf52/RAK4631Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static RAK4631Board board;
#elif defined(HELTEC_T114)
#include <helpers/nrf52/T114Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static T114Board board;
#elif defined(LILYGO_TECHO)
#include <helpers/nrf52/TechoBoard.h>
#include <helpers/CustomSX1262Wrapper.h>
static TechoBoard board;
#else
#error "need to provide a 'board' object"
#endif
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
static DISPLAY_CLASS display;
#include "UITask.h"
static UITask ui_task(display);
#endif
#define PACKET_LOG_FILE "/packet_log"
/* ------------------------------ Code -------------------------------- */
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
while (*sp && *sp >= '0' && *sp <= '9') {
n *= 10;
n += (*sp++ - '0');
}
return n;
}
#define CMD_GET_STATUS 0x01
#define CMD_GET_STATS 0x01
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
struct RepeaterStats {
uint16_t batt_milli_volts;
@ -103,12 +124,14 @@ struct RepeaterStats {
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint32_t n_full_events;
uint16_t n_full_events;
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
};
struct ClientInfo {
mesh::Identity id;
uint32_t last_timestamp;
uint32_t last_timestamp, last_activity;
uint8_t secret[PUB_KEY_SIZE];
bool is_admin;
int8_t out_path_len;
@ -120,52 +143,42 @@ struct ClientInfo {
// NOTE: need to space the ACK and the reply text apart (in CLI)
#define CLI_REPLY_DELAY_MILLIS 1500
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
uint8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t unused;
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
};
class MyMesh : public mesh::Mesh {
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RadioLibWrapper* my_radio;
FILESYSTEM* _fs;
RADIO_CLASS* _phy;
mesh::MainBoard* _board;
unsigned long next_local_advert;
bool _logging;
NodePrefs _prefs;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
int num_clients;
ClientInfo known_clients[MAX_CLIENTS];
ClientInfo* putClient(const mesh::Identity& id) {
for (int i = 0; i < num_clients; i++) {
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = &known_clients[0];
for (int i = 0; i < MAX_CLIENTS; i++) {
if (known_clients[i].last_activity < min_time) {
oldest = &known_clients[i];
min_time = oldest->last_activity;
}
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
}
if (num_clients < MAX_CLIENTS) {
auto newClient = &known_clients[num_clients++];
newClient->id = id;
newClient->out_path_len = -1; // initially out_path is unknown
newClient->last_timestamp = 0;
self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret
return newClient;
}
return NULL; // table is full
oldest->id = id;
oldest->out_path_len = -1; // initially out_path is unknown
oldest->last_timestamp = 0;
self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret
return oldest;
}
int handleRequest(ClientInfo* sender, uint8_t* payload, size_t payload_len) {
uint32_t now = getRTCClock()->getCurrentTime();
int handleRequest(ClientInfo* sender, uint8_t* payload, size_t payload_len) {
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
switch (payload[0]) {
case CMD_GET_STATS: { // guests can also access this now
case CMD_GET_STATUS: { // guests can also access this now
RepeaterStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundCount();
@ -180,6 +193,9 @@ class MyMesh : public mesh::Mesh {
stats.n_recv_flood = getNumRecvFlood();
stats.n_recv_direct = getNumRecvDirect();
stats.n_full_events = getNumFullEvents();
stats.last_snr = (int16_t)(my_radio->getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
memcpy(&reply_data[4], &stats, sizeof(stats));
@ -190,20 +206,6 @@ class MyMesh : public mesh::Mesh {
return 0; // reply_len
}
void checkAdvertInterval() {
if (_prefs.advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
_prefs.advert_interval = 0; // turn it off, now that device has been manually configured
}
}
void updateAdvertTimer() {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
} else {
next_local_advert = 0; // stop the timer
}
}
mesh::Packet* createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@ -215,6 +217,14 @@ class MyMesh : public mesh::Mesh {
return createAdvert(self_id, app_data, app_data_len);
}
File openAppend(const char* fname) {
#if defined(NRF52_PLATFORM)
return _fs->open(fname, FILE_O_WRITE);
#else
return _fs->open(fname, "a", true);
#endif
}
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
@ -224,6 +234,63 @@ protected:
return !_prefs.disable_fwd;
}
const char* getLogDateTime() override {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year());
return tmp;
}
void logRx(mesh::Packet* pkt, int len, float score) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void logTx(mesh::Packet* pkt, int len) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void logTxFail(mesh::Packet* pkt, int len) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
f.close();
}
}
}
int calcRxDelay(float score, uint32_t air_time) const override {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
@ -233,6 +300,10 @@ protected:
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
@ -240,39 +311,48 @@ protected:
memcpy(&timestamp, data, 4);
bool is_admin;
if (memcmp(&data[4], _prefs.password, strlen(_prefs.password)) == 0) { // check for valid password
data[len] = 0; // ensure null terminator
if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password
is_admin = true;
} else if (memcmp(&data[4], _prefs.guest_password, strlen(_prefs.guest_password)) == 0) { // check guest password
} else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password
is_admin = false;
} else {
#if MESH_DEBUG
data[len] = 0; // ensure null terminator
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
#endif
return;
}
auto client = putClient(sender); // add to known clients (if not already known)
if (client == NULL || timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Client table full, or replay attack!");
return; // FATAL: client table is full -OR- replay attack
if (timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return; // FATAL: client table is full -OR- replay attack
}
MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
client->is_admin = is_admin;
uint32_t now = getRTCClock()->getCurrentTime();
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
memcpy(&reply_data[4], "OK", 2);
#if 0
memcpy(&reply_data[4], "OK", 2); // legacy response
#else
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
reply_data[6] = is_admin ? 1 : 0;
reply_data[7] = 0; // FUTURE: reserved
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
#endif
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 4 + 2);
PAYLOAD_TYPE_RESPONSE, reply_data, 12);
if (path) sendFlood(path);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 4 + 2);
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
@ -288,7 +368,7 @@ protected:
int searchPeersByHash(const uint8_t* hash) override {
int n = 0;
for (int i = 0; i < num_clients; i++) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (known_clients[i].id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
@ -298,7 +378,7 @@ protected:
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_clients) {
if (i >= 0 && i < MAX_CLIENTS) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
} else {
@ -308,7 +388,7 @@ protected:
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_clients) { // get from our known_clients table (sender SHOULD already be known in this context)
if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
return;
}
@ -317,11 +397,12 @@ protected:
uint32_t timestamp;
memcpy(&timestamp, data, 4);
if (timestamp > client->last_timestamp) { // prevent replay attacks
if (timestamp > client->last_timestamp) { // prevent replay attacks
int reply_len = handleRequest(client, &data[4], len - 4);
if (reply_len == 0) return; // invalid command
client->last_timestamp = timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
@ -348,38 +429,43 @@ protected:
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
bool is_retry = (sender_timestamp == client->last_timestamp);
client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE);
if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE);
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack);
} else {
sendDirect(ack, client->out_path, client->out_path_len);
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack);
} else {
sendDirect(ack, client->out_path, client->out_path_len);
}
}
}
uint8_t temp[166];
handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
if (is_retry) {
temp[0] = 0;
} else {
_cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
}
int text_len = strlen((char *) &temp[5]);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTime();
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_PLAIN << 2); // TODO: change this to TXT_TYPE_CLI_DATA soon
// calc expected ACK reply
//mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) {
@ -400,7 +486,7 @@ protected:
// TODO: prevent replay attacks
int i = matching_peer_indexes[sender_idx];
if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context)
if (i >= 0 && i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len);
auto client = &known_clients[i];
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
@ -413,61 +499,69 @@ protected:
}
public:
MyMesh(mesh::MainBoard& board, RadioLibWrapper& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _board(&board)
MyMesh(RADIO_CLASS& phy, mesh::MainBoard& board, RadioLibWrapper& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_phy(&phy), _board(&board), _cli(board, this, &_prefs, this)
{
my_radio = &radio;
num_clients = 0;
memset(known_clients, 0, sizeof(known_clients));
next_local_advert = 0;
_logging = false;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f
strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)-1);
_prefs.node_name[sizeof(_prefs.node_name)-1] = 0; // truncate if necessary
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password)-1);
_prefs.password[sizeof(_prefs.password)-1] = 0; // truncate if necessary
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
}
float getFreqPref() const { return _prefs.freq; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
CommonCLI* getCLI() { return &_cli; }
void begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
if (_fs->exists("/node_prefs")) {
File file = _fs->open("/node_prefs");
if (file) {
file.read((uint8_t *) &_prefs, sizeof(_prefs));
file.close();
}
}
_cli.loadPrefs(_fs);
_phy->setFrequency(_prefs.freq);
_phy->setSpreadingFactor(_prefs.sf);
_phy->setBandwidth(_prefs.bw);
_phy->setCodingRate(_prefs.cr);
_phy->setOutputPower(_prefs.tx_power_dbm);
updateAdvertTimer();
}
void savePrefs() {
#if defined(NRF52_PLATFORM)
File file = _fs->open("/node_prefs", FILE_O_WRITE);
if (file) { file.seek(0); file.truncate(); }
#else
File file = _fs->open("/node_prefs", "w", true);
#endif
if (file) {
file.write((const uint8_t *)&_prefs, sizeof(_prefs));
file.close();
}
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getNodeName() { return _prefs.node_name; }
void savePrefs() override {
_cli.savePrefs(_fs);
}
void sendSelfAdvertisement(int delay_millis) {
bool formatFileSystem() override {
#if defined(NRF52_PLATFORM)
return InternalFS.format();
#elif defined(ESP32)
return SPIFFS.format();
#else
#error "need to implement file system erase"
return false;
#endif
}
void sendSelfAdvertisement(int delay_millis) override {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
@ -476,6 +570,36 @@ public:
}
}
void updateAdvertTimer() override {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
} else {
next_local_advert = 0; // stop the timer
}
}
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() override {
File f = _fs->open(PACKET_LOG_FILE);
if (f) {
while (f.available()) {
int c = f.read();
if (c < 0) break;
Serial.print((char)c);
}
f.close();
}
}
void setTxPower(uint8_t power_dbm) override {
_phy->setOutputPower(power_dbm);
}
void loop() {
mesh::Mesh::loop();
@ -487,141 +611,17 @@ public:
updateAdvertTimer(); // schedule next local advert
}
}
void handleCommand(uint32_t sender_timestamp, const char* command, char reply[]) {
while (*command == ' ') command++; // skip leading spaces
if (memcmp(command, "reboot", 6) == 0) {
board.reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
sendSelfAdvertisement(400);
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (sender_timestamp > curr) {
getRTCClock()->setCurrentTime(sender_timestamp + 1);
strcpy(reply, "OK - clock set");
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
} else if (memcmp(command, "start ota", 9) == 0) {
if (_board->startOTAUpdate()) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error");
}
} else if (memcmp(command, "clock", 5) == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds)
uint32_t secs = _atoi(&command[5]);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs > curr) {
getRTCClock()->setCurrentTime(secs);
strcpy(reply, "(OK - clock set!)");
} else {
strcpy(reply, "(ERR: clock cannot go backwards)");
}
} else if (memcmp(command, "password ", 9) == 0) {
// change admin password
strncpy(_prefs.password, &command[9], sizeof(_prefs.password)-1);
_prefs.password[sizeof(_prefs.password)-1] = 0; // truncate if necesary
checkAdvertInterval();
savePrefs();
sprintf(reply, "password now: %s", _prefs.password); // echo back just to let admin know for sure!!
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
_prefs.airtime_factor = atof(&config[3]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "advert.interval ", 16) == 0) {
int mins = _atoi(&config[16]);
if (mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) {
sprintf(reply, "Error: min is %d mins", MIN_LOCAL_ADVERT_INTERVAL);
} else if (mins > 240) {
strcpy(reply, "Error: max is 240 mins");
} else {
_prefs.advert_interval = (uint8_t)(mins / 2);
updateAdvertTimer();
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "guest.password ", 15) == 0) {
strncpy(_prefs.guest_password, &config[15], sizeof(_prefs.guest_password)-1);
_prefs.guest_password[sizeof(_prefs.guest_password)-1] = 0; // truncate if necessary
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "name ", 5) == 0) {
strncpy(_prefs.node_name, &config[5], sizeof(_prefs.node_name)-1);
_prefs.node_name[sizeof(_prefs.node_name)-1] = 0; // truncate if nec
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "repeat ", 7) == 0) {
_prefs.disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
strcpy(reply, _prefs.disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs.node_lat = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs.node_lon = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float db = atof(&config[8]);
if (db >= 0) {
_prefs.rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "txdelay ", 8) == 0) {
float f = atof(&config[8]);
if (f >= 0) {
_prefs.tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs.tx_power_dbm = atoi(&config[3]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
_prefs.freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
sprintf(reply, "unknown config: %s", config);
}
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
#if defined(NRF52_PLATFORM)
bool s = InternalFS.format();
#elif defined(ESP32)
bool s = SPIFFS.format();
#else
#error "need to implement file system erase"
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
} else if (memcmp(command, "ver", 3) == 0) {
strcpy(reply, FIRMWARE_VER_TEXT);
} else {
sprintf(reply, "Unknown: %s (commands: reboot, advert, clock, set, ver, password, start ota)", command);
}
}
};
#if defined(NRF52_PLATFORM)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#elif defined(LILYGO_TLORA)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#elif defined(P_LORA_SCLK)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
@ -632,12 +632,13 @@ StdRNG fast_rng;
SimpleMeshTables tables;
#ifdef ESP32
ESP32RTCClock rtc_clock;
ESP32RTCClock fallback_clock;
#else
VolatileRTCClock rtc_clock;
VolatileRTCClock fallback_clock;
#endif
AutoDiscoverRTCClock rtc_clock(fallback_clock);
MyMesh the_mesh(board, *new WRAPPER_CLASS(radio, board), *new ArduinoMillis(), fast_rng, rtc_clock, tables);
MyMesh the_mesh(radio, board, *new WRAPPER_CLASS(radio, board), *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
@ -651,8 +652,9 @@ void setup() {
board.begin();
#ifdef ESP32
rtc_clock.begin();
fallback_clock.begin();
#endif
rtc_clock.begin(Wire);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
@ -660,6 +662,10 @@ void setup() {
float tcxo = 1.6f;
#endif
#ifdef DISPLAY_CLASS
display.begin();
#endif
#if defined(NRF52_PLATFORM)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
@ -712,12 +718,9 @@ void setup() {
the_mesh.begin(fs);
if (LORA_FREQ != the_mesh.getFreqPref()) {
radio.setFrequency(the_mesh.getFreqPref());
}
if (LORA_TX_POWER != the_mesh.getTxPowerPref()) {
radio.setOutputPower(the_mesh.getTxPowerPref());
}
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(2000);
@ -727,7 +730,7 @@ void loop() {
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
if (c != '\n') {
if (c != '\n') {
command[len++] = c;
command[len] = 0;
}
@ -740,7 +743,7 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@ -749,6 +752,4 @@ void loop() {
}
the_mesh.loop();
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
}

View file

@ -0,0 +1,79 @@
#include "UITask.h"
#include <Arduino.h>
#define AUTO_OFF_MILLIS 20000 // 20 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(const char* node_name, const char* build_date) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_name = node_name;
_build_date = build_date;
_display->turnOn();
}
void UITask::renderCurrScreen() {
char tmp[80];
// render 'home' screen
_display->drawXbm(0, 0, meshcore_logo, 128, 13);
_display->setCursor(0, 20);
_display->setTextSize(1);
_display->print(_node_name);
sprintf(tmp, "Build: %s", _build_date);
_display->setCursor(0, 32);
_display->print(tmp);
_display->setCursor(0, 43);
_display->print("< Room Server >");
//_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);
//_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr);
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
const char* _node_name;
const char* _build_date;
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(const char* node_name, const char* build_date);
void loop();
};

View file

@ -13,13 +13,21 @@
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <RTClib.h>
/* ------------------------------ Config -------------------------------- */
#define FIRMWARE_VER_TEXT "v4 (build: 17 Feb 2025)"
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Mar 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.1.0"
#endif
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
@ -59,9 +67,6 @@
#define MAX_UNSYNCED_POSTS 16
#endif
#define MIN_LOCAL_ADVERT_INTERVAL 8
#if defined(HELTEC_LORA_V3)
#include <helpers/HeltecV3Board.h>
#include <helpers/CustomSX1262Wrapper.h>
@ -75,6 +80,10 @@
#include <helpers/ESP32Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static ESP32Board board;
#elif defined(LILYGO_TLORA)
#include <helpers/LilyGoTLoraBoard.h>
#include <helpers/CustomSX1276Wrapper.h>
static LilyGoTLoraBoard board;
#elif defined(RAK_4631)
#include <helpers/nrf52/RAK4631Board.h>
#include <helpers/CustomSX1262Wrapper.h>
@ -83,17 +92,16 @@
#error "need to provide a 'board' object"
#endif
/* ------------------------------ Code -------------------------------- */
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
while (*sp && *sp >= '0' && *sp <= '9') {
n *= 10;
n += (*sp++ - '0');
}
return n;
}
static DISPLAY_CLASS display;
#include "UITask.h"
static UITask ui_task(display);
#endif
/* ------------------------------ Code -------------------------------- */
struct ClientInfo {
mesh::Identity id;
@ -128,31 +136,19 @@ struct PostInfo {
#define CLIENT_KEEP_ALIVE_SECS 128
#define REQ_TYPE_KEEP_ALIVE 1
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
uint8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes
uint8_t unused;
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
};
class MyMesh : public mesh::Mesh {
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RadioLibWrapper* my_radio;
FILESYSTEM* _fs;
RADIO_CLASS* _phy;
mesh::MainBoard* _board;
unsigned long next_local_advert;
NodePrefs _prefs;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
int num_clients;
ClientInfo known_clients[MAX_CLIENTS];
@ -197,11 +193,9 @@ class MyMesh : public mesh::Mesh {
void addPost(ClientInfo* client, const char* postData) {
// TODO: suggested postData format: <title>/<descrption>
posts[next_post_idx].author = client->id; // add to cyclic queue
strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
posts[next_post_idx].text[MAX_POST_TEXT_LEN] = 0;
StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTime();
// TODO: only post at maximum of ONE PER SECOND, so that post_timestamps are UNIQUE!!
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS;
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);
@ -219,7 +213,7 @@ class MyMesh : public mesh::Mesh {
memcpy(&reply_data[len], post.text, text_len); len += text_len;
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, self_id.pub_key, PUB_KEY_SIZE);
mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
client->push_post_timestamp = post.post_timestamp;
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len);
@ -250,20 +244,6 @@ class MyMesh : public mesh::Mesh {
return false;
}
void checkAdvertInterval() {
if (_prefs.advert_interval < MIN_LOCAL_ADVERT_INTERVAL) {
_prefs.advert_interval = 0; // turn it off, now that device has been manually configured
}
}
void updateAdvertTimer() {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis(_prefs.advert_interval * 60 * 1000);
} else {
next_local_advert = 0; // stop the timer
}
}
mesh::Packet* createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@ -285,10 +265,22 @@ protected:
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
const char* getLogDateTime() override {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year());
return tmp;
}
uint32_t getRetransmitDelay(const mesh::Packet* packet) override {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
bool allowPacketForward(const mesh::Packet* packet) override {
return !_prefs.disable_fwd;
@ -301,12 +293,12 @@ protected:
memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp
bool is_admin;
if (memcmp(&data[8], _prefs.password, strlen(_prefs.password)) == 0) { // check for valid admin password
data[len] = 0; // ensure null terminator
if (strcmp((char *) &data[8], _prefs.password) == 0) { // check for valid admin password
is_admin = true;
} else {
is_admin = false;
int len = strlen(_prefs.guest_password);
if (len > 0 && memcmp(&data[8], _prefs.guest_password, len) != 0) { // check the room/public password
if (strcmp((char *) &data[8], _prefs.guest_password) != 0) { // check the room/public password
MESH_DEBUG_PRINTLN("Incorrect room password");
return; // no response. Client will timeout
}
@ -328,14 +320,17 @@ protected:
uint32_t now = getRTCClock()->getCurrentTime();
client->last_activity = now;
now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
// TODO: maybe reply with count of messages waiting to be synced for THIS client?
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16)
reply_data[6] = 0; // FUTURE: reserved
reply_data[6] = is_admin ? 1 : 0;
reply_data[7] = 0; // FUTURE: reserved
memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
@ -390,10 +385,11 @@ protected:
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
bool is_retry = (sender_timestamp == client->last_timestamp);
client->last_timestamp = sender_timestamp;
uint32_t now = getRTCClock()->getCurrentTime();
uint32_t now = getRTCClock()->getCurrentTimeUnique();
client->last_activity = now;
client->push_failures = 0; // reset so push can resume (if prev failed)
@ -407,18 +403,26 @@ protected:
bool send_ack;
if (flags == TXT_TYPE_CLI_DATA) {
if (client->is_admin) {
handleAdminCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
send_ack = true;
if (is_retry) {
temp[5] = 0; // no reply
} else {
_cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
}
send_ack = false;
} else {
temp[5] = 0; // no reply
send_ack = false; // and no ACK... user shoudn't be sending these
}
} else { // TXT_TYPE_PLAIN
addPost(client, (const char *) &data[5]);
if (!is_retry) {
addPost(client, (const char *) &data[5]);
}
temp[5] = 0; // no reply (ACK is enough)
send_ack = true;
}
uint32_t delay_millis;
if (send_ack) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
@ -428,6 +432,9 @@ protected:
sendDirect(ack, client->out_path, client->out_path_len);
}
}
delay_millis = REPLY_DELAY_MILLIS;
} else {
delay_millis = 0;
}
int text_len = strlen((char *) &temp[5]);
@ -437,7 +444,6 @@ protected:
now++;
}
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_PLAIN << 2); // attempt and flags
// calc expected ACK reply
//mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
@ -445,9 +451,9 @@ protected:
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply, REPLY_DELAY_MILLIS);
sendFlood(reply, delay_millis);
} else {
sendDirect(reply, client->out_path, client->out_path_len, REPLY_DELAY_MILLIS);
sendDirect(reply, client->out_path, client->out_path_len, delay_millis);
}
}
}
@ -461,17 +467,17 @@ protected:
uint32_t forceSince = 0;
if (len >= 9) { // optional - last post_timestamp client received
memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING!
} else {
memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below)
}
if (forceSince > 0) {
client->sync_since = forceSince; // force-update the 'sync since'
len = 9; // for ACK hash calc below
} else {
len = 5; // for ACK hash calc below
}
uint32_t now = getRTCClock()->getCurrentTime();
client->last_activity = now; // <-- THIS will keep client connection alive
client->push_failures = 0; // reset so push can resume (if prev failed)
client->pending_ack = 0;
// TODO: Throttle KEEP_ALIVE requests!
// if client sends too quickly, evict()
@ -479,7 +485,7 @@ protected:
// RULE: only send keep_alive response DIRECT!
if (client->out_path_len >= 0) {
uint32_t ack_hash; // calc ACK to prove to sender that we got request
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, len, client->id.pub_key, PUB_KEY_SIZE);
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE);
auto reply = createAck(ack_hash);
if (reply) {
@ -518,8 +524,9 @@ protected:
}
public:
MyMesh(mesh::MainBoard& board, RadioLibWrapper& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _board(&board)
MyMesh(RADIO_CLASS& phy, mesh::MainBoard& board, RadioLibWrapper& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_phy(&phy), _board(&board), _cli(board, this, &_prefs, this)
{
my_radio = &radio;
next_local_advert = 0;
@ -529,19 +536,19 @@ public:
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)-1);
_prefs.node_name[sizeof(_prefs.node_name)-1] = 0; // truncate if necessary
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password)-1);
_prefs.password[sizeof(_prefs.password)-1] = 0; // truncate if necessary
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1;
_prefs.advert_interval = 2; // default to 2 minutes for NEW installs
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
#ifdef ROOM_PASSWORD
strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)-1);
_prefs.guest_password[sizeof(_prefs.guest_password)-1] = 0; // truncate if necessary
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
num_clients = 0;
@ -551,38 +558,43 @@ public:
memset(posts, 0, sizeof(posts));
}
float getFreqPref() const { return _prefs.freq; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
CommonCLI* getCLI() { return &_cli; }
void begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
if (_fs->exists("/node_prefs")) {
File file = _fs->open("/node_prefs");
if (file) {
file.read((uint8_t *) &_prefs, sizeof(_prefs));
file.close();
}
}
_cli.loadPrefs(_fs);
_phy->setFrequency(_prefs.freq);
_phy->setSpreadingFactor(_prefs.sf);
_phy->setBandwidth(_prefs.bw);
_phy->setCodingRate(_prefs.cr);
_phy->setOutputPower(_prefs.tx_power_dbm);
updateAdvertTimer();
}
void savePrefs() {
#if defined(NRF52_PLATFORM)
File file = _fs->open("/node_prefs", FILE_O_WRITE);
if (file) { file.seek(0); file.truncate(); }
#else
File file = _fs->open("/node_prefs", "w", true);
#endif
if (file) {
file.write((const uint8_t *)&_prefs, sizeof(_prefs));
file.close();
}
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getNodeName() { return _prefs.node_name; }
void savePrefs() override {
_cli.savePrefs(_fs);
}
void sendSelfAdvertisement(int delay_millis) {
bool formatFileSystem() override {
#if defined(NRF52_PLATFORM)
return InternalFS.format();
#elif defined(ESP32)
return SPIFFS.format();
#else
#error "need to implement file system erase"
return false;
#endif
}
void sendSelfAdvertisement(int delay_millis) override {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
@ -591,123 +603,22 @@ public:
}
}
void handleAdminCommand(uint32_t sender_timestamp, const char* command, char reply[]) {
while (*command == ' ') command++; // skip leading spaces
if (memcmp(command, "reboot", 6) == 0) {
board.reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
sendSelfAdvertisement(400);
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (sender_timestamp > curr) {
getRTCClock()->setCurrentTime(sender_timestamp + 1);
strcpy(reply, "OK - clock set");
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
} else if (memcmp(command, "start ota", 9) == 0) {
if (_board->startOTAUpdate()) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error");
}
} else if (memcmp(command, "clock", 5) == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds)
uint32_t secs = _atoi(&command[5]);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs > curr) {
getRTCClock()->setCurrentTime(secs);
strcpy(reply, "(OK - clock set!)");
} else {
strcpy(reply, "(ERR: clock cannot go backwards)");
}
} else if (memcmp(command, "password ", 9) == 0) {
// change admin password
strncpy(_prefs.password, &command[9], sizeof(_prefs.password)-1);
_prefs.password[sizeof(_prefs.password)-1] = 0; // truncate if necesary
savePrefs();
sprintf(reply, "password now: %s", _prefs.password); // echo back just to let admin know for sure!!
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
_prefs.airtime_factor = atof(&config[3]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "advert.interval ", 16) == 0) {
int mins = _atoi(&config[16]);
if (mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) {
sprintf(reply, "Error: min is %d mins", MIN_LOCAL_ADVERT_INTERVAL);
} else if (mins > 120) {
strcpy(reply, "Error: max is 120 mins");
} else {
_prefs.advert_interval = (uint8_t)mins;
updateAdvertTimer();
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "guest.password ", 15) == 0) {
strncpy(_prefs.guest_password, &config[15], sizeof(_prefs.guest_password)-1);
_prefs.guest_password[sizeof(_prefs.guest_password)-1] = 0; // truncate if necessary
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "name ", 5) == 0) {
strncpy(_prefs.node_name, &config[5], sizeof(_prefs.node_name)-1);
_prefs.node_name[sizeof(_prefs.node_name)-1] = 0; // truncate if nec
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "repeat ", 7) == 0) {
_prefs.disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
strcpy(reply, _prefs.disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs.node_lat = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs.node_lon = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float db = atof(&config[8]);
if (db >= 0) {
_prefs.rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "txdelay ", 8) == 0) {
float f = atof(&config[8]);
if (f >= 0) {
_prefs.tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs.tx_power_dbm = atoi(&config[3]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
_prefs.freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
sprintf(reply, "unknown config: %s", config);
}
} else if (memcmp(command, "ver", 3) == 0) {
strcpy(reply, FIRMWARE_VER_TEXT);
void updateAdvertTimer() override {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
} else {
strcpy(reply, "?"); // unknown command
next_local_advert = 0; // stop the timer
}
}
void setLoggingOn(bool enable) override { /* no-op */ }
void eraseLogFile() override { /* no-op */ }
void dumpLogFile() override { /* no-op */ }
void setTxPower(uint8_t power_dbm) override {
_phy->setOutputPower(power_dbm);
}
void loop() {
mesh::Mesh::loop();
@ -752,12 +663,19 @@ public:
updateAdvertTimer(); // schedule next local advert
}
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
}
};
#if defined(NRF52_PLATFORM)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#elif defined(LILYGO_TLORA)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#elif defined(P_LORA_SCLK)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
@ -768,12 +686,13 @@ StdRNG fast_rng;
SimpleMeshTables tables;
#ifdef ESP32
ESP32RTCClock rtc_clock;
ESP32RTCClock fallback_clock;
#else
VolatileRTCClock rtc_clock;
VolatileRTCClock fallback_clock;
#endif
AutoDiscoverRTCClock rtc_clock(fallback_clock);
MyMesh the_mesh(board, *new WRAPPER_CLASS(radio, board), *new ArduinoMillis(), fast_rng, rtc_clock, tables);
MyMesh the_mesh(radio, board, *new WRAPPER_CLASS(radio, board), *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
@ -787,8 +706,9 @@ void setup() {
board.begin();
#ifdef ESP32
rtc_clock.begin();
fallback_clock.begin();
#endif
rtc_clock.begin(Wire);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
@ -796,6 +716,10 @@ void setup() {
float tcxo = 1.6f;
#endif
#ifdef DISPLAY_CLASS
display.begin();
#endif
#if defined(NRF52_PLATFORM)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
@ -847,12 +771,9 @@ void setup() {
the_mesh.begin(fs);
if (LORA_FREQ != the_mesh.getFreqPref()) {
radio.setFrequency(the_mesh.getFreqPref());
}
if (LORA_TX_POWER != the_mesh.getTxPowerPref()) {
radio.setOutputPower(the_mesh.getTxPowerPref());
}
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(2000);
@ -875,7 +796,7 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleAdminCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}

View file

@ -66,10 +66,18 @@
#include <helpers/ESP32Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static ESP32Board board;
#elif defined(LILYGO_TLORA)
#include <helpers/LilyGoTLoraBoard.h>
#include <helpers/CustomSX1276Wrapper.h>
static LilyGoTLoraBoard board;
#elif defined(RAK_4631)
#include <helpers/nrf52/RAK4631Board.h>
#include <helpers/CustomSX1262Wrapper.h>
static RAK4631Board board;
#elif defined(T1000_E)
#include <helpers/nrf52/T1000eBoard.h>
#include <helpers/CustomLR1110Wrapper.h>
static T1000eBoard board;
#else
#error "need to provide a 'board' object"
#endif
@ -255,6 +263,11 @@ protected:
}
}
void onCommandDataRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override {
}
void onSignedMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
}
void onChannelMessageRecv(const mesh::GroupChannel& channel, int in_path_len, uint32_t timestamp, const char *text) override {
if (in_path_len < 0) {
Serial.printf("PUBLIC CHANNEL MSG -> (Direct!)\n");
@ -466,8 +479,7 @@ public:
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "name ", 5) == 0) {
strncpy(_prefs.node_name, &config[5], sizeof(_prefs.node_name)-1);
_prefs.node_name[sizeof(_prefs.node_name)-1] = 0; // truncate if nec
StrHelper::strncpy(_prefs.node_name, &config[5], sizeof(_prefs.node_name));
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "lat ", 4) == 0) {
@ -537,6 +549,9 @@ public:
#if defined(NRF52_PLATFORM)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#elif defined(LILYGO_TLORA)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#elif defined(P_LORA_SCLK)
SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);

View file

@ -1,315 +0,0 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <SPIFFS.h>
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 125
#endif
#ifndef LORA_SF
#define LORA_SF 9
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#define ADMIN_PASSWORD "h^(kl@#)"
#ifdef HELTEC_LORA_V3
#include <helpers/HeltecV3Board.h>
static HeltecV3Board board;
#else
#error "need to provide a 'board' object"
#endif
/* -------------------------------------------------------------------------------------- */
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - FROM_HASH_LEN - CIPHER_MAC_SIZE - 1)
#define CMD_GET_STATS 0x01
#define CMD_SET_CLOCK 0x02
#define CMD_SEND_ANNOUNCE 0x03
#define CMD_SET_CONFIG 0x04
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
uint16_t curr_free_queue_len;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint32_t n_full_events;
};
class MyMesh : public mesh::Mesh {
uint32_t last_advert_timestamp = 0;
mesh::Identity server_id;
uint8_t server_secret[PUB_KEY_SIZE];
int server_path_len = -1;
uint8_t server_path[MAX_PATH_SIZE];
bool got_adv = false;
protected:
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override {
if (memcmp(app_data, "repeater:", 9) == 0) {
Serial.println("Received advertisement from a repeater!");
// check for replay attacks
if (timestamp > last_advert_timestamp) {
last_advert_timestamp = timestamp;
server_id = id;
self_id.calcSharedSecret(server_secret, id); // calc ECDH shared secret
got_adv = true;
// 'login' to repeater. (mainly lets it know our public key)
uint32_t now = getRTCClock()->getCurrentTime(); // important, need timestamp in packet, so that packet_hash will be unique
uint8_t temp[4 + 8];
memcpy(temp, &now, 4);
memcpy(&temp[4], ADMIN_PASSWORD, 8);
mesh::Packet* login = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, server_id, server_secret, temp, sizeof(temp));
if (login) sendFlood(login); // server_path won't be known yet
}
}
}
void handleResponse(const uint8_t* reply, size_t reply_len) {
if (reply_len >= 4 + sizeof(RepeaterStats)) { // got an GET_STATS reply from repeater
RepeaterStats stats;
memcpy(&stats, &reply[4], sizeof(stats));
Serial.println("Repeater Stats:");
Serial.printf(" battery: %d mV\n", (uint32_t) stats.batt_milli_volts);
Serial.printf(" tx queue: %d\n", (uint32_t) stats.curr_tx_queue_len);
Serial.printf(" free queue: %d\n", (uint32_t) stats.curr_free_queue_len);
Serial.printf(" last RSSI: %d\n", (int) stats.last_rssi);
Serial.printf(" num recv: %d\n", stats.n_packets_recv);
Serial.printf(" num sent: %d\n", stats.n_packets_sent);
Serial.printf(" air time (secs): %d\n", stats.total_air_time_secs);
Serial.printf(" up time (secs): %d\n", stats.total_up_time_secs);
} else if (reply_len > 4) { // got an SET_* reply from repeater
char tmp[MAX_PACKET_PAYLOAD];
memcpy(tmp, &reply[4], reply_len - 4);
tmp[reply_len - 4] = 0; // make a C string of reply
Serial.print("Reply: "); Serial.println(tmp);
}
}
int searchPeersByHash(const uint8_t* hash) override {
if (got_adv && server_id.isHashMatch(hash)) {
return 1;
}
return 0; // not found
}
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
// lookup pre-calculated shared_secret
memcpy(dest_secret, server_secret, PUB_KEY_SIZE);
}
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_RESPONSE) {
handleResponse(data, len);
if (packet->isRouteFlood()) {
// let server know path TO here, so they can use sendDirect() for future ping responses
mesh::Packet* path = createPathReturn(server_id, secret, packet->path, packet->path_len, 0, NULL, 0);
if (path) sendFlood(path);
}
}
}
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
// must be from server_id
Serial.printf("PATH to repeater, path_len=%d\n", (uint32_t) path_len);
memcpy(server_path, path, server_path_len = path_len); // store a copy of path, for sendDirect()
if (extra_type == PAYLOAD_TYPE_RESPONSE) {
handleResponse(extra, extra_len);
}
return true; // send reciprocal path if necessary
}
public:
MyMesh(mesh::Radio& radio, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables)
{
}
mesh::Packet* createStatsRequest(uint32_t max_age) {
uint8_t payload[9];
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(payload, &now, 4);
payload[4] = CMD_GET_STATS;
memcpy(&payload[5], &max_age, 4);
return createDatagram(PAYLOAD_TYPE_REQ, server_id, server_secret, payload, sizeof(payload));
}
mesh::Packet* createSetClockRequest(uint32_t timestamp) {
uint8_t payload[9];
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(payload, &now, 4);
payload[4] = CMD_SET_CLOCK;
memcpy(&payload[5], &now, 4); // repeated :-(
return createDatagram(PAYLOAD_TYPE_REQ, server_id, server_secret, payload, sizeof(payload));
}
mesh::Packet* createSetAirtimeFactorRequest(float airtime_factor) {
uint8_t payload[16];
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(payload, &now, 4);
payload[4] = CMD_SET_CONFIG;
sprintf((char *) &payload[5], "AF%f", airtime_factor);
return createDatagram(PAYLOAD_TYPE_REQ, server_id, server_secret, payload, sizeof(payload));
}
mesh::Packet* createAnnounceRequest() {
uint8_t payload[5];
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(payload, &now, 4);
payload[4] = CMD_SEND_ANNOUNCE;
return createDatagram(PAYLOAD_TYPE_REQ, server_id, server_secret, payload, sizeof(payload));
}
mesh::Packet* parseCommand(char* command) {
if (strcmp(command, "stats") == 0) {
return createStatsRequest(60*60); // max_age = one hour
} else if (memcmp(command, "setclock ", 9) == 0) {
uint32_t timestamp = atol(&command[9]);
return createSetClockRequest(timestamp);
} else if (memcmp(command, "set AF=", 7) == 0) {
float factor = atof(&command[7]);
return createSetAirtimeFactorRequest(factor);
} else if (strcmp(command, "ann") == 0) {
return createAnnounceRequest();
}
return NULL; // unknown command
}
void sendCommand(mesh::Packet* pkt) {
if (server_path_len < 0) {
sendFlood(pkt);
} else {
sendDirect(pkt, server_path, server_path_len);
}
}
};
StdRNG fast_rng;
SimpleMeshTables tables;
#if defined(P_LORA_SCLK)
SPIClass spi;
CustomSX1262 radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
CustomSX1262 radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
MyMesh the_mesh(*new CustomSX1262Wrapper(radio, board), fast_rng, *new VolatileRTCClock(), tables);
void halt() {
while (1) ;
}
static char command[MAX_TEXT_LEN+1];
#include <SHA256.h>
void setup() {
Serial.begin(115200);
delay(5000);
board.begin();
#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#else
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 22, 8);
#endif
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
halt();
}
fast_rng.begin(radio.random(0x7FFFFFFF));
/* add this to tests
uint8_t mac_encrypted[CIPHER_MAC_SIZE+CIPHER_BLOCK_SIZE];
const char *orig_msg = "original";
int enc_len = mesh::Utils::encryptThenMAC(mesh.admin_secret, mac_encrypted, (const uint8_t *) orig_msg, strlen(orig_msg));
char decrypted[CIPHER_BLOCK_SIZE*2];
int len = mesh::Utils::MACThenDecrypt(mesh.admin_secret, (uint8_t *)decrypted, mac_encrypted, enc_len);
if (len > 0) {
decrypted[len] = 0;
Serial.print("decrypted text: "); Serial.println(decrypted);
} else {
Serial.println("MACs DONT match!");
}
*/
Serial.println("Help:");
Serial.println(" enter 'key' to generate new keypair");
Serial.println(" enter 'stats' to request repeater stats");
Serial.println(" enter 'setclock {unix-epoch-seconds}' to set repeater's clock");
Serial.println(" enter 'set AF={factor}' to set airtime budget factor");
Serial.println(" enter 'ann' to make repeater re-announce to mesh");
the_mesh.begin();
command[0] = 0;
}
void loop() {
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
if (c != '\n') {
command[len++] = c;
command[len] = 0;
}
Serial.print(c);
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
if (strcmp(command, "key") == 0) {
mesh::LocalIdentity new_id(the_mesh.getRNG());
new_id.printTo(Serial);
} else {
mesh::Packet* pkt = the_mesh.parseCommand(command);
if (pkt) {
the_mesh.sendCommand(pkt);
Serial.println(" (request sent)");
} else {
Serial.print(" ERROR: unknown command: "); Serial.println(command);
}
}
command[0] = 0; // reset command buffer
}
the_mesh.loop();
}