Merge upstream/main (v1.16.0) into wio-unified

Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.

Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
  upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
  env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
  ::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
  upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
  inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
  upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
  auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
  prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).

Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
  getAvailableLPPTypes() relied on; removed that override and the SENSORS page
  now derives available LPP types from the populated sensors_lpp buffer.

README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-06 17:21:41 +02:00
356 changed files with 6668 additions and 3108 deletions

View File

@@ -504,7 +504,7 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3");
}
}
void DataStore::saveContacts(DataStoreHost* host) {
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
File file = ::openWrite(_getContactsChannelsFS(), "/contacts3");
if (file) {
uint32_t idx = 0;
@@ -512,6 +512,10 @@ void DataStore::saveContacts(DataStoreHost* host) {
uint8_t unused = 0;
while (host->getContactForSave(idx, c)) {
if (filter && !filter(c)) {
idx++; // advance to next contact
continue;
}
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);

View File

@@ -36,7 +36,7 @@ public:
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 saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
void loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
void migrateToSecondaryFS();

View File

@@ -31,7 +31,7 @@
#define CMD_REBOOT 19
#define CMD_GET_BATT_AND_STORAGE 20 // was CMD_GET_BATTERY_VOLTAGE
#define CMD_SET_TUNING_PARAMS 21
#define CMD_DEVICE_QEURY 22
#define CMD_DEVICE_QUERY 22
#define CMD_EXPORT_PRIVATE_KEY 23
#define CMD_IMPORT_PRIVATE_KEY 24
#define CMD_SEND_RAW_DATA 25
@@ -68,6 +68,7 @@
#define CMD_SEND_CHANNEL_DATA 62
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_SEND_RAW_PACKET 65
#ifdef ENABLE_SCREENSHOT
#define CMD_GET_SCREENSHOT 66 // Request screenshot from display
#endif
@@ -90,7 +91,7 @@
#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE
#define RESP_CODE_EXPORT_CONTACT 11
#define RESP_CODE_BATT_AND_STORAGE 12 // a reply to a CMD_GET_BATT_AND_STORAGE
#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QEURY
#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QUERY
#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY
#define RESP_CODE_DISABLED 15
#define RESP_CODE_CONTACT_MSG_RECV_V3 16 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
@@ -506,19 +507,27 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
if (send_unscoped) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
if (send_unscoped) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
}
}
@@ -1072,6 +1081,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_ping_callback_arg = NULL;
memset(_ping_results, 0, sizeof(_ping_results));
send_unscoped = false;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0;
@@ -1186,8 +1197,8 @@ void MyMesh::begin(bool has_display) {
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
@@ -1208,9 +1219,13 @@ struct FreqRange {
};
static FreqRange repeat_freq_ranges[] = {
#ifdef ALLOWED_REPEAT_FREQ_RANGE
ALLOWED_REPEAT_FREQ_RANGE
#else
{ 433000, 433000 },
{ 869000, 869000 },
{ 869495, 869495 },
{ 918000, 918000 }
#endif
};
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
@@ -1227,7 +1242,7 @@ void MyMesh::startInterface(BaseSerialInterface &serial) {
}
void MyMesh::handleCmdFrame(size_t len) {
if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection
if (cmd_frame[0] == CMD_DEVICE_QUERY && len >= 2) { // sent when app establishes connection
app_target_ver = cmd_frame[1]; // which version of protocol does app understand
int i = 0;
@@ -1333,7 +1348,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(recipient == NULL
? ERR_CODE_NOT_FOUND
: ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsuported TXT_TYPE_*
: ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsupported TXT_TYPE_*
}
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg
int i = 1;
@@ -1611,7 +1626,7 @@ void MyMesh::handleCmdFrame(size_t len) {
_prefs.client_repeat = repeat;
savePrefs();
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
(uint32_t)cr);
@@ -1628,7 +1643,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
_prefs.tx_power_dbm = power;
savePrefs();
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setTxPower(_prefs.tx_power_dbm);
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
@@ -1759,6 +1774,15 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
ContactInfo anon;
if (recipient == NULL) { // FIRMWARE_VER_CODE 13+, allow non-contact requests
memset(&anon, 0, sizeof(anon));
memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE);
anon.out_path_len = 0; // default to zero-hop direct
anon.type = ADV_TYPE_NONE; // unknown
if (addContact(anon)) recipient = &anon;
}
uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE];
if (recipient) {
uint32_t tag, est_timeout;
@@ -1775,7 +1799,7 @@ void MyMesh::handleCmdFrame(size_t len) {
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
writeErrFrame(ERR_CODE_TABLE_FULL); // contacts full
}
} else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
@@ -2131,10 +2155,14 @@ void MyMesh::handleCmdFrame(size_t len) {
}
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set scope override TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
memset(send_scope.key, 0, sizeof(send_scope.key)); // reset scope override
}
send_unscoped = false;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 1) { // ver 12+
send_unscoped = true;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
if (len >= 1+31+16) {
@@ -2195,6 +2223,19 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
}
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SEND_RAW_PACKET && len >= 4) {
auto pkt = obtainNewPacket();
if (pkt) {
uint8_t priority = cmd_frame[1];
if (tryParsePacket(pkt, &cmd_frame[2], len - 2)) {
sendPacket(pkt, priority, 0);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
#ifdef ENABLE_SCREENSHOT
} else if (cmd_frame[0] == CMD_GET_SCREENSHOT) {
handleScreenshotRequest();
@@ -2268,6 +2309,14 @@ void MyMesh::sendScreenshotResponse(DisplayDriver* display, const uint8_t* buffe
}
#endif // ENABLE_SCREENSHOT
static bool save_filter(const ContactInfo& c) {
return c.type != ADV_TYPE_NONE; // don't save the transient/anon entries
}
void MyMesh::saveContacts() {
_store->saveContacts(this, save_filter);
}
void MyMesh::enterCLIRescue() {
_cli_rescue = true;
cli_command[0] = 0;
@@ -2454,7 +2503,15 @@ void MyMesh::checkSerialInterface() {
&& !_serial->isWriteBusy() // don't spam the Serial Interface too quickly!
) {
ContactInfo contact;
if (_iter.hasNext(this, contact)) {
bool found = false;
while (_iter.hasNext(this, contact)) {
if (contact.type != ADV_TYPE_NONE) {
found = true;
break;
}
}
if (found) {
if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter
writeContactRespFrame(RESP_CODE_CONTACT, contact);
if (contact.lastmod > _most_recent_lastmod) {
@@ -2521,4 +2578,9 @@ bool MyMesh::advert() {
}
}
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0;
}
#include "MyMeshBot.h"

View File

@@ -9,15 +9,15 @@
class UITask;
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 11
#define FIRMWARE_VER_CODE 13
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "12 May 2026"
#define FIRMWARE_BUILD_DATE "6 Jun 2026"
#endif
// Versioning: vX.Y = upstream base, solo.N = fork revision
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15-solo.1"
#define FIRMWARE_VERSION "v1.16-solo.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -227,6 +227,9 @@ public:
}
#endif
// To check if there is pending work
bool hasPendingWork() const;
private:
void tryBotReplyDM(const ContactInfo& from, const char* text);
void tryBotReplyChannel(uint8_t channel_idx, const char* text);
@@ -255,7 +258,7 @@ private:
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
void saveContacts() { _store->saveContacts(this); }
void saveContacts();
DataStore* _store;
NodePrefs _prefs;
@@ -272,6 +275,7 @@ private:
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
uint8_t app_target_ver;
uint8_t *sign_data;

View File

@@ -108,6 +108,12 @@ void halt() {
while (1) ;
}
/* WIFI RECONNECT TRACKERS */
#if defined(ESP32) && defined(WIFI_SSID)
bool wifi_needs_reconnect = false;
unsigned long last_wifi_reconnect_attempt = 0;
#endif
void setup() {
Serial.begin(115200);
@@ -128,7 +134,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
@@ -200,6 +206,18 @@ void setup() {
#ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
wifi_needs_reconnect = true;
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
wifi_needs_reconnect = false;
}
});
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
@@ -237,6 +255,7 @@ void setup() {
NRF_WDT->RREN = 0x01; // enable reload register 0
NRF_WDT->TASKS_START = 1;
#endif
board.onBootComplete();
}
void loop() {
@@ -249,5 +268,22 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
board.sleep(0); // CPU sleeps until next interrupt (radio, timer, BLE); nRF52 ignores the seconds param
// CPU sleeps until next interrupt (radio, timer, BLE) — but only when the
// mesh has no pending work, so queued TX/processing isn't delayed.
if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#endif
}
#if defined(ESP32) && defined(WIFI_SSID)
// Safely attempt to reconnect every 10 seconds if flagged
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
WiFi.disconnect();
WiFi.reconnect();
last_wifi_reconnect_attempt = millis();
}
#endif
}

View File

@@ -507,7 +507,7 @@ class HomeScreen : public UIScreen {
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();
@@ -531,7 +531,7 @@ class HomeScreen : public UIScreen {
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
void poll() override {
@@ -706,10 +706,10 @@ public:
} else {
snprintf(tmp, sizeof(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);
@@ -804,8 +804,22 @@ public:
int y = content_y;
refresh_sensors();
// Enumerate the distinct telemetry types directly from the freshly
// populated buffer. (Upstream replaced the per-sensor *_initialized flags
// with a generic registration model, so we derive availability from what
// querySensors() actually produced instead of asking the manager.)
uint8_t avail_types[16];
int avail_count = _sensors ? _sensors->getAvailableLPPTypes(avail_types, 16) : 0;
int avail_count = 0;
{
LPPReader er(sensors_lpp.getBuffer(), sensors_lpp.getSize());
uint8_t ech, etype;
while (er.readHeader(ech, etype) && avail_count < 16) {
er.skipData(etype);
bool dup = false;
for (int k = 0; k < avail_count; k++) if (avail_types[k] == etype) { dup = true; break; }
if (!dup) avail_types[avail_count++] = etype;
}
}
bool need_scroll = avail_count > UI_RECENT_LIST_SIZE;
int offset = need_scroll ? (sensors_scroll_offset % avail_count) : 0;
int show_n = need_scroll ? UI_RECENT_LIST_SIZE : avail_count;
@@ -1740,7 +1754,7 @@ void UITask::loop() {
#endif
#if defined(PIN_USER_BTN_ANA)
if (millis() - _analogue_pin_read_millis > 10) {
ev = analog_btn.check();
int ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
@@ -1897,6 +1911,15 @@ void UITask::loop() {
_display->endFrame();
}
#if AUTO_OFF_MILLIS > 0
#ifdef KEEP_DISPLAY_ON_USB
// Opt-in: refresh the auto-off deadline while externally powered, so the
// timer counts from the moment external power is removed. Off by default
// because OLED panels burn in quickly; only enable for LCD targets or
// where the display is replaceable.
if (board.isExternalPowered()) {
_auto_off = millis() + AUTO_OFF_MILLIS;
}
#endif
if (!_locked && autoOffMillis() > 0 && millis() > _auto_off) {
_display->turnOff();
#ifdef PIN_LED
@@ -1921,7 +1944,8 @@ void UITask::loop() {
_batt_mv = (_batt_mv == 0) ? raw : (uint16_t)((_batt_mv * 4u + raw) / 5u);
}
uint16_t low_mv = _node_prefs ? _node_prefs->low_batt_mv : 0;
if (low_mv > 0 && _batt_mv > 0 && _batt_mv < low_mv) {
// Don't shut down while on external power (charging) — avoids a shutdown loop.
if (low_mv > 0 && _batt_mv > 0 && _batt_mv < low_mv && !board.isExternalPowered()) {
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(1);
@@ -1931,7 +1955,7 @@ void UITask::loop() {
_display->drawTextCentered(_display->width() / 2, mid - step, "Low Battery");
_display->drawTextCentered(_display->width() / 2, mid, "Shutting down");
_display->endFrame();
delay(2000);
if (_display->isEink() == false) { delay(2000); }
}
shutdown();
}
@@ -2064,6 +2088,7 @@ char UITask::handleLongPress(char c) {
}
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double-click triggered");
checkDisplayOn(c);
return c;
}
@@ -2082,7 +2107,7 @@ bool UITask::getGPSState() {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
}
return false;
}
@@ -2112,7 +2137,7 @@ void UITask::toggleGPS() {
void UITask::applyTxPower() {
if (_node_prefs == NULL) return;
radio_set_tx_power(_node_prefs->tx_power_dbm);
radio_driver.setTxPower(_node_prefs->tx_power_dbm);
}
void UITask::applyBrightness() {

View File

@@ -57,6 +57,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
buzzer.startup();
#endif
// Initialize digital button if available
@@ -341,6 +342,15 @@ void UITask::loop() {
_next_refresh = millis() + 1000; // refresh every second
}
#ifdef KEEP_DISPLAY_ON_USB
// Opt-in: refresh the auto-off deadline while externally powered, so the
// timer counts from the moment external power is removed. Off by default
// because OLED panels burn in quickly; only enable for LCD targets or
// where the display is replaceable.
if (board.isExternalPowered()) {
_auto_off = millis() + AUTO_OFF_MILLIS;
}
#endif
if (millis() > _auto_off) {
_display->turnOff();
}

View File

@@ -0,0 +1,137 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#ifndef STATUS_BAR_SCROLL_MS
#define STATUS_BAR_SCROLL_MS 80
#endif
#ifndef STATUS_BAR_SEPARATOR
#define STATUS_BAR_SEPARATOR " | "
#endif
#ifndef STATUS_BAR_UPDATE_MS
#define STATUS_BAR_UPDATE_MS 2000 // rebuild status string every 2s
#endif
class ScrollingStatusBar {
char _status[160];
int _text_width;
int _scroll_x;
int _display_width;
unsigned long _next_scroll;
unsigned long _next_update;
bool _needs_redraw;
// cached state for change detection
char _last_name[32];
uint16_t _last_batt_mv;
bool _last_buzzer_quiet;
bool _last_gps_on;
bool _last_ble_on;
public:
ScrollingStatusBar() : _text_width(0), _scroll_x(0), _display_width(72),
_next_scroll(0), _next_update(0), _needs_redraw(true),
_last_batt_mv(0), _last_buzzer_quiet(false),
_last_gps_on(false), _last_ble_on(false) {
_status[0] = 0;
_last_name[0] = 0;
}
void begin(int display_width) {
_display_width = display_width;
_scroll_x = 0;
_next_scroll = 0;
_next_update = 0;
}
// Call periodically to update the status string content.
// Only rebuilds if values have changed or update interval has elapsed.
void update(DisplayDriver& display, const char* node_name, uint16_t batt_millivolts,
bool buzzer_quiet, bool gps_on, bool ble_on) {
bool changed = (batt_millivolts != _last_batt_mv)
|| (buzzer_quiet != _last_buzzer_quiet)
|| (gps_on != _last_gps_on)
|| (ble_on != _last_ble_on)
|| (strcmp(node_name, _last_name) != 0);
if (!changed) return;
// cache current values
strncpy(_last_name, node_name, sizeof(_last_name) - 1);
_last_name[sizeof(_last_name) - 1] = 0;
_last_batt_mv = batt_millivolts;
_last_buzzer_quiet = buzzer_quiet;
_last_gps_on = gps_on;
_last_ble_on = ble_on;
float volts = batt_millivolts / 1000.0f;
snprintf(_status, sizeof(_status),
"%s" STATUS_BAR_SEPARATOR
"%.2fV" STATUS_BAR_SEPARATOR
"BUZ:%s" STATUS_BAR_SEPARATOR
"GPS:%s" STATUS_BAR_SEPARATOR
"BLE:%s"
" - ", // trailing gap before the text loops
node_name,
volts,
buzzer_quiet ? "OFF" : "ON",
gps_on ? "ON" : "OFF",
ble_on ? "ON" : "OFF"
);
display.setTextSize(1);
_text_width = display.getTextWidth(_status);
_next_update = millis() + STATUS_BAR_UPDATE_MS;
_needs_redraw = true;
}
// Returns true if the status bar needs a redraw this frame.
bool needsRedraw() {
if (_text_width <= _display_width) return _needs_redraw; // static, no scrolling
return millis() >= _next_scroll;
}
// Render the status bar via DisplayDriver.
// U8g2 full-buffer mode clips to display bounds automatically,
// and the font height stays within STATUS_BAR_HEIGHT, so no
// explicit clip window is needed.
void render(DisplayDriver& display) {
if (_status[0] == 0) return;
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
// if (_needs_redraw) {
// _text_width = display.getTextWidth(_status);
// }
// static text: no scrolling needed
if (_text_width <= _display_width) {
display.setCursor(0, 0);
display.print(_status);
_needs_redraw = false;
return;
}
int x = _scroll_x;
do {
display.setCursor(x, 0);
display.print(_status);
x += _text_width;
} while (x < _display_width);
// advance scroll position
_scroll_x--;
if (_scroll_x <= -_text_width) _scroll_x = 0;
_next_scroll = millis() + STATUS_BAR_SCROLL_MS;
_needs_redraw = false;
}
};

View File

@@ -0,0 +1,837 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#include "u8g2_icons.h"
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
#define BOOT_SCREEN_MILLIS 4000 // 4 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
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
unsigned long version_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;
version_after = millis() + BOOT_SCREEN_MILLIS / 2;
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
}
int render(DisplayDriver& display) override {
if (millis() < version_after) {
// meshcore logo
display.setColor(DisplayDriver::BLUE);
int logoWidth = 72;
display.drawXbm(0, 0, meshcore_logo, 72, 36);
} else {
// meshcore website
const char* website = "meshcore.io";
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1);
uint16_t websiteWidth = display.getTextWidth(website);
display.setCursor((display.width() - websiteWidth) / 2, 9);
display.print(website);
// version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 18, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 27, 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];
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];
if (_page == HomePage::FIRST) {
// // 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);
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.setCursor(0, 10);
display.print(tmp);
sprintf(tmp, "BATT: %.2fV", _task->getCachedBattMV() / 1000.0f);
display.setCursor(0, 19);
display.print(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, display.height()-8, "< 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, display.height()-8, tmp);
}
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN);
int y = 8;
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);
// frequency and spreading factor
display.setCursor(0, 8);
sprintf(tmp, "FQ %06.3f", _node_prefs->freq);
display.print(tmp);
sprintf(tmp, "SF%d", _node_prefs->sf);
display.drawTextRightAlign(display.width(), 8, tmp);
// bandwidth and coding rate
display.setCursor(0, 17);
sprintf(tmp, "BW %03.2f", _node_prefs->bw);
display.print(tmp);
sprintf(tmp, "CR%d", _node_prefs->cr);
display.drawTextRightAlign(display.width(), 17, tmp);
// tx power and noise floor
display.setCursor(0, 26);
sprintf(tmp, "NF %ddB", radio_driver.getNoiseFloor());
display.print(tmp);
sprintf(tmp, "TX%d", _node_prefs->tx_power_dbm);
display.drawTextRightAlign(display.width(), 26, tmp);
} else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 8,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1);
// display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32);
// display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider();
char buf[50];
int y = 8;
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 + 8;
display.drawTextLeftAlign(0, y, "Can't access GPS");
} else {
if (!gps_state || !nmea->isValid()) {
strcpy(buf, "no fix");
} else {
sprintf(buf, "%d sat", nmea->satellitesCount());
}
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 8;
sprintf(buf, "lat %.4f",
nmea->getLatitude()/1000000.);
display.drawTextLeftAlign(0, y, buf);
y = y + 8;
sprintf(buf, "lon %.4f",
nmea->getLongitude()/1000000.);
display.drawTextLeftAlign(0, y, buf);
y = y + 8;
sprintf(buf, "alt %.1f", nmea->getAltitude()/1000.);
display.drawTextLeftAlign(0, y, buf);
}
#endif
#if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) {
int y = 8;
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, 20, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, 8, power_icon, 32, 32);
// display.drawTextCentered(display.width() / 2, 40 - 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;
}
};
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
_auto_off = millis() + AUTO_OFF_MILLIS;
_cached_batt_mv = getBattMilliVolts();
#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();
}
_statusBar.begin(_display->width());
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
buzzer.startup();
#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);
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;
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) {
int 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 defined(HAS_TORCH)
ev = back_btn.check();
if (ev == BUTTON_EVENT_CLICK && c == 0) {
c = checkDisplayOn(KEY_PREV);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
board.toggleTorch();
c = 0;
}
#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()) {
_statusBar.update(*_display,
_node_prefs->node_name,
_cached_batt_mv,
isBuzzerQuiet(),
getGPSState(),
isSerialEnabled());
bool status_dirty = _statusBar.needsRedraw();
bool content_dirty = (millis() >= _next_refresh && curr);
if (status_dirty || content_dirty) {
_display->startFrame();
_statusBar.render(*_display);
if (curr) {
int delay_millis = curr->render(*_display);
if (content_dirty) {
_next_refresh = millis() + delay_millis;
}
}
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
}
_display->endFrame();
}
#if AUTO_OFF_MILLIS > 0
#ifdef KEEP_DISPLAY_ON_USB
// Opt-in: refresh the auto-off deadline while externally powered, so the
// timer counts from the moment external power is removed. Off by default
// because OLED panels burn in quickly; only enable for LCD targets or
// where the display is replaceable.
if (board.isExternalPowered()) {
_auto_off = millis() + AUTO_OFF_MILLIS;
}
#endif
if (millis() > _auto_off) {
_display->turnOff();
}
#endif
}
#ifdef PIN_VIBRATION
vibration.loop();
#endif
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
if (millis() > next_batt_chck) {
_cached_batt_mv = getBattMilliVolts();
if (_cached_batt_mv > 0 && _cached_batt_mv < AUTO_SHUTDOWN_MILLIVOLTS) {
if(!board.isExternalPowered()) {
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
_display->drawTextCentered(_display->width() / 2, 6, "Low battery!");
_display->setTextSize(1);
_display->drawTextCentered(_display->width() / 2, 18, "Shutting down!");
_display->endFrame();
if (_display->isEink() == false) { delay(3000); }
}
shutdown();
}
}
next_batt_chck = millis() + 8000;
}
#else
if (_display != NULL && _display->isOn() && millis() >= next_batt_chck) {
_cached_batt_mv = getBattMilliVolts();
next_batt_chck = millis() + 8000;
}
#endif
}
char UITask::checkDisplayOn(char c) {
if (_display != NULL) {
if (!_display->isOn()) {
_display->turnOn(); // turn display on and consume event
c = 0;
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
}
return c;
}
char UITask::handleLongPress(char c) {
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
c = 0; // consume event
}
return c;
}
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double-click triggered");
checkDisplayOn(c);
return c;
}
char UITask::handleTripleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
checkDisplayOn(c);
toggleBuzzer();
c = 0;
return c;
}
bool UITask::getGPSState() {
if (_sensors != NULL) {
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
return false;
}
void UITask::toggleGPS() {
if (_sensors != NULL) {
// toggle GPS on/off
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
_node_prefs->gps_enabled = 0;
notify(UIEventType::ack);
} else {
_sensors->setSettingValue("gps", "1");
_node_prefs->gps_enabled = 1;
notify(UIEventType::ack);
}
the_mesh.savePrefs();
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
_next_refresh = 0;
break;
}
}
}
}
void UITask::toggleBuzzer() {
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
} else {
buzzer.quiet(true);
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
_next_refresh = 0; // trigger refresh
#endif
}

