Initial commit: Simple Sensor firmware for Heltec T114 with BME280
Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Has been cancelled
PR Build Check / build (Heltec_v3_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (Heltec_v3_repeater) (push) Has been cancelled
PR Build Check / build (Heltec_v3_room_server) (push) Has been cancelled
PR Build Check / build (LilyGo_Tlora_C6_repeater_) (push) Has been cancelled
PR Build Check / build (PicoW_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (RAK_4631_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_room_server) (push) Has been cancelled
PR Build Check / build (wio-e5-mini_repeater) (push) Has been cancelled

- MeshCore-based Simple Sensor firmware
- Heltec T114 without display
- BME280 sensor support (temp/humidity/pressure)
- Radio: 868.856018 MHz, SF7, BW62.5 kHz, CR4/7
- 2-byte path hash
- PlatformIO project
This commit is contained in:
UA1ZBE
2026-06-01 16:19:09 +03:00
commit 42f55e12ae
725 changed files with 98374 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
enum class UIEventType {
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class AbstractUITask {
protected:
mesh::MainBoard* _board;
BaseSerialInterface* _serial;
bool _connected;
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) {
_connected = false;
}
public:
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _serial->isEnabled(); }
void enableSerial() { _serial->enable(); }
void disableSerial() { _serial->disable(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void loop() = 0;
};

View File

@@ -0,0 +1,622 @@
#include <Arduino.h>
#include "DataStore.h"
#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
#else
#define MAX_BLOBRECS 20
#endif
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
identity_store(fs, "/identity")
#endif
{
}
#if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
identity_store(fs, "/identity")
#endif
{
}
#endif
static File openWrite(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
fs->remove(filename);
return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "w");
#else
return fs->open(filename, "w", true);
#endif
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
static uint32_t _ContactsChannelsTotalBlocks = 0;
#endif
void DataStore::begin() {
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
checkAdvBlobFile();
#if defined(EXTRAFS) || defined(QSPIFLASH)
migrateToSecondaryFS();
#endif
#else
// init 'blob store' support
_fs->mkdir("/bl");
#endif
}
#if defined(ESP32)
#include <SPIFFS.h>
#include <nvs_flash.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
#elif defined(EXTRAFS)
#include <CustomLFS.h>
#else
#include <InternalFileSystem.h>
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
int _countLfsBlock(void *p, lfs_block_t block){
if (block > _ContactsChannelsTotalBlocks) {
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully
}
lfs_size_t *size = (lfs_size_t*) p;
*size += 1;
return 0;
}
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
lfs_size_t size = 0;
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size);
if (err) {
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
return 0;
}
return size;
}
#endif
uint32_t DataStore::getStorageUsedKb() const {
#if defined(ESP32)
return SPIFFS.usedBytes() / 1024;
#elif defined(RP2040_PLATFORM)
FSInfo info;
info.usedBytes = 0;
_fs->info(info);
return info.usedBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
int usedBytes = config->block_size * usedBlockCount;
return usedBytes / 1024;
#else
return 0;
#endif
}
uint32_t DataStore::getStorageTotalKb() const {
#if defined(ESP32)
return SPIFFS.totalBytes() / 1024;
#elif defined(RP2040_PLATFORM)
FSInfo info;
info.totalBytes = 0;
_fs->info(info);
return info.totalBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int totalBytes = config->block_size * config->block_count;
return totalBytes / 1024;
#else
return 0;
#endif
}
File DataStore::openRead(const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "r");
#else
return _fs->open(filename, "r", false);
#endif
}
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "r");
#else
return fs->open(filename, "r", false);
#endif
}
bool DataStore::removeFile(const char* filename) {
return _fs->remove(filename);
}
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
return fs->remove(filename);
}
bool DataStore::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
if (_fsExtra == nullptr) {
return _fs->format();
} else {
return _fs->format() && _fsExtra->format();
}
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
bool fs_success = ((fs::SPIFFSFS *)_fs)->format();
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
return fs_success && (nvs_err == ESP_OK);
#else
#error "need to implement format()"
#endif
}
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
return identity_store.load("_main", identity);
}
bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
return identity_store.save("_main", identity);
}
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
if (_fs->exists("/new_prefs")) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
} else if (_fs->exists("/node_prefs")) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // save to new filename
_fs->remove("/node_prefs"); // remove old
}
}
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
File file = openRead(_fs, filename);
if (file) {
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 *)&node_lat, sizeof(node_lat)); // 40
file.read((uint8_t *)&node_lon, sizeof(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.client_repeat, sizeof(_prefs.client_repeat)); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 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.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
file.read(pad, 1); // 79
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.close();
}
}
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
File file = openWrite(_fs, "/new_prefs");
if (file) {
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 *)&node_lat, sizeof(node_lat)); // 40
file.write((uint8_t *)&node_lon, sizeof(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.client_repeat, sizeof(_prefs.client_repeat)); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 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.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
file.write(pad, 1); // 79
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.close();
}
}
void DataStore::loadContacts(DataStoreHost* host) {
File file = openRead(_getContactsChannelsFS(), "/contacts3");
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused;
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
success = success && (file.read(&c.type, 1) == 1);
success = success && (file.read(&c.flags, 1) == 1);
success = success && (file.read(&unused, 1) == 1);
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (!host->onContactLoaded(c)) full = true;
}
file.close();
}
}
void DataStore::saveContacts(DataStoreHost* host) {
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
while (host->getContactForSave(idx, c)) {
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // write failed
idx++; // advance to next contact
}
file.close();
}
}
void DataStore::loadChannels(DataStoreHost* host) {
File file = openRead(_getContactsChannelsFS(), "/channels2");
if (file) {
bool full = false;
uint8_t channel_idx = 0;
while (!full) {
ChannelDetails ch;
uint8_t unused[4];
bool success = (file.read(unused, 4) == 4);
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // EOF
if (host->onChannelLoaded(channel_idx, ch)) {
channel_idx++;
} else {
full = true;
}
}
file.close();
}
}
void DataStore::saveChannels(DataStoreHost* host) {
File file = openWrite(_getContactsChannelsFS(), "/channels2");
if (file) {
uint8_t channel_idx = 0;
ChannelDetails ch;
uint8_t unused[4];
memset(unused, 0, 4);
while (host->getChannelForSave(channel_idx, ch)) {
bool success = (file.write(unused, 4) == 4);
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // write failed
channel_idx++;
}
file.close();
}
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)
struct BlobRec {
uint32_t timestamp;
uint8_t key[7];
uint8_t len;
uint8_t data[MAX_ADVERT_PKT_LEN];
};
void DataStore::checkAdvBlobFile() {
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
File file = openWrite(_getContactsChannelsFS(), "/adv_blobs");
if (file) {
BlobRec zeroes;
memset(&zeroes, 0, sizeof(zeroes));
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
file.write((uint8_t *) &zeroes, sizeof(zeroes));
}
file.close();
}
}
}
void DataStore::migrateToSecondaryFS() {
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
if (!_fsExtra->exists("/adv_blobs")) {
if (_fs->exists("/adv_blobs")) {
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
File newAdvBlobs = openWrite(_fsExtra, "/adv_blobs");
if (oldAdvBlobs && newAdvBlobs) {
BlobRec rec;
size_t count = 0;
// Copy 20 BlobRecs from old to new
while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
newAdvBlobs.seek(count * sizeof(BlobRec));
newAdvBlobs.write((uint8_t *)&rec, sizeof(rec));
count++;
}
}
if (oldAdvBlobs) oldAdvBlobs.close();
if (newAdvBlobs) newAdvBlobs.close();
_fs->remove("/adv_blobs");
}
}
if (!_fsExtra->exists("/contacts3")) {
if (_fs->exists("/contacts3")) {
File oldFile = openRead(_fs, "/contacts3");
File newFile = openWrite(_fsExtra, "/contacts3");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fs->remove("/contacts3");
}
}
if (!_fsExtra->exists("/channels2")) {
if (_fs->exists("/channels2")) {
File oldFile = openRead(_fs, "/channels2");
File newFile = openWrite(_fsExtra, "/channels2");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fs->remove("/channels2");
}
}
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
if (_fsExtra->exists("/_main.id")) {
if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");}
File oldFile = openRead(_fsExtra, "/_main.id");
File newFile = openWrite(_fs, "/_main.id");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fsExtra->remove("/_main.id");
}
if (_fsExtra->exists("/new_prefs")) {
if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");}
File oldFile = openRead(_fsExtra, "/new_prefs");
File newFile = openWrite(_fs, "/new_prefs");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fsExtra->remove("/new_prefs");
}
// remove files from where they should not be anymore
if (_fs->exists("/adv_blobs")) {
_fs->remove("/adv_blobs");
}
if (_fs->exists("/contacts3")) {
_fs->remove("/contacts3");
}
if (_fs->exists("/channels2")) {
_fs->remove("/channels2");
}
if (_fsExtra->exists("/_main.id")) {
_fsExtra->remove("/_main.id");
}
if (_fsExtra->exists("/new_prefs")) {
_fsExtra->remove("/new_prefs");
}
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
uint8_t len = 0; // 0 = not found
if (file) {
BlobRec tmp;
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
len = tmp.len;
memcpy(dest_buf, tmp.data, len);
break;
}
}
file.close();
}
return len;
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
checkAdvBlobFile();
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
if (file) {
uint32_t pos = 0, found_pos = 0;
uint32_t min_timestamp = 0xFFFFFFFF;
// search for matching key OR evict by oldest timestmap
BlobRec tmp;
file.seek(0);
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
found_pos = pos;
break;
}
if (tmp.timestamp < min_timestamp) {
min_timestamp = tmp.timestamp;
found_pos = pos;
}
pos += sizeof(tmp);
}
memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key
memcpy(tmp.data, src_buf, len);
tmp.len = len;
tmp.timestamp = _clock->getCurrentTime();
file.seek(found_pos);
file.write((uint8_t *) &tmp, sizeof(tmp));
file.close();
return true;
}
return false; // error
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
return true; // this is just a stub on NRF52/STM32 platforms
}
#else
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
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);
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
if (_fs->exists(path)) {
File f = openRead(_fs, path);
if (f) {
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
f.close();
return len;
}
}
return 0; // not found
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
File f = openWrite(_fs, path);
if (f) {
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
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
_fs->remove(path);
return true; // return true even if file did not exist
}
#endif

View File

@@ -0,0 +1,55 @@
#pragma once
#include <helpers/IdentityStore.h>
#include <helpers/ContactInfo.h>
#include <helpers/ChannelDetails.h>
#include "NodePrefs.h"
class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo& contact) =0;
virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0;
};
class DataStore {
FILESYSTEM* _fs;
FILESYSTEM* _fsExtra;
mesh::RTCClock* _clock;
IdentityStore identity_store;
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
void checkAdvBlobFile();
#endif
public:
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock);
void begin();
bool formatFileSystem();
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host);
void loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
void migrateToSecondaryFS();
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
File openRead(const char* filename);
File openRead(FILESYSTEM* fs, const char* filename);
bool removeFile(const char* filename);
bool removeFile(FILESYSTEM* fs, const char* filename);
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
private:
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include "DataStore.h"
#include "NodePrefs.h"
#include <RTClib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/BaseSerialInterface.h>
#include <helpers/IdentityStore.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <target.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef MAX_LORA_TX_POWER
#define MAX_LORA_TX_POWER LORA_TX_POWER
#endif
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 100
#endif
#ifndef OFFLINE_QUEUE_SIZE
#define OFFLINE_QUEUE_SIZE 16
#endif
#ifndef BLE_NAME_PREFIX
#define BLE_NAME_PREFIX "MeshCore-"
#endif
#include <helpers/BaseChatMesh.h>
#include <helpers/TransportKeyStore.h>
/* -------------------------------------------------------------------------------------- */
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
char name[32];
uint32_t recv_timestamp;
uint8_t path[MAX_PATH_SIZE];
};
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
void begin(bool has_display);
void startInterface(BaseSerialInterface &serial);
const char *getNodeName();
NodePrefs *getNodePrefs();
uint32_t getBLEPin();
void loop();
void handleCmdFrame(size_t len);
bool advert();
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
bool allowPacketForward(const mesh::Packet* packet) override;
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis);
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
uint8_t getAutoAddMaxHops() const override;
void onContactsFull() override;
void onContactOverwrite(const uint8_t* pub_key) override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;
ContactInfo* processAck(const uint8_t *data) override;
void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *extra, int extra_len, const char *text);
void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) override;
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
const char *text) override;
void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
const uint8_t *data, size_t data_len) override;
uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) override;
void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
void onControlDataRecv(mesh::Packet *packet) override;
void onRawDataRecv(mesh::Packet *packet) override;
void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override;
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override;
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override;
void onSendTimeout() override;
// DataStoreHost methods
bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); }
bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); }
bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); }
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
void clearPendingReqs() {
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
}
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
if (_prefs.gps_interval > 0) {
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
sprintf(interval_str, "%u", _prefs.gps_interval);
sensors.setSettingValue("gps_interval", interval_str);
}
}
#endif
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
void writeDisabledFrame();
void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
void addToOfflineQueue(const uint8_t frame[], int len);
int getFromOfflineQueue(uint8_t frame[]);
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
return _store->getBlobByKey(key, key_len, dest_buf);
}
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override {
return _store->putBlobByKey(key, key_len, src_buf, len);
}
void checkCLIRescueCmd();
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
void saveContacts() { _store->saveContacts(this); }
DataStore* _store;
NodePrefs _prefs;
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
AbstractUITask* _ui;
ContactsIterator _iter;
uint32_t _iter_filter_since;
uint32_t _most_recent_lastmod;
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
char cli_command[80];
uint8_t app_target_ver;
uint8_t *sign_data;
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
TransportKey send_scope;
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
uint8_t out_frame[MAX_FRAME_SIZE + 1];
CayenneLPP telemetry;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
bool isChannelMsg() const;
};
int offline_queue_len;
Frame offline_queue[OFFLINE_QUEUE_SIZE];
struct AckTableEntry {
unsigned long msg_sent;
uint32_t ack;
ContactInfo* contact;
};
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
int next_ack_idx;
#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};
extern MyMesh the_mesh;

