diff --git a/.gitignore b/.gitignore index a0ad5f6e..8c229550 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ compile_commands.json .venv/ venv/ platformio.local.ini +CODE_REVIEW.md diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index b3792f51..a02923d6 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -200,142 +200,103 @@ void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) } void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { + // Set hardware defaults before reading — if the file is older and lacks these fields, + // the compile-time values apply rather than the zero from memset in MyMesh::begin(). +#ifdef DISPLAY_ROTATION + _prefs.display_rotation = DISPLAY_ROTATION; +#endif +#ifdef JOYSTICK_ROTATION + _prefs.joystick_rotation = JOYSTICK_ROTATION; +#endif + File file = openRead(_fs, filename); - if (file) { - uint8_t pad[8]; + if (!file) return; - file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); - file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); - file.read(pad, 4); - file.read((uint8_t *)&node_lat, sizeof(node_lat)); - file.read((uint8_t *)&node_lon, sizeof(node_lon)); - file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); - file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); - file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); - file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); - file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); - file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); - file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); - file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); - file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); - file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); - file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); - file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); - file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); - file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); - file.read(pad, 1); - file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); - file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); - file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); - file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); - file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); - file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); - file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); - file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); - file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); - file.read((uint8_t *)&_prefs.display_brightness, sizeof(_prefs.display_brightness)); - file.read((uint8_t *)&_prefs.auto_off_secs, sizeof(_prefs.auto_off_secs)); - file.read((uint8_t *)&_prefs.tz_offset_hours, sizeof(_prefs.tz_offset_hours)); - file.read((uint8_t *)&_prefs.low_batt_mv, sizeof(_prefs.low_batt_mv)); - file.read((uint8_t *)&_prefs.batt_display_mode, sizeof(_prefs.batt_display_mode)); - if (file.available()) { - file.read((uint8_t *)_prefs.custom_msgs, sizeof(_prefs.custom_msgs)); - file.read((uint8_t *)&_prefs.ch_notif_override, sizeof(_prefs.ch_notif_override)); - file.read((uint8_t *)&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted)); - file.read((uint8_t *)&_prefs.dm_show_all, sizeof(_prefs.dm_show_all)); - file.read((uint8_t *)&_prefs.room_fav_only, sizeof(_prefs.room_fav_only)); - if (file.available()) { - file.read((uint8_t *)&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume)); - if (file.available()) { - file.read((uint8_t *)&_prefs.ringtone_bpm_idx, sizeof(_prefs.ringtone_bpm_idx)); - file.read((uint8_t *)&_prefs.ringtone_len, sizeof(_prefs.ringtone_len)); - if (_prefs.ringtone_len > 32) _prefs.ringtone_len = 0; - file.read((uint8_t *)_prefs.ringtone_notes, sizeof(_prefs.ringtone_notes)); - if (file.available()) { - file.read((uint8_t *)&_prefs.home_pages_mask, sizeof(_prefs.home_pages_mask)); - if (file.available()) { - file.read((uint8_t *)&_prefs.bot_enabled, sizeof(_prefs.bot_enabled)); - file.read((uint8_t *)&_prefs.bot_channel_enabled, sizeof(_prefs.bot_channel_enabled)); - file.read((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx)); - file.read((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger)); - file.read((uint8_t *)_prefs.bot_reply_dm, sizeof(_prefs.bot_reply_dm)); - file.read((uint8_t *)_prefs.bot_reply_ch, sizeof(_prefs.bot_reply_ch)); - if (file.available()) { - file.read((uint8_t *)&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds)); - if (file.available()) { - file.read((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto)); - if (file.available()) { - file.read((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif)); - if (file.available()) { - file.read((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); - if (file.available()) { - file.read((uint8_t *)&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec)); - if (file.available()) { - file.read((uint8_t *)&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx)); - file.read((uint8_t *)&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len)); - if (_prefs.ringtone2_len > 32) _prefs.ringtone2_len = 0; - file.read((uint8_t *)_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes)); - if (file.available()) { - file.read((uint8_t *)&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm)); - file.read((uint8_t *)&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch)); - if (file.available()) { - file.read((uint8_t *)&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set)); - file.read((uint8_t *)&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2)); - if (file.available()) { - file.read((uint8_t *)_prefs.dm_melody, sizeof(_prefs.dm_melody)); - if (file.available()) { - file.read((uint8_t *)&_prefs.auto_lock, sizeof(_prefs.auto_lock)); - if (file.available()) { - file.read((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h)); - if (file.available()) { - file.read((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); - if (file.available()) { - file.read((uint8_t *)&_prefs.display_rotation, sizeof(_prefs.display_rotation)); - if (file.available()) { - file.read((uint8_t *)_prefs.page_order, sizeof(_prefs.page_order)); - if (file.available()) { - file.read((uint8_t *)&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation)); - } else { -#ifdef JOYSTICK_ROTATION - _prefs.joystick_rotation = JOYSTICK_ROTATION; -#endif - } - } - } else { -#ifdef DISPLAY_ROTATION - _prefs.display_rotation = DISPLAY_ROTATION; -#endif -#ifdef JOYSTICK_ROTATION - _prefs.joystick_rotation = JOYSTICK_ROTATION; -#endif - } - } - } else { -#ifdef DISPLAY_ROTATION - _prefs.display_rotation = DISPLAY_ROTATION; -#endif -#ifdef JOYSTICK_ROTATION - _prefs.joystick_rotation = JOYSTICK_ROTATION; -#endif - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } + uint8_t pad[8]; - file.close(); - } + // Core fields — present in every saved file (written unconditionally since the beginning). + file.read((uint8_t *)&_prefs.airtime_factor, sizeof(_prefs.airtime_factor)); + file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); + file.read(pad, 4); + file.read((uint8_t *)&node_lat, sizeof(node_lat)); + file.read((uint8_t *)&node_lon, sizeof(node_lon)); + file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); + file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); + file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); + file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); + file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); + file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); + file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); + file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); + file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); + file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); + file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); + file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); + file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); + file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); + file.read(pad, 1); + file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); + file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); + file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); + file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); + file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); + file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); + file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); + file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); + file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); + file.read((uint8_t *)&_prefs.display_brightness, sizeof(_prefs.display_brightness)); + file.read((uint8_t *)&_prefs.auto_off_secs, sizeof(_prefs.auto_off_secs)); + file.read((uint8_t *)&_prefs.tz_offset_hours, sizeof(_prefs.tz_offset_hours)); + file.read((uint8_t *)&_prefs.low_batt_mv, sizeof(_prefs.low_batt_mv)); + file.read((uint8_t *)&_prefs.batt_display_mode, sizeof(_prefs.batt_display_mode)); + + // Extension fields — append-only, newest at the bottom. + // Each read is gated on file.available(); fields absent in older files stay at their + // zero-initialised or hardware-default values set above. + auto rd = [&](void* p, size_t n) { + if (file.available() >= (int)n) file.read((uint8_t*)p, n); + }; + + rd(_prefs.custom_msgs, sizeof(_prefs.custom_msgs)); + rd(&_prefs.ch_notif_override, sizeof(_prefs.ch_notif_override)); + rd(&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted)); + rd(&_prefs.dm_show_all, sizeof(_prefs.dm_show_all)); + rd(&_prefs.room_fav_only, sizeof(_prefs.room_fav_only)); + rd(&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume)); + rd(&_prefs.ringtone_bpm_idx, sizeof(_prefs.ringtone_bpm_idx)); + rd(&_prefs.ringtone_len, sizeof(_prefs.ringtone_len)); + if (_prefs.ringtone_len > 32) _prefs.ringtone_len = 0; + rd(_prefs.ringtone_notes, sizeof(_prefs.ringtone_notes)); + rd(&_prefs.home_pages_mask, sizeof(_prefs.home_pages_mask)); + rd(&_prefs.bot_enabled, sizeof(_prefs.bot_enabled)); + rd(&_prefs.bot_channel_enabled, sizeof(_prefs.bot_channel_enabled)); + rd(&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx)); + rd(_prefs.bot_trigger, sizeof(_prefs.bot_trigger)); + for (char* p = _prefs.bot_trigger; *p; p++) *p = (char)tolower((uint8_t)*p); + rd(_prefs.bot_reply_dm, sizeof(_prefs.bot_reply_dm)); + rd(_prefs.bot_reply_ch, sizeof(_prefs.bot_reply_ch)); + rd(&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds)); + rd(&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto)); + rd(_prefs.dm_notif, sizeof(_prefs.dm_notif)); + rd(_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); + rd(&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec)); + rd(&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx)); + rd(&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len)); + if (_prefs.ringtone2_len > 32) _prefs.ringtone2_len = 0; + rd(_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes)); + rd(&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm)); + rd(&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch)); + rd(&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set)); + rd(&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2)); + rd(_prefs.dm_melody, sizeof(_prefs.dm_melody)); + rd(&_prefs.auto_lock, sizeof(_prefs.auto_lock)); + rd(&_prefs.clock_12h, sizeof(_prefs.clock_12h)); + rd(&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); + rd(&_prefs.display_rotation, sizeof(_prefs.display_rotation)); + rd(_prefs.page_order, sizeof(_prefs.page_order)); + rd(&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation)); + + file.close(); } void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index d75af84e..2c38cb0e 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -8,12 +8,12 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) { millis() - _bot_last_dm_reply_ms > 10000UL)) return; - const char* tr = _prefs.bot_trigger; + const char* tr = _prefs.bot_trigger; // already lowercase (normalised at save/load time) int tlen = strlen(tr); bool matched = false; for (const char* t = text; *t && !matched; t++) { int i = 0; - while (i < tlen && tolower((uint8_t)t[i]) == tolower((uint8_t)tr[i])) i++; + while (i < tlen && tolower((uint8_t)t[i]) == (uint8_t)tr[i]) i++; if (i == tlen) matched = true; } if (!matched) return; @@ -45,12 +45,12 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text) { const char* sep = strstr(text, ": "); if (sep) msg = sep + 2; - const char* tr = _prefs.bot_trigger; + const char* tr = _prefs.bot_trigger; // already lowercase (normalised at save/load time) int tlen = strlen(tr); bool matched = false; for (const char* t = msg; *t && !matched; t++) { int i = 0; - while (i < tlen && tolower((uint8_t)t[i]) == tolower((uint8_t)tr[i])) i++; + while (i < tlen && tolower((uint8_t)t[i]) == (uint8_t)tr[i]) i++; if (i == tlen) matched = true; } if (!matched) return; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index b7c9a2eb..01277b33 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,5 +1,6 @@ #pragma once -#include // For uint8_t, uint32_t +#include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -84,4 +85,44 @@ struct NodePrefs { // persisted to file // 0 = end of list (also uninitialized legacy). hasCustomOrder iff page_order[0] in 1..11. uint8_t page_order[11]; uint8_t joystick_rotation; // 0-3 steps CW; independent of display_rotation + + // Bitmasks for home_pages_mask (bit=1 → page visible; 0 field = all visible). + static const uint16_t HP_CLOCK = 1 << 0; + static const uint16_t HP_RECENT = 1 << 1; + static const uint16_t HP_RADIO = 1 << 2; + static const uint16_t HP_BLUETOOTH = 1 << 3; + static const uint16_t HP_ADVERT = 1 << 4; + static const uint16_t HP_GPS = 1 << 5; + static const uint16_t HP_SENSORS = 1 << 6; + static const uint16_t HP_TOOLS = 1 << 7; + static const uint16_t HP_SHUTDOWN = 1 << 8; + static const uint16_t HP_ALL = 0x01FF; + + // Label for home page by bit-index (0–10); returns "" for out-of-range. + static const char* homePageLabel(uint8_t bit) { + static const char* labels[] = { + "Clock", "Recent", "Radio", "Bluetooth", "Advert", + "GPS", "Sensors", "Tools", "Shutdown", "Settings", "Messages" + }; + return (bit < 11) ? labels[bit] : ""; + } + + static void buildRTTTLString(const uint8_t* notes, uint8_t len, uint8_t bpm_idx, + char* buf, int size) { + static const uint16_t BPM_OPTS[] = { 60, 90, 120, 150, 180 }; + static const uint8_t DUR_VALS[] = { 4, 8, 16, 32 }; + static const char PITCHES[] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' }; + if (len == 0) { buf[0] = '\0'; return; } + uint16_t bpm = BPM_OPTS[bpm_idx < 5 ? bpm_idx : 2]; + int pos = snprintf(buf, size, "Ring:d=8,o=5,b=%u:", bpm); + for (int i = 0; i < len && pos < size - 8; i++) { + if (i > 0 && pos < size - 1) buf[pos++] = ','; + uint8_t pitch = notes[i] & 0x07; + uint8_t octave = ((notes[i] >> 3) & 0x03) + 4; + uint8_t dur_val = DUR_VALS[(notes[i] >> 5) & 0x03]; + if (pitch == 0) pos += snprintf(buf + pos, size - pos, "%dp", dur_val); + else pos += snprintf(buf + pos, size - pos, "%d%c%d", dur_val, PITCHES[pitch], octave); + } + if (pos < size) buf[pos] = '\0'; + } }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 1d14d76d..393d43f6 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -117,6 +117,7 @@ public: if (_kb_field == 2) { strncpy(_prefs->bot_trigger, _kb.buf, sizeof(_prefs->bot_trigger) - 1); _prefs->bot_trigger[sizeof(_prefs->bot_trigger) - 1] = '\0'; + for (char* p = _prefs->bot_trigger; *p; p++) *p = (char)tolower((uint8_t)*p); } else if (_kb_field == 3) { strncpy(_prefs->bot_reply_dm, _kb.buf, sizeof(_prefs->bot_reply_dm) - 1); _prefs->bot_reply_dm[sizeof(_prefs->bot_reply_dm) - 1] = '\0'; @@ -184,7 +185,7 @@ public: } _kb.begin(initial, max); if (_sel == 2) { - _kb._ph_count = 0; // trigger is literal text — placeholders never match incoming msgs + _kb.clearPlaceholders(); // trigger is literal text — placeholders never match incoming msgs } else { kbAddSensorPlaceholders(_kb, &sensors); } diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 81560b7b..a8b1019f 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -46,6 +46,8 @@ struct KeyboardWidget { addPlaceholder("{time}"); } + void clearPlaceholders() { _ph_count = 0; } + void addPlaceholder(const char* ph) { if (_ph_count < KB_PH_MAX) { strncpy(_ph_buf[_ph_count], ph, KB_PH_LEN - 1); diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 575f5953..cde19441 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -125,8 +125,7 @@ class QuickMsgScreen : public UIScreen { } - static void fmtMsgAge(char* buf, int n, uint32_t timestamp) { - uint32_t now = rtc_clock.getCurrentTime(); + static void fmtMsgAge(char* buf, int n, uint32_t timestamp, uint32_t now) { if (timestamp == 0 || now < timestamp) { buf[0] = '\0'; return; } uint32_t age = now - timestamp; if (age < 60) snprintf(buf, n, "%us", age); @@ -645,6 +644,7 @@ public: display.fillRect(0, lh + 1, display.width(), display.sepH()); int dm_count = dmHistCountForContact(_sel_contact.id.pub_key); + uint32_t now_ts = rtc_clock.getCurrentTime(); for (int i = 0; i < _hist_visible && (_dm_hist_scroll + i) < dm_count; i++) { int item = _dm_hist_scroll + i; @@ -657,7 +657,7 @@ public: const DmHistEntry& e = _dm_hist[ring_pos]; const char* sender = e.outgoing ? "Me" : filtered_name; - char age[6]; fmtMsgAge(age, sizeof(age), e.timestamp); + char age[6]; fmtMsgAge(age, sizeof(age), e.timestamp, now_ts); int age_w = age[0] ? display.getTextWidth(age) + 3 : 0; if (sel) { @@ -748,6 +748,7 @@ public: display.fillRect(0, lh + 1, display.width(), display.sepH()); int ch_hist_count = histCountForChannel(_sel_channel_idx); + uint32_t now_ts = rtc_clock.getCurrentTime(); for (int i = 0; i < _hist_visible && (_hist_scroll + i) < ch_hist_count; i++) { int item = _hist_scroll + i; @@ -771,7 +772,7 @@ public: msg_part[sizeof(msg_part) - 1] = '\0'; } - char age[6]; fmtMsgAge(age, sizeof(age), _hist[ring_pos].timestamp); + char age[6]; fmtMsgAge(age, sizeof(age), _hist[ring_pos].timestamp, now_ts); int age_w = age[0] ? display.getTextWidth(age) + 3 : 0; if (sel) { @@ -1048,7 +1049,8 @@ public: } else if (res == FullscreenMsgView::REPLY) { int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { - snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name); + char _tname[32]; DisplayDriver::translateUTF8Static(_tname, _sel_contact.name, sizeof(_tname)); + snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _tname); _ctx_menu.begin("Options", 1); _ctx_menu.addItem("Reply"); } @@ -1096,7 +1098,8 @@ public: if (c == KEY_CONTEXT_MENU && _dm_hist_sel >= 0) { int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { - snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name); + char _tname[32]; DisplayDriver::translateUTF8Static(_tname, _sel_contact.name, sizeof(_tname)); + snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _tname); _ctx_menu.begin("Options", 1); _ctx_menu.addItem("Reply"); } diff --git a/examples/companion_radio/ui-new/RingtoneEditorScreen.h b/examples/companion_radio/ui-new/RingtoneEditorScreen.h index ddd30d8c..c323b53e 100644 --- a/examples/companion_radio/ui-new/RingtoneEditorScreen.h +++ b/examples/companion_radio/ui-new/RingtoneEditorScreen.h @@ -47,19 +47,7 @@ class RingtoneEditorScreen : public UIScreen { } void buildRTTTL() { - uint16_t bpm = BPM_OPTS[_bpm_idx < 5 ? _bpm_idx : 2]; - int pos = snprintf(_play_buf, sizeof(_play_buf), "Ring:d=8,o=5,b=%u:", bpm); - for (int i = 0; i < _len && pos < (int)sizeof(_play_buf) - 8; i++) { - if (i > 0 && pos < (int)sizeof(_play_buf) - 1) _play_buf[pos++] = ','; - uint8_t pitch = notePitch(_notes[i]); - uint8_t octave = noteOctave(_notes[i]); - uint8_t dur_val = DUR_VALS[noteDurIdx(_notes[i])]; - if (pitch == 0) - pos += snprintf(_play_buf + pos, sizeof(_play_buf) - pos, "%dp", dur_val); - else - pos += snprintf(_play_buf + pos, sizeof(_play_buf) - pos, "%d%c%d", dur_val, PITCH_NAMES[pitch], octave); - } - if (pos < (int)sizeof(_play_buf)) _play_buf[pos] = '\0'; + NodePrefs::buildRTTTLString(_notes, _len, _bpm_idx, _play_buf, sizeof(_play_buf)); } void previewNote(uint8_t note_byte) { diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 61cabb8c..dab22ad6 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -168,47 +168,20 @@ class SettingsScreen : public UIScreen { } uint16_t homePageBit(int item) const { - if (item == HOME_CLOCK) return HP_CLOCK; - if (item == HOME_RECENT) return HP_RECENT; - if (item == HOME_RADIO) return HP_RADIO; - if (item == HOME_BT) return HP_BLUETOOTH; - if (item == HOME_ADVERT) return HP_ADVERT; - if (item == HOME_TOOLS) return HP_TOOLS; - if (item == HOME_SHUTDOWN) return HP_SHUTDOWN; - // HOME_SETTINGS and HOME_QUICK_MSG have no mask bit — always visible -#if ENV_INCLUDE_GPS == 1 - if (item == HOME_GPS) return HP_GPS; -#endif -#if UI_SENSORS_PAGE == 1 - if (item == HOME_SENSORS) return HP_SENSORS; -#endif - return 0; + int bit = homePageBitIndex(item); + return (bit >= 0 && bit < 9) ? (uint16_t)(1 << bit) : 0; } const char* homePageLabel(int item) const { - if (item == HOME_CLOCK) return "Clock"; - if (item == HOME_RECENT) return "Recent"; - if (item == HOME_RADIO) return "Radio"; - if (item == HOME_BT) return "Bluetooth"; - if (item == HOME_ADVERT) return "Advert"; - if (item == HOME_TOOLS) return "Tools"; - if (item == HOME_SHUTDOWN) return "Shutdown"; - if (item == HOME_SETTINGS) return "Settings"; - if (item == HOME_QUICK_MSG) return "Messages"; -#if ENV_INCLUDE_GPS == 1 - if (item == HOME_GPS) return "GPS"; -#endif -#if UI_SENSORS_PAGE == 1 - if (item == HOME_SENSORS) return "Sensors"; -#endif - return ""; + int bit = homePageBitIndex(item); + return NodePrefs::homePageLabel((uint8_t)(bit >= 0 ? bit : 11)); } bool homePageVisible(int item, const NodePrefs* p) const { if (item == HOME_SETTINGS || item == HOME_QUICK_MSG) return true; uint16_t bit = homePageBit(item); if (!bit) return false; - uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : HP_ALL; + uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : NodePrefs::HP_ALL; return (mask & bit) != 0; } @@ -600,7 +573,7 @@ public: return true; } if (enter && homePageToggleable(_selected)) { - if (!p->home_pages_mask) p->home_pages_mask = HP_ALL; + if (!p->home_pages_mask) p->home_pages_mask = NodePrefs::HP_ALL; p->home_pages_mask ^= homePageBit(_selected); _dirty = true; return true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 4a2153a0..9cf55661 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -111,18 +111,6 @@ public: static const int QUICK_MSGS_MAX = 10; -// Bit positions for NodePrefs::home_pages_mask. -// Bit=1 means page is shown. 0 in the field means ALL visible (default/unset). -static const uint16_t HP_CLOCK = 1 << 0; -static const uint16_t HP_RECENT = 1 << 1; -static const uint16_t HP_RADIO = 1 << 2; -static const uint16_t HP_BLUETOOTH = 1 << 3; -static const uint16_t HP_ADVERT = 1 << 4; -static const uint16_t HP_GPS = 1 << 5; -static const uint16_t HP_SENSORS = 1 << 6; -static const uint16_t HP_TOOLS = 1 << 7; -static const uint16_t HP_SHUTDOWN = 1 << 8; -static const uint16_t HP_ALL = 0x01FF; #include "KeyboardWidget.h" #include "FullscreenMsgView.h" @@ -216,7 +204,7 @@ class HomeScreen : public UIScreen { bool isPageVisible(int page) const { int bit = pageBit(page); if (bit < 0) return true; - uint16_t mask = (_node_prefs && _node_prefs->home_pages_mask) ? _node_prefs->home_pages_mask : HP_ALL; + uint16_t mask = (_node_prefs && _node_prefs->home_pages_mask) ? _node_prefs->home_pages_mask : NodePrefs::HP_ALL; return (mask >> bit) & 1; } @@ -698,7 +686,6 @@ public: // scan LPP buffer for this type LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize()); uint8_t ch, type; - bool found = false; char buf[22] = "--"; while (r.readHeader(ch, type)) { if (type == target) { @@ -721,12 +708,10 @@ public: case LPP_CONCENTRATION: r.readConcentration(v); snprintf(buf, sizeof(buf), "%.0fppm", v); break; default: r.skipData(type); continue; } - found = true; break; } r.skipData(type); } - (void)found; static const struct { uint8_t type; const char* name; } TYPE_NAMES[] = { { LPP_VOLTAGE, "voltage" }, @@ -1015,24 +1000,10 @@ void UITask::showAlert(const char* text, int duration_millis) { } static void buildMelodyFromPrefs(const NodePrefs* p, int slot, char* buf, int size) { - static const uint16_t BPM_OPTS[] = { 60, 90, 120, 150, 180 }; - static const uint8_t DUR_VALS[] = { 4, 8, 16, 32 }; - static const char PITCHES[] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' }; - const uint8_t* notes = (slot == 2) ? p->ringtone2_notes : p->ringtone_notes; - uint8_t len = (slot == 2) ? p->ringtone2_len : p->ringtone_len; - uint8_t bpm_i = (slot == 2) ? p->ringtone2_bpm_idx : p->ringtone_bpm_idx; - if (len == 0) { buf[0] = '\0'; return; } - uint16_t bpm = BPM_OPTS[bpm_i < 5 ? bpm_i : 2]; - int pos = snprintf(buf, size, "Ring:d=8,o=5,b=%u:", bpm); - for (int i = 0; i < len && pos < size - 8; i++) { - if (i > 0 && pos < size - 1) buf[pos++] = ','; - uint8_t pitch = notes[i] & 0x07; - uint8_t octave = ((notes[i] >> 3) & 0x03) + 4; - uint8_t dur_val = DUR_VALS[(notes[i] >> 5) & 0x03]; - if (pitch == 0) pos += snprintf(buf + pos, size - pos, "%dp", dur_val); - else pos += snprintf(buf + pos, size - pos, "%d%c%d", dur_val, PITCHES[pitch], octave); - } - if (pos < size) buf[pos] = '\0'; + const uint8_t* notes = (slot == 2) ? p->ringtone2_notes : p->ringtone_notes; + uint8_t len = (slot == 2) ? p->ringtone2_len : p->ringtone_len; + uint8_t bpm_i = (slot == 2) ? p->ringtone2_bpm_idx : p->ringtone_bpm_idx; + NodePrefs::buildRTTTLString(notes, len, bpm_i, buf, size); } void UITask::notify(UIEventType t) { @@ -1066,6 +1037,7 @@ switch(t){ } bool custom_played = false; if (slot > 0 && _node_prefs) { + if (buzzer.isPlaying()) buzzer.stop(); // stop before overwriting _notif_mel_buf buildMelodyFromPrefs(_node_prefs, slot, _notif_mel_buf, sizeof(_notif_mel_buf)); if (_notif_mel_buf[0]) { if (force) buzzer.playForced(_notif_mel_buf); else buzzer.play(_notif_mel_buf); @@ -1101,6 +1073,7 @@ switch(t){ } bool custom_played = false; if (slot > 0 && _node_prefs) { + if (buzzer.isPlaying()) buzzer.stop(); // stop before overwriting _notif_mel_buf buildMelodyFromPrefs(_node_prefs, slot, _notif_mel_buf, sizeof(_notif_mel_buf)); if (_notif_mel_buf[0]) { if (force) buzzer.playForced(_notif_mel_buf); else buzzer.play(_notif_mel_buf); @@ -1181,7 +1154,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i void UITask::userLedHandler() { #ifdef PIN_STATUS_LED - int cur_time = millis(); + unsigned long cur_time = millis(); if (cur_time > next_led_change) { if (led_state == 0) { led_state = 1; @@ -1276,8 +1249,7 @@ static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_m case DASH_CO2: lpp_type = LPP_CONCENTRATION; break; } if (lpp_type) { - CayenneLPP local_lpp(200); - if (!lpp) { local_lpp.reset(); sensors.querySensors(0xFF, local_lpp); lpp = &local_lpp; } + if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; } LPPReader r(lpp->getBuffer(), lpp->getSize()); uint8_t ch, type; while (r.readHeader(ch, type)) { @@ -1383,7 +1355,7 @@ void UITask::loop() { } #endif #if defined(PIN_USER_BTN_ANA) - if (abs(millis() - _analogue_pin_read_millis) > 10) { + if (millis() - _analogue_pin_read_millis > 10) { ev = analog_btn.check(); if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); @@ -1474,14 +1446,13 @@ void UITask::loop() { // Two sensor values side by side (dashboard_fields[0] and [1]) if (_node_prefs) { char v0[20] = "", v1[20] = ""; - CayenneLPP shared_lpp(200); CayenneLPP* lpp_ptr = nullptr; uint8_t f0 = _node_prefs->dashboard_fields[0], f1 = _node_prefs->dashboard_fields[1]; auto isLPP = [](uint8_t f) { return f==DASH_TEMP||f==DASH_HUM||f==DASH_PRES||f==DASH_ALT||f==DASH_LUX||f==DASH_CO2; }; if (isLPP(f0) || isLPP(f1)) { - shared_lpp.reset(); sensors.querySensors(0xFF, shared_lpp); lpp_ptr = &shared_lpp; + _dash_lpp.reset(); sensors.querySensors(0xFF, _dash_lpp); lpp_ptr = &_dash_lpp; } formatDashVal(f0, v0, sizeof(v0), _batt_mv, lpp_ptr); formatDashVal(f1, v1, sizeof(v1), _batt_mv, lpp_ptr); @@ -1597,6 +1568,8 @@ char UITask::checkDisplayOn(char c) { _next_refresh = 0; return 0; // eat the waking key press } + _lock_seq_count = 0; + _lock_seq_used = false; c = 0; } if (!_locked) { @@ -1627,8 +1600,7 @@ char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); toggleBuzzer(); - c = 0; - return c; + return 0; } bool UITask::getGPSState() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 806f8401..dd8d4baa 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -51,11 +51,11 @@ class UITask : public AbstractUITask { DMUnreadEntry _dm_unread_table[DM_UNREAD_TABLE_SIZE]; unsigned long ui_started_at, next_batt_chck; uint16_t _batt_mv; // EMA-filtered battery voltage - int next_backlight_btn_check = 0; + unsigned long next_backlight_btn_check = 0; #ifdef PIN_STATUS_LED int led_state = 0; - int next_led_change = 0; - int last_led_increment = 0; + unsigned long next_led_change = 0; + unsigned long last_led_increment = 0; #endif #ifdef PIN_USER_BTN_ANA @@ -73,6 +73,7 @@ class UITask : public AbstractUITask { UIScreen* dashboard_config; UIScreen* auto_advert_screen; UIScreen* curr; + CayenneLPP _dash_lpp; void userLedHandler(); @@ -86,7 +87,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { + UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL), _dash_lpp(200) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; _batt_mv = 0; diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index e0fd0b82..ec335cde 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -6,6 +6,8 @@ class DisplayDriver { int _w, _h; protected: + bool _vw_dirty = true; + bool _vw_result = false; DisplayDriver(int w, int h) { _w = w; _h = h; } void setDimensions(int w, int h) { _w = w; _h = h; } public: @@ -151,8 +153,8 @@ public: } } - // convert UTF-8 to ASCII, transliterating accented/diacritic characters - virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { + // convert UTF-8 to ASCII, transliterating accented/diacritic characters (callable without a display instance) + static void translateUTF8Static(char* dest, const char* src, size_t dest_size) { size_t j = 0; const uint8_t* p = (const uint8_t*)src; while (*p && j < dest_size - 1) { @@ -178,6 +180,10 @@ public: } dest[j] = 0; } + + virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { + translateUTF8Static(dest, src, dest_size); + } // draw text with ellipsis if it exceeds max_width virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) { @@ -194,9 +200,11 @@ public: // for fixed-width fonts (OLED), keep tight spacing to save precious characters const char* ellipsis; // use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width - int i_width = getTextWidth("i"); - int l_width = getTextWidth("l"); - if (i_width != l_width) { + if (_vw_dirty) { + _vw_result = (getTextWidth("i") != getTextWidth("l")); + _vw_dirty = false; + } + if (_vw_result) { ellipsis = "... "; // variable-width fonts: add space } else { ellipsis = "..."; // fixed-width fonts: no space diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index c30b8ee9..0314caa6 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -80,6 +80,7 @@ void GxEPDDisplay::startFrame(Color bkg) { void GxEPDDisplay::setTextSize(int sz) { _text_sz = sz; + _vw_dirty = true; display_crc.update(sz); // Size 1 scales with orientation: 1× in portrait (≈OLED width), 2× in landscape. // Size 2 always uses 2× built-in (12×16) — fixed because layout Y-positions are hardcoded. @@ -163,7 +164,7 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display_crc.update(bits, w * h / 8); + display_crc.update((uintptr_t)bits); uint16_t widthInBytes = (w + 7) / 8; for (uint16_t by = 0; by < h; by++) { for (uint16_t bx = 0; bx < w; bx++) { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 8cde8e0e..2d64efe5 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -82,7 +82,7 @@ public: if (_text_sz == 2) return 16 * sc; return (_use_lemon ? 10 : 8) * sc; } - void setLemonFont(bool enabled) override { _use_lemon = enabled; } + void setLemonFont(bool enabled) override { _use_lemon = enabled; _vw_dirty = true; } bool isLemonFont() const override { return _use_lemon; } bool begin(); diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 79455b04..67384954 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -50,6 +50,7 @@ void SH1106Display::startFrame(Color bkg) void SH1106Display::setTextSize(int sz) { _text_sz = sz; + _vw_dirty = true; display.setTextSize(sz); } diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index b5994697..ebc6553b 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -50,7 +50,7 @@ public: uint16_t getTextWidth(const char *str) override; int getCharWidth() const override { return (_use_lemon ? 5 : 6) * _text_sz; } int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; } - void setLemonFont(bool enabled) override { _use_lemon = enabled; } + void setLemonFont(bool enabled) override { _use_lemon = enabled; _vw_dirty = true; } bool isLemonFont() const override { return _use_lemon; } void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override; void setBrightness(uint8_t level) override;