mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
5.9 KiB
C++
149 lines
5.9 KiB
C++
#pragma once
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <cstdint>
|
|
#include <helpers/SensorManager.h>
|
|
#include <helpers/sensors/LPPDataHelpers.h>
|
|
|
|
// 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)
|
|
// {name} — sender's name (bot replies only; requires sender_name, else left literal)
|
|
// {hops} — hop count: "direct" or "N hops" (bot replies only; requires hops>=0, else left literal)
|
|
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,
|
|
SensorManager* sm = nullptr,
|
|
float batt_volts = -1.0f,
|
|
const char* sender_name = nullptr,
|
|
int hops = -1) {
|
|
// 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) { 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");
|
|
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;
|
|
time_t t = (time_t)local_ts;
|
|
struct tm* ti = gmtime(&t);
|
|
char tb[8];
|
|
snprintf(tb, sizeof(tb), "%02d:%02d", ti->tm_hour, ti->tm_min);
|
|
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 if (strncmp(p, "{name}", 6) == 0) {
|
|
if (sender_name && sender_name[0]) { APPEND(sender_name, strlen(sender_name)); }
|
|
else { APPEND("{name}", 6); }
|
|
p += 6;
|
|
} else if (strncmp(p, "{hops}", 6) == 0) {
|
|
if (hops >= 0) {
|
|
char b[10];
|
|
if (hops == 0) strcpy(b, "direct");
|
|
else snprintf(b, sizeof(b), "%u hops", (unsigned)hops);
|
|
APPEND(b, strlen(b));
|
|
} else { APPEND("{hops}", 6); }
|
|
p += 6;
|
|
} else {
|
|
out[oi++] = *p++;
|
|
}
|
|
|
|
#undef APPEND
|
|
}
|
|
out[oi] = '\0';
|
|
}
|