View File

@@ -0,0 +1,37 @@
#pragma once
#include <cstdint> // For uint8_t, uint32_t
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
#define TELEM_MODE_ALLOW_ALL 2
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
float freq;
uint8_t sf;
uint8_t cr;
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
float rx_delay_base;
uint32_t ble_pin;
uint8_t advert_loc_policy;
uint8_t buzzer_quiet;
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31];
uint8_t default_scope_key[16];
};

View File

@@ -0,0 +1,232 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "MyMesh.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;
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
DataStore store(InternalFS, QSPIFlash, rtc_clock);
#else
#if defined(EXTRAFS)
#include <CustomLFS.h>
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock);
#else
DataStore store(InternalFS, rtc_clock);
#endif
#endif
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
DataStore store(LittleFS, rtc_clock);
#elif defined(ESP32)
#include <SPIFFS.h>
DataStore store(SPIFFS, rtc_clock);
#endif
#ifdef ESP32
#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;
#elif defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
HardwareSerial companion_serial(1);
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(RP2040_PLATFORM)
//#ifdef WIFI_SSID
// #include <helpers/rp2040/SerialWifiInterface.h>
// SerialWifiInterface serial_interface;
// #ifndef TCP_PORT
// #define TCP_PORT 5000
// #endif
// #elif defined(BLE_PIN_CODE)
// #include <helpers/rp2040/SerialBLEInterface.h>
// SerialBLEInterface serial_interface;
#if defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
HardwareSerial companion_serial(1);
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(NRF52_PLATFORM)
#ifdef BLE_PIN_CODE
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(STM32_PLATFORM)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#else
#error "need to define a serial interface"
#endif
/* GLOBAL OBJECTS */
#ifdef DISPLAY_CLASS
#include "UITask.h"
UITask ui_task(&board, &serial_interface);
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#ifdef DISPLAY_CLASS
, &ui_task
#endif
);
/* END GLOBAL OBJECTS */
void halt() {
while (1) ;
}
void setup() {
Serial.begin(115200);
board.begin();
#ifdef DISPLAY_CLASS
DisplayDriver* disp = NULL;
if (display.begin()) {
disp = &display;
disp->startFrame();
#ifdef ST7789
disp->setTextSize(2);
#endif
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
disp->endFrame();
}
#endif
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
#if defined(QSPIFLASH)
if (!QSPIFlash.begin()) {
// debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here?
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: failed to initialize");
} else {
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: initialized successfully");
}
#else
#if defined(EXTRAFS)
ExtraFS.begin();
#endif
#endif
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
#else
false
#endif
);
#ifdef BLE_PIN_CODE
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
#else
false
#endif
);
//#ifdef WIFI_SSID
// WiFi.begin(WIFI_SSID, WIFI_PWD);
// serial_interface.begin(TCP_PORT);
// #elif defined(BLE_PIN_CODE)
// char dev_name[32+16];
// sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
// serial_interface.begin(dev_name, the_mesh.getBLEPin());
#if defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
serial_interface.begin(companion_serial);
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#elif defined(ESP32)
SPIFFS.begin(true);
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
#else
false
#endif
);
#ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
serial_interface.begin(companion_serial);
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#else
#error "need to define filesystem"
#endif
sensors.begin();
#if ENV_INCLUDE_GPS == 1
the_mesh.applyGpsPrefs();
#endif
#ifdef DISPLAY_CLASS
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
#endif
}
void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -0,0 +1,923 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
#define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000
#endif
#define LONG_PRESS_MILLIS 1200
#ifndef UI_RECENT_LIST_SIZE
#define UI_RECENT_LIST_SIZE 4
#endif
#if UI_HAS_JOYSTICK
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
#endif
#include "icons.h"
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
char _version_info[12];
public:
SplashScreen(UITask* task) : _task(task) {
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
const char *ver = FIRMWARE_VERSION;
const char *dash = strchr(ver, '-');
int len = dash ? dash - ver : strlen(ver);
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1;
memcpy(_version_info, ver, len);
_version_info[len] = 0;
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
}
int render(DisplayDriver& display) override {
// meshcore logo
display.setColor(DisplayDriver::BLUE);
int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2);
display.drawTextCentered(display.width()/2, 22, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
return 1000;
}
void poll() override {
if (millis() >= dismiss_after) {
_task->gotoHomeScreen();
}
}
};
class HomeScreen : public UIScreen {
enum HomePage {
FIRST,
RECENT,
RADIO,
BLUETOOTH,
ADVERT,
#if ENV_INCLUDE_GPS == 1
GPS,
#endif
#if UI_SENSORS_PAGE == 1
SENSORS,
#endif
SHUTDOWN,
Count // keep as last
};
UITask* _task;
mesh::RTCClock* _rtc;
SensorManager* _sensors;
NodePrefs* _node_prefs;
uint8_t _page;
bool _shutdown_init;
AdvertPath recent[UI_RECENT_LIST_SIZE];
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
// battery icon
int iconWidth = 24;
int iconHeight = 10;
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0;
display.setColor(DisplayDriver::GREEN);
// battery outline
display.drawRect(iconX, iconY, iconWidth, iconHeight);
// battery "cap"
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
// show muted icon if buzzer is muted
#ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::RED);
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
}
#endif
}
CayenneLPP sensors_lpp;
int sensors_nb = 0;
bool sensors_scroll = false;
int sensors_scroll_offset = 0;
int next_sensors_refresh = 0;
void refresh_sensors() {
if (millis() > next_sensors_refresh) {
sensors_lpp.reset();
sensors_nb = 0;
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
sensors.querySensors(0xFF, sensors_lpp);
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
uint8_t channel, type;
while(reader.readHeader(channel, type)) {
reader.skipData(type);
sensors_nb ++;
}
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
#if AUTO_OFF_MILLIS > 0
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
#else
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
#endif
}
}
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
_task->shutdown();
}
}
int render(DisplayDriver& display) override {
char tmp[80];
// node name
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
char filtered_name[sizeof(_node_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
display.setCursor(0, 0);
display.print(filtered_name);
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// curr page indicator
int y = 14;
int x = display.width() / 2 - 5 * (HomePage::Count-1);
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
if (i == _page) {
display.fillRect(x-1, y-1, 3, 3);
} else {
display.fillRect(x, y, 1, 1);
}
}
if (_page == HomePage::FIRST) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 20, tmp);
#ifdef WIFI_SSID
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 54, tmp);
#endif
if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
} else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(DisplayDriver::RED);
display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, 43, tmp);
}
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN);
int y = 20;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
int timestamp_width = display.getTextWidth(tmp);
int max_name_width = display.width() - timestamp_width - 1;
char filtered_recent_name[sizeof(a->name)];
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
display.setCursor(display.width() - timestamp_width - 1, y);
display.print(tmp);
}
} else if (_page == HomePage::RADIO) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(1);
// freq / sf
display.setCursor(0, 20);
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
display.print(tmp);
display.setCursor(0, 31);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
display.print(tmp);
// tx power, noise floor
display.setCursor(0, 42);
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm);
display.print(tmp);
display.setCursor(0, 53);
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider();
char buf[50];
int y = 18;
bool gps_state = _task->getGPSState();
#ifdef PIN_GPS_SWITCH
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH);
if (gps_state != hw_gps_state) {
strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)");
} else {
strcpy(buf, gps_state ? "gps on" : "gps off");
}
#else
strcpy(buf, gps_state ? "gps on" : "gps off");
#endif
display.drawTextLeftAlign(0, y, buf);
if (nmea == NULL) {
y = y + 12;
display.drawTextLeftAlign(0, y, "Can't access GPS");
} else {
strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "sat");
sprintf(buf, "%d", nmea->satellitesCount());
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "pos");
sprintf(buf, "%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "alt");
sprintf(buf, "%.2f", nmea->getAltitude()/1000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
}
#endif
#if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) {
int y = 18;
refresh_sensors();
char buf[30];
char name[30];
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
for (int i = 0; i < sensors_scroll_offset; i++) {
uint8_t channel, type;
r.readHeader(channel, type);
r.skipData(type);
}
for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) {
uint8_t channel, type;
if (!r.readHeader(channel, type)) { // reached end, reset
r.reset();
r.readHeader(channel, type);
}
display.setCursor(0, y);
float v;
switch (type) {
case LPP_GPS: // GPS
float lat, lon, alt;
r.readGPS(lat, lon, alt);
strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon);
break;
case LPP_VOLTAGE:
r.readVoltage(v);
strcpy(name, "voltage"); sprintf(buf, "%6.2f", v);
break;
case LPP_CURRENT:
r.readCurrent(v);
strcpy(name, "current"); sprintf(buf, "%.3f", v);
break;
case LPP_TEMPERATURE:
r.readTemperature(v);
strcpy(name, "temperature"); sprintf(buf, "%.2f", v);
break;
case LPP_RELATIVE_HUMIDITY:
r.readRelativeHumidity(v);
strcpy(name, "humidity"); sprintf(buf, "%.2f", v);
break;
case LPP_BAROMETRIC_PRESSURE:
r.readPressure(v);
strcpy(name, "pressure"); sprintf(buf, "%.2f", v);
break;
case LPP_ALTITUDE:
r.readAltitude(v);
strcpy(name, "altitude"); sprintf(buf, "%.0f", v);
break;
case LPP_POWER:
r.readPower(v);
strcpy(name, "power"); sprintf(buf, "%6.2f", v);
break;
default:
r.skipData(type);
strcpy(name, "unk"); sprintf(buf, "");
}
display.setCursor(0, y);
display.print(name);
display.setCursor(
display.width()-display.getTextWidth(buf)-1, y
);
display.print(buf);
y = y + 12;
}
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
else sensors_scroll_offset = 0;
#endif
} else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
if (_shutdown_init) {
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
}
bool handleInput(char c) override {
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
}
if (c == KEY_NEXT || c == KEY_RIGHT) {
_page = (_page + 1) % HomePage::Count;
if (_page == HomePage::RECENT) {
_task->showAlert("Recent adverts", 800);
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
_task->disableSerial();
} else {
_task->enableSerial();
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
_task->notify(UIEventType::ack);
if (the_mesh.advert()) {
_task->showAlert("Advert sent!", 1000);
} else {
_task->showAlert("Advert failed..", 1000);
}
return true;
}
#if ENV_INCLUDE_GPS == 1
if (c == KEY_ENTER && _page == HomePage::GPS) {
_task->toggleGPS();
return true;
}
#endif
#if UI_SENSORS_PAGE == 1
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
_task->toggleGPS();
next_sensors_refresh=0;
return true;
}
#endif
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true; // need to wait for button to be released
return true;
}
return false;
}
};
class MsgPreviewScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
};
#define MAX_UNREAD_MSGS 32
int num_unread;
int head = MAX_UNREAD_MSGS - 1; // index of latest unread message
MsgEntry unread[MAX_UNREAD_MSGS];
public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
head = (head + 1) % MAX_UNREAD_MSGS;
if (num_unread < MAX_UNREAD_MSGS) num_unread++;
auto p = &unread[head];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
} else {
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name);
}
StrHelper::strncpy(p->msg, msg, sizeof(p->msg));
}
int render(DisplayDriver& display) override {
char tmp[16];
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[head];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
display.drawRect(0, 11, display.width(), 1); // horiz line
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
char filtered_origin[sizeof(p->origin)];
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
display.print(filtered_origin);
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width());
#if AUTO_OFF_MILLIS==0 // probably e-ink
return 10000; // 10 s
#else
return 1000; // next render after 1000 ms
#endif
}
bool handleInput(char c) override {
if (c == KEY_NEXT || c == KEY_RIGHT) {
head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS;
num_unread--;
if (num_unread == 0) {
_task->gotoHomeScreen();
}
return true;
}
if (c == KEY_ENTER) {
num_unread = 0; // clear unread queue
_task->gotoHomeScreen();
return true;
}
return false;
}
};
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
_auto_off = millis() + AUTO_OFF_MILLIS;
#if defined(PIN_USER_BTN)
user_btn.begin();
#endif
#if defined(PIN_USER_BTN_ANA)
analog_btn.begin();
#endif
_node_prefs = node_prefs;
if (_display != NULL) {
_display->turnOn();
}
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
#ifdef PIN_VIBRATION
vibration.begin();
#endif
ui_started_at = millis();
_alert_expiry = 0;
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
setCurrScreen(splash);
}
void UITask::showAlert(const char* text, int duration_millis) {
strcpy(_alert, text);
_alert_expiry = millis() + duration_millis;
}
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
break;
case UIEventType::channelMessage:
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
break;
case UIEventType::ack:
buzzer.play("ack:d=32,o=8,b=120:c");
break;
case UIEventType::roomMessage:
case UIEventType::newContactMessage:
case UIEventType::none:
default:
break;
}
#endif
#ifdef PIN_VIBRATION
// Trigger vibration for all UI events except none
if (t != UIEventType::none) {
vibration.trigger();
}
#endif
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
gotoHomeScreen();
}
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
setCurrScreen(msg_preview);
if (_display != NULL) {
if (!_display->isOn() && !hasConnection()) {
_display->turnOn();
}
if (_display->isOn()) {
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 100; // trigger refresh
}
}
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
int cur_time = millis();
if (cur_time > next_led_change) {
if (led_state == 0) {
led_state = 1;
if (_msgcount > 0) {
last_led_increment = LED_ON_MSG_MILLIS;
} else {
last_led_increment = LED_ON_MILLIS;
}
next_led_change = cur_time + last_led_increment;
} else {
led_state = 0;
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
}
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON);
}
#endif
}
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 100;
}
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
#ifdef PIN_BUZZER
/* note: we have a choice here -
we can do a blocking buzzer.loop() with non-deterministic consequences
or we can set a flag and delay the shutdown for a couple of seconds
while a non-blocking buzzer.loop() plays out in UITask::loop()
*/
buzzer.shutdown();
uint32_t buzzer_timer = millis(); // fail-safe shutdown
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
buzzer.loop();
#endif // PIN_BUZZER
if (restart) {
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
_board->powerOff();
}
}
bool UITask::isButtonPressed() const {
#ifdef PIN_USER_BTN
return user_btn.isPressed();
#else
return false;
#endif
}
void UITask::loop() {
char c = 0;
#if UI_HAS_JOYSTICK
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
}
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_LEFT);
}
ev = joystick_right.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_RIGHT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
ev = back_btn.check();
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#elif defined(PIN_USER_BTN)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
_analogue_pin_read_millis = millis();
}
#endif
#if defined(BACKLIGHT_BTN)
if (millis() > next_backlight_btn_check) {
bool touch_state = digitalRead(PIN_BUTTON2);
#if defined(DISP_BACKLIGHT)
digitalWrite(DISP_BACKLIGHT, !touch_state);
#elif defined(EXP_PIN_BACKLIGHT)
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
#endif
next_backlight_btn_check = millis() + 300;
}
#endif
if (c != 0 && curr) {
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 100; // trigger refresh
}
userLedHandler();
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
if (curr) curr->poll();
if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
_display->startFrame();
int delay_millis = curr->render(*_display);
if (millis() < _alert_expiry) { // render alert popup
_display->setTextSize(1);
int y = _display->height() / 3;
int p = _display->height() / 32;
_display->setColor(DisplayDriver::DARK);
_display->fillRect(p, y, _display->width() - p*2, y);
_display->setColor(DisplayDriver::LIGHT); // draw box border
_display->drawRect(p, y, _display->width() - p*2, y);
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
} else {
_next_refresh = millis() + delay_millis;
}
_display->endFrame();
}
#if AUTO_OFF_MILLIS > 0
if (millis() > _auto_off) {
_display->turnOff();
}
#endif
}
#ifdef PIN_VIBRATION
vibration.loop();
#endif
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
if (millis() > next_batt_chck) {
uint16_t milliVolts = getBattMilliVolts();
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
// show low battery shutdown alert
// we should only do this for eink displays, which will persist after power loss
#if defined(THINKNODE_M1) || defined(LILYGO_TECHO)
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
_display->setColor(DisplayDriver::RED);
_display->drawTextCentered(_display->width() / 2, 20, "Low Battery.");
_display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!");
_display->endFrame();
}
#endif
shutdown();
}
next_batt_chck = millis() + 8000;
}
#endif
}
char UITask::checkDisplayOn(char c) {
if (_display != NULL) {
if (!_display->isOn()) {
_display->turnOn(); // turn display on and consume event
c = 0;
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
}
return c;
}
char UITask::handleLongPress(char c) {
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
c = 0; // consume event
}
return c;
}
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double click triggered");
checkDisplayOn(c);
return c;
}
char UITask::handleTripleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
checkDisplayOn(c);
toggleBuzzer();
c = 0;
return c;
}
bool UITask::getGPSState() {
if (_sensors != NULL) {
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
return false;
}
void UITask::toggleGPS() {
if (_sensors != NULL) {
// toggle GPS on/off
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
_node_prefs->gps_enabled = 0;
notify(UIEventType::ack);
} else {
_sensors->setSettingValue("gps", "1");
_node_prefs->gps_enabled = 1;
notify(UIEventType::ack);
}
the_mesh.savePrefs();
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
_next_refresh = 0;
break;
}
}
}
}
void UITask::toggleBuzzer() {
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
} else {
buzzer.quiet(true);
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
_next_refresh = 0; // trigger refresh
#endif
}