View File

@@ -0,0 +1,109 @@
#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>
#include "ScrollingStatusBar.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;
ScrollingStatusBar _statusBar;
#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;
uint16_t _cached_batt_mv;
#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;
_cached_batt_mv = 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; }
uint16_t getCachedBattMV() const { return _cached_batt_mv; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isBuzzerQuiet() {
#ifdef PIN_BUZZER
return buzzer.isQuiet();
#else
return true;
#endif
}
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
};

View File

@@ -0,0 +1,104 @@
#pragma once
#include <stdint.h>
// icons converted for use with U8g2 which needs a different format of xbm data.
// 'meshcore', 72x36px
static const uint8_t meshcore_logo [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x00, 0x3e, 0xfe, 0x3f, 0xfe, 0x3f, 0x1e, 0x78,
0xf8, 0x00, 0x1f, 0xff, 0x3f, 0xff, 0x3f, 0x1e, 0x78, 0xf8, 0x01, 0x1f,
0xff, 0x9f, 0xff, 0x1f, 0x0e, 0x78, 0xf8, 0x81, 0x1f, 0x0f, 0x80, 0x07,
0x00, 0x0f, 0x38, 0xf8, 0xc1, 0x1f, 0x0f, 0x80, 0x07, 0x00, 0x0f, 0x3c,
0xf8, 0xc3, 0x1f, 0xff, 0x87, 0xff, 0x07, 0xff, 0x3f, 0xf8, 0xe3, 0x1f,
0xff, 0x87, 0xff, 0x0f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0xff, 0x07, 0xff,
0x1f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x0f, 0x1e,
0xbc, 0x7f, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x07, 0x1e, 0x9c, 0x3f, 0x8f,
0x07, 0x00, 0x00, 0x9f, 0x07, 0x1e, 0x9c, 0x3f, 0x8f, 0xff, 0xcf, 0xff,
0x8f, 0x07, 0x1e, 0x1e, 0x1f, 0xc7, 0xff, 0xcf, 0xff, 0x87, 0x07, 0x1e,
0x1e, 0x0f, 0xc7, 0xff, 0xc7, 0xff, 0x83, 0x03, 0x0e, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0xff, 0xf0, 0xff, 0xe0, 0xff, 0xc1, 0xff, 0x07, 0xf0, 0xff, 0xf8,
0xff, 0xf1, 0xff, 0xc3, 0xff, 0x07, 0xf0, 0xff, 0xfc, 0xff, 0xf1, 0xff,
0xc7, 0xff, 0x07, 0x78, 0x00, 0x3c, 0xe0, 0xf1, 0xc0, 0xe7, 0x01, 0x00,
0x78, 0x00, 0x1e, 0xe0, 0xf1, 0x80, 0xe7, 0x01, 0x00, 0x78, 0x00, 0x1e,
0xe0, 0xf1, 0xc0, 0xe3, 0x01, 0x00, 0x78, 0x00, 0x1e, 0xe0, 0x71, 0xc0,
0xe3, 0xff, 0x00, 0x3c, 0x00, 0x1e, 0xe0, 0xf9, 0xff, 0xe3, 0xff, 0x00,
0x3c, 0x00, 0x1e, 0xe0, 0xf8, 0xff, 0xf1, 0xff, 0x00, 0x3c, 0x00, 0x0e,
0xf0, 0xf8, 0xff, 0xf0, 0x00, 0x00, 0x3c, 0x00, 0x1f, 0xf0, 0x78, 0x7c,
0xf0, 0x00, 0x00, 0xfc, 0x3f, 0xff, 0xff, 0x38, 0xf8, 0xf0, 0xff, 0x01,
0xfc, 0x3f, 0xfe, 0x7f, 0x3c, 0xf0, 0xf0, 0xff, 0x01, 0xf8, 0x3f, 0xfe,
0x3f, 0x3c, 0xf0, 0xf1, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// bluetooth on icon, 32x32px, horizontal
static const uint8_t bluetooth_on[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00,
0x00, 0xfc, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xdc, 0x07, 0x00,
0x0c, 0x1c, 0x1f, 0x00, 0x3c, 0x1c, 0x3e, 0x00, 0x7c, 0x1c, 0x3e, 0x00,
0xf8, 0x1d, 0x1f, 0x0e, 0xe0, 0x9f, 0x0f, 0x1e, 0xc0, 0xff, 0x03, 0x1e,
0x00, 0xff, 0x01, 0x3c, 0x00, 0xfe, 0xe0, 0x38, 0x00, 0x7e, 0xe0, 0x38,
0xc0, 0xff, 0x41, 0x38, 0xc0, 0xff, 0x03, 0x1e, 0xe0, 0xdf, 0x07, 0x1e,
0xf0, 0x1d, 0x1f, 0x0e, 0x7c, 0x1c, 0x3e, 0x00, 0x3c, 0x1c, 0x3e, 0x00,
0x1c, 0x1c, 0x1f, 0x00, 0x00, 0x9c, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0xfc, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00,
0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// bluetooth off icon, 32x32px, horizontal
static const uint8_t bluetooth_off[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00,
0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x1c, 0xc0, 0x1f, 0x00,
0x3c, 0xc0, 0x3f, 0x00, 0x7c, 0xc0, 0xfd, 0x00, 0xf0, 0xc1, 0xf1, 0x01,
0xe0, 0xc3, 0xe1, 0x03, 0xc0, 0x0f, 0xc0, 0x03, 0x00, 0x1f, 0xf0, 0x01,
0x00, 0x3e, 0xf0, 0x00, 0x00, 0xf8, 0x70, 0x00, 0x00, 0xf0, 0x01, 0x00,
0x00, 0xe0, 0x07, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0xf0, 0x1f, 0x00,
0x00, 0xfc, 0x7d, 0x00, 0x00, 0xfe, 0xf9, 0x00, 0x00, 0xdf, 0xf1, 0x03,
0xc0, 0xc7, 0xc1, 0x07, 0xc0, 0xc3, 0xe1, 0x0f, 0xc0, 0xc1, 0xf1, 0x3f,
0x00, 0xc0, 0xfd, 0x3c, 0x00, 0xc0, 0x3f, 0x38, 0x00, 0xc0, 0x1f, 0x00,
0x00, 0xc0, 0x07, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// power icon, 32x32px, horizontal
static const uint8_t power_icon[] = {
0x00, 0x80, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00,
0x00, 0xcc, 0x33, 0x00, 0x00, 0xcf, 0xf3, 0x00, 0x80, 0xcf, 0xf3, 0x01,
0xc0, 0xcf, 0xf3, 0x03, 0xe0, 0xcf, 0xf3, 0x07, 0xf0, 0xc7, 0xe3, 0x0f,
0xf8, 0xc3, 0xc3, 0x1f, 0xf8, 0xc1, 0x83, 0x1f, 0xfc, 0xc0, 0x03, 0x3f,
0x7c, 0xc0, 0x03, 0x3e, 0x7c, 0xc0, 0x03, 0x3e, 0x7e, 0x80, 0x01, 0x7e,
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c,
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x7c, 0x00, 0x00, 0x3e,
0x7c, 0x00, 0x00, 0x3e, 0xfc, 0x00, 0x00, 0x3f, 0xf8, 0x01, 0x80, 0x1f,
0xf8, 0x03, 0xc0, 0x1f, 0xf0, 0x07, 0xe0, 0x0f, 0xf0, 0x1f, 0xf8, 0x0f,
0xe0, 0xff, 0xff, 0x07, 0xc0, 0xff, 0xff, 0x03, 0x00, 0xff, 0xff, 0x00,
0x00, 0xfc, 0x3f, 0x00, 0x00, 0xf0, 0x0f, 0x00 };
// 'advert', 32x32px, horizontal
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, 0x30, 0x00, 0x00, 0x0c,
0x38, 0x00, 0x00, 0x1c, 0x18, 0x00, 0x00, 0x18, 0x0c, 0x00, 0x00, 0x30,
0x0c, 0x06, 0x60, 0x30, 0x06, 0x07, 0xe0, 0x60, 0x86, 0x03, 0xc0, 0x61,
0x87, 0x81, 0x81, 0xe1, 0xc3, 0xe0, 0x07, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3,
0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3,
0xc3, 0xe0, 0x07, 0xc3, 0x83, 0xc1, 0x83, 0xc1, 0x86, 0x01, 0x80, 0x61,
0x06, 0x03, 0xc0, 0x60, 0x0e, 0x07, 0xe0, 0x70, 0x0c, 0x02, 0x40, 0x30,
0x1c, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0c,
0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// 'muted, 8x8px, horizontal
static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 };

View File

@@ -129,6 +129,8 @@ void KissModem::processFrame() {
memcpy(_pending_tx, data, data_len);
_pending_tx_len = data_len;
_has_pending_tx = true;
} else if (_has_pending_tx) {
writeHardwareError(HW_ERR_TX_BUSY);
}
break;
@@ -257,6 +259,7 @@ void KissModem::processTx() {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_timer = millis();
_tx_state = TX_WAIT_CLEAR;
}
}
@@ -273,19 +276,30 @@ void KissModem::processTx() {
_tx_timer = millis();
_tx_state = TX_SLOT_WAIT;
}
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(KISS_MAX_PACKET_SIZE) * KISS_TX_TIMEOUT_FACTOR) {
_tx_timer = millis();
_tx_state = TX_DELAY;
}
break;
case TX_SLOT_WAIT:
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
_tx_timer = millis();
_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;
if (_radio.startSendRaw(_pending_tx, _pending_tx_len)) {
_tx_timer = millis();
_tx_state = TX_SENDING;
} else {
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
}
break;
@@ -296,6 +310,12 @@ void KissModem::processTx() {
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
_radio.onSendFinished();
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
break;
}

