LilyGo T-Display P4 (ESP32-P4 + factory ESP-AT C6) reaches POC: LoRa mesh, Wi-Fi companion (TCP:5000), GPS, SD, battery gauge, chat persistence, and now: - Web VNC/REMOTE/terminal via a first-byte router on the single ESP-AT listener (HTTP verbs -> WS server, '<' frames -> phone companion; both live at once). C6Server gains a _begun gate so never-listening instances stay inert. - VNC freeze fixed: bounded one-CIPSEND-chunk drains under the WS client mutex (a whole-band blocking write starved the main loop), fast-drop of stalled mirror clients, SEND-OK wait 8s->4s. - Wi-Fi robustness: CWJAP? miss-streak tolerance, CIPSERVER liveness verify + re-arm (a re-association silently killed the listener until reboot). - Time sync: HTTP-Date fallback when SNTP is blocked; ClockFloorRTC gains a MAX_PLAUSIBLE_EPOCH guard (all boards) after a garbage-future hardware RTC read (2043) latched the ratchet. - Brightness: RM69A10 DCS 0x51 driven live (CC slider + persisted pref). - Sound: ES8311 + NS4150B notification chimes (P4Audio, esp-bsp-derived register sequence on Wire1), full HAS_UI_SOUND surface + CC Screen/Sound sliders; ccVolumeReleaseCb's hardcoded tanBeep -> uiSoundPreview. - tdisplay_p4/: IDF project (build.sh, c6_at AT-over-SDIO driver, sdkconfigs, fetch-deps.sh vendoring, .gitignore for local payloads). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
513 lines
19 KiB
C++
513 lines
19 KiB
C++
#include "MultiTransportCompanionInterface.h"
|
|
#include <helpers/RepeaterTcpOtaEmit.h>
|
|
#include "WifiRuntimeStore.h" // persist BLE on/off (ble_en) across reboots
|
|
#include "WebMirror.h" // web UI mirror bridge (served over the WS server)
|
|
#include <string.h>
|
|
|
|
// Companion push code for the per-packet RX log (matches MyMesh.cpp). It is kept OFF
|
|
// the BLE transport in writeFrameToAll — see the note there (issues #46, #54) — EXCEPT
|
|
// for one-shot passes armed via bleAllowNextRxLog() (echoes of our own sends, #94).
|
|
#define PUSH_CODE_LOG_RX_DATA 0x88
|
|
|
|
bool MultiTransportCompanionInterface::s_ble_rxlog_once = false;
|
|
|
|
MultiTransportCompanionInterface::MultiTransportCompanionInterface()
|
|
: _tcp_port(0), _ws_port(0), _tcp_started(false), _ws_started(false), _tcp_enabled(true), _isEnabled(false), _broadcast(false), _last_reply_target(REPLY_TARGET_USB), _ota_tcp_suspended(false), _ota_ws_suspended(false), _ota_ws_listen_paused(false)
|
|
#ifdef BLE_PIN_CODE
|
|
, _ble_begun(false), _ble_enabled(false), _ota_ble_released(false), _ble_pin_code(0)
|
|
#endif
|
|
{
|
|
for (size_t i = 0; i < sizeof(_client_ids) / sizeof(_client_ids[0]); i++)
|
|
_client_ids[i][0] = '\0';
|
|
#ifdef BLE_PIN_CODE
|
|
_ble_prefix[0] = '\0';
|
|
_ble_name[0] = '\0';
|
|
#endif
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::begin(Stream& usb_serial, uint16_t tcp_port, uint16_t ws_port) {
|
|
_usb.begin(usb_serial);
|
|
_tcp_port = tcp_port;
|
|
_ws_port = ws_port;
|
|
_last_reply_target = REPLY_TARGET_USB;
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::startTcpServer(bool wifi_connected) {
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
// ESP-AT allows ONE listening port, so the router server (_p4_srv) owns it and dispatches
|
|
// each inbound connection by first byte — see p4RouteClients(). _tcp/_ws never listen
|
|
// themselves here; started flags only mark them willing to adopt routed clients.
|
|
if (_tcp_enabled && !_tcp_started && _tcp_port != 0) {
|
|
_p4_srv.begin(_tcp_port);
|
|
_tcp_started = true;
|
|
}
|
|
if (_tcp_enabled && !_ws_started && _ws_port != 0 && wifi_connected) {
|
|
_ws_started = true; // enables tickHandshake + the mirror stream task; _ws itself stays listen-less
|
|
printf("[WS] P4 router: web UI shares TCP:%u (first-byte dispatch)\n", (unsigned)_tcp_port);
|
|
}
|
|
#else
|
|
if (_tcp_enabled && !_tcp_started && _tcp_port != 0) {
|
|
_tcp.begin(_tcp_port);
|
|
_tcp_started = true;
|
|
}
|
|
// Plain WebSocket: start only when Wi-Fi has an address (caller defers TCP/WS start after splash).
|
|
if (_tcp_enabled && !_ws_started && _ws_port != 0 && wifi_connected) {
|
|
_ws.begin(_ws_port);
|
|
_ws_started = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
// One AT listener serves BOTH protocols. Accept every inbound connection here, peek its first
|
|
// byte, and hand the socket to the right server: HTTP requests start with an ASCII verb
|
|
// (GET/HEAD/POST/PUT/OPTIONS/DELETE...) and cover the web viewer page, the mirror/terminal
|
|
// WebSockets and the WS companion; companion frames from the phone app always start with '<'.
|
|
// A peer that stays silent gets the legacy benefit of the doubt after 3 s: some companion
|
|
// clients connect and only listen for pushes without ever sending.
|
|
void MultiTransportCompanionInterface::p4RouteClients() {
|
|
if (!_tcp_started && !_ws_started) return;
|
|
const int NPEND = (int)(sizeof(_p4_pend) / sizeof(_p4_pend[0]));
|
|
while (_p4_srv.hasClient()) {
|
|
int slot = -1;
|
|
for (int i = 0; i < NPEND; i++) {
|
|
if (!_p4_pend[i].used) {
|
|
slot = i;
|
|
break;
|
|
}
|
|
}
|
|
if (slot < 0) break; // all pending slots mid-peek; the accept FIFO holds the rest
|
|
WiFiClient incoming = _p4_srv.accept();
|
|
if (!incoming) continue;
|
|
_p4_pend[slot].c = incoming;
|
|
_p4_pend[slot].ms = millis();
|
|
_p4_pend[slot].used = true;
|
|
}
|
|
for (int i = 0; i < NPEND; i++) {
|
|
if (!_p4_pend[i].used) continue;
|
|
WiFiClient& c = _p4_pend[i].c;
|
|
if (!c.connected()) {
|
|
c.stop();
|
|
_p4_pend[i].used = false;
|
|
continue;
|
|
}
|
|
int b = c.peek();
|
|
bool is_http = (b == 'G' || b == 'H' || b == 'P' || b == 'O' || b == 'D');
|
|
if (b < 0 && millis() - _p4_pend[i].ms < 3000) continue; // no first byte yet
|
|
if (is_http && _ws_started) {
|
|
_ws.adoptClient(c);
|
|
} else if (!is_http && _tcp_started) {
|
|
_tcp.adoptClient(c);
|
|
} else {
|
|
c.stop(); // the matching server is suspended (OTA) — refuse politely
|
|
}
|
|
_p4_pend[i].used = false;
|
|
_p4_pend[i].c = WiFiClient(); // drop our shared link ref; the adopting server holds its own
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Web-mirror streaming runs on CORE 0 (parallel to the UI on core 1) so the browser's
|
|
// socket stays fed at full link speed even while core 1 is busy with a heavy LVGL render.
|
|
// The WS client array is guarded by WebSocketCompanionServer's recursive mutex, so this
|
|
// task and the main-loop companion/handshake code never corrupt each other. serviceMirror
|
|
// is non-blocking, so the mutex is held only briefly and cross-core contention is tiny.
|
|
static void wsMirrorStreamTask(void* arg) {
|
|
WebSocketCompanionServer* ws = static_cast<WebSocketCompanionServer*>(arg);
|
|
for (;;) {
|
|
ws->serviceMirror(g_web_mirror);
|
|
vTaskDelay(pdMS_TO_TICKS(g_web_mirror.clients() > 0 ? 2 : 25)); // fast when a browser is watching, idle otherwise
|
|
}
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::tickWebSocketHandshake() {
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
p4RouteClients(); // dispatch newly accepted AT-listener connections to _tcp / _ws
|
|
#endif
|
|
if (_ws_started) {
|
|
_ws.tickHandshake(); // accept + WS handshake stay on core 1 (the main loop)
|
|
if (!_mirror_task) { // spawn the core-0 streamer once, on first tick after the WS server is up
|
|
xTaskCreatePinnedToCore(wsMirrorStreamTask, "ws_mirror", 4096, &_ws, 2,
|
|
reinterpret_cast<TaskHandle_t*>(&_mirror_task), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::stopTcpServer() {
|
|
if (_ws_started) {
|
|
_ws.stop();
|
|
_ws_started = false;
|
|
}
|
|
if (_tcp_started) {
|
|
_tcp.stop();
|
|
_tcp_started = false;
|
|
}
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
_p4_srv.stop(); // the router owns the AT listener (see startTcpServer)
|
|
for (size_t i = 0; i < sizeof(_p4_pend) / sizeof(_p4_pend[0]); i++) {
|
|
if (_p4_pend[i].used) {
|
|
_p4_pend[i].c.stop();
|
|
_p4_pend[i].c = WiFiClient();
|
|
_p4_pend[i].used = false;
|
|
}
|
|
}
|
|
#endif
|
|
_tcp_enabled = false;
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::enableTcp() {
|
|
_tcp_enabled = true;
|
|
// Restart immediately: don't wait for the next main-loop tick, and don't
|
|
// require wifi_started to be true (TCP itself doesn't need an IP address).
|
|
startTcpServer(false);
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::disableTcp() {
|
|
stopTcpServer();
|
|
}
|
|
|
|
#ifdef BLE_PIN_CODE
|
|
void MultiTransportCompanionInterface::prepareBle(const char* prefix, char* name, uint32_t pin_code) {
|
|
if (prefix) {
|
|
strncpy(_ble_prefix, prefix, sizeof(_ble_prefix) - 1);
|
|
_ble_prefix[sizeof(_ble_prefix) - 1] = '\0';
|
|
} else {
|
|
_ble_prefix[0] = '\0';
|
|
}
|
|
if (name) {
|
|
strncpy(_ble_name, name, sizeof(_ble_name) - 1);
|
|
_ble_name[sizeof(_ble_name) - 1] = '\0';
|
|
} else {
|
|
_ble_name[0] = '\0';
|
|
}
|
|
_ble_pin_code = pin_code;
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::beginBle(const char* prefix, char* name, uint32_t pin_code) {
|
|
prepareBle(prefix, name, pin_code);
|
|
_ble.begin(prefix, name, pin_code);
|
|
_ble_begun = true;
|
|
_ble_enabled = true;
|
|
_ota_ble_released = false;
|
|
_ble.enable();
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::enableBle() {
|
|
if (!_ble_begun) {
|
|
// Deferred at boot (heap guard) or toggled on from off: bring the stack up
|
|
// now, live, from the params stashed by prepareBle()/beginBle().
|
|
if (_ble_prefix[0] == '\0' && _ble_name[0] == '\0') return; // no params known
|
|
char name[sizeof(_ble_name)];
|
|
strncpy(name, _ble_name, sizeof(name) - 1);
|
|
name[sizeof(name) - 1] = '\0';
|
|
_ble.begin(_ble_prefix, name, _ble_pin_code);
|
|
_ble_begun = true;
|
|
}
|
|
_ble_enabled = true;
|
|
wifiConfigSetBleEnabled(true); // persist so it survives reboot
|
|
_ble.enable();
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::disableBle() {
|
|
_ble_enabled = false;
|
|
wifiConfigSetBleEnabled(false); // persist so BT stays off across reboot
|
|
_ble.disable(); // stop advertising + drop any connection
|
|
// NOTE: we deliberately do NOT NimBLEDevice::deinit() here. Tearing the BT
|
|
// controller down while Wi-Fi+BLE coexistence is active crashes — the esp_coex
|
|
// layer still holds a reference to the controller — so "off" stops advertising
|
|
// but keeps the NimBLE host resident. Its RAM is only fully reclaimed on reboot.
|
|
}
|
|
|
|
bool MultiTransportCompanionInterface::getBlePeerAddress(char* buf, size_t len) const {
|
|
if (!_ble_begun || !_ble_enabled) {
|
|
if (buf && len > 0) buf[0] = '\0';
|
|
return false;
|
|
}
|
|
return _ble.getConnectedPeerAddress(buf, len);
|
|
}
|
|
#endif
|
|
|
|
void MultiTransportCompanionInterface::enable() {
|
|
_isEnabled = true;
|
|
_usb.enable();
|
|
_last_reply_target = REPLY_TARGET_USB;
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ble_begun && _ble_enabled)
|
|
_ble.enable();
|
|
#endif
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::disable() {
|
|
_isEnabled = false;
|
|
_usb.disable();
|
|
#ifdef BLE_PIN_CODE
|
|
_ble.disable();
|
|
#endif
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::prepareForHttpOta() {
|
|
_ota_tcp_suspended = false;
|
|
_ota_ws_suspended = false;
|
|
_ota_ws_listen_paused = false;
|
|
|
|
// Keep the Wi-Fi control path that issued `ota url` (TCP or WebSocket) alive so the meshcomod
|
|
// client stays connected for progress and the final OK/reboot message. Suspend the other
|
|
// transport plus BLE to free RAM for HTTPS/TLS.
|
|
const bool preserve_tcp = (_last_reply_target >= 0 && _last_reply_target < REPLY_TARGET_WS_0);
|
|
const bool preserve_ws = (_last_reply_target >= REPLY_TARGET_WS_0);
|
|
|
|
char line[168];
|
|
if (preserve_tcp) {
|
|
snprintf(line, sizeof(line), "OTA: minimal companion preserve=tcp suspend=ws,ble heap=%u",
|
|
(unsigned)ESP.getFreeHeap());
|
|
} else if (preserve_ws) {
|
|
snprintf(line, sizeof(line), "OTA: minimal companion preserve=ws suspend=tcp,ble heap=%u",
|
|
(unsigned)ESP.getFreeHeap());
|
|
} else {
|
|
snprintf(line, sizeof(line), "OTA: minimal companion unexpected reply_target=%d heap=%u", _last_reply_target,
|
|
(unsigned)ESP.getFreeHeap());
|
|
}
|
|
meshcoreRepeaterTcpOtaEmitLine(line);
|
|
|
|
if (preserve_ws && _tcp_started) {
|
|
_tcp.stop();
|
|
_tcp_started = false;
|
|
_ota_tcp_suspended = true;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: suspended companion TCP server");
|
|
}
|
|
if (preserve_tcp && _ws_started) {
|
|
_ws.stop();
|
|
_ws_started = false;
|
|
_ota_ws_suspended = true;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: suspended companion WebSocket server");
|
|
}
|
|
if (preserve_ws && _ws_started) {
|
|
_ws.pauseListen();
|
|
_ota_ws_listen_paused = true;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: paused WS listen (client kept, no new connections)");
|
|
}
|
|
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ble_begun && _ble_enabled) {
|
|
_ble.disable();
|
|
NimBLEDevice::deinit(true);
|
|
_ble_begun = false;
|
|
_ble_enabled = false;
|
|
_ota_ble_released = true;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: released BLE stack");
|
|
}
|
|
#endif
|
|
|
|
snprintf(line, sizeof(line), "OTA: minimal companion after heap=%u max=%u", (unsigned)ESP.getFreeHeap(),
|
|
(unsigned)ESP.getMaxAllocHeap());
|
|
meshcoreRepeaterTcpOtaEmitLine(line);
|
|
}
|
|
|
|
bool MultiTransportCompanionInterface::isHttpOtaWifiControlSession() const {
|
|
return _last_reply_target != REPLY_TARGET_USB && _last_reply_target != REPLY_TARGET_BLE;
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::restoreAfterHttpOta() {
|
|
if (_ota_ws_listen_paused) {
|
|
_ws.resumeListen();
|
|
_ota_ws_listen_paused = false;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: resumed WebSocket listen");
|
|
}
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ota_ble_released && _ble_prefix[0] && _ble_name[0]) {
|
|
char ble_name[sizeof(_ble_name)];
|
|
strncpy(ble_name, _ble_name, sizeof(ble_name) - 1);
|
|
ble_name[sizeof(ble_name) - 1] = '\0';
|
|
_ble.begin(_ble_prefix, ble_name, _ble_pin_code);
|
|
_ble_begun = true;
|
|
_ble_enabled = true;
|
|
_ble.enable();
|
|
_ota_ble_released = false;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: restored BLE stack");
|
|
}
|
|
#endif
|
|
if (_ota_tcp_suspended) {
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
_tcp_started = true; // the router still owns the AT listener; just resume adopting
|
|
#else
|
|
_tcp.begin(_tcp_port);
|
|
_tcp_started = true;
|
|
#endif
|
|
_ota_tcp_suspended = false;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: restored companion TCP server");
|
|
}
|
|
if (_ota_ws_suspended) {
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
_ws_started = true; // ditto — never begin a second AT listener on _ws_port
|
|
#else
|
|
_ws.begin(_ws_port);
|
|
_ws_started = true;
|
|
#endif
|
|
_ota_ws_suspended = false;
|
|
meshcoreRepeaterTcpOtaEmitLine("OTA: restored companion WebSocket server");
|
|
}
|
|
}
|
|
|
|
bool MultiTransportCompanionInterface::isConnected() const {
|
|
if (_usb.isConnected()) return true;
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ble_begun && _ble_enabled && _ble.isConnected()) return true;
|
|
#endif
|
|
if (_tcp_started && _tcp.connectedCount() > 0) return true;
|
|
if (_ws_started && _ws.connectedCount() > 0) return true;
|
|
return false;
|
|
}
|
|
|
|
bool MultiTransportCompanionInterface::isWriteBusy() const {
|
|
if (_usb.isWriteBusy()) return true;
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ble_begun && _ble_enabled && _ble.isWriteBusy()) return true;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
size_t MultiTransportCompanionInterface::checkRecvFrame(uint8_t dest[]) {
|
|
if (!_isEnabled) return 0;
|
|
|
|
#ifdef BLE_PIN_CODE
|
|
// Drain BLE send queue every loop so PC (or any BLE client) gets pushes even when USB/TCP are polled first.
|
|
if (_ble_begun && _ble_enabled)
|
|
_ble.drainSendQueue();
|
|
#endif
|
|
|
|
// Poll USB first (preserve Home Assistant / USB priority). Do not overwrite _last_reply_target
|
|
// when caller is in the middle of a contact-list stream (handled by caller saving/restoring target).
|
|
size_t len = _usb.checkRecvFrame(dest);
|
|
if (len > 0) {
|
|
_last_reply_target = REPLY_TARGET_USB;
|
|
return len;
|
|
}
|
|
|
|
// Then poll TCP clients (only after TCP server was started)
|
|
if (_tcp_started) {
|
|
int tcp_client = -1;
|
|
len = _tcp.pollRecvFrame(dest, &tcp_client);
|
|
if (len > 0) {
|
|
_last_reply_target = tcp_client;
|
|
return len;
|
|
}
|
|
}
|
|
|
|
// Then poll WebSocket clients
|
|
if (_ws_started) {
|
|
int ws_client = -1;
|
|
len = _ws.pollRecvFrame(dest, &ws_client);
|
|
if (len > 0) {
|
|
_last_reply_target = REPLY_TARGET_WS_0 + ws_client;
|
|
return len;
|
|
}
|
|
}
|
|
|
|
#ifdef BLE_PIN_CODE
|
|
if (_ble_begun && _ble_enabled) {
|
|
len = _ble.checkRecvFrame(dest);
|
|
if (len > 0) {
|
|
_last_reply_target = REPLY_TARGET_BLE;
|
|
return len;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
size_t MultiTransportCompanionInterface::writeFrame(const uint8_t src[], size_t len) {
|
|
if (len > MAX_FRAME_SIZE) return 0;
|
|
// Single-target only (command responses, sync history). Never broadcast.
|
|
if (_last_reply_target == REPLY_TARGET_USB)
|
|
return _usb.writeFrame(src, len);
|
|
#ifdef BLE_PIN_CODE
|
|
if (_last_reply_target == REPLY_TARGET_BLE && _ble_begun && _ble_enabled)
|
|
return _ble.writeFrame(src, len);
|
|
#endif
|
|
if (_last_reply_target >= REPLY_TARGET_WS_0 && _last_reply_target < REPLY_TARGET_WS_0 + WS_COMPANION_MAX_CLIENTS && _ws_started)
|
|
return _ws.writeToClient(_last_reply_target - REPLY_TARGET_WS_0, src, len);
|
|
if (_tcp_started)
|
|
return _tcp.writeToClient(_last_reply_target, src, len);
|
|
return 0;
|
|
}
|
|
|
|
size_t MultiTransportCompanionInterface::writeFrameToAll(const uint8_t src[], size_t len) {
|
|
if (len > MAX_FRAME_SIZE) return 0;
|
|
if (!_broadcast)
|
|
return writeFrame(src, len);
|
|
bool all_ok = true;
|
|
if (_usb.isConnected() && _usb.writeFrame(src, len) != len)
|
|
all_ok = false;
|
|
#ifdef BLE_PIN_CODE
|
|
// The per-packet RX log floods BLE's ~16 frames/sec budget on a busy mesh, starving
|
|
// the frames that matter — chat messages + admin responses (issues #46, #54). So it
|
|
// is kept OFF BLE (USB/TCP/WS have the bandwidth) — EXCEPT when MyMesh::logRxRaw
|
|
// armed a one-shot pass because this frame is an echo of OUR OWN flood send: the
|
|
// app's "Repeats heard" is computed exactly from those (it broke on BLE when the
|
|
// blanket skip landed in beta_23 — issue #94), and a few echoes per send are
|
|
// nowhere near the flood that caused #46/#54.
|
|
const bool rxlog = (len > 0 && src[0] == PUSH_CODE_LOG_RX_DATA);
|
|
const bool ble_pass = rxlog && s_ble_rxlog_once;
|
|
if (rxlog) s_ble_rxlog_once = false; // consume the one-shot either way
|
|
const bool skip_ble = rxlog && !ble_pass;
|
|
if (!skip_ble && _ble_begun && _ble_enabled && _ble.isConnected() && _ble.writeFrame(src, len) != len)
|
|
all_ok = false;
|
|
#endif
|
|
if (_tcp_started && _tcp.connectedCount() > 0 && _tcp.writeToAllClients(src, len) != len)
|
|
all_ok = false;
|
|
if (_ws_started && _ws.connectedCount() > 0 && _ws.writeToAllClients(src, len) != len)
|
|
all_ok = false;
|
|
return all_ok ? len : 0;
|
|
}
|
|
|
|
int MultiTransportCompanionInterface::_clientIdSlot() const {
|
|
#ifdef BLE_PIN_CODE
|
|
if (_last_reply_target == REPLY_TARGET_USB) return 0;
|
|
if (_last_reply_target == REPLY_TARGET_BLE) return 1;
|
|
if (_last_reply_target >= REPLY_TARGET_WS_0 && _last_reply_target < REPLY_TARGET_WS_0 + WS_COMPANION_MAX_CLIENTS)
|
|
return 2 + TCP_COMPANION_MAX_CLIENTS + (_last_reply_target - REPLY_TARGET_WS_0);
|
|
return _last_reply_target + 2; // TCP 0..N -> slots 2..
|
|
#else
|
|
if (_last_reply_target >= REPLY_TARGET_WS_0 && _last_reply_target < REPLY_TARGET_WS_0 + WS_COMPANION_MAX_CLIENTS)
|
|
return 1 + TCP_COMPANION_MAX_CLIENTS + (_last_reply_target - REPLY_TARGET_WS_0);
|
|
return _last_reply_target + 1;
|
|
#endif
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::setCurrentClientId(const char* id) {
|
|
int slot = _clientIdSlot();
|
|
if (slot >= 0 && slot < (int)(sizeof(_client_ids) / sizeof(_client_ids[0]))) {
|
|
if (id) {
|
|
strncpy(_client_ids[slot], id, _max_client_id_len - 1);
|
|
_client_ids[slot][_max_client_id_len - 1] = '\0';
|
|
} else {
|
|
_client_ids[slot][0] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
void MultiTransportCompanionInterface::getCurrentClientId(char* dest, size_t max_len) const {
|
|
if (!dest || max_len == 0) return;
|
|
dest[0] = '\0';
|
|
int slot = _clientIdSlot();
|
|
if (slot < 0 || slot >= (int)(sizeof(_client_ids) / sizeof(_client_ids[0]))) return;
|
|
// If app sent client_id in CMD_APP_START, use it; otherwise use connection-based id
|
|
// so non-custom clients (HA, MeshCore app) still get per-connection history without sending anything.
|
|
if (_client_ids[slot][0] != '\0') {
|
|
strncpy(dest, _client_ids[slot], max_len - 1);
|
|
dest[max_len - 1] = '\0';
|
|
return;
|
|
}
|
|
#ifdef BLE_PIN_CODE
|
|
static const char* const default_ids[] = { "usb", "ble", "tcp0", "tcp1", "tcp2", "ws0", "ws1" };
|
|
#else
|
|
static const char* const default_ids[] = { "usb", "tcp0", "tcp1", "tcp2", "ws0", "ws1" };
|
|
#endif
|
|
size_t n = sizeof(default_ids) / sizeof(default_ids[0]);
|
|
if ((size_t)slot < n) {
|
|
strncpy(dest, default_ids[slot], max_len - 1);
|
|
dest[max_len - 1] = '\0';
|
|
}
|
|
}
|