View File

@@ -0,0 +1,101 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h>
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#ifdef PIN_VIBRATION
#include <helpers/ui/GenericVibration.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
class UITask : public AbstractUITask {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef PIN_VIBRATION
GenericVibration vibration;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
char _alert[80];
unsigned long _alert_expiry;
int _msgcount;
unsigned long ui_started_at, next_batt_chck;
int next_backlight_btn_check = 0;
#ifdef PIN_STATUS_LED
int led_state = 0;
int next_led_change = 0;
int last_led_increment = 0;
#endif
#ifdef PIN_USER_BTN_ANA
unsigned long _analogue_pin_read_millis = millis();
#endif
UIScreen* splash;
UIScreen* home;
UIScreen* msg_preview;
UIScreen* curr;
void userLedHandler();
// Button action handlers
char checkDisplayOn(char c);
char handleLongPress(char c);
char handleDoubleClick(char c);
char handleTripleClick(char c);
void setCurrScreen(UIScreen* c);
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
next_batt_chck = _next_refresh = 0;
ui_started_at = 0;
curr = NULL;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void showAlert(const char* text, int duration_millis);
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isBuzzerQuiet() {
#ifdef PIN_BUZZER
return buzzer.isQuiet();
#else
return true;
#endif
}
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
};

View File

