mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase, lock-seq cleared on display wake), plus optimisations (fmtMsgAge/ getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_* constants and homePageLabel table moved to NodePrefs, DataStore flat lambda schema, translateUTF8Static factored out) and minor cleanup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ compile_commands.json
|
||||
.venv/
|
||||
venv/
|
||||
platformio.local.ini
|
||||
CODE_REVIEW.md
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint> // For uint8_t, uint32_t
|
||||
#include <cstdint>
|
||||
#include <stdio.h>
|
||||
|
||||
#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';
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -80,6 +80,7 @@ void GxEPDDisplay::startFrame(Color bkg) {
|
||||
|
||||
void GxEPDDisplay::setTextSize(int sz) {
|
||||
_text_sz = sz;
|
||||
_vw_dirty = true;
|
||||
display_crc.update<int>(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<int>(y);
|
||||
display_crc.update<int>(w);
|
||||
display_crc.update<int>(h);
|
||||
display_crc.update<uint8_t>(bits, w * h / 8);
|
||||
display_crc.update<uintptr_t>((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++) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ void SH1106Display::startFrame(Color bkg)
|
||||
void SH1106Display::setTextSize(int sz)
|
||||
{
|
||||
_text_sz = sz;
|
||||
_vw_dirty = true;
|
||||
display.setTextSize(sz);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user