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
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:
46
examples/companion_radio/AbstractUITask.h
Normal file
46
examples/companion_radio/AbstractUITask.h
Normal 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;
|
||||
};
|
||||
622
examples/companion_radio/DataStore.cpp
Normal file
622
examples/companion_radio/DataStore.cpp
Normal 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
|
||||
55
examples/companion_radio/DataStore.h
Normal file
55
examples/companion_radio/DataStore.h
Normal 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;};
|
||||
};
|
||||
2193
examples/companion_radio/MyMesh.cpp
Normal file
2193
examples/companion_radio/MyMesh.cpp
Normal file
File diff suppressed because it is too large
Load Diff
252
examples/companion_radio/MyMesh.h
Normal file
252
examples/companion_radio/MyMesh.h
Normal 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;
|
||||
37
examples/companion_radio/NodePrefs.h
Normal file
37
examples/companion_radio/NodePrefs.h
Normal 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];
|
||||
};
|
||||
232
examples/companion_radio/main.cpp
Normal file
232
examples/companion_radio/main.cpp
Normal 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();
|
||||
}
|
||||
923
examples/companion_radio/ui-new/UITask.cpp
Normal file
923
examples/companion_radio/ui-new/UITask.cpp
Normal 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
|
||||
}
|
||||
101
examples/companion_radio/ui-new/UITask.h
Normal file
101
examples/companion_radio/ui-new/UITask.h
Normal 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);
|
||||
};
|
||||
122
examples/companion_radio/ui-new/icons.h
Normal file
122
examples/companion_radio/ui-new/icons.h
Normal 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
|
||||
};
|
||||
131
examples/companion_radio/ui-orig/Button.cpp
Normal file
131
examples/companion_radio/ui-orig/Button.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
80
examples/companion_radio/ui-orig/Button.h
Normal file
80
examples/companion_radio/ui-orig/Button.h
Normal 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);
|
||||
};
|
||||
446
examples/companion_radio/ui-orig/UITask.cpp
Normal file
446
examples/companion_radio/ui-orig/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
73
examples/companion_radio/ui-orig/UITask.h
Normal file
73
examples/companion_radio/ui-orig/UITask.h
Normal 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);
|
||||
};
|
||||
581
examples/kiss_modem/KissModem.cpp
Normal file
581
examples/kiss_modem/KissModem.cpp
Normal 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);
|
||||
}
|
||||
183
examples/kiss_modem/KissModem.h
Normal file
183
examples/kiss_modem/KissModem.h
Normal 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; }
|
||||
};
|
||||
146
examples/kiss_modem/main.cpp
Normal file
146
examples/kiss_modem/main.cpp
Normal 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();
|
||||
}
|
||||
1310
examples/simple_repeater/MyMesh.cpp
Normal file
1310
examples/simple_repeater/MyMesh.cpp
Normal file
File diff suppressed because it is too large
Load Diff
255
examples/simple_repeater/MyMesh.h
Normal file
255
examples/simple_repeater/MyMesh.h
Normal 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
|
||||
};
|
||||
23
examples/simple_repeater/RateLimiter.h
Normal file
23
examples/simple_repeater/RateLimiter.h
Normal 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;
|
||||
}
|
||||
};
|
||||
118
examples/simple_repeater/UITask.cpp
Normal file
118
examples/simple_repeater/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
examples/simple_repeater/UITask.h
Normal file
19
examples/simple_repeater/UITask.h
Normal 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();
|
||||
};
|
||||
170
examples/simple_repeater/main.cpp
Normal file
170
examples/simple_repeater/main.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
1024
examples/simple_room_server/MyMesh.cpp
Normal file
1024
examples/simple_room_server/MyMesh.cpp
Normal file
File diff suppressed because it is too large
Load Diff
225
examples/simple_room_server/MyMesh.h
Normal file
225
examples/simple_room_server/MyMesh.h
Normal 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();
|
||||
};
|
||||
118
examples/simple_room_server/UITask.cpp
Normal file
118
examples/simple_room_server/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
examples/simple_room_server/UITask.h
Normal file
19
examples/simple_room_server/UITask.h
Normal 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();
|
||||
};
|
||||
116
examples/simple_room_server/main.cpp
Normal file
116
examples/simple_room_server/main.cpp
Normal 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();
|
||||
}
|
||||
594
examples/simple_secure_chat/main.cpp
Normal file
594
examples/simple_secure_chat/main.cpp
Normal 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, ×tamp, 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();
|
||||
}
|
||||
975
examples/simple_sensor/SensorMesh.cpp
Normal file
975
examples/simple_sensor/SensorMesh.cpp
Normal 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(×tamp, 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(×tamp, 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, ×tamp, 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;
|
||||
}
|
||||
}
|
||||
166
examples/simple_sensor/SensorMesh.h
Normal file
166
examples/simple_sensor/SensorMesh.h
Normal 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
|
||||
};
|
||||
45
examples/simple_sensor/TimeSeriesData.cpp
Normal file
45
examples/simple_sensor/TimeSeriesData.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
29
examples/simple_sensor/TimeSeriesData.h
Normal file
29
examples/simple_sensor/TimeSeriesData.h
Normal 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;
|
||||
};
|
||||
|
||||
118
examples/simple_sensor/UITask.cpp
Normal file
118
examples/simple_sensor/UITask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
examples/simple_sensor/UITask.h
Normal file
19
examples/simple_sensor/UITask.h
Normal 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();
|
||||
};
|
||||
150
examples/simple_sensor/main.cpp
Normal file
150
examples/simple_sensor/main.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user