@@ -0,0 +1,122 @@
#pragma once
#include <stdint.h>
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] = {
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,
};
static const uint8_t bluetooth_on[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3B, 0xE0, 0x00,
0x30, 0x38, 0xF8, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x3E, 0x38, 0x7C, 0x00,
0x1F, 0xB8, 0xF8, 0x70,
0x07, 0xF9, 0xF0, 0x78,
0x03, 0xFF, 0xC0, 0x78,
0x00, 0xFF, 0x80, 0x3C,
0x00, 0x7F, 0x07, 0x1C,
0x00, 0x7E, 0x07, 0x1C,
0x03, 0xFF, 0x82, 0x1C,
0x03, 0xFF, 0xC0, 0x78,
0x07, 0xFB, 0xE0, 0x78,
0x0F, 0xB8, 0xF8, 0x70,
0x3E, 0x38, 0x7C, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x38, 0x38, 0xF8, 0x00,
0x00, 0x39, 0xF0, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t bluetooth_off[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x38, 0x03, 0xF8, 0x00,
0x3C, 0x03, 0xFC, 0x00,
0x3E, 0x03, 0xBF, 0x00,
0x0F, 0x83, 0x8F, 0x80,
0x07, 0xC3, 0x87, 0xC0,
0x03, 0xF0, 0x03, 0xC0,
0x00, 0xF8, 0x0F, 0x80,
0x00, 0x7C, 0x0F, 0x00,
0x00, 0x1F, 0x0E, 0x00,
0x00, 0x0F, 0x80, 0x00,
0x00, 0x07, 0xE0, 0x00,
0x00, 0x07, 0xF0, 0x00,
0x00, 0x0F, 0xF8, 0x00,
0x00, 0x3F, 0xBE, 0x00,
0x00, 0x7F, 0x9F, 0x00,
0x00, 0xFB, 0x8F, 0xC0,
0x03, 0xE3, 0x83, 0xE0,
0x03, 0xC3, 0x87, 0xF0,
0x03, 0x83, 0x8F, 0xFC,
0x00, 0x03, 0xBF, 0x3C,
0x00, 0x03, 0xFC, 0x1C,
0x00, 0x03, 0xF8, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t power_icon[] = {
0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80,
0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0,
0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC,
0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C,
0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8,
0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0,
0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00,
};
static const uint8_t advert_icon[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30,
0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C,
0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86,
0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86,
0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C,
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
};

View File

@@ -0,0 +1,131 @@
#include "Button.h"
Button::Button(uint8_t pin, bool activeState)
: _pin(pin), _activeState(activeState), _isAnalog(false), _analogThreshold(20) {
_currentState = false; // Initialize as not pressed
_lastState = _currentState;
}
Button::Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold)
: _pin(pin), _activeState(activeState), _isAnalog(isAnalog), _analogThreshold(analogThreshold) {
_currentState = false; // Initialize as not pressed
_lastState = _currentState;
}
void Button::begin() {
_currentState = readButton();
_lastState = _currentState;
}
void Button::update() {
uint32_t now = millis();
// Read button at specified interval
if (now - _lastReadTime < BUTTON_READ_INTERVAL_MS) {
return;
}
_lastReadTime = now;
bool newState = readButton();
// Check if state has changed
if (newState != _lastState) {
_stateChangeTime = now;
}
// Debounce check
if ((now - _stateChangeTime) > BUTTON_DEBOUNCE_TIME_MS) {
if (newState != _currentState) {
_currentState = newState;
handleStateChange();
}
}
_lastState = newState;
// Handle multi-click timeout
if (_state == WAITING_FOR_MULTI_CLICK && (now - _releaseTime) > BUTTON_CLICK_TIMEOUT_MS) {
// Timeout reached, process the clicks
if (_clickCount == 1) {
triggerEvent(SHORT_PRESS);
} else if (_clickCount == 2) {
triggerEvent(DOUBLE_PRESS);
} else if (_clickCount == 3) {
triggerEvent(TRIPLE_PRESS);
} else if (_clickCount >= 4) {
triggerEvent(QUADRUPLE_PRESS);
}
_clickCount = 0;
_state = IDLE;
}
// Handle long press while button is held
if (_state == PRESSED && (now - _pressTime) > BUTTON_LONG_PRESS_TIME_MS) {
triggerEvent(LONG_PRESS);
_state = IDLE; // Prevent multiple press events
_clickCount = 0;
}
}
bool Button::readButton() {
if (_isAnalog) {
return (analogRead(_pin) < _analogThreshold);
} else {
return (digitalRead(_pin) == _activeState);
}
}
void Button::handleStateChange() {
uint32_t now = millis();
if (_currentState) {
// Button pressed
_pressTime = now;
_state = PRESSED;
triggerEvent(ANY_PRESS);
} else {
// Button released
if (_state == PRESSED) {
uint32_t pressDuration = now - _pressTime;
if (pressDuration < BUTTON_LONG_PRESS_TIME_MS) {
// Short press detected
_clickCount++;
_releaseTime = now;
_state = WAITING_FOR_MULTI_CLICK;
} else {
// Long press already handled in update()
_state = IDLE;
_clickCount = 0;
}
}
}
}
void Button::triggerEvent(EventType event) {
_lastEvent = event;
switch (event) {
case ANY_PRESS:
if (_onAnyPress) _onAnyPress();
break;
case SHORT_PRESS:
if (_onShortPress) _onShortPress();
break;
case DOUBLE_PRESS:
if (_onDoublePress) _onDoublePress();
break;
case TRIPLE_PRESS:
if (_onTriplePress) _onTriplePress();
break;
case QUADRUPLE_PRESS:
if (_onQuadruplePress) _onQuadruplePress();
break;
case LONG_PRESS:
if (_onLongPress) _onLongPress();
break;
default:
break;
}
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <Arduino.h>
#include <functional>
// Button timing configuration
#define BUTTON_DEBOUNCE_TIME_MS 50 // Debounce time in ms
#define BUTTON_CLICK_TIMEOUT_MS 500 // Max time between clicks for multi-click
#define BUTTON_LONG_PRESS_TIME_MS 3000 // Time to trigger long press (3 seconds)
#define BUTTON_READ_INTERVAL_MS 10 // How often to read the button
class Button {
public:
enum EventType {
NONE,
SHORT_PRESS,
DOUBLE_PRESS,
TRIPLE_PRESS,
QUADRUPLE_PRESS,
LONG_PRESS,
ANY_PRESS
};
using EventCallback = std::function<void()>;
Button(uint8_t pin, bool activeState = LOW);
Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold = 20);
void begin();
void update();
// Set callbacks for different events
void onShortPress(EventCallback callback) { _onShortPress = callback; }
void onDoublePress(EventCallback callback) { _onDoublePress = callback; }
void onTriplePress(EventCallback callback) { _onTriplePress = callback; }
void onQuadruplePress(EventCallback callback) { _onQuadruplePress = callback; }
void onLongPress(EventCallback callback) { _onLongPress = callback; }
void onAnyPress(EventCallback callback) { _onAnyPress = callback; }
// State getters
bool isPressed() const { return _currentState; }
EventType getLastEvent() const { return _lastEvent; }
private:
enum State {
IDLE,
PRESSED,
RELEASED,
WAITING_FOR_MULTI_CLICK
};
uint8_t _pin;
bool _activeState;
bool _isAnalog;
uint16_t _analogThreshold;
State _state = IDLE;
bool _currentState;
bool _lastState;
uint32_t _stateChangeTime = 0;
uint32_t _pressTime = 0;
uint32_t _releaseTime = 0;
uint32_t _lastReadTime = 0;
uint8_t _clickCount = 0;
EventType _lastEvent = NONE;
// Callbacks
EventCallback _onShortPress = nullptr;
EventCallback _onDoublePress = nullptr;
EventCallback _onTriplePress = nullptr;
EventCallback _onQuadruplePress = nullptr;
EventCallback _onLongPress = nullptr;
EventCallback _onAnyPress = nullptr;
bool readButton();
void handleStateChange();
void triggerEvent(EventType event);
};

View File

@@ -0,0 +1,446 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
#define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000
#endif
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
// '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(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
_auto_off = millis() + AUTO_OFF_MILLIS;
clearMsgPreview();
_node_prefs = node_prefs;
if (_display != NULL) {
_display->turnOn();
}
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(FIRMWARE_VERSION);
char *dash = strchr(version, '-');
if (dash) {
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, FIRMWARE_BUILD_DATE);
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
// Initialize digital button if available
#ifdef PIN_USER_BTN
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED);
_userButton->begin();
// Set up digital button callbacks
_userButton->onShortPress([this]() { handleButtonShortPress(); });
_userButton->onDoublePress([this]() { handleButtonDoublePress(); });
_userButton->onTriplePress([this]() { handleButtonTriplePress(); });
_userButton->onQuadruplePress([this]() { handleButtonQuadruplePress(); });
_userButton->onLongPress([this]() { handleButtonLongPress(); });
_userButton->onAnyPress([this]() { handleButtonAnyPress(); });
#endif
// Initialize analog button if available
#ifdef PIN_USER_BTN_ANA
_userButtonAnalog = new Button(PIN_USER_BTN_ANA, USER_BTN_PRESSED, true, 20);
_userButtonAnalog->begin();
// Set up analog button callbacks
_userButtonAnalog->onShortPress([this]() { handleButtonShortPress(); });
_userButtonAnalog->onDoublePress([this]() { handleButtonDoublePress(); });
_userButtonAnalog->onTriplePress([this]() { handleButtonTriplePress(); });
_userButtonAnalog->onQuadruplePress([this]() { handleButtonQuadruplePress(); });
_userButtonAnalog->onLongPress([this]() { handleButtonLongPress(); });
_userButtonAnalog->onAnyPress([this]() { handleButtonAnyPress(); });
#endif
ui_started_at = millis();
}
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
break;
case UIEventType::channelMessage:
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
break;
case UIEventType::ack:
buzzer.play("ack:d=32,o=8,b=120:c");
break;
case UIEventType::roomMessage:
case UIEventType::newContactMessage:
case UIEventType::none:
default:
break;
}
#endif
// Serial.print("DBG: Alert user -> ");
// Serial.println((int) t);
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
clearMsgPreview();
}
}
void UITask::clearMsgPreview() {
_origin[0] = 0;
_msg[0] = 0;
_need_refresh = true;
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
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 != NULL) {
if (!_display->isOn() && !hasConnection()) {
_display->turnOn();
}
if (_display->isOn()) {
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_need_refresh = true;
}
}
}
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
// battery icon
int iconWidth = 24;
int iconHeight = 12;
int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0;
_display->setColor(DisplayDriver::GREEN);
// battery outline
_display->drawRect(iconX, iconY, iconWidth, iconHeight);
// battery "cap"
_display->fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
_display->fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
}
void UITask::renderCurrScreen() {
if (_display == NULL) return; // assert() ??
char tmp[80];
if (_alert[0]) {
_display->setTextSize(1.4);
uint16_t textWidth = _display->getTextWidth(_alert);
_display->setCursor((_display->width() - textWidth) / 2, 22);
_display->setColor(DisplayDriver::GREEN);
_display->print(_alert);
_alert[0] = 0;
_need_refresh = true;
return;
} else if (_origin[0] && _msg[0]) { // message preview
// render message preview
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
_display->setCursor(0, 12);
_display->setColor(DisplayDriver::YELLOW);
_display->print(_origin);
_display->setCursor(0, 24);
_display->setColor(DisplayDriver::LIGHT);
_display->print(_msg);
_display->setCursor(_display->width() - 28, 9);
_display->setTextSize(2);
_display->setColor(DisplayDriver::ORANGE);
sprintf(tmp, "%d", _msgcount);
_display->print(tmp);
_display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114
} else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t textWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - textWidth) / 2, 22);
_display->print(_version_info);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
// battery voltage
renderBatteryIndicator(_board->getBattMilliVolts());
// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
// BT pin
if (!_connected && the_mesh.getBLEPin() != 0) {
_display->setColor(DisplayDriver::RED);
_display->setTextSize(2);
_display->setCursor(0, 43);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
_display->print(tmp);
_display->setColor(DisplayDriver::GREEN);
} else {
_display->setColor(DisplayDriver::LIGHT);
}
}
_need_refresh = false;
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
static int state = 0;
static int next_change = 0;
static int last_increment = 0;
int cur_time = millis();
if (cur_time > next_change) {
if (state == 0) {
state = 1;
if (_msgcount > 0) {
last_increment = LED_ON_MSG_MILLIS;
} else {
last_increment = LED_ON_MILLIS;
}
next_change = cur_time + last_increment;
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
}
digitalWrite(PIN_STATUS_LED, state == LED_STATE_ON);
}
#endif
}
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
#ifdef PIN_BUZZER
/* note: we have a choice here -
we can do a blocking buzzer.loop() with non-deterministic consequences
or we can set a flag and delay the shutdown for a couple of seconds
while a non-blocking buzzer.loop() plays out in UITask::loop()
*/
buzzer.shutdown();
uint32_t buzzer_timer = millis(); // fail-safe shutdown
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
buzzer.loop();
#endif // PIN_BUZZER
if (restart) {
_board->reboot();
} else {
radio_driver.powerOff();
_board->powerOff();
}
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (_userButton) {
_userButton->update();
}
#endif
#ifdef PIN_USER_BTN_ANA
if (_userButtonAnalog) {
_userButtonAnalog->update();
}
#endif
userLedHandler();
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
if (_display != NULL && _display->isOn()) {
static bool _firstBoot = true;
if(_firstBoot && (millis() - ui_started_at) >= BOOT_SCREEN_MILLIS) {
_need_refresh = true;
_firstBoot = false;
}
if (millis() >= _next_refresh && _need_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}
void UITask::handleButtonAnyPress() {
MESH_DEBUG_PRINTLN("UITask: any press triggered");
// called on any button press before other events, to wake up the display quickly
// do not refresh the display here, as it may block the button handler
if (_display != NULL) {
_displayWasOn = _display->isOn(); // Track display state before any action
if (!_displayWasOn) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
}
void UITask::handleButtonShortPress() {
MESH_DEBUG_PRINTLN("UITask: short press triggered");
if (_display != NULL) {
// Only clear message preview if display was already on before button press
if (_displayWasOn) {
// If display was on and showing message preview, clear it
if (_origin[0] && _msg[0]) {
clearMsgPreview();
} else {
// Otherwise, refresh the display
_need_refresh = true;
}
} else {
_need_refresh = true; // display just turned on, so we need to refresh
}
// Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress
}
}
void UITask::handleButtonDoublePress() {
MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert");
// ADVERT
#ifdef PIN_BUZZER
notify(UIEventType::ack);
#endif
if (the_mesh.advert()) {
MESH_DEBUG_PRINTLN("Advert sent!");
sprintf(_alert, "Advert sent!");
} else {
MESH_DEBUG_PRINTLN("Advert failed!");
sprintf(_alert, "Advert failed..");
}
_need_refresh = true;
}
void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
sprintf(_alert, "Buzzer: ON");
} else {
buzzer.quiet(true);
sprintf(_alert, "Buzzer: OFF");
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
_need_refresh = true;
#endif
}
void UITask::handleButtonQuadruplePress() {
MESH_DEBUG_PRINTLN("UITask: quad press triggered");
if (_sensors != NULL) {
// toggle GPS onn/off
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
notify(UIEventType::ack);
sprintf(_alert, "GPS: Disabled");
} else {
_sensors->setSettingValue("gps", "1");
notify(UIEventType::ack);
sprintf(_alert, "GPS: Enabled");
}
break;
}
}
}
_need_refresh = true;
}
void UITask::handleButtonLongPress() {
MESH_DEBUG_PRINTLN("UITask: long press triggered");
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
} else {
shutdown();
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/SensorManager.h>
#include <stddef.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
#include "Button.h"
class UITask : public AbstractUITask {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
char _version_info[32];
char _origin[62];
char _msg[80];
char _alert[80];
int _msgcount;
bool _need_refresh = true;
bool _displayWasOn = false; // Track display state before button press
unsigned long ui_started_at;
// Button handlers
#ifdef PIN_USER_BTN
Button* _userButton = nullptr;
#endif
#ifdef PIN_USER_BTN_ANA
Button* _userButtonAnalog = nullptr;
#endif
void renderCurrScreen();
void userLedHandler();
void renderBatteryIndicator(uint16_t batteryMilliVolts);
// Button action handlers
void handleButtonAnyPress();
void handleButtonShortPress();
void handleButtonDoublePress();
void handleButtonTriplePress();
void handleButtonQuadruplePress();
void handleButtonLongPress();
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
_next_refresh = 0;
ui_started_at = 0;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
bool hasDisplay() const { return _display != NULL; }
void clearMsgPreview();
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
};

View File

@@ -0,0 +1,581 @@
#include "KissModem.h"
#include <CayenneLPP.h>
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors)
: _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_pending_tx_len = 0;
_txdelay = KISS_DEFAULT_TXDELAY;
_persistence = KISS_DEFAULT_PERSISTENCE;
_slottime = KISS_DEFAULT_SLOTTIME;
_txtail = 0;
_fullduplex = 0;
_tx_state = TX_IDLE;
_tx_timer = 0;
_setRadioCallback = nullptr;
_setTxPowerCallback = nullptr;
_getCurrentRssiCallback = nullptr;
_getStatsCallback = nullptr;
_config = {0, 0, 0, 0, 0};
_signal_report_enabled = true;
}
void KissModem::begin() {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
void KissModem::writeByte(uint8_t b) {
if (b == KISS_FEND) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFEND);
} else if (b == KISS_FESC) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFESC);
} else {
_serial.write(b);
}
}
void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(type);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(KISS_CMD_SETHARDWARE);
writeByte(sub_cmd);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareError(uint8_t error_code) {
writeHardwareFrame(HW_RESP_ERROR, &error_code, 1);
}
void KissModem::loop() {
while (_serial.available()) {
uint8_t b = _serial.read();
if (b == KISS_FEND) {
if (_rx_active && _rx_len > 0) {
processFrame();
}
_rx_len = 0;
_rx_escaped = false;
_rx_active = true;
continue;
}
if (!_rx_active) continue;
if (b == KISS_FESC) {
_rx_escaped = true;
continue;
}
if (_rx_escaped) {
_rx_escaped = false;
if (b == KISS_TFEND) b = KISS_FEND;
else if (b == KISS_TFESC) b = KISS_FESC;
else continue;
}
if (_rx_len < KISS_MAX_FRAME_SIZE) {
_rx_buf[_rx_len++] = b;
} else {
/* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
}
}
processTx();
}
void KissModem::processFrame() {
if (_rx_len < 1) return;
uint8_t type_byte = _rx_buf[0];
if (type_byte == KISS_CMD_RETURN) return;
uint8_t port = (type_byte >> 4) & 0x0F;
uint8_t cmd = type_byte & 0x0F;
if (port != 0) return;
const uint8_t* data = &_rx_buf[1];
uint16_t data_len = _rx_len - 1;
switch (cmd) {
case KISS_CMD_DATA:
if (data_len > 0 && data_len <= KISS_MAX_PACKET_SIZE && !_has_pending_tx) {
memcpy(_pending_tx, data, data_len);
_pending_tx_len = data_len;
_has_pending_tx = true;
}
break;
case KISS_CMD_TXDELAY:
if (data_len >= 1) _txdelay = data[0];
break;
case KISS_CMD_PERSISTENCE:
if (data_len >= 1) _persistence = data[0];
break;
case KISS_CMD_SLOTTIME:
if (data_len >= 1) _slottime = data[0];
break;
case KISS_CMD_TXTAIL:
if (data_len >= 1) _txtail = data[0];
break;
case KISS_CMD_FULLDUPLEX:
if (data_len >= 1) _fullduplex = data[0];
break;
case KISS_CMD_SETHARDWARE:
if (data_len >= 1) {
handleHardwareCommand(data[0], data + 1, data_len - 1);
}
break;
default:
break;
}
}
void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
switch (sub_cmd) {
case HW_CMD_GET_IDENTITY:
handleGetIdentity();
break;
case HW_CMD_GET_RANDOM:
handleGetRandom(data, len);
break;
case HW_CMD_VERIFY_SIGNATURE:
handleVerifySignature(data, len);
break;
case HW_CMD_SIGN_DATA:
handleSignData(data, len);
break;
case HW_CMD_ENCRYPT_DATA:
handleEncryptData(data, len);
break;
case HW_CMD_DECRYPT_DATA:
handleDecryptData(data, len);
break;
case HW_CMD_KEY_EXCHANGE:
handleKeyExchange(data, len);
break;
case HW_CMD_HASH:
handleHash(data, len);
break;
case HW_CMD_SET_RADIO:
handleSetRadio(data, len);
break;
case HW_CMD_SET_TX_POWER:
handleSetTxPower(data, len);
break;
case HW_CMD_GET_RADIO:
handleGetRadio();
break;
case HW_CMD_GET_TX_POWER:
handleGetTxPower();
break;
case HW_CMD_GET_VERSION:
handleGetVersion();
break;
case HW_CMD_GET_CURRENT_RSSI:
handleGetCurrentRssi();
break;
case HW_CMD_IS_CHANNEL_BUSY:
handleIsChannelBusy();
break;
case HW_CMD_GET_AIRTIME:
handleGetAirtime(data, len);
break;
case HW_CMD_GET_NOISE_FLOOR:
handleGetNoiseFloor();
break;
case HW_CMD_GET_STATS:
handleGetStats();
break;
case HW_CMD_GET_BATTERY:
handleGetBattery();
break;
case HW_CMD_PING:
handlePing();
break;
case HW_CMD_GET_SENSORS:
handleGetSensors(data, len);
break;
case HW_CMD_GET_MCU_TEMP:
handleGetMCUTemp();
break;
case HW_CMD_REBOOT:
handleReboot();
break;
case HW_CMD_GET_DEVICE_NAME:
handleGetDeviceName();
break;
case HW_CMD_SET_SIGNAL_REPORT:
handleSetSignalReport(data, len);
break;
case HW_CMD_GET_SIGNAL_REPORT:
handleGetSignalReport();
break;
default:
writeHardwareError(HW_ERR_UNKNOWN_CMD);
break;
}
}
void KissModem::processTx() {
switch (_tx_state) {
case TX_IDLE:
if (_has_pending_tx) {
if (_fullduplex) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_state = TX_WAIT_CLEAR;
}
}
break;
case TX_WAIT_CLEAR:
if (!_radio.isReceiving()) {
uint8_t rand_val;
_rng.random(&rand_val, 1);
if (rand_val <= _persistence) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_timer = millis();
_tx_state = TX_SLOT_WAIT;
}
}
break;
case TX_SLOT_WAIT:
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
_tx_state = TX_WAIT_CLEAR;
}
break;
case TX_DELAY:
if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) {
_radio.startSendRaw(_pending_tx, _pending_tx_len);
_tx_state = TX_SENDING;
}
break;
case TX_SENDING:
if (_radio.isSendComplete()) {
_radio.onSendFinished();
uint8_t result = 0x01;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
break;
}
}
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
writeFrame(KISS_CMD_DATA, packet, len);
if (_signal_report_enabled) {
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
writeHardwareFrame(HW_RESP_RX_META, meta, 2);
}
}
void KissModem::handleGetIdentity() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE);
}
void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t requested = data[0];
if (requested < 1 || requested > 64) {
writeHardwareError(HW_ERR_INVALID_PARAM);
return;
}
uint8_t buf[64];
_rng.random(buf, requested);
writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested);
}
void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
mesh::Identity signer(data);
const uint8_t* signature = data + PUB_KEY_SIZE;
const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE;
uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE;
uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_VERIFY_SIGNATURE), &result, 1);
}
void KissModem::handleSignData(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t signature[SIGNATURE_SIZE];
_identity.sign(signature, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_SIGN_DATA), signature, SIGNATURE_SIZE);
}
void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* plaintext = data + PUB_KEY_SIZE;
uint16_t plaintext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len);
if (encrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_ENCRYPT_DATA), buf, encrypted_len);
} else {
writeHardwareError(HW_ERR_ENCRYPT_FAILED);
}
}
void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* ciphertext = data + PUB_KEY_SIZE;
uint16_t ciphertext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len);
if (decrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_DECRYPT_DATA), buf, decrypted_len);
} else {
writeHardwareError(HW_ERR_MAC_FAILED);
}
}
void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t shared_secret[PUB_KEY_SIZE];
_identity.calcSharedSecret(shared_secret, data);
writeHardwareFrame(HW_RESP(HW_CMD_KEY_EXCHANGE), shared_secret, PUB_KEY_SIZE);
}
void KissModem::handleHash(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t hash[32];
mesh::Utils::sha256(hash, 32, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32);
}
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) {
if (len < 10) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setRadioCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
memcpy(&_config.freq_hz, data, 4);
memcpy(&_config.bw_hz, data + 4, 4);
_config.sf = data[8];
_config.cr = data[9];
_setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setTxPowerCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
_config.tx_power = data[0];
_setTxPowerCallback(data[0]);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleGetRadio() {
uint8_t buf[10];
memcpy(buf, &_config.freq_hz, 4);
memcpy(buf + 4, &_config.bw_hz, 4);
buf[8] = _config.sf;
buf[9] = _config.cr;
writeHardwareFrame(HW_RESP(HW_CMD_GET_RADIO), buf, 10);
}
void KissModem::handleGetTxPower() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_TX_POWER), &_config.tx_power, 1);
}
void KissModem::handleGetVersion() {
uint8_t buf[2];
buf[0] = KISS_FIRMWARE_VERSION;
buf[1] = 0;
writeHardwareFrame(HW_RESP(HW_CMD_GET_VERSION), buf, 2);
}
void KissModem::handleGetCurrentRssi() {
if (!_getCurrentRssiCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
float rssi = _getCurrentRssiCallback();
int8_t rssi_byte = (int8_t)rssi;
writeHardwareFrame(HW_RESP(HW_CMD_GET_CURRENT_RSSI), (uint8_t*)&rssi_byte, 1);
}
void KissModem::handleIsChannelBusy() {
uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_IS_CHANNEL_BUSY), &busy, 1);
}
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t packet_len = data[0];
uint32_t airtime = _radio.getEstAirtimeFor(packet_len);
writeHardwareFrame(HW_RESP(HW_CMD_GET_AIRTIME), (uint8_t*)&airtime, 4);
}
void KissModem::handleGetNoiseFloor() {
int16_t noise_floor = _radio.getNoiseFloor();
writeHardwareFrame(HW_RESP(HW_CMD_GET_NOISE_FLOOR), (uint8_t*)&noise_floor, 2);
}
void KissModem::handleGetStats() {
if (!_getStatsCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
uint32_t rx, tx, errors;
_getStatsCallback(&rx, &tx, &errors);
uint8_t buf[12];
memcpy(buf, &rx, 4);
memcpy(buf + 4, &tx, 4);
memcpy(buf + 8, &errors, 4);
writeHardwareFrame(HW_RESP(HW_CMD_GET_STATS), buf, 12);
}
void KissModem::handleGetBattery() {
uint16_t mv = _board.getBattMilliVolts();
writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2);
}
void KissModem::handlePing() {
writeHardwareFrame(HW_RESP(HW_CMD_PING), nullptr, 0);
}
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t permissions = data[0];
CayenneLPP telemetry(255);
if (_sensors.querySensors(permissions, telemetry)) {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize());
} else {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), nullptr, 0);
}
}
void KissModem::handleGetMCUTemp() {
float temp = _board.getMCUTemperature();
if (isnan(temp)) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
int16_t temp_tenths = (int16_t)(temp * 10.0f);
writeHardwareFrame(HW_RESP(HW_CMD_GET_MCU_TEMP), (uint8_t*)&temp_tenths, 2);
}
void KissModem::handleReboot() {
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
_serial.flush();
delay(50);
_board.reboot();
}
void KissModem::handleGetDeviceName() {
const char* name = _board.getManufacturerName();
writeHardwareFrame(HW_RESP(HW_CMD_GET_DEVICE_NAME), (const uint8_t*)name, strlen(name));
}
void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
_signal_report_enabled = (data[0] != 0x00);
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}
void KissModem::handleGetSignalReport() {
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}

View File

@@ -0,0 +1,183 @@
#pragma once
#include <Arduino.h>
#include <Identity.h>
#include <Utils.h>
#include <Mesh.h>
#include <helpers/SensorManager.h>
#define KISS_FEND 0xC0
#define KISS_FESC 0xDB
#define KISS_TFEND 0xDC
#define KISS_TFESC 0xDD
#define KISS_MAX_FRAME_SIZE 512
#define KISS_MAX_PACKET_SIZE 255
#define KISS_CMD_DATA 0x00
#define KISS_CMD_TXDELAY 0x01
#define KISS_CMD_PERSISTENCE 0x02
#define KISS_CMD_SLOTTIME 0x03
#define KISS_CMD_TXTAIL 0x04
#define KISS_CMD_FULLDUPLEX 0x05
#define KISS_CMD_SETHARDWARE 0x06
#define KISS_CMD_RETURN 0xFF
#define KISS_DEFAULT_TXDELAY 50
#define KISS_DEFAULT_PERSISTENCE 63
#define KISS_DEFAULT_SLOTTIME 10
#define HW_CMD_GET_IDENTITY 0x01
#define HW_CMD_GET_RANDOM 0x02
#define HW_CMD_VERIFY_SIGNATURE 0x03
#define HW_CMD_SIGN_DATA 0x04
#define HW_CMD_ENCRYPT_DATA 0x05
#define HW_CMD_DECRYPT_DATA 0x06
#define HW_CMD_KEY_EXCHANGE 0x07
#define HW_CMD_HASH 0x08
#define HW_CMD_SET_RADIO 0x09
#define HW_CMD_SET_TX_POWER 0x0A
#define HW_CMD_GET_RADIO 0x0B
#define HW_CMD_GET_TX_POWER 0x0C
#define HW_CMD_GET_CURRENT_RSSI 0x0D
#define HW_CMD_IS_CHANNEL_BUSY 0x0E
#define HW_CMD_GET_AIRTIME 0x0F
#define HW_CMD_GET_NOISE_FLOOR 0x10
#define HW_CMD_GET_VERSION 0x11
#define HW_CMD_GET_STATS 0x12
#define HW_CMD_GET_BATTERY 0x13
#define HW_CMD_GET_MCU_TEMP 0x14
#define HW_CMD_GET_SENSORS 0x15
#define HW_CMD_GET_DEVICE_NAME 0x16
#define HW_CMD_PING 0x17
#define HW_CMD_REBOOT 0x18
#define HW_CMD_SET_SIGNAL_REPORT 0x19
#define HW_CMD_GET_SIGNAL_REPORT 0x1A
/* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */
#define HW_RESP(cmd) ((cmd) | 0x80)
/* Generic responses (shared by multiple commands) */
#define HW_RESP_OK 0xF0
#define HW_RESP_ERROR 0xF1
/* Unsolicited notifications (no corresponding request) */
#define HW_RESP_TX_DONE 0xF8
#define HW_RESP_RX_META 0xF9
#define HW_ERR_INVALID_LENGTH 0x01
#define HW_ERR_INVALID_PARAM 0x02
#define HW_ERR_NO_CALLBACK 0x03
#define HW_ERR_MAC_FAILED 0x04
#define HW_ERR_UNKNOWN_CMD 0x05
#define HW_ERR_ENCRYPT_FAILED 0x06
#define KISS_FIRMWARE_VERSION 1
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr);
typedef void (*SetTxPowerCallback)(uint8_t power);
typedef float (*GetCurrentRssiCallback)();
typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors);
struct RadioConfig {
uint32_t freq_hz;
uint32_t bw_hz;
uint8_t sf;
uint8_t cr;
uint8_t tx_power;
};
enum TxState {
TX_IDLE,
TX_WAIT_CLEAR,
TX_SLOT_WAIT,
TX_DELAY,
TX_SENDING
};
class KissModem {
Stream& _serial;
mesh::LocalIdentity& _identity;
mesh::RNG& _rng;
mesh::Radio& _radio;
mesh::MainBoard& _board;
SensorManager& _sensors;
uint8_t _rx_buf[KISS_MAX_FRAME_SIZE];
uint16_t _rx_len;
bool _rx_escaped;
bool _rx_active;
uint8_t _pending_tx[KISS_MAX_PACKET_SIZE];
uint16_t _pending_tx_len;
bool _has_pending_tx;
uint8_t _txdelay;
uint8_t _persistence;
uint8_t _slottime;
uint8_t _txtail;
uint8_t _fullduplex;
TxState _tx_state;
uint32_t _tx_timer;
SetRadioCallback _setRadioCallback;
SetTxPowerCallback _setTxPowerCallback;
GetCurrentRssiCallback _getCurrentRssiCallback;
GetStatsCallback _getStatsCallback;
RadioConfig _config;
bool _signal_report_enabled;
void writeByte(uint8_t b);
void writeFrame(uint8_t type, const uint8_t* data, uint16_t len);
void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void writeHardwareError(uint8_t error_code);
void processFrame();
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void processTx();
void handleGetIdentity();
void handleGetRandom(const uint8_t* data, uint16_t len);
void handleVerifySignature(const uint8_t* data, uint16_t len);
void handleSignData(const uint8_t* data, uint16_t len);
void handleEncryptData(const uint8_t* data, uint16_t len);
void handleDecryptData(const uint8_t* data, uint16_t len);
void handleKeyExchange(const uint8_t* data, uint16_t len);
void handleHash(const uint8_t* data, uint16_t len);
void handleSetRadio(const uint8_t* data, uint16_t len);
void handleSetTxPower(const uint8_t* data, uint16_t len);
void handleGetRadio();
void handleGetTxPower();
void handleGetVersion();
void handleGetCurrentRssi();
void handleIsChannelBusy();
void handleGetAirtime(const uint8_t* data, uint16_t len);
void handleGetNoiseFloor();
void handleGetStats();
void handleGetBattery();
void handlePing();
void handleGetSensors(const uint8_t* data, uint16_t len);
void handleGetMCUTemp();
void handleReboot();
void handleGetDeviceName();
void handleSetSignalReport(const uint8_t* data, uint16_t len);
void handleGetSignalReport();
public:
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors);
void begin();
void loop();
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; }
void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; }
void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; }
void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len);
bool isTxBusy() const { return _tx_state != TX_IDLE; }
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
};