View File

@@ -26,6 +26,7 @@
#define KISS_DEFAULT_TXDELAY 50
#define KISS_DEFAULT_PERSISTENCE 63
#define KISS_DEFAULT_SLOTTIME 10
#define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime
#define HW_CMD_GET_IDENTITY 0x01
#define HW_CMD_GET_RANDOM 0x02
@@ -71,6 +72,7 @@
#define HW_ERR_MAC_FAILED 0x04
#define HW_ERR_UNKNOWN_CMD 0x05
#define HW_ERR_ENCRYPT_FAILED 0x06
#define HW_ERR_TX_BUSY 0x07
#define KISS_FIRMWARE_VERSION 1

View File

@@ -10,7 +10,10 @@
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#else
#include <InternalFileSystem.h>
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
#endif
@@ -29,7 +32,7 @@ void halt() {
}
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
@@ -53,11 +56,11 @@ void loadOrCreateIdentity() {
}
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_set_params(freq, bw, sf, cr);
radio_driver.setParams(freq, bw, sf, cr);
}
void onSetTxPower(uint8_t power) {
radio_set_tx_power(power);
radio_driver.setTxPower(power);
}
float onGetCurrentRssi() {
@@ -79,7 +82,7 @@ void setup() {
radio_driver.begin();
rng.begin(radio_get_rng_seed());
rng.begin(radio_driver.getRngSeed());
loadOrCreateIdentity();
sensors.begin();
@@ -116,6 +119,8 @@ void setup() {
modem->setGetCurrentRssiCallback(onGetCurrentRssi);
modem->setGetStatsCallback(onGetStats);
modem->begin();
board.onBootComplete();
}
void loop() {

View File

@@ -428,7 +428,11 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->isRouteFlood()) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
}
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false;
@@ -635,7 +639,7 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl
// if this a zero hop advert (and not via 'Share'), add it to neighbours
if (packet->path_len == 0 && !isShare(packet)) {
if (packet->getPathHashCount() == 0 && !isShare(packet)) {
AdvertDataParser parser(app_data, app_data_len);
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
putNeighbour(id, timestamp, packet->getSNR());
@@ -884,8 +888,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_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 = 12; // 12 hours
_prefs.flood_advert_interval = 47; // 47 hours
_prefs.flood_max = 64;
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
// bridge defaults
@@ -953,8 +959,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
@@ -1050,7 +1056,7 @@ void MyMesh::dumpLogFile() {
}
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
#if defined(USE_SX1262) || defined(USE_SX1268)
@@ -1279,13 +1285,13 @@ void MyMesh::loop() {
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);
radio_driver.setParams(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);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

View File

@@ -69,11 +69,11 @@ struct NeighbourInfo {
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#define FIRMWARE_BUILD_DATE "6 Jun 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.0"
#define FIRMWARE_VERSION "v1.16.0"
#endif
#define FIRMWARE_ROLE "repeater"

View File

@@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_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->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

View File

@@ -20,8 +20,7 @@ void halt() {
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
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long userBtnDownAt = 0;
@@ -40,9 +39,6 @@ void setup() {
delay(5000);
#endif
// For power saving
lastActive = millis(); // mark last active time since boot
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
@@ -57,7 +53,7 @@ void setup() {
halt();
}
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -103,6 +99,8 @@ void setup() {
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
board.onBootComplete();
}
void loop() {
@@ -155,16 +153,12 @@ void loop() {
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
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#else
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet
}
#endif
#endif
}
}

View File

@@ -282,7 +282,11 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->isRouteFlood()) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
}
return true;
}
@@ -641,8 +645,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_advert_interval = 47; // 47 hours
_prefs.flood_max = 64;
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
@@ -691,8 +697,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
}
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
@@ -796,7 +802,7 @@ void MyMesh::dumpLogFile() {
}
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
@@ -978,7 +984,7 @@ void MyMesh::loop() {
if (did_push) {
next_push = futureMillis(SYNC_PUSH_INTERVAL);
} else {
// were no unsynced posts for curr client, so proccess next client much quicker! (in next loop())
// were no unsynced posts for curr client, so process next client much quicker! (in next loop())
next_push = futureMillis(SYNC_PUSH_INTERVAL / 8);
}
}
@@ -999,13 +1005,13 @@ void MyMesh::loop() {
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);
radio_driver.setParams(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);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

View File

@@ -27,11 +27,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#define FIRMWARE_BUILD_DATE "6 Jun 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.0"
#define FIRMWARE_VERSION "v1.16.0"
#endif
#ifndef LORA_FREQ

View File

@@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_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->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

View File

@@ -35,7 +35,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM)
@@ -80,6 +80,8 @@ void setup() {
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
board.onBootComplete();
}
void loop() {

View File

@@ -562,7 +562,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
#if defined(NRF52_PLATFORM)
InternalFS.begin();
@@ -577,8 +577,8 @@ void setup() {
#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());
radio_driver.setParams(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
radio_driver.setTxPower(the_mesh.getTxPowerPref());
the_mesh.showWelcome();

View File

@@ -764,8 +764,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
}
}
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
@@ -842,7 +842,7 @@ void SensorMesh::updateFloodAdvertTimer() {
}
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
void SensorMesh::formatStatsReply(char *reply) {
@@ -908,13 +908,13 @@ void SensorMesh::loop() {
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);
radio_driver.setParams(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);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

View File

@@ -34,11 +34,11 @@
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
#define FIRMWARE_BUILD_DATE "6 Jun 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.14.1"
#define FIRMWARE_VERSION "v1.16.0"
#endif
#define FIRMWARE_ROLE "sensor"

View File

@@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_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->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

View File

@@ -68,7 +68,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)