From 42b51935b139e3c49d767bf15d0aaabb693f8fcf Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 14 May 2026 00:12:07 +0200 Subject: [PATCH] feat: sensor placeholders in messages and keyboard picker expandMsg now expands {temp}, {hum}, {pres}, {batt}, {alt}, {lux}, {dist}, {co2} from live sensor readings. The [{}] picker in the on-screen keyboard shows only sensors currently detected by SensorManager and scrolls when more than 3 items fit the overlay. New SensorPlaceholders.h helper keeps sensor-to-keyboard wiring out of UITask.cpp. MsgExpand.h handles all expansion logic centrally; MyMeshBot and QuickMsgScreen pass &sensors and batt_volts to each call. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/MsgExpand.h | 118 +++++++++++++++--- examples/companion_radio/MyMeshBot.h | 6 +- examples/companion_radio/ui-new/BotScreen.h | 2 + .../companion_radio/ui-new/KeyboardWidget.h | 68 +++++++--- .../ui-new/SensorPlaceholders.h | 31 +++++ examples/companion_radio/ui-new/UITask.cpp | 7 +- 6 files changed, 200 insertions(+), 32 deletions(-) create mode 100644 examples/companion_radio/ui-new/SensorPlaceholders.h diff --git a/examples/companion_radio/MsgExpand.h b/examples/companion_radio/MsgExpand.h index ef18f28e..b149fbb3 100644 --- a/examples/companion_radio/MsgExpand.h +++ b/examples/companion_radio/MsgExpand.h @@ -3,26 +3,83 @@ #include #include #include +#include +#include -// Expands {loc} and {time} placeholders in tmpl into out (out_len bytes). -// lat/lon — GPS coordinates; gps_valid must be true for {loc} to show coords -// utc_ts — unix timestamp from RTC (0 or <1e9 = no valid time) -// tz_hours — local timezone offset from UTC (-12..+14) +// Expands placeholders in tmpl into out (out_len bytes). +// {loc} — GPS coordinates (lat/lon) or "no GPS" +// {time} — local time HH:MM from RTC +// {temp} — temperature in °C (requires sm) +// {hum} — relative humidity % (requires sm) +// {pres} — barometric pressure hPa (requires sm) +// {batt} — battery voltage V (batt_volts >= 0 or LPP_VOLTAGE from sm) +// {alt} — altitude m (requires sm) +// {lux} — luminosity lux (requires sm) +// {dist} — distance m (requires sm) +// {co2} — CO2 concentration ppm (requires sm) inline void expandMsg(const char* tmpl, char* out, int out_len, double lat, double lon, bool gps_valid, - uint32_t utc_ts, int8_t tz_hours) { + uint32_t utc_ts, int8_t tz_hours, + SensorManager* sm = nullptr, + float batt_volts = -1.0f) { + // sv indices: 0=temp 1=hum 2=pres 3=batt 4=alt 5=lux 6=dist 7=co2 + float sv[8] = {}; + bool sv_ok[8] = {}; + + if (sm) { + CayenneLPP lpp(100); + sm->querySensors(0xFF, lpp); + LPPReader r(lpp.getBuffer(), lpp.getSize()); + uint8_t ch, type; + while (r.readHeader(ch, type)) { + float tmp; + switch (type) { + case LPP_TEMPERATURE: + r.readTemperature(tmp); + if (!sv_ok[0]) { sv[0] = tmp; sv_ok[0] = true; } break; + case LPP_RELATIVE_HUMIDITY: + r.readRelativeHumidity(tmp); + if (!sv_ok[1]) { sv[1] = tmp; sv_ok[1] = true; } break; + case LPP_BAROMETRIC_PRESSURE: + r.readPressure(tmp); + if (!sv_ok[2]) { sv[2] = tmp; sv_ok[2] = true; } break; + case LPP_VOLTAGE: + r.readVoltage(tmp); + if (!sv_ok[3]) { sv[3] = tmp; sv_ok[3] = true; } break; + case LPP_ALTITUDE: + r.readAltitude(tmp); + if (!sv_ok[4]) { sv[4] = tmp; sv_ok[4] = true; } break; + case LPP_LUMINOSITY: + r.readLuminosity(tmp); + if (!sv_ok[5]) { sv[5] = tmp; sv_ok[5] = true; } break; + case LPP_DISTANCE: + r.readDistance(tmp); + if (!sv_ok[6]) { sv[6] = tmp; sv_ok[6] = true; } break; + case LPP_CONCENTRATION: + r.readConcentration(tmp); + if (!sv_ok[7]) { sv[7] = tmp; sv_ok[7] = true; } break; + default: + r.skipData(type); break; + } + } + } + // board battery takes precedence over INA sensor voltage + if (batt_volts >= 0.0f) { sv[3] = batt_volts; sv_ok[3] = true; } + int oi = 0; const char* p = tmpl; while (*p && oi < out_len - 1) { + // helper macro: append a buffer whose length is already known + #define APPEND(s, slen) do { \ + int _l = (slen); \ + if (oi + _l < out_len - 1) { memcpy(out + oi, (s), _l); oi += _l; } \ + } while(0) + if (strncmp(p, "{loc}", 5) == 0) { char lb[32]; - if (gps_valid) - snprintf(lb, sizeof(lb), "%.5f,%.5f", lat, lon); - else - strcpy(lb, "no GPS"); - int ll = strlen(lb); - if (oi + ll < out_len - 1) { memcpy(out + oi, lb, ll); oi += ll; } - p += 5; + if (gps_valid) snprintf(lb, sizeof(lb), "%.5f,%.5f", lat, lon); + else strcpy(lb, "no GPS"); + APPEND(lb, strlen(lb)); p += 5; } else if (strncmp(p, "{time}", 6) == 0) { if (utc_ts > 1000000000UL) { uint32_t local_ts = utc_ts + (int32_t)tz_hours * 3600; @@ -30,13 +87,46 @@ inline void expandMsg(const char* tmpl, char* out, int out_len, struct tm* ti = gmtime(&t); char tb[8]; snprintf(tb, sizeof(tb), "%02d:%02d", ti->tm_hour, ti->tm_min); - int tl = strlen(tb); - if (oi + tl < out_len - 1) { memcpy(out + oi, tb, tl); oi += tl; } + APPEND(tb, strlen(tb)); } p += 6; + } else if (strncmp(p, "{temp}", 6) == 0) { + if (sv_ok[0]) { char b[10]; snprintf(b,sizeof(b),"%.1fC",sv[0]); APPEND(b,strlen(b)); } + else { APPEND("{temp}", 6); } + p += 6; + } else if (strncmp(p, "{hum}", 5) == 0) { + if (sv_ok[1]) { char b[8]; snprintf(b,sizeof(b),"%.0f%%",sv[1]); APPEND(b,strlen(b)); } + else { APPEND("{hum}", 5); } + p += 5; + } else if (strncmp(p, "{pres}", 6) == 0) { + if (sv_ok[2]) { char b[12]; snprintf(b,sizeof(b),"%.0fhPa",sv[2]); APPEND(b,strlen(b)); } + else { APPEND("{pres}", 6); } + p += 6; + } else if (strncmp(p, "{batt}", 6) == 0) { + if (sv_ok[3]) { char b[8]; snprintf(b,sizeof(b),"%.2fV",sv[3]); APPEND(b,strlen(b)); } + else { APPEND("{batt}", 6); } + p += 6; + } else if (strncmp(p, "{alt}", 5) == 0) { + if (sv_ok[4]) { char b[10]; snprintf(b,sizeof(b),"%.0fm",sv[4]); APPEND(b,strlen(b)); } + else { APPEND("{alt}", 5); } + p += 5; + } else if (strncmp(p, "{lux}", 5) == 0) { + if (sv_ok[5]) { char b[10]; snprintf(b,sizeof(b),"%.0flux",sv[5]); APPEND(b,strlen(b)); } + else { APPEND("{lux}", 5); } + p += 5; + } else if (strncmp(p, "{dist}", 6) == 0) { + if (sv_ok[6]) { char b[12]; snprintf(b,sizeof(b),"%.2fm",sv[6]); APPEND(b,strlen(b)); } + else { APPEND("{dist}", 6); } + p += 6; + } else if (strncmp(p, "{co2}", 5) == 0) { + if (sv_ok[7]) { char b[12]; snprintf(b,sizeof(b),"%.0fppm",sv[7]); APPEND(b,strlen(b)); } + else { APPEND("{co2}", 5); } + p += 5; } else { out[oi++] = *p++; } + + #undef APPEND } out[oi] = '\0'; } diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index b5068237..673af4cf 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -23,7 +23,8 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text) { expandMsg(_prefs.bot_reply_dm, expanded, sizeof(expanded), sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours); + ts, _prefs.tz_offset_hours, + &sensors, (float)board.getBattMilliVolts() / 1000.0f); uint32_t expected_ack, est_timeout; if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED) { _bot_last_reply_ms = millis(); @@ -57,7 +58,8 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text) { expandMsg(_prefs.bot_reply_ch, expanded, sizeof(expanded), sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours); + ts, _prefs.tz_offset_hours, + &sensors, (float)board.getBattMilliVolts() / 1000.0f); int rlen = strlen(expanded); if (sendGroupMessage(ts, ch.channel, _prefs.node_name, expanded, rlen)) { _bot_last_reply_ms = millis(); diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index f1b9747d..9edd2f57 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -175,6 +175,8 @@ public: max = sizeof(_prefs->bot_reply_ch) - 1; } _kb.begin(initial, max); + if (_sel != 2) // trigger field doesn't support sensor placeholders + kbAddSensorPlaceholders(_kb, &sensors); return true; } return false; diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 1fa91c23..ab917551 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -12,9 +12,7 @@ static const char KB_CHARS[4][10] = { }; static const int KB_ROWS_CHAR = 4; static const int KB_COLS_CHAR = 10; -static const int KB_SPECIAL = 5; // [^] [Sp] [Del] [{}] [OK] -static const char* KB_PH_LIST[] = { "{loc}", "{time}" }; -static const int KB_PH_COUNT = 2; +static const int KB_SPECIAL = 5; // [^] [Sp] [De] [{}] [OK] static const int KB_MAX_LEN = 139; static const int KB_CELL_W = 12; // 10 cols × 12px = 120px of 128px display static const int KB_CELL_H = 9; @@ -23,6 +21,10 @@ static const int KB_SEP_Y = 9; static const int KB_CHARS_Y = 11; static const int KB_SPECIAL_Y = 48; +static const int KB_PH_MAX = 12; // max placeholders in list +static const int KB_PH_LEN = 9; // max placeholder string length incl. null +static const int KB_PH_VISIBLE = 3; // items shown at once in overlay + struct KeyboardWidget { char buf[KB_MAX_LEN + 1]; int len; @@ -31,6 +33,9 @@ struct KeyboardWidget { bool caps; bool ph_mode; int ph_sel; + int ph_scroll; + char _ph_buf[KB_PH_MAX][KB_PH_LEN]; + int _ph_count; enum Result { NONE, DONE, CANCELLED }; @@ -41,7 +46,19 @@ struct KeyboardWidget { len = strlen(buf); row = col = 0; caps = ph_mode = false; - ph_sel = 0; + ph_sel = ph_scroll = 0; + // default placeholders — always available + _ph_count = 0; + addPlaceholder("{loc}"); + addPlaceholder("{time}"); + } + + void addPlaceholder(const char* ph) { + if (_ph_count < KB_PH_MAX) { + strncpy(_ph_buf[_ph_count], ph, KB_PH_LEN - 1); + _ph_buf[_ph_count][KB_PH_LEN - 1] = '\0'; + _ph_count++; + } } int render(DisplayDriver& display) { @@ -99,16 +116,28 @@ struct KeyboardWidget { // placeholder picker overlay if (ph_mode) { + int box_h = 12 + KB_PH_VISIBLE * 10; display.setColor(DisplayDriver::DARK); - display.fillRect(20, 20, 88, 12 + KB_PH_COUNT * 10); + display.fillRect(20, 20, 88, box_h); display.setColor(DisplayDriver::LIGHT); - display.drawRect(20, 20, 88, 12 + KB_PH_COUNT * 10); + display.drawRect(20, 20, 88, box_h); display.setCursor(24, 21); display.print("Placeholder:"); display.fillRect(20, 30, 88, 1); - for (int i = 0; i < KB_PH_COUNT; i++) { - int py = 33 + i * 10; - if (i == ph_sel) { + + if (ph_scroll > 0) { + display.setCursor(100, 21); + display.print("^"); + } + if (ph_scroll + KB_PH_VISIBLE < _ph_count) { + display.setCursor(100, 33 + (KB_PH_VISIBLE - 1) * 10); + display.print("v"); + } + + for (int i = 0; i < KB_PH_VISIBLE && (ph_scroll + i) < _ph_count; i++) { + int idx = ph_scroll + i; + int py = 33 + i * 10; + if (idx == ph_sel) { display.setColor(DisplayDriver::LIGHT); display.fillRect(21, py - 1, 86, 10); display.setColor(DisplayDriver::DARK); @@ -116,7 +145,7 @@ struct KeyboardWidget { display.setColor(DisplayDriver::LIGHT); } display.setCursor(24, py); - display.print(KB_PH_LIST[i]); + display.print(_ph_buf[idx]); } display.setColor(DisplayDriver::LIGHT); } @@ -127,10 +156,18 @@ struct KeyboardWidget { Result handleInput(char c) { // placeholder overlay consumes all input if (ph_mode) { - if (c == KEY_UP && ph_sel > 0) { ph_sel--; return NONE; } - if (c == KEY_DOWN && ph_sel < KB_PH_COUNT - 1){ ph_sel++; return NONE; } + if (c == KEY_UP && ph_sel > 0) { + ph_sel--; + if (ph_sel < ph_scroll) ph_scroll = ph_sel; + return NONE; + } + if (c == KEY_DOWN && ph_sel < _ph_count - 1) { + ph_sel++; + if (ph_sel >= ph_scroll + KB_PH_VISIBLE) ph_scroll = ph_sel - KB_PH_VISIBLE + 1; + return NONE; + } if (c == KEY_ENTER) { - const char* ph = KB_PH_LIST[ph_sel]; + const char* ph = _ph_buf[ph_sel]; int ph_len = strlen(ph); if (len + ph_len <= max_len) { memcpy(buf + len, ph, ph_len); @@ -189,8 +226,9 @@ struct KeyboardWidget { if (len > 0) buf[--len] = '\0'; break; case 3: - ph_mode = true; - ph_sel = 0; + ph_mode = true; + ph_sel = 0; + ph_scroll = 0; break; case 4: return DONE; diff --git a/examples/companion_radio/ui-new/SensorPlaceholders.h b/examples/companion_radio/ui-new/SensorPlaceholders.h new file mode 100644 index 00000000..e244b236 --- /dev/null +++ b/examples/companion_radio/ui-new/SensorPlaceholders.h @@ -0,0 +1,31 @@ +#pragma once +// Populates a KeyboardWidget's placeholder list with sensor types currently +// detected by SensorManager. Always adds {loc} and {time} via begin(); this +// helper appends the sensor-specific entries on top. + +#include "KeyboardWidget.h" +#include +#include + +inline void kbAddSensorPlaceholders(KeyboardWidget& kb, SensorManager* sm) { + if (!sm) return; + uint8_t types[16]; + int tc = sm->getAvailableLPPTypes(types, 16); + + static const struct { uint8_t t; const char* ph; } MAP[] = { + { LPP_TEMPERATURE, "{temp}" }, + { LPP_RELATIVE_HUMIDITY, "{hum}" }, + { LPP_BAROMETRIC_PRESSURE, "{pres}" }, + { LPP_VOLTAGE, "{batt}" }, + { LPP_ALTITUDE, "{alt}" }, + { LPP_LUMINOSITY, "{lux}" }, + { LPP_DISTANCE, "{dist}" }, + { LPP_CONCENTRATION, "{co2}" }, + }; + + for (const auto& m : MAP) { + for (int i = 0; i < tc; i++) { + if (types[i] == m.t) { kb.addPlaceholder(m.ph); break; } + } + } +} diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ded3d814..a6324b9b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -99,6 +99,7 @@ static const uint16_t HP_ALL = 0x01FF; #include "KeyboardWidget.h" #include "FullscreenMsgView.h" +#include "SensorPlaceholders.h" class SettingsScreen : public UIScreen { UITask* _task; @@ -575,6 +576,7 @@ public: int slot = msgSlotIndex(_selected); _edit_slot = slot; _kb.begin(p ? p->custom_msgs[slot] : ""); + kbAddSensorPlaceholders(_kb, &sensors); return true; } return false; @@ -667,9 +669,11 @@ class QuickMsgScreen : public UIScreen { } #endif NodePrefs* np = _task->getNodePrefs(); + float batt = (float)board.getBattMilliVolts() / 1000.0f; ::expandMsg(tmpl, out, out_len, lat, lon, gps_valid, rtc_clock.getCurrentTime(), - np ? np->tz_offset_hours : 0); + np ? np->tz_offset_hours : 0, + &sensors, batt); } void setupMsgPick() { @@ -1624,6 +1628,7 @@ public: if (c == KEY_ENTER) { if (_msg_sel == 0) { _kb.begin(); + kbAddSensorPlaceholders(_kb, &sensors); _phase = KEYBOARD; return true; }