View File

@@ -0,0 +1,146 @@
#include <Arduino.h>
#include <target.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/IdentityStore.h>
#include "KissModem.h"
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
#endif
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
#define AGC_RESET_INTERVAL_MS 30000
StdRNG rng;
mesh::LocalIdentity identity;
KissModem* modem;
static uint32_t next_noise_floor_calib_ms = 0;
static uint32_t next_agc_reset_ms = 0;
void halt() {
while (1) ;
}
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "Filesystem not defined"
#endif
if (!store.load("_main", identity)) {
identity = radio_new_identity();
while (identity.pub_key[0] == 0x00 || identity.pub_key[0] == 0xFF) {
identity = radio_new_identity();
}
store.save("_main", identity);
}
}
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_set_params(freq, bw, sf, cr);
}
void onSetTxPower(uint8_t power) {
radio_set_tx_power(power);
}
float onGetCurrentRssi() {
return radio_driver.getCurrentRSSI();
}
void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) {
*rx = radio_driver.getPacketsRecv();
*tx = radio_driver.getPacketsSent();
*errors = radio_driver.getPacketsRecvErrors();
}
void setup() {
board.begin();
if (!radio_init()) {
halt();
}
radio_driver.begin();
rng.begin(radio_get_rng_seed());
loadOrCreateIdentity();
sensors.begin();
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#if defined(ESP32)
Serial1.setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(NRF52_PLATFORM)
((Uart *)&Serial1)->setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(RP2040_PLATFORM)
((SerialUART *)&Serial1)->setRX(KISS_UART_RX);
((SerialUART *)&Serial1)->setTX(KISS_UART_TX);
Serial1.begin(115200);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)&Serial1)->setRx(KISS_UART_RX);
((HardwareSerial *)&Serial1)->setTx(KISS_UART_TX);
Serial1.begin(115200);
#else
#error "KISS UART not supported on this platform"
#endif
modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors);
#else
Serial.begin(115200);
uint32_t start = millis();
while (!Serial && millis() - start < 3000) delay(10);
delay(100);
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
#endif
modem->setRadioCallback(onSetRadio);
modem->setTxPowerCallback(onSetTxPower);
modem->setGetCurrentRssiCallback(onGetCurrentRssi);
modem->setGetStatsCallback(onGetStats);
modem->begin();
}
void loop() {
modem->loop();
if (!modem->isActuallyTransmitting()) {
if (!modem->isTxBusy()) {
if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) {
radio_driver.resetAGC();
next_agc_reset_ms = millis();
}
}
uint8_t rx_buf[256];
int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf));
if (rx_len > 0) {
int8_t snr = (int8_t)(radio_driver.getLastSNR() * 4);
int8_t rssi = (int8_t)radio_driver.getLastRSSI();
modem->onPacketReceived(snr, rssi, rx_buf, rx_len);
}
}
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
radio_driver.triggerNoiseFloorCalibrate(0);
next_noise_floor_calib_ms = millis();
}
radio_driver.loop();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#include <RTClib.h>
#include <target.h>
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#ifdef WITH_RS232_BRIDGE
#include "helpers/bridges/RS232Bridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_ESPNOW_BRIDGE
#include "helpers/bridges/ESPNowBridge.h"
#define WITH_BRIDGE
#endif
#include <helpers/AdvertDataHelpers.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/ClientACL.h>
#include <helpers/CommonCLI.h>
#include <helpers/IdentityStore.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include "RateLimiter.h"
#ifdef WITH_BRIDGE
extern AbstractBridge* bridge;
#endif
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events; // was 'n_full_events'
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
uint32_t n_recv_errors;
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 32
#endif
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
uint32_t heard_timestamp;
int8_t snr; // multiplied by 4, user should divide to get float value
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.0"
#endif
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
uint8_t reply_path[MAX_PATH_SIZE];
int8_t reply_path_len;
uint8_t reply_path_hash_size;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
TransportKey default_scope;
RateLimiter discover_limiter, anon_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
CayenneLPP telemetry;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
int matching_peer_indexes[MAX_CLIENTS];
#if defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
bool allowPacketForward(const mesh::Packet* packet) override;
const char* getLogDateTime() override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
void logTx(mesh::Packet* pkt, int len) override;
void logTxFail(mesh::Packet* pkt, int len) override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
void sendNodeDiscoverReq();
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
}
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
#if defined(WITH_BRIDGE)
void setBridgeState(bool enable) override {
if (enable == bridge.isRunning()) return;
if (enable)
{
bridge.begin();
}
else
{
bridge.end();
}
}
void restartBridge() override {
if (!bridge.isRunning()) return;
bridge.end();
bridge.begin();
}
#endif
// To check if there is pending work
bool hasPendingWork() const;
#if defined(USE_SX1262) || defined(USE_SX1268)
void setRxBoostedGain(bool enable) override;
#endif
};

