mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-04-20 22:13:47 +00:00
Merge branch 'dev' into trace
# Conflicts: # src/Dispatcher.cpp # src/Mesh.cpp # src/helpers/BaseChatMesh.cpp
This commit is contained in:
commit
b03aac18c0
112 changed files with 23323 additions and 1359 deletions
119
examples/companion_radio/UITask.cpp
Normal file
119
examples/companion_radio/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
examples/companion_radio/UITask.h
Normal file
25
examples/companion_radio/UITask.h
Normal 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();
|
||||
};
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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(×tamp, 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
|
||||
}
|
||||
79
examples/simple_repeater/UITask.cpp
Normal file
79
examples/simple_repeater/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/simple_repeater/UITask.h
Normal file
18
examples/simple_repeater/UITask.h
Normal 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();
|
||||
};
|
||||
|
|
@ -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(×tamp, 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(×tamp, 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, ×tamp, 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
|
||||
}
|
||||
|
|
|
|||
79
examples/simple_room_server/UITask.cpp
Normal file
79
examples/simple_room_server/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/simple_room_server/UITask.h
Normal file
18
examples/simple_room_server/UITask.h
Normal 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();
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue