console mode Phase 3: mem, contacts, chans, to, msg

* 'mem' prints free DRAM and PSRAM plus the largest PSRAM block. This is the
  number the whole feature is justified by, so it is a command rather than
  something you have to instrument a build to see: read it in the console,
  reboot into the UI, read it there. Phase 0, self-service.

* contacts / chans / to <name> / msg <text>.

The send path is REUSED, not reimplemented. luaHostMeshSendChannel matches the
channel by name at transmit time, and a cached slot index is how messages once
went out encrypted to the wrong channel; duplicating that logic here would have
been an invitation to reintroduce it. It was gated on CAP_LUA_SDK_EXT, which is
off on the V4 -- exactly the board console mode exists for -- so the guard is
now CAP_LUA_SDK_EXT || CAP_CONSOLE.

'to' stores a name rather than an index for the same reason: an index goes
stale the moment the contact list changes underneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kaj SchittecatandClaude Opus 5 committed 2026-08-22 19:32:38 +02:00
1 parent 7d98d359ee
commit bc87645df7
2 files changed
+90 -5

No files matched your search

+83 -3
View File
@@ -19,6 +19,17 @@
#include "../helpers/esp32/TouchPrefsStore.h"
#endif
// Contacts, channels and the send path, all from UITask so the console reuses
// exactly what the UI and the Lua host use. In particular the channel send
// matches BY NAME at transmit time; a cached slot index is how messages once
// went out encrypted to the wrong channel.
extern int luaHostContactAt(int idx, char* name, size_t name_cap, int* type, uint32_t* secs_ago,
double* lat, double* lon, char* pk_hex, size_t pk_cap,
int32_t* lat_e6, int32_t* lon_e6);
extern int luaHostMeshChannelNames(char out[][32], int max_n);
extern bool luaHostMeshSendChannel(const char* chan_name, const char* text);
extern bool luaHostMeshSendDM(const char* to_name, const char* text, bool* was_room);
#if CAP_TOUCH
// Board touch driver, read directly. lvglTouchRead() is only an adapter that
// feeds LVGL, so there is nothing to unpick here: the driver polls either way.
@@ -263,6 +274,33 @@ void render() {
s_dirty = false;
}
// Current recipient, set by `to`. A name rather than an index or a slot, for the
// same reason the send path matches by name: an index goes stale the moment the
// contact list changes underneath it.
char s_to[40] = {0};
void cmdContacts() {
char name[36], pk[12]; int type; uint32_t ago; double lat, lon; int32_t la6, lo6;
int shown = 0;
for (int i = 0; i < 200 && shown < 40; i++) {
if (!luaHostContactAt(i, name, sizeof name, &type, &ago, &lat, &lon, pk, sizeof pk, &la6, &lo6)) break;
static const char* kType[5] = { "?", "chat", "repeater", "room", "sensor" };
char line[kLineCap];
snprintf(line, sizeof line, "%-16s %-8s %s", name,
(type >= 1 && type <= 4) ? kType[type] : "?", pk);
consoleWriteLine(line);
shown++;
}
if (!shown) consoleWriteLine("(no contacts yet)");
}
void cmdChannels() {
static char names[8][32];
const int n = luaHostMeshChannelNames(names, 8);
for (int i = 0; i < n; i++) consoleWriteLine(names[i]);
if (!n) consoleWriteLine("(no channels)");
}
// ---- command dispatch -------------------------------------------------------
void submit() {
if (s_input_len == 0) return;
@@ -296,9 +334,51 @@ void submit() {
}
#endif
if (!strcasecmp(cmd, "help")) {
consoleWriteLine("console: clear, help, ui");
consoleWriteLine("node: everything the CLI answers, e.g.");
consoleWriteLine(" advert, get name, set name <x>, time, ver");
consoleWriteLine("console clear, help, mem, ui");
consoleWriteLine("mesh contacts, chans");
consoleWriteLine(" to <name> pick a contact or channel");
consoleWriteLine(" msg <text> send to it");
consoleWriteLine("node anything the CLI answers:");
consoleWriteLine(" advert, get name, set name <x>, time, ver");
return;
}
// Free memory. This is the number console mode is justified by, so it is a
// command rather than something you have to instrument a build to see:
// read it here, reboot into the UI, read it there.
if (!strcasecmp(cmd, "mem")) {
#if defined(ESP32)
const size_t dr_f = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
const size_t dr_t = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
const size_t ps_f = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
const size_t ps_t = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
const size_t ps_b = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
consolePrintf("DRAM %u / %u KB free", (unsigned)(dr_f / 1024), (unsigned)(dr_t / 1024));
consolePrintf("PSRAM %u / %u KB free", (unsigned)(ps_f / 1024), (unsigned)(ps_t / 1024));
consolePrintf("PSRAM largest block %u KB", (unsigned)(ps_b / 1024));
#else
consoleWriteLine("not available on this build");
#endif
return;
}
if (!strcasecmp(cmd, "contacts")) { cmdContacts(); return; }
if (!strcasecmp(cmd, "chans") || !strcasecmp(cmd, "channels")) { cmdChannels(); return; }
if (!strncasecmp(cmd, "to ", 3)) {
snprintf(s_to, sizeof s_to, "%s", cmd + 3);
consolePrintf("sending to: %s", s_to);
return;
}
if (!strncasecmp(cmd, "msg ", 4)) {
if (!s_to[0]) { consoleWriteLine("no recipient - use 'to <name>' first"); return; }
const char* text = cmd + 4;
// Try a channel first, then a contact. Channels and contacts share a name
// space here on purpose: the user typed a name, not a category.
if (luaHostMeshSendChannel(s_to, text)) { consolePrintf("sent to #%s", s_to); return; }
bool was_room = false;
if (luaHostMeshSendDM(s_to, text, &was_room)) {
consolePrintf("sent to %s%s", s_to, was_room ? " (room)" : "");
return;
}
consolePrintf("no channel or contact named '%s'", s_to);
return;
}
the_mesh.runLocalCli(cmd);