View File

@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
class RateLimiter {
uint32_t _start_timestamp;
uint32_t _secs;
uint16_t _maximum, _count;
public:
RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
bool allow(uint32_t now) {
if (now < _start_timestamp + _secs) {
_count++;
if (_count > _maximum) return false; // deny
} else { // time window now expired
_start_timestamp = now;
_count = 1;
}
return true;
}
};

View File

@@ -0,0 +1,118 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_display->turnOn();
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
}
void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);
// node type
const char* node_type = "< Repeater >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
}
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop();
};

View File

@@ -0,0 +1,170 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "MyMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
}
static char command[160];
// For power saving
unsigned long lastActive = 0; // mark last active time
unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long userBtnDownAt = 0;
#define USER_BTN_HOLD_OFF_MILLIS 1500
#endif
void setup() {
Serial.begin(115200);
delay(1000);
board.begin();
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
// give some extra time for serial to settle so
// boot debug messages can be seen on terminal
delay(5000);
#endif
// For power saving
lastActive = millis(); // mark last active time since boot
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
display.setCursor(0, 0);
display.print("Please wait...");
display.endFrame();
}
#endif
if (!radio_init()) {
MESH_DEBUG_PRINTLN("Radio init failed!");
halt();
}
fast_rng.begin(radio_get_rng_seed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "need to define filesystem"
#endif
if (!store.load("_main", the_mesh.self_id)) {
MESH_DEBUG_PRINTLN("Generating new keypair");
the_mesh.self_id = radio_new_identity(); // create new random identity
int count = 0;
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes
the_mesh.self_id = radio_new_identity(); count++;
}
store.save("_main", the_mesh.self_id);
}
Serial.print("Repeater ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
sensors.begin();
the_mesh.begin(fs);
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
if (c != '\n') {
command[len++] = c;
command[len] = 0;
Serial.print(c);
}
if (c == '\r') break;
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
command[0] = 0; // reset command buffer
}
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
// Hold the user button to power off the SenseCAP Solar repeater.
int btnState = digitalRead(PIN_USER_BTN);
if (btnState == LOW) {
if (userBtnDownAt == 0) {
userBtnDownAt = millis();
} else if ((unsigned long)(millis() - userBtnDownAt) >= USER_BTN_HOLD_OFF_MILLIS) {
Serial.println("Powering off...");
board.powerOff(); // does not return
}
} else {
userBtnDownAt = 0;
}
#endif
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible
#else
if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet
lastActive = millis();
nextSleepinSecs = 5; // Default: To work for 5s and sleep again
} else {
nextSleepinSecs += 5; // When there is pending work, to work another 5s
}
#endif
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#include <RTClib.h>
#include <target.h>
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.0"
#endif
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef ADVERT_NAME
#define ADVERT_NAME "Test BBS"
#endif
#ifndef ADVERT_LAT
#define ADVERT_LAT 0.0
#endif
#ifndef ADVERT_LON
#define ADVERT_LON 0.0
#endif
#ifndef ADMIN_PASSWORD
#define ADMIN_PASSWORD "password"
#endif
#ifndef MAX_UNSYNCED_POSTS
#define MAX_UNSYNCED_POSTS 32
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#define FIRMWARE_ROLE "room_server"
#define PACKET_LOG_FILE "/packet_log"
#define MAX_POST_TEXT_LEN (160-9)
struct PostInfo {
mesh::Identity author;
uint32_t post_timestamp; // by OUR clock
char text[MAX_POST_TEXT_LEN+1];
};
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
bool region_load_active;
NodePrefs _prefs;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
ClientACL acl;
CommonCLI _cli;
unsigned long dirty_contacts_expiry;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
unsigned long next_push;
uint16_t _num_posted, _num_post_pushes;
int next_client_idx; // for round-robin polling
int next_post_idx;
PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue
CayenneLPP telemetry;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
TransportKey default_scope;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
int matching_peer_indexes[MAX_CLIENTS];
void addPost(ClientInfo* client, const char* postData);
void pushPostToClient(ClientInfo* client, PostInfo& post);
uint8_t getUnsyncedCount(ClientInfo* client);
bool processAck(const uint8_t *data);
mesh::Packet* createSelfAdvert();
File openAppend(const char* fname);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
void logTx(mesh::Packet* pkt, int len) override;
void logTxFail(mesh::Packet* pkt, int len) override;
int calcRxDelay(float score, uint32_t air_time) const override;
const char* getLogDateTime() override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
bool allowPacketForward(const mesh::Packet* packet) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override ;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
}
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
static bool saveFilter(ClientInfo* client);
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
};

View File

@@ -0,0 +1,118 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_display->turnOn();
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
}
void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);
// node type
const char* node_type = "< Room Server >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
}
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop();
};

View File

