mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-core,ui-lvgl): message placeholders and quick messages in the Core, L2 quick messages, advert, Bluetooth toggle, read all, node waypoint/pin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3462,12 +3462,10 @@ bool MyMesh::advert() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SIM_PLATFORM
|
||||
bool MyMesh::advertFlood() {
|
||||
// Mirrors the CMD_SEND_SELF_ADVERT handler's flood=1 branch above
|
||||
// (createSelfAdvert() + sendFloodScoped() with the default transport
|
||||
// scope key) -- real protocol logic, just reached from a test-only entry
|
||||
// point instead of a parsed serial command frame.
|
||||
// scope key), for the on-device UI and the sim's test harness.
|
||||
mesh::Packet* pkt;
|
||||
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
|
||||
pkt = createSelfAdvert(_prefs.node_name);
|
||||
@@ -3483,7 +3481,6 @@ bool MyMesh::advertFlood() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// To check if there is pending work
|
||||
bool MyMesh::hasPendingWork() const {
|
||||
|
||||
@@ -204,18 +204,8 @@ public:
|
||||
|
||||
void loop();
|
||||
void handleCmdFrame(size_t len);
|
||||
bool advert();
|
||||
#ifdef SIM_PLATFORM
|
||||
// Phase 3 sim-only test hook: same as advert() but FLOOD-routed (like the
|
||||
// real phone-app CMD_SEND_SELF_ADVERT command's flood=1 branch) instead of
|
||||
// zero-hop, so a JS test harness can make an instance's identity actually
|
||||
// propagate through an intermediate repeater instance -- advert() alone
|
||||
// (zero-hop) never leaves the immediate ether link. Not reachable from any
|
||||
// real hardware build (no phone app exists in the sim to send the real
|
||||
// CMD_SEND_SELF_ADVERT command over), so this is additive/dead code
|
||||
// everywhere else, not a behavior change.
|
||||
bool advertFlood();
|
||||
#endif
|
||||
bool advert(); // zero-hop: only nodes in direct range hear it
|
||||
bool advertFlood(); // flooded (default scope), as the app's "Advert" with flood
|
||||
void sendNodeDiscoverReq();
|
||||
void enterCLIRescue();
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
// Outgoing message text, shared by ui-new's Messages and ui-lvgl's thread:
|
||||
// the placeholders a message can carry ({loc}, {time}, {batt}, live sensor
|
||||
// readings), expanding them with this device's position / clock / battery /
|
||||
// sensors when it's sent, and the ten quick messages (NodePrefs::custom_msgs),
|
||||
// sent with one tap and expanded the same way.
|
||||
//
|
||||
// Include after target.h (sensors, board, rtc_clock).
|
||||
|
||||
#include "../MsgExpand.h"
|
||||
|
||||
namespace msgtext {
|
||||
|
||||
// ── Quick messages ────────────────────────────────────────────────────────────
|
||||
static const int QUICK_COUNT = 10;
|
||||
static const int QUICK_LEN = (int)sizeof(((NodePrefs*)0)->custom_msgs[0]); // incl. the terminator
|
||||
|
||||
static const char* quick(const NodePrefs* p, int i) {
|
||||
return p && i >= 0 && i < QUICK_COUNT ? p->custom_msgs[i] : "";
|
||||
}
|
||||
// "" clears the slot. The caller saves the prefs.
|
||||
static void setQuick(NodePrefs* p, int i, const char* text) {
|
||||
if (p && i >= 0 && i < QUICK_COUNT) snprintf(p->custom_msgs[i], QUICK_LEN, "%s", text ? text : "");
|
||||
}
|
||||
static int quickUsed(const NodePrefs* p) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < QUICK_COUNT; i++) if (quick(p, i)[0]) n++;
|
||||
return n;
|
||||
}
|
||||
// A fresh device starts with "OK" in the first slot.
|
||||
static void seedQuick(NodePrefs* p) {
|
||||
if (p && !p->custom_msgs[0][0]) setQuick(p, 0, "OK");
|
||||
}
|
||||
|
||||
// ── Placeholders ──────────────────────────────────────────────────────────────
|
||||
|
||||
// The sensor readings this device has right now, as placeholders ({temp},
|
||||
// {hum}, ...; {batt} only from a voltage sensor).
|
||||
static void sensorPlaceholders(SensorManager* sm, void (*add)(const char* ph, void* ctx), void* ctx) {
|
||||
if (!sm) return;
|
||||
CayenneLPP lpp(100);
|
||||
sm->querySensors(0xFF, lpp);
|
||||
uint8_t present[16];
|
||||
int pc = 0;
|
||||
LPPReader r(lpp.getBuffer(), lpp.getSize());
|
||||
uint8_t ch, type;
|
||||
while (r.readHeader(ch, type)) {
|
||||
bool seen = false;
|
||||
for (int i = 0; i < pc; i++) if (present[i] == type) { seen = true; break; }
|
||||
if (!seen && pc < 16) present[pc++] = type;
|
||||
r.skipData(type);
|
||||
}
|
||||
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 < pc; i++)
|
||||
if (present[i] == m.t) { add(m.ph, ctx); break; }
|
||||
}
|
||||
|
||||
// Everything a message can carry here: {loc}, {time}, {batt} (the board's
|
||||
// voltage), then the sensors'.
|
||||
static void placeholders(SensorManager* sm, void (*add)(const char* ph, void* ctx), void* ctx) {
|
||||
add("{loc}", ctx);
|
||||
add("{time}", ctx);
|
||||
add("{batt}", ctx);
|
||||
struct Skip { void (*add)(const char*, void*); void* ctx; } skip = { add, ctx };
|
||||
sensorPlaceholders(sm, [](const char* ph, void* c) {
|
||||
Skip* s = (Skip*)c;
|
||||
if (strcmp(ph, "{batt}") != 0) s->add(ph, s->ctx); // already there
|
||||
}, &skip);
|
||||
}
|
||||
|
||||
// ── Expanding ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Placeholders in `tmpl` replaced with this device's values.
|
||||
static void expand(const char* tmpl, char* out, int n, const NodePrefs* p) {
|
||||
double lat = 0, lon = 0;
|
||||
bool gps_valid = false;
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
LocationProvider* loc = sensors.getLocationProvider();
|
||||
if (loc && loc->isValid()) {
|
||||
lat = loc->getLatitude() / 1000000.0;
|
||||
lon = loc->getLongitude() / 1000000.0;
|
||||
gps_valid = true;
|
||||
}
|
||||
#endif
|
||||
float batt = (float)board.getBattMilliVolts() / 1000.0f;
|
||||
::expandMsg(tmpl, out, n, lat, lon, gps_valid, rtc_clock.getCurrentTime(),
|
||||
p ? p->tz_offset_hours : 0, &sensors, batt);
|
||||
}
|
||||
|
||||
// A typed message: a reply's leading "@[nick] " stays as it is (a nick may
|
||||
// hold braces), the rest is expanded.
|
||||
static void expandOutgoing(const char* text, char* out, int n, const NodePrefs* p) {
|
||||
int pre = 0;
|
||||
if (text[0] == '@' && text[1] == '[') {
|
||||
const char* e = strstr(text, "] ");
|
||||
if (e) pre = (int)(e + 2 - text);
|
||||
}
|
||||
if (pre >= n) pre = n - 1;
|
||||
memcpy(out, text, pre);
|
||||
expand(text + pre, out + pre, n - pre, p);
|
||||
}
|
||||
|
||||
} // namespace msgtext
|
||||
@@ -87,6 +87,8 @@ public:
|
||||
bool anyDMUnreadOverflow() const { return dm_unread.anyOverflow(); }
|
||||
void clearDMUnread(const uint8_t* pub_key) { dm_unread.clear(pub_key); }
|
||||
void clearAllDMUnread() { dm_unread.clearAll(); }
|
||||
// Every conversation read: direct, channels, rooms.
|
||||
void markAllRead() { _room_unread = 0; dm_unread.clearAll(); history.clearAllChannelUnread(); }
|
||||
void reconcileDMUnread() { dm_unread.reconcile(history); }
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
@@ -153,11 +155,7 @@ public:
|
||||
|
||||
void onQueueSizeChanged(int msgcount) override {
|
||||
_queue_len = msgcount;
|
||||
if (msgcount == 0) { // the app drained the queue: nothing is unread any more
|
||||
_room_unread = 0;
|
||||
dm_unread.clearAll();
|
||||
history.clearAllChannelUnread();
|
||||
}
|
||||
if (msgcount == 0) markAllRead(); // the app drained the queue: nothing is unread any more
|
||||
}
|
||||
|
||||
void onACKRecv(uint32_t ack_crc) override { history.markDmDelivered(ack_crc); }
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// Quick messages and placeholders (the compose bar's "+", Settings > Messages
|
||||
// & contacts > Quick messages), Settings > Send advert and > Bluetooth, and
|
||||
// Messages' "Read all". The text side is ui-core/MessageText.h.
|
||||
// Included by UITask.cpp (single translation unit).
|
||||
|
||||
namespace qview {
|
||||
|
||||
static const int PH_MAX = 12;
|
||||
static const char* s_ph[PH_MAX]; // placeholders on offer (static strings)
|
||||
static int s_ph_n = 0;
|
||||
static int s_edit = -1; // quick message being edited
|
||||
|
||||
static void collect() {
|
||||
s_ph_n = 0;
|
||||
msgtext::placeholders(&sensors, [](const char* ph, void*) { if (s_ph_n < PH_MAX) s_ph[s_ph_n++] = ph; }, nullptr);
|
||||
}
|
||||
|
||||
// The placeholders as small buttons; `cb` gets the index as user data.
|
||||
static lv_obj_t* chips(lv_obj_t* parent, lv_event_cb_t cb, bool wrap) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::BG);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, wrap ? LV_FLEX_FLOW_ROW_WRAP : LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(row, 4, 0);
|
||||
lv_obj_set_style_pad_row(row, 4, 0);
|
||||
if (wrap) lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
else { lv_obj_set_scroll_dir(row, LV_DIR_HOR); lv_obj_set_scrollbar_mode(row, LV_SCROLLBAR_MODE_OFF); }
|
||||
for (int i = 0; i < s_ph_n; i++) {
|
||||
lv_obj_t* b = lv_button_create(row);
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
|
||||
lv_obj_set_style_pad_hor(b, 8, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 8, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
lv_obj_center(label(b, s_ph[i], THEME_FONT_SMALL, theme::TEXT));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
static void onQuickSend(lv_event_t* e) { s_ui->quickSend((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onQuickInsert(lv_event_t* e) { s_ui->quickInsert((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onQuickSlot(lv_event_t* e) { s_ui->quickEdit((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onQuickEditPh(lv_event_t* e) { s_ui->quickEditInsert((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onQuickEditKb(lv_event_t* e) { s_ui->quickEditDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
||||
static void onQuickList(lv_event_t* e) { (void)e; s_ui->showQuickMsgs(); }
|
||||
static void onAdvertGo(lv_event_t* e) { s_ui->advertSend((uintptr_t)lv_event_get_user_data(e) != 0); }
|
||||
static void onBtSwitch(lv_event_t* e) {
|
||||
s_ui->setBluetooth(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
|
||||
}
|
||||
|
||||
} // namespace qview
|
||||
|
||||
static void onAdvertRow(lv_event_t* e) { (void)e; s_ui->advertPopup(); }
|
||||
static void onOpenQuickMsgs(lv_event_t* e) { (void)e; s_ui->showQuickMsgs(); }
|
||||
|
||||
// ── The compose bar's "+" ─────────────────────────────────────────────────────
|
||||
|
||||
void UITask::quickPopup() {
|
||||
using namespace qview;
|
||||
if (!_compose_ta) return;
|
||||
collect();
|
||||
lv_obj_t* panel = navPopupPanel("Quick", true);
|
||||
lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
label(panel, "Insert, filled in when sent:", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
chips(panel, onQuickInsert, true);
|
||||
label(panel, "Send now:", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
int shown = 0;
|
||||
for (int i = 0; i < msgtext::QUICK_COUNT; i++) {
|
||||
const char* t = msgtext::quick(_prefs, i);
|
||||
if (!t[0]) continue;
|
||||
char sent[MSG_TEXT_BUF];
|
||||
msgtext::expand(t, sent, sizeof(sent), _prefs);
|
||||
listRow(panel, t, strcmp(sent, t) ? sent : NULL, onQuickSend, (void*)(uintptr_t)i); // what goes out
|
||||
shown++;
|
||||
}
|
||||
if (!shown) label(panel, "No quick messages yet.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
listRow(panel, LV_SYMBOL_EDIT " Edit quick messages", NULL, onQuickList, NULL);
|
||||
}
|
||||
|
||||
void UITask::quickSend(int slot) {
|
||||
const char* t = msgtext::quick(_prefs, slot);
|
||||
if (!t[0]) return;
|
||||
char text[MSG_TEXT_BUF];
|
||||
msgtext::expand(t, text, sizeof(text), _prefs);
|
||||
navClosePopup();
|
||||
if (sendThreadText(text)) refreshThread();
|
||||
}
|
||||
|
||||
void UITask::quickInsert(int ph) {
|
||||
using namespace qview;
|
||||
if (ph < 0 || ph >= s_ph_n || !_compose_ta) return;
|
||||
navClosePopup();
|
||||
lv_textarea_add_text(_compose_ta, s_ph[ph]); // the byte cap still applies (onComposeInsert)
|
||||
setKeyboardVisible(true);
|
||||
}
|
||||
|
||||
// ── Settings > Messages & contacts > Quick messages ───────────────────────────
|
||||
|
||||
void UITask::showQuickMsgs() {
|
||||
navClosePopup();
|
||||
_screen = SCR_QUICK;
|
||||
buildQuickMsgs();
|
||||
}
|
||||
|
||||
void UITask::buildQuickMsgs() {
|
||||
using namespace qview;
|
||||
lv_obj_t* body = newScreen("Quick messages", true);
|
||||
lv_obj_t* t = label(body, "Sent with one tap from a chat's \"+\". {loc}, {time}, {batt} are filled in when sent.",
|
||||
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
for (int i = 0; i < msgtext::QUICK_COUNT; i++) {
|
||||
const char* q = msgtext::quick(_prefs, i);
|
||||
char title[msgtext::QUICK_LEN + 8];
|
||||
snprintf(title, sizeof(title), "%d %s", i + 1, q[0] ? q : "(empty)");
|
||||
lv_obj_t* row = listRow(body, title, NULL, onQuickSlot, (void*)(uintptr_t)i);
|
||||
if (!q[0]) lv_obj_set_style_text_color(lv_obj_get_child(row, 0), lv_color_hex(theme::TEXT_MUTED), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A field and the placeholders over the keyboard (a titled popup doesn't fit
|
||||
// above 124 px of keys); the field's placeholder says which slot it is.
|
||||
void UITask::quickEdit(int slot) {
|
||||
using namespace qview;
|
||||
if (!_prefs || slot < 0 || slot >= msgtext::QUICK_COUNT) return;
|
||||
collect();
|
||||
s_edit = slot;
|
||||
navClosePopup();
|
||||
_nav_overlay = lv_obj_create(lv_screen_active());
|
||||
lv_obj_remove_style_all(_nav_overlay);
|
||||
lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(_nav_overlay, LV_OPA_80, 0);
|
||||
lv_obj_add_flag(_nav_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
int w = lv_display_get_horizontal_resolution(NULL) - 16;
|
||||
|
||||
char ph[40];
|
||||
snprintf(ph, sizeof(ph), "Quick message %d (empty clears it)", slot + 1);
|
||||
_nav_ta = wifiField(_nav_overlay, ph, false);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
lv_obj_set_width(_nav_ta, w);
|
||||
lv_obj_set_pos(_nav_ta, 8, theme::STATUS_H + 4);
|
||||
lv_textarea_set_text(_nav_ta, msgtext::quick(_prefs, slot));
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)(msgtext::QUICK_LEN - 1));
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
|
||||
lv_obj_t* row = chips(_nav_overlay, onQuickEditPh, false);
|
||||
lv_obj_set_width(row, w);
|
||||
lv_obj_set_pos(row, 8, theme::STATUS_H + 4 + 34 + 6);
|
||||
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onQuickEditKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onQuickEditKb, LV_EVENT_CANCEL, NULL);
|
||||
}
|
||||
|
||||
void UITask::quickEditInsert(int ph) {
|
||||
using namespace qview;
|
||||
if (ph >= 0 && ph < s_ph_n && _nav_ta) lv_textarea_add_text(_nav_ta, s_ph[ph]);
|
||||
}
|
||||
|
||||
void UITask::quickEditDone(bool ok) {
|
||||
using namespace qview;
|
||||
if (ok && _nav_ta && _prefs && s_edit >= 0) {
|
||||
msgtext::setQuick(_prefs, s_edit, lv_textarea_get_text(_nav_ta));
|
||||
the_mesh.savePrefs();
|
||||
showToast(msgtext::quick(_prefs, s_edit)[0] ? "Quick message saved" : "Quick message cleared", 1200);
|
||||
}
|
||||
navClosePopup();
|
||||
if (ok && _screen == SCR_QUICK) {
|
||||
lv_obj_t* body = _body;
|
||||
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
|
||||
buildQuickMsgs();
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings > Send advert ────────────────────────────────────────────────────
|
||||
|
||||
void UITask::advertPopup() {
|
||||
using namespace qview;
|
||||
lv_obj_t* panel = navPopupPanel("Send advert", false);
|
||||
lv_obj_t* t = label(panel, "Tells other nodes who you are (and where, if you share your position). "
|
||||
"Auto-advert can do it on a timer.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
listRow(panel, "Nearby only", "Nodes in direct range, not repeated", onAdvertGo, (void*)(uintptr_t)0);
|
||||
listRow(panel, "Everyone", "Through repeaters, across the mesh", onAdvertGo, (void*)(uintptr_t)1);
|
||||
}
|
||||
|
||||
void UITask::advertSend(bool flood) {
|
||||
navClosePopup();
|
||||
bool ok = flood ? the_mesh.advertFlood() : the_mesh.advert();
|
||||
if (ok) notify(UIEventType::ack);
|
||||
showToast(!ok ? "Advert failed" : flood ? "Advert sent across the mesh" : "Advert sent to nodes in range");
|
||||
}
|
||||
|
||||
// ── Settings > Bluetooth ──────────────────────────────────────────────────────
|
||||
// BLE only: the USB link to a computer stays on. Not kept over a reboot
|
||||
// (Bluetooth starts on, as on L1).
|
||||
|
||||
static void bluetoothRow(lv_obj_t* body) {
|
||||
char sub[48];
|
||||
bool on = s_ui->isSerialEnabled();
|
||||
if (!on) snprintf(sub, sizeof(sub), "Off - the phone app can't connect");
|
||||
else if (s_ui->hasConnection()) snprintf(sub, sizeof(sub), "On - the app is connected");
|
||||
else if (the_mesh.getBLEPin()) snprintf(sub, sizeof(sub), "On - pairing PIN %06lu", (unsigned long)the_mesh.getBLEPin());
|
||||
else snprintf(sub, sizeof(sub), "On - waiting for the app");
|
||||
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr);
|
||||
if (on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, qview::onBtSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
}
|
||||
|
||||
void UITask::setBluetooth(bool on) {
|
||||
if (on) enableSerial(); else disableSerial();
|
||||
showToast(on ? "Bluetooth on" : "Bluetooth off - USB still works", 1500);
|
||||
if (_screen == SCR_SETTINGS) { // the row's subtitle
|
||||
lv_obj_t* body = _body;
|
||||
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
|
||||
buildSettings();
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
|
||||
}
|
||||
refreshStatusBar();
|
||||
}
|
||||
|
||||
// ── Messages > Read all ───────────────────────────────────────────────────────
|
||||
|
||||
void UITask::markAllRead() {
|
||||
_core->markAllRead();
|
||||
showToast("All marked read", 1200);
|
||||
if (_screen == SCR_CHATS) buildChats();
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../ui-core/BotConfig.h"
|
||||
#include "../ui-core/SoundControl.h"
|
||||
#include "../ui-core/SoundNotifier.h"
|
||||
#include "../ui-core/MessageText.h"
|
||||
#include "Theme.h"
|
||||
#include "LvglPort.h"
|
||||
#include "../ui-core/KeyboardData.h"
|
||||
@@ -134,6 +135,7 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
||||
|
||||
_core = new UiCore();
|
||||
_core->begin(node_prefs, sensors, this);
|
||||
msgtext::seedQuick(node_prefs); // "OK" in quick message 1 on first boot
|
||||
_nearby = new NearbyModel();
|
||||
_nearby->bindModel(_core, node_prefs);
|
||||
_scan = new NearbyModel();
|
||||
@@ -562,6 +564,11 @@ void UITask::back() {
|
||||
case SCR_ADMIN: if (_nav_overlay) navClosePopup(); else adminLeave(); break;
|
||||
case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break;
|
||||
case SCR_SETTINGS_NAV: showSettings(); break;
|
||||
case SCR_QUICK: // back to Messages & contacts' bottom, where the row is
|
||||
if (_nav_overlay) { navClosePopup(); break; }
|
||||
showSchemaSettings(settings::PG_MESSAGES);
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); }
|
||||
break;
|
||||
case SCR_MELODY: // back to the Sound page's bottom, where the melodies are
|
||||
melodySave();
|
||||
stopMelody();
|
||||
@@ -863,8 +870,13 @@ void UITask::showChats() {
|
||||
buildChats();
|
||||
}
|
||||
|
||||
static void onMarkAllRead(lv_event_t* e) { (void)e; s_ui->markAllRead(); }
|
||||
static lv_obj_t* headerButton(lv_obj_t* hdr, const char* text, lv_event_cb_t cb, int right, lv_obj_t** label_out);
|
||||
|
||||
void UITask::buildChats() {
|
||||
lv_obj_t* body = newScreen("Messages", true);
|
||||
if (_header && _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread() > 0)
|
||||
headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 4, NULL);
|
||||
|
||||
// Channels: favourites first (unless turned off), hold a row for its options
|
||||
bool ch_fav_only = _prefs && _prefs->ch_fav_only;
|
||||
@@ -1003,7 +1015,7 @@ void UITask::buildContacts() {
|
||||
// model, since it is a different set: who is in range, not who is known.
|
||||
|
||||
static NearbyModel::Entry s_node; // the node open in SCR_NODE (a copy: the list re-sorts)
|
||||
enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV, NODE_ADMIN };
|
||||
enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV, NODE_ADMIN, NODE_WAYPOINT, NODE_PIN };
|
||||
|
||||
static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onNearbySort(lv_event_t* e) { (void)e; s_ui->toggleNearbySort(); }
|
||||
@@ -1310,14 +1322,25 @@ void UITask::buildNode() {
|
||||
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
|
||||
bool contact = e.contact_idx >= 0;
|
||||
if (contact && e.type == ADV_TYPE_CHAT) actionButton(acts, LV_SYMBOL_ENVELOPE " Message", NODE_MSG, true);
|
||||
if (e.has_key) actionButton(acts, LV_SYMBOL_LOOP " Ping", NODE_PING, false);
|
||||
if (e.lat_e6 != 0 || e.lon_e6 != 0) actionButton(acts, UI_SYMBOL_COMPASS, NODE_NAV, false);
|
||||
bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM);
|
||||
if (contact) actionButton(acts, admin ? UI_SYMBOL_STAR : e.fav ? UI_SYMBOL_STAR " Unfav" : UI_SYMBOL_STAR " Fav", NODE_FAV, e.fav && admin);
|
||||
if (admin) actionButton(acts, LV_SYMBOL_SETTINGS " Admin", NODE_ADMIN, false);
|
||||
if (!contact && e.has_key && !e.is_known) actionButton(acts, LV_SYMBOL_PLUS " Add", NODE_ADD, true);
|
||||
if (contact) _node_delete_lbl = actionButton(acts, LV_SYMBOL_TRASH, NODE_DELETE, false);
|
||||
bool pos = e.lat_e6 != 0 || e.lon_e6 != 0;
|
||||
struct Act { const char* icon; const char* text; uint8_t action; bool accent; } list[9];
|
||||
int n = 0;
|
||||
if (contact && e.type == ADV_TYPE_CHAT) list[n++] = { LV_SYMBOL_ENVELOPE, " Message", NODE_MSG, true };
|
||||
if (e.has_key) list[n++] = { LV_SYMBOL_LOOP, " Ping", NODE_PING, false };
|
||||
if (pos) list[n++] = { UI_SYMBOL_COMPASS, "", NODE_NAV, false };
|
||||
if (pos) list[n++] = { UI_SYMBOL_FLAG, "", NODE_WAYPOINT, false }; // save where it was seen
|
||||
if (contact) list[n++] = { UI_SYMBOL_STAR, admin ? "" : e.fav ? " Unfav" : " Fav", NODE_FAV, e.fav && admin };
|
||||
if (admin) list[n++] = { LV_SYMBOL_SETTINGS, " Admin", NODE_ADMIN, false };
|
||||
if (!contact && e.has_key && !e.is_known) list[n++] = { LV_SYMBOL_PLUS, " Add", NODE_ADD, true };
|
||||
if (contact && e.has_key) list[n++] = { UI_SYMBOL_PIN, "", NODE_PIN, favslots::findContact(_prefs, e.pub_key) >= 0 };
|
||||
if (contact) list[n++] = { LV_SYMBOL_TRASH, "", NODE_DELETE, false };
|
||||
for (int i = 0; i < n; i++) { // five or more: icons only, so every button fits one row
|
||||
char t[24];
|
||||
snprintf(t, sizeof(t), "%s%s", list[i].icon, n >= 5 ? "" : list[i].text);
|
||||
lv_obj_t* l = actionButton(acts, t, list[i].action, list[i].accent);
|
||||
if (list[i].action == NODE_DELETE) _node_delete_lbl = l;
|
||||
}
|
||||
|
||||
refreshNode();
|
||||
}
|
||||
@@ -1436,11 +1459,25 @@ void UITask::nodeAction(uint8_t action) {
|
||||
case NODE_ADMIN:
|
||||
openAdmin(e.pub_key);
|
||||
break;
|
||||
case NODE_WAYPOINT: {
|
||||
if (_core->waypoints.full()) { showToast("Waypoints full (16)"); break; }
|
||||
char t[48];
|
||||
if (_core->waypoints.add(e.lat_e6, e.lon_e6, rtc_clock.getCurrentTime(), e.name[0] ? e.name : "Node")) {
|
||||
snprintf(t, sizeof(t), "Saved %s", _core->waypoints.at(_core->waypoints.count() - 1).label);
|
||||
the_mesh.savePrefs();
|
||||
showToast(t);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NODE_PIN: // to the favourites dial (Home), as from a chat's options
|
||||
pinPopup(false, 0, e.pub_key);
|
||||
break;
|
||||
case NODE_DELETE:
|
||||
if (!_delete_armed_ms) { // destructive: second tap within 3 s confirms
|
||||
_delete_armed_ms = millis();
|
||||
if (_delete_armed_ms == 0) _delete_armed_ms = 1;
|
||||
lv_label_set_text(_node_delete_lbl, "Delete?");
|
||||
lv_label_set_text(_node_delete_lbl, LV_SYMBOL_TRASH "?"); // fits an icon-only button
|
||||
showToast("Tap again to delete the contact", 2500);
|
||||
break;
|
||||
}
|
||||
if (the_mesh.deleteContactByKey(e.pub_key)) {
|
||||
@@ -1482,6 +1519,7 @@ static void onComposeInsert(lv_event_t* e) {
|
||||
}
|
||||
|
||||
static void onComposeClicked(lv_event_t* e) { (void)e; s_ui->setKeyboardVisible(true); }
|
||||
static void onComposeMore(lv_event_t* e) { (void)e; s_ui->quickPopup(); }
|
||||
|
||||
// Keyboard up: the screen header goes away and the body takes its 32 px, so
|
||||
// a line or two of the conversation stays visible above the compose field
|
||||
@@ -1566,12 +1604,21 @@ void UITask::buildThread() {
|
||||
lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(bar, LV_PCT(100), 42);
|
||||
lv_obj_set_style_pad_all(bar, 4, 0);
|
||||
lv_obj_t* more = lv_button_create(bar); // quick messages, placeholders
|
||||
lv_obj_set_size(more, 34, 34);
|
||||
lv_obj_align(more, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_style_pad_all(more, 0, 0);
|
||||
lv_obj_set_style_radius(more, theme::RADIUS, 0);
|
||||
lv_obj_set_style_shadow_width(more, 0, 0);
|
||||
lv_obj_set_style_bg_color(more, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(more, onComposeMore, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(more, LV_SYMBOL_PLUS, THEME_FONT_BODY, theme::TEXT));
|
||||
_compose_ta = lv_textarea_create(bar);
|
||||
lv_textarea_set_one_line(_compose_ta, true);
|
||||
lv_textarea_set_placeholder_text(_compose_ta, "Message");
|
||||
lv_obj_add_event_cb(_compose_ta, onComposeInsert, LV_EVENT_INSERT, NULL);
|
||||
lv_obj_set_size(_compose_ta, LV_PCT(100), 34);
|
||||
lv_obj_align(_compose_ta, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_size(_compose_ta, lv_display_get_horizontal_resolution(NULL) - 8 - 34 - 4, 34); // beside "+"
|
||||
lv_obj_align(_compose_ta, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
// The theme's padding leaves less than one line inside 36 px, which makes
|
||||
// the field scroll vertically (text jumps as it's typed). Pad so exactly one
|
||||
// body line (20 px) fits: 34 = 2*1 border + 2*6 pad + 20.
|
||||
@@ -1834,10 +1881,8 @@ void UITask::refreshThread() {
|
||||
lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
void UITask::sendFromCompose() {
|
||||
if (!_compose_ta) return;
|
||||
const char* text = lv_textarea_get_text(_compose_ta);
|
||||
if (!text || !text[0]) return;
|
||||
// To the open conversation (channel or DM). The caller refreshes the thread.
|
||||
bool UITask::sendThreadText(const char* text) {
|
||||
bool ok;
|
||||
if (_thread_is_channel) {
|
||||
ok = _core->sendChannelText(_thread_channel, text);
|
||||
@@ -1845,13 +1890,20 @@ void UITask::sendFromCompose() {
|
||||
ContactInfo c;
|
||||
ok = MessageHistory::contactByPrefix(_thread_key, c) && _core->sendDirectText(c, text);
|
||||
}
|
||||
if (ok) {
|
||||
lv_textarea_set_text(_compose_ta, "");
|
||||
setKeyboardVisible(false);
|
||||
refreshThread();
|
||||
} else {
|
||||
showToast("Send failed");
|
||||
}
|
||||
if (!ok) showToast("Send failed");
|
||||
return ok;
|
||||
}
|
||||
|
||||
void UITask::sendFromCompose() {
|
||||
if (!_compose_ta) return;
|
||||
const char* typed = lv_textarea_get_text(_compose_ta);
|
||||
if (!typed || !typed[0]) return;
|
||||
char text[MSG_TEXT_BUF];
|
||||
msgtext::expandOutgoing(typed, text, sizeof(text), _prefs); // {loc}, {time}, ... (MessageText.h)
|
||||
if (!sendThreadText(text)) return;
|
||||
lv_textarea_set_text(_compose_ta, "");
|
||||
setKeyboardVisible(false);
|
||||
refreshThread();
|
||||
}
|
||||
|
||||
// ── Settings (first rows; the declarative schema replaces this later) ────────
|
||||
@@ -1930,6 +1982,9 @@ static void onSchemaDropdown(lv_event_t* e) {
|
||||
}
|
||||
static void onOpenSchemaPage(lv_event_t* e) { s_ui->showSchemaSettings((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onPruneContacts(lv_event_t* e) { (void)e; s_ui->pruneContacts(); }
|
||||
static void onAdvertRow(lv_event_t* e); // QuickScreen.h
|
||||
static void onOpenQuickMsgs(lv_event_t* e);
|
||||
static void bluetoothRow(lv_obj_t* body);
|
||||
static void onVolumeSlider(lv_event_t* e) {
|
||||
s_ui->setSoundVolume((int)lv_slider_get_value((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
@@ -2069,6 +2124,12 @@ void UITask::buildSchemaSettings() {
|
||||
lv_obj_center(_prune_lbl);
|
||||
}
|
||||
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies
|
||||
if (_settings_page == settings::PG_MESSAGES) {
|
||||
sectionTitle(body, "QUICK MESSAGES");
|
||||
char sub[48];
|
||||
snprintf(sub, sizeof(sub), "%d of %d set - sent with one tap", msgtext::quickUsed(_prefs), msgtext::QUICK_COUNT);
|
||||
listRow(body, LV_SYMBOL_EDIT " Quick messages", sub, onOpenQuickMsgs, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::setSchemaValue(int idx, int v) {
|
||||
@@ -2099,7 +2160,9 @@ void UITask::buildSettings() {
|
||||
lv_obj_t* body = newScreen("Settings", true);
|
||||
sectionTitle(body, "NODE");
|
||||
listRow(body, LV_SYMBOL_EDIT " Name", the_mesh.getNodeName(), onNodeName, NULL);
|
||||
listRow(body, UI_SYMBOL_RADIO " Send advert", "Let other nodes see you now", onAdvertRow, NULL);
|
||||
sectionTitle(body, "CONNECTIVITY");
|
||||
bluetoothRow(body);
|
||||
char ssid[33], pass[65];
|
||||
bool have = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass));
|
||||
listRow(body, LV_SYMBOL_WIFI " WiFi", have ? ssid : "Not set (for map downloads)", onOpenWifi, NULL);
|
||||
@@ -2174,3 +2237,4 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
||||
#include "RadioExtras.h"
|
||||
#include "RepeaterScreen.h"
|
||||
#include "SoundScreen.h"
|
||||
#include "QuickScreen.h"
|
||||
|
||||
@@ -128,6 +128,18 @@ public:
|
||||
uint16_t buzzerFreqHz() const;
|
||||
uint8_t buzzerVolume() const { return _buzzer.getVolume(); }
|
||||
#endif
|
||||
// Quick messages, placeholders, advert, Bluetooth (QuickScreen.h)
|
||||
void quickPopup(); // the compose bar's "+"
|
||||
void quickSend(int slot);
|
||||
void quickInsert(int ph);
|
||||
void showQuickMsgs();
|
||||
void quickEdit(int slot);
|
||||
void quickEditDone(bool ok);
|
||||
void quickEditInsert(int ph);
|
||||
void advertPopup();
|
||||
void advertSend(bool flood);
|
||||
void setBluetooth(bool on);
|
||||
void markAllRead();
|
||||
// Settings > Radio (RadioScreen.h)
|
||||
void showRadio();
|
||||
void radioSet(int which, int v);
|
||||
@@ -209,7 +221,7 @@ public:
|
||||
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)ci; (void)from_picker; }
|
||||
|
||||
private:
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_FAVS, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY };
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_FAVS, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK };
|
||||
|
||||
void buildStatusBar();
|
||||
void refreshStatusBar();
|
||||
@@ -269,6 +281,8 @@ private:
|
||||
void rebuildRepeater();
|
||||
void buildSoundRows(lv_obj_t* body, bool top);
|
||||
void buildMelodies();
|
||||
void buildQuickMsgs();
|
||||
bool sendThreadText(const char* text);
|
||||
void refreshMelody();
|
||||
void melodySave();
|
||||
void radioCloseFreq();
|
||||
|
||||
@@ -150,23 +150,8 @@ class MessagesScreen : public UIScreen {
|
||||
|
||||
int _hist_visible = 2; // updated in render(); for history list scroll clamping
|
||||
|
||||
void expandMsg(const char* tmpl, char* out, int out_len) const {
|
||||
double lat = 0, lon = 0;
|
||||
bool gps_valid = false;
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
LocationProvider* loc = sensors.getLocationProvider();
|
||||
if (loc && loc->isValid()) {
|
||||
lat = loc->getLatitude() / 1000000.0;
|
||||
lon = loc->getLongitude() / 1000000.0;
|
||||
gps_valid = true;
|
||||
}
|
||||
#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,
|
||||
&sensors, batt);
|
||||
void expandMsg(const char* tmpl, char* out, int out_len) const { // ui-core/MessageText.h
|
||||
msgtext::expand(tmpl, out, out_len, _task->getNodePrefs());
|
||||
}
|
||||
|
||||
// Scrollbar metrics for a history list. The track is pinned to the full list
|
||||
|
||||
@@ -7,40 +7,6 @@
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
|
||||
inline void kbAddSensorPlaceholders(KeyboardWidget& kb, SensorManager* sm) {
|
||||
if (!sm) return;
|
||||
|
||||
CayenneLPP lpp(100);
|
||||
sm->querySensors(0xFF, lpp);
|
||||
|
||||
// collect unique LPP types that have actual data
|
||||
uint8_t present[16];
|
||||
int pc = 0;
|
||||
{
|
||||
LPPReader r(lpp.getBuffer(), lpp.getSize());
|
||||
uint8_t ch, type;
|
||||
while (r.readHeader(ch, type)) {
|
||||
bool already = false;
|
||||
for (int i = 0; i < pc; i++) if (present[i] == type) { already = true; break; }
|
||||
if (!already && pc < 16) present[pc++] = type;
|
||||
r.skipData(type);
|
||||
}
|
||||
}
|
||||
|
||||
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 < pc; i++) {
|
||||
if (present[i] == m.t) { kb.addPlaceholder(m.ph); break; }
|
||||
}
|
||||
}
|
||||
inline void kbAddSensorPlaceholders(KeyboardWidget& kb, SensorManager* sm) { // list: ui-core/MessageText.h
|
||||
msgtext::sensorPlaceholders(sm, [](const char* ph, void* k) { ((KeyboardWidget*)k)->addPlaceholder(ph); }, &kb);
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ static const int QUICK_MSGS_MAX = 10;
|
||||
// second .cpp is a duplicate-symbol link error. Keep them UITask-internal;
|
||||
// anything genuinely shareable belongs in a real header (icons.h, GeoUtils.h).
|
||||
#include "FullscreenMsgView.h"
|
||||
#include "../ui-core/MessageText.h"
|
||||
#include "SensorPlaceholders.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "../ui-core/UiCore.h" // shared UI Core: history rings + unread models (MessagesScreen views them)
|
||||
@@ -1599,10 +1600,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
vibration.begin();
|
||||
#endif
|
||||
|
||||
// Set default quick message if slot 0 is empty (first boot)
|
||||
if (_node_prefs && _node_prefs->custom_msgs[0][0] == '\0') {
|
||||
strncpy(_node_prefs->custom_msgs[0], "OK", sizeof(_node_prefs->custom_msgs[0]) - 1);
|
||||
}
|
||||
msgtext::seedQuick(_node_prefs); // "OK" in quick message 1 on first boot
|
||||
|
||||
ui_started_at = millis();
|
||||
_alert_expiry = 0;
|
||||
|
||||
Reference in New Issue
Block a user