@@ -0,0 +1,116 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "MyMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
}
static char command[MAX_POST_TEXT_LEN+1];
void setup() {
Serial.begin(115200);
delay(1000);
board.begin();
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
display.setCursor(0, 0);
display.print("Please wait...");
display.endFrame();
}
#endif
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#else
#error "need to define filesystem"
#endif
if (!store.load("_main", the_mesh.self_id)) {
the_mesh.self_id = radio_new_identity(); // create new random identity
int count = 0;
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes
the_mesh.self_id = radio_new_identity(); count++;
}
store.save("_main", the_mesh.self_id);
}
Serial.print("Room ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
sensors.begin();
the_mesh.begin(fs);
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
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
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
command[0] = 0; // reset command buffer
}
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -0,0 +1,594 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <RTClib.h>
#include <target.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
#define FIRMWARE_VER_TEXT "v2 (build: 4 Feb 2025)"
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 100
#endif
#include <helpers/BaseChatMesh.h>
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
// 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;
}
/* -------------------------------------------------------------------------------------- */
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
float freq;
int8_t tx_power_dbm;
uint8_t unused[3];
};
class MyMesh : public BaseChatMesh, ContactVisitor {
FILESYSTEM* _fs;
NodePrefs _prefs;
uint32_t expected_ack_crc;
ChannelDetails* _public;
unsigned long last_msg_sent;
ContactInfo* curr_recipient;
char command[512+10];
uint8_t tmp_buf[256];
char hex_buf[512];
const char* getTypeName(uint8_t type) const {
if (type == ADV_TYPE_CHAT) return "Chat";
if (type == ADV_TYPE_REPEATER) return "Repeater";
if (type == ADV_TYPE_ROOM) return "Room";
return "??"; // unknown
}
void loadContacts() {
if (_fs->exists("/contacts")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/contacts", "r");
#else
File file = _fs->open("/contacts");
#endif
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused;
uint32_t reserved;
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.name, 32) == 32);
success = success && (file.read(&c.type, 1) == 1);
success = success && (file.read(&c.flags, 1) == 1);
success = success && (file.read(&unused, 1) == 1);
success = success && (file.read((uint8_t *) &reserved, 4) == 4);
success = success && (file.read((uint8_t *) &c.out_path_len, 1) == 1);
success = success && (file.read((uint8_t *) &c.last_advert_timestamp, 4) == 4);
success = success && (file.read(c.out_path, 64) == 64);
c.gps_lat = c.gps_lon = 0; // not yet supported
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
c.lastmod = 0;
if (!addContact(c)) full = true;
}
file.close();
}
}
}
void saveContacts() {
#if defined(NRF52_PLATFORM)
_fs->remove("/contacts");
File file = _fs->open("/contacts", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open("/contacts", "w");
#else
File file = _fs->open("/contacts", "w", true);
#endif
if (file) {
ContactsIterator iter;
ContactInfo c;
uint8_t unused = 0;
uint32_t reserved = 0;
while (iter.hasNext(this, c)) {
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *) &reserved, 4) == 4);
success = success && (file.write((uint8_t *) &c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *) &c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
if (!success) break; // write failed
}
file.close();
}
}
void setClock(uint32_t timestamp) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (timestamp > curr) {
getRTCClock()->setCurrentTime(timestamp);
Serial.println(" (OK - clock set!)");
} else {
Serial.println(" (ERR: clock cannot go backwards)");
}
}
void importCard(const char* command) {
while (*command == ' ') command++; // skip leading spaces
if (memcmp(command, "meshcore://", 11) == 0) {
command += 11; // skip the prefix
char *ep = strchr(command, 0); // find end of string
while (ep > command) {
ep--;
if (mesh::Utils::isHexChar(*ep)) break; // found tail end of card
*ep = 0; // remove trailing spaces and other junk
}
int len = strlen(command);
if (len % 2 == 0) {
len >>= 1; // halve, for num bytes
if (mesh::Utils::fromHex(tmp_buf, len, command)) {
importContact(tmp_buf, len);
return;
}
}
}
Serial.println(" error: invalid format");
}
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
int calcRxDelay(float score, uint32_t air_time) const override {
return 0; // disable rxdelay
}
bool allowPacketForward(const mesh::Packet* packet) override {
return true;
}
void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) override {
// TODO: if not in favs, prompt to add as fav(?)
Serial.printf("ADVERT from -> %s\n", contact.name);
Serial.printf(" type: %s\n", getTypeName(contact.type));
Serial.print(" public key: "); mesh::Utils::printHex(Serial, contact.id.pub_key, PUB_KEY_SIZE); Serial.println();
saveContacts();
}
void onContactPathUpdated(const ContactInfo& contact) override {
Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (uint32_t) contact.out_path_len);
saveContacts();
}
ContactInfo* processAck(const uint8_t *data) override {
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
// NOTE: the same ACK can be received multiple times!
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return NULL; // TODO: really should return ContactInfo pointer
}
//uint32_t crc;
//memcpy(&crc, data, 4);
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
return NULL;
}
void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
Serial.printf("(%s) MSG -> from %s\n", pkt->isRouteDirect() ? "DIRECT" : "FLOOD", from.name);
Serial.printf(" %s\n", text);
if (strcmp(text, "clock sync") == 0) { // special text command
setClock(sender_timestamp + 1);
}
}
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
}
void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
}
void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) override {
if (pkt->isRouteDirect()) {
Serial.printf("PUBLIC CHANNEL MSG -> (Direct!)\n");
} else {
Serial.printf("PUBLIC CHANNEL MSG -> (Flood) hops %d\n", pkt->path_len);
}
Serial.printf(" %s\n", text);
}
uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) override {
return 0; // unknown
}
void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override {
// not supported
}
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override {
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 {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS +
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_hash_count + 1));
}
void onSendTimeout() override {
Serial.println(" ERROR: timed out, no ACK.");
}
public:
MyMesh(mesh::Radio& radio, StdRNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables)
{
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER;
command[0] = 0;
curr_recipient = NULL;
}
float getFreqPref() const { return _prefs.freq; }
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
void begin(FILESYSTEM& fs) {
_fs = &fs;
BaseChatMesh::begin();
#if defined(NRF52_PLATFORM)
IdentityStore store(fs, "");
#elif defined(RP2040_PLATFORM)
IdentityStore store(fs, "/identity");
store.begin();
#else
IdentityStore store(fs, "/identity");
#endif
if (!store.load("_main", self_id, _prefs.node_name, sizeof(_prefs.node_name))) { // legacy: node_name was from identity file
// Need way to get some entropy to seed RNG
Serial.println("Press ENTER to generate key:");
char c = 0;
while (c != '\n') { // wait for ENTER to be pressed
if (Serial.available()) c = Serial.read();
}
((StdRNG *)getRNG())->begin(millis());
self_id = mesh::LocalIdentity(getRNG()); // create new random identity
int count = 0;
while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes
self_id = mesh::LocalIdentity(getRNG()); count++;
}
store.save("_main", self_id);
}
// load persisted prefs
if (_fs->exists("/node_prefs")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/node_prefs", "r");
#else
File file = _fs->open("/node_prefs");
#endif
if (file) {
file.read((uint8_t *) &_prefs, sizeof(_prefs));
file.close();
}
}
loadContacts();
_public = addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
}
void savePrefs() {
#if defined(NRF52_PLATFORM)
_fs->remove("/node_prefs");
File file = _fs->open("/node_prefs", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open("/node_prefs", "w");
#else
File file = _fs->open("/node_prefs", "w", true);
#endif
if (file) {
file.write((const uint8_t *)&_prefs, sizeof(_prefs));
file.close();
}
}
void showWelcome() {
Serial.println("===== MeshCore Chat Terminal =====");
Serial.println();
Serial.printf("WELCOME %s\n", _prefs.node_name);
mesh::Utils::printHex(Serial, self_id.pub_key, PUB_KEY_SIZE);
Serial.println();
Serial.println(" (enter 'help' for basic commands)");
Serial.println();
}
void sendSelfAdvert(int delay_millis) {
auto pkt = createSelfAdvert(_prefs.node_name, _prefs.node_lat, _prefs.node_lon);
if (pkt) {
sendFlood(pkt, delay_millis);
}
}
// ContactVisitor
void onContactVisit(const ContactInfo& contact) override {
Serial.printf(" %s - ", contact.name);
char tmp[40];
int32_t secs = contact.last_advert_timestamp - getRTCClock()->getCurrentTime();
AdvertTimeHelper::formatRelativeTimeDiff(tmp, secs, false);
Serial.println(tmp);
}
void handleCommand(const char* command) {
while (*command == ' ') command++; // skip leading spaces
if (memcmp(command, "send ", 5) == 0) {
if (curr_recipient) {
const char *text = &command[5];
uint32_t est_timeout;
int result = sendMessage(*curr_recipient, getRTCClock()->getCurrentTime(), 0, text, expected_ack_crc, est_timeout);
if (result == MSG_SEND_FAILED) {
Serial.println(" ERROR: unable to send.");
} else {
last_msg_sent = _ms->getMillis();
Serial.printf(" (message sent - %s)\n", result == MSG_SEND_SENT_FLOOD ? "FLOOD" : "DIRECT");
}
} else {
Serial.println(" ERROR: no recipient selected (use 'to' cmd).");
}
} else if (memcmp(command, "public ", 7) == 0) { // send GroupChannel msg
uint8_t temp[5+MAX_TEXT_LEN+32];
uint32_t timestamp = getRTCClock()->getCurrentTime();
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = 0; // attempt and flags
sprintf((char *) &temp[5], "%s: %s", _prefs.node_name, &command[7]); // <sender>: <msg>
temp[5 + MAX_TEXT_LEN] = 0; // truncate if too long
int len = strlen((char *) &temp[5]);
auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, _public->channel, temp, 5 + len);
if (pkt) {
sendFlood(pkt);
Serial.println(" Sent.");
} else {
Serial.println(" ERROR: unable to send");
}
} else if (memcmp(command, "list", 4) == 0) { // show Contact list, by most recent
int n = 0;
if (command[4] == ' ') { // optional param, last 'N'
n = atoi(&command[5]);
}
scanRecentContacts(n, this);
} else if (strcmp(command, "clock") == 0) { // show current time
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
Serial.printf( "%02d:%02d - %d/%d/%d UTC\n", 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]);
setClock(secs);
} else if (memcmp(command, "to ", 3) == 0) { // set current recipient
curr_recipient = searchContactsByPrefix(&command[3]);
if (curr_recipient) {
Serial.printf(" Recipient %s now selected.\n", curr_recipient->name);
} else {
Serial.println(" Error: Name prefix not found.");
}
} else if (strcmp(command, "to") == 0) { // show current recipient
if (curr_recipient) {
Serial.printf(" Current: %s\n", curr_recipient->name);
} else {
Serial.println(" Err: no recipient selected");
}
} else if (strcmp(command, "advert") == 0) {
auto pkt = createSelfAdvert(_prefs.node_name, _prefs.node_lat, _prefs.node_lon);
if (pkt) {
sendZeroHop(pkt);
Serial.println(" (advert sent, zero hop).");
} else {
Serial.println(" ERR: unable to send");
}
} else if (strcmp(command, "reset path") == 0) {
if (curr_recipient) {
resetPathTo(*curr_recipient);
saveContacts();
Serial.println(" Done.");
}
} else if (memcmp(command, "card", 4) == 0) {
Serial.printf("Hello %s\n", _prefs.node_name);
auto pkt = createSelfAdvert(_prefs.node_name, _prefs.node_lat, _prefs.node_lon);
if (pkt) {
uint8_t len = pkt->writeTo(tmp_buf);
releasePacket(pkt); // undo the obtainNewPacket()
mesh::Utils::toHex(hex_buf, tmp_buf, len);
Serial.println("Your MeshCore biz card:");
Serial.print("meshcore://"); Serial.println(hex_buf);
Serial.println();
} else {
Serial.println(" Error");
}
} else if (memcmp(command, "import ", 7) == 0) {
importCard(&command[7]);
} 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();
Serial.println(" OK");
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs.node_name, &config[5], sizeof(_prefs.node_name));
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs.node_lat = atof(&config[4]);
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs.node_lon = atof(&config[4]);
savePrefs();
Serial.println(" OK");
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs.tx_power_dbm = atoi(&config[3]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else if (memcmp(config, "freq ", 5) == 0) {
_prefs.freq = atof(&config[5]);
savePrefs();
Serial.println(" OK - reboot to apply");
} else {
Serial.printf(" ERROR: unknown config: %s\n", config);
}
} else if (memcmp(command, "ver", 3) == 0) {
Serial.println(FIRMWARE_VER_TEXT);
} else if (memcmp(command, "help", 4) == 0) {
Serial.println("Commands:");
Serial.println(" set {name|lat|lon|freq|tx|af} {value}");
Serial.println(" card");
Serial.println(" import {biz card}");
Serial.println(" clock");
Serial.println(" time <epoch-seconds>");
Serial.println(" list {n}");
Serial.println(" to <recipient name or prefix>");
Serial.println(" to");
Serial.println(" send <text>");
Serial.println(" advert");
Serial.println(" reset path");
Serial.println(" public <text>");
} else {
Serial.print(" ERROR: unknown command: "); Serial.println(command);
}
}
void loop() {
BaseChatMesh::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
handleCommand(command);
command[0] = 0; // reset command buffer
}
}
};
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
}
void setup() {
Serial.begin(115200);
board.begin();
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
#if defined(NRF52_PLATFORM)
InternalFS.begin();
the_mesh.begin(InternalFS);
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
the_mesh.begin(LittleFS);
#elif defined(ESP32)
SPIFFS.begin(true);
the_mesh.begin(SPIFFS);
#else
#error "need to define filesystem"
#endif
radio_set_params(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
radio_set_tx_power(the_mesh.getTxPowerPref());
the_mesh.showWelcome();
// send out initial Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvert(1200); // add slight delay
#endif
}
void loop() {
the_mesh.loop();
rtc_clock.tick();
}

View File

@@ -0,0 +1,975 @@
#include "SensorMesh.h"
/* ------------------------------ Config -------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef ADVERT_NAME
#define ADVERT_NAME "sensor"
#endif
#ifndef ADVERT_LAT
#define ADVERT_LAT 0.0
#endif
#ifndef ADVERT_LON
#define ADVERT_LON 0.0
#endif
#ifndef ADMIN_PASSWORD
#define ADMIN_PASSWORD "password"
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#ifndef SENSOR_READ_INTERVAL_SECS
#define SENSOR_READ_INTERVAL_SECS 60
#endif
/* ------------------------------ Code -------------------------------- */
#define FIRMWARE_VER_LEVEL 1
#define REQ_TYPE_LOGIN 0x00
#define REQ_TYPE_GET_STATUS 0x01
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
#define REQ_TYPE_GET_ACCESS_LIST 0x05
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
#define CLI_REPLY_DELAY_MILLIS 1000
#define LAZY_CONTACTS_WRITE_DELAY 5000
#define ALERT_ACK_EXPIRY_MILLIS 8000 // wait 8 secs for ACKs to alert messages
static File openAppend(FILESYSTEM* _fs, const char* fname) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(fname, "a");
#else
return _fs->open(fname, "a", true);
#endif
}
static uint8_t getDataSize(uint8_t type) {
switch (type) {
case LPP_GPS:
return 9;
case LPP_POLYLINE:
return 8; // TODO: this is MINIMIUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
return 6;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
return 4;
case LPP_COLOUR:
return 3;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
return 2;
}
return 1;
}
static uint32_t getMultiplier(uint8_t type) {
switch (type) {
case LPP_CURRENT:
case LPP_DISTANCE:
case LPP_ENERGY:
return 1000;
case LPP_VOLTAGE:
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
return 100;
case LPP_TEMPERATURE:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
return 10;
}
return 1;
}
static bool isSigned(uint8_t type) {
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
}
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
uint32_t value = 0;
for (uint8_t i = 0; i < size; i++) {
value = (value << 8) + buffer[i];
}
int sign = 1;
if (is_signed) {
uint32_t bit = 1ul << ((size * 8) - 1);
if ((value & bit) == bit) {
value = (bit << 1) - value;
sign = -1;
}
}
return sign * ((float) value / multiplier);
}
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
// check sign
bool sign = value < 0;
if (sign) value = -value;
// get value to store
uint32_t v = value * multiplier;
// format an uint32_t as if it was an int32_t
if (is_signed & sign) {
uint32_t mask = (1 << (size * 8)) - 1;
v = v & mask;
if (sign) v = mask - v + 1;
}
// add bytes (MSB first)
for (uint8_t i=1; i<=size; i++) {
dest[size - i] = (v & 0xFF);
v >>= 8;
}
return size;
}
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
uint32_t start_secs_ago, end_secs_ago;
memcpy(&start_secs_ago, &payload[0], 4);
memcpy(&end_secs_ago, &payload[4], 4);
uint8_t res1 = payload[8]; // reserved for future (extra query params)
uint8_t res2 = payload[9];
MinMaxAvg data[8];
int n;
if (res1 == 0 && res2 == 0) {
n = querySeriesData(start_secs_ago, end_secs_ago, data, 8);
} else {
n = 0;
}
uint8_t ofs = 4;
{
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[ofs], &now, 4); ofs += 4;
}
for (int i = 0; i < n; i++) {
auto d = &data[i];
reply_data[ofs++] = d->_channel;
reply_data[ofs++] = d->_lpp_type;
uint8_t sz = getDataSize(d->_lpp_type);
uint32_t mult = getMultiplier(d->_lpp_type);
bool is_signed = isSigned(d->_lpp_type);
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
}
return ofs;
}
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
uint8_t res1 = payload[0]; // reserved for future (extra query params)
uint8_t res2 = payload[1];
if (res1 == 0 && res2 == 0) {
uint8_t ofs = 4;
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
reply_data[ofs++] = c->permissions;
}
return ofs;
}
}
return 0; // unknown command
}
mesh::Packet* SensorMesh::createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
return createAdvert(self_id, app_data, app_data_len);
}
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
int text_len = strlen(t->text);
uint8_t data[MAX_PACKET_PAYLOAD];
memcpy(data, &t->timestamp, 4);
data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt; // attempt and flags
memcpy(&data[5], t->text, text_len);
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
t->attempt++;
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
if (pkt) {
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(pkt, c->out_path, c->out_path_len);
} else {
unsigned long delay_millis = 0;
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
}
}
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
}
void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
if (condition) {
if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) {
StrHelper::strncpy(t.text, text, sizeof(t.text));
t.pri = pri;
t.send_expiry = 0; // signal that initial send is needed
t.attempt = 4;
t.curr_contact_idx = -1; // start iterating thru contacts[]
alert_tasks[num_alert_tasks++] = &t; // add to queue
}
} else {
if (t.isTriggered()) {
t.text[0] = 0;
// remove 't' from alert queue
int i = 0;
while (i < num_alert_tasks && alert_tasks[i] != &t) i++;
if (i < num_alert_tasks) { // found, now delete from array
num_alert_tasks--;
while (i < num_alert_tasks) {
alert_tasks[i] = alert_tasks[i + 1];
i++;
}
}
}
}
}
float SensorMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
}
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
return true;
}
int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
ClientInfo* client;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
if (client == NULL) {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
#endif
return 0;
}
} else {
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
#endif
return 0;
}
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
}
MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
client->permissions |= PERM_ACL_ADMIN;
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
if (is_flood) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0;
reply_data[6] = client->isAdmin() ? 1 : 0;
reply_data[7] = client->permissions;
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL;
return 13; // reply length
}
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
command += 3;
}
// first, see if this is a custom-handled CLI command (ie. in main.cpp)
if (handleCustomCommand(sender_timestamp, command, reply)) {
return; // command has been handled
}
// handle sensor-specific CLI commands
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
char* hex = &command[8];
char* sp = strchr(hex, ' '); // look for separator char
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
}
} else {
strcpy(reply, "Err - bad pubkey");
}
}
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
Serial.println("ACL:");
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
Serial.printf("%02X ", c->permissions);
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
Serial.printf("\n");
}
reply[0] = 0;
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
if (command[2] == ' ') { // it's a write
uint32_t val;
uint32_t g = board.getGpio();
if (command[3] == 'r') { // reset bits
sscanf(&command[4], "%x", &val);
val = g & ~val;
} else if (command[3] == 's') { // set bits
sscanf(&command[4], "%x", &val);
val |= g;
} else if (command[3] == 't') { // toggle bits
sscanf(&command[4], "%x", &val);
val ^= g;
} else { // set value
sscanf(&command[3], "%x", &val);
}
board.setGpio(val);
}
sprintf(reply, "%x", board.getGpio());
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
}
void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
reply_len = 0; // unknown request type
}
if (reply_len == 0) return; // invalid request
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, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
int n = 0;
for (int i = 0; i < acl.getNumClients() && n < MAX_SEARCH_RESULTS; i++) {
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
return n;
}
void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < acl.getNumClients()) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
return;
}
ClientInfo* from = acl.getClientByIdx(i);
if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
if (reply_len == 0) return; // invalid command
from->last_timestamp = timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) {
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from->isAdmin()) { // a CLI command
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (sender_timestamp > from->last_timestamp) { // prevent replay attacks
if (flags == TXT_TYPE_PLAIN) {
bool handled = handleIncomingMsg(*from, sender_timestamp, &data[5], flags, len - 5);
if (handled) { // if msg was handled then send an ack
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]), from->id.pub_key, PUB_KEY_SIZE);
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize());
} else {
sendAckTo(*from, ack_hash, packet->getPathHashSize());
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
from->last_timestamp = sender_timestamp;
from->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
uint8_t temp[166];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
handleCommand(sender_timestamp, command, reply);
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
if (reply) {
if (from->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
} else {
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
}
}
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len) {
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
#ifdef MESH_DEBUG
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
Serial.printf(": %s\n", data);
#endif
return false;
}
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
void SensorMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) {
// TODO: apply rate limiting to these!
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
memcpy(&tag, &packet->payload[i], 4); i += 4;
uint32_t since;
if (packet->payload_len >= i+4) { // optional since field
memcpy(&since, &packet->payload[i], 4); i += 4;
} else {
since = 0;
}
if ((filter & (1 << ADV_TYPE_SENSOR)) != 0 && _prefs.discovery_mod_timestamp >= since) {
bool prefix_only = packet->payload[0] & 1;
uint8_t data[6 + PUB_KEY_SIZE];
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_SENSOR; // low 4-bits for node type
data[1] = packet->_snr; // let sender know the inbound SNR ( x 4)
memcpy(&data[2], &tag, 4); // include tag from request, for client to match to
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
if (resp) {
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
}
}
}
}
bool SensorMesh::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) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
return false;
}
ClientInfo* from = acl.getClientByIdx(i);
MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
from->out_path_len = mesh::Packet::copyPath(from->out_path, path, path_len); // store a copy of path, for sendDirect()
from->last_activity = getRTCClock()->getCurrentTime();
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
if (from->isAdmin()) {
// only do saveContacts() (of this out_path change) if this is an admin
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
// NOTE: no reciprocal path send!!
return false;
}
void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
if (num_alert_tasks > 0) {
auto t = alert_tasks[0]; // check current alert task
for (int i = 0; i < t->attempt; i++) {
if (ack_crc == t->expected_acks[i]) { // matching ACK!
t->attempt = 4; // signal to move to next contact
t->send_expiry = 0;
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
return;
}
}
}
}
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
region_map(key_store),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
telemetry(MAX_PACKET_PAYLOAD - 4)
{
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
last_read_time = 0;
num_alert_tasks = 0;
set_radio_at = revert_radio_at = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.2f; // was zero
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
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
_prefs.flood_advert_interval = 0; // disabled
_prefs.disable_fwd = true;
_prefs.path_hash_mode = 1; // 2-byte path hash
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
// GPS defaults
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
memset(default_scope.key, 0, sizeof(default_scope.key));
}
void SensorMesh::begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs, self_id);
region_map.load(_fs);
// establish default-scope
{
RegionEntry* r = region_map.getDefaultRegion();
if (r) {
region_map.getTransportKeysFor(*r, &default_scope, 1);
} else {
#ifdef DEFAULT_FLOOD_SCOPE_NAME
r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
if (r == NULL) {
r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0); // auto-create the default scope region
if (r) { r->flags = 0; } // Allow-flood
}
if (r) {
region_map.setDefaultRegion(r);
region_map.getTransportKeysFor(*r, &default_scope, 1);
}
#endif
}
}
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
}
bool SensorMesh::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return InternalFS.format();
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
return SPIFFS.format();
#else
#error "need to implement file system erase"
return false;
#endif
}
void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
IdentityStore store(*_fs, "/identity");
#elif defined(RP2040_PLATFORM)
IdentityStore store(*_fs, "/identity");
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", new_id);
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
pending_bw = bw;
pending_sf = sf;
pending_cr = cr;
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
}
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
}
void SensorMesh::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
}
}
void SensorMesh::updateFloodAdvertTimer() {
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
} else {
next_flood_advert = 0; // stop the timer
}
}
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
void SensorMesh::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
void SensorMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}
void SensorMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
}
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
auto buf = telemetry.getBuffer();
uint8_t size = telemetry.getSize();
uint8_t i = 0;
while (i + 2 < size) {
// Get channel #
uint8_t ch = buf[i++];
// Get data type
uint8_t t = buf[i++];
uint8_t sz = getDataSize(t);
if (ch == channel && t == type) {
return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t));
}
i += sz; // skip
}
return 0.0f; // not found
}
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
if (channel == TELEM_CHANNEL_SELF) {
lat = sensors.node_lat;
lon = sensors.node_lon;
alt = sensors.node_altitude;
return true;
}
// REVISIT: custom GPS channels??
return false;
}
void SensorMesh::loop() {
mesh::Mesh::loop();
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet* pkt = createSelfAdvert();
unsigned long delay_millis = 0;
if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
updateFloodAdvertTimer(); // schedule next flood advert
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) sendZeroHop(pkt);
updateAdvertTimer(); // schedule next local advert
}
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}
uint32_t curr = getRTCClock()->getCurrentTime();
if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
onSensorDataRead();
last_read_time = curr;
}
// check the alert send queue
if (num_alert_tasks > 0) {
auto t = alert_tasks[0]; // process head of queue
if (millisHasNowPassed(t->send_expiry)) { // next send needed?
if (t->attempt >= 4) { // max attempts reached, try next contact
t->curr_contact_idx++;
if (t->curr_contact_idx >= acl.getNumClients()) { // no more contacts to try?
num_alert_tasks--; // remove t from queue
for (int i = 0; i < num_alert_tasks; i++) {
alert_tasks[i] = alert_tasks[i + 1];
}
} else {
auto c = acl.getClientByIdx(t->curr_contact_idx);
uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
if (c->permissions & pri_mask) { // contact wants alert
// reset attempts
t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0; // Low pri alerts, start at attempt #3 (ie. only make ONE attempt)
t->timestamp = getRTCClock()->getCurrentTimeUnique(); // need unique timestamp per contact
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
} else {
// next contact tested in next ::loop()
}
}
} else if (t->curr_contact_idx < acl.getNumClients()) {
auto c = acl.getClientByIdx(t->curr_contact_idx); // send next attempt
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
} else {
// contact list has likely been modified while waiting for alert ACK, cancel this task
t->attempt = 4; // next ::loop() will remove t from queue
}
}
}
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
acl.save(_fs);
dirty_contacts_expiry = 0;
}
}

View File

@@ -0,0 +1,166 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "TimeSeriesData.h"
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#include <RTClib.h>
#include <target.h>
#define PERM_RESERVED1 (1 << 2)
#define PERM_RESERVED2 (1 << 3)
#define PERM_RESERVED3 (1 << 4)
#define PERM_RESERVED4 (1 << 5)
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.14.1"
#endif
#define FIRMWARE_ROLE "sensor"
#define MAX_SEARCH_RESULTS 8
#define MAX_CONCURRENT_ALERTS 4
class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
public:
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
void loop();
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
// CommonCLI callbacks
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() { return &_prefs; }
void savePrefs() override { _cli.savePrefs(_fs); }
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { }
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
float getTelemValue(uint8_t channel, uint8_t type);
protected:
// current telemetry data queries
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); }
float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); }
float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); }
float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); }
float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); }
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
// alerts
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
struct Trigger {
uint32_t timestamp;
AlertPriority pri;
uint32_t expected_acks[4];
int8_t curr_contact_idx;
uint8_t attempt;
unsigned long send_expiry;
char text[MAX_PACKET_PAYLOAD];
Trigger() { text[0] = 0; }
bool isTriggered() const { return text[0] != 0; }
};
void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
virtual void onSensorDataRead() = 0; // for app to implement
virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement
virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
// Mesh overrides
float getAirtimeBudgetFactor() const override;
bool allowPacketForward(const mesh::Packet* packet) override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override;
int getAGCResetInterval() const override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
private:
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
TransportKeyStore key_store;
RegionMap region_map;
TransportKey default_scope;
uint32_t last_read_time;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
int num_alert_tasks;
Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
void sendAlert(const ClientInfo* c, Trigger* t);
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
};

View File

@@ -0,0 +1,45 @@
#include "TimeSeriesData.h"
void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) {
uint32_t now = clock->getCurrentTime();
if (now >= last_timestamp + interval_secs) {
last_timestamp = now;
data[next] = value; // append to cycle table
next = (next + 1) % num_slots;
}
}
void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const {
int i = next, n = num_slots;
uint32_t ago = clock->getCurrentTime() - last_timestamp;
int num_values = 0;
float total = 0.0f;
dest->_channel = channel;
dest->_lpp_type = lpp_type;
// start at most recet recording, back-track through to oldest
while (n > 0) {
n--;
i = (i + num_slots - 1) % num_slots; // go back by one
if (ago >= end_secs_ago && ago < start_secs_ago) { // filter by the desired time range
float v = data[i];
num_values++;
total += v;
if (num_values == 1) {
dest->_max = dest->_min = v;
} else {
if (v < dest->_min) dest->_min = v;
if (v > dest->_max) dest->_max = v;
}
}
ago += interval_secs;
}
// calc average
if (num_values > 0) {
dest->_avg = total / num_values;
} else {
dest->_max = dest->_min = dest->_avg = NAN;
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
struct MinMaxAvg {
float _min, _max, _avg;
uint8_t _lpp_type, _channel;
};
class TimeSeriesData {
float* data;
int num_slots, next;
uint32_t last_timestamp;
uint32_t interval_secs;
public:
TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) {
memset(data, 0, sizeof(float)*num);
}
TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) {
data = new float[num];
memset(data, 0, sizeof(float)*num);
}
void recordData(mesh::RTCClock* clock, float value);
void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const;
};

View File

@@ -0,0 +1,118 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_display->turnOn();
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
}
void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);
// node type
const char* node_type = "< Sensor >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
}
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop();
};

View File

@@ -0,0 +1,150 @@
#include "SensorMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
class MyMesh : public SensorMesh {
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: SensorMesh(board, radio, ms, rng, rtc, tables),
battery_data(12*24, 5*60) // 24 hours worth of battery data, every 5 minutes
{
}
protected:
/* ========================== custom logic here ========================== */
Trigger low_batt, critical_batt;
TimeSeriesData battery_data;
void onSensorDataRead() override {
float batt_voltage = getVoltage(TELEM_CHANNEL_SELF);
battery_data.recordData(getRTCClock(), batt_voltage); // record battery
alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!");
alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low");
}
int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override {
battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE);
return 1;
}
bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override {
if (strcmp(command, "magic") == 0) { // example 'custom' command handling
strcpy(reply, "**Magic now done**");
return true; // handled
}
return false; // not handled
}
/* ======================================================================= */
};
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
}
static char command[160];
void setup() {
Serial.begin(115200);
delay(1000);
board.begin();
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
display.print("Please wait...");
display.endFrame();
}
#endif
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "need to define filesystem"
#endif
if (!store.load("_main", the_mesh.self_id)) {
MESH_DEBUG_PRINTLN("Generating new keypair");
the_mesh.self_id = radio_new_identity(); // create new random identity
int count = 0;
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes
the_mesh.self_id = radio_new_identity(); count++;
}
store.save("_main", the_mesh.self_id);
}
Serial.print("Sensor ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
sensors.begin();
the_mesh.begin(fs);
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
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
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
command[0] = 0; // reset command buffer
}
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}