From 411864dab81ce2dc7cffa5e8353ae4d3ef66447f Mon Sep 17 00:00:00 2001 From: Scott Penrose Date: Tue, 16 Jun 2026 21:00:39 +1000 Subject: [PATCH] multiple buttons --- data/index.html | 26 ++++++++++++++++++++++++-- platformio.ini | 9 +++++---- src/main.cpp | 40 ++++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/data/index.html b/data/index.html index 72c8c76..e0b324c 100644 --- a/data/index.html +++ b/data/index.html @@ -243,7 +243,8 @@ - + + @@ -308,6 +309,7 @@ // Terminal setup const term = new Terminal({ cursorBlink: true, + convertEol: true, // target sends bare '\n'; treat it as CRLF so lines don't staircase fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { @@ -434,6 +436,7 @@ updateStatus('btStatus', msg.btConnected); document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`; updateLogUi(msg); + updateButtonUi(msg); // Show WiFi info const wifiInfo = document.getElementById('wifiInfo'); @@ -600,7 +603,26 @@ .catch(() => term.writeln(`\r\n\x1b[31m[${label} failed]\x1b[0m`)); } function doReset() { pulseLine('/api/reset', 'reset'); } - function doButton() { pulseLine('/api/button', 'button'); } + function doButton() { + fetch('/api/button?ms=200').then(r => r.json()).then(s => { + updateButtonUi(s); + term.writeln('\r\n\x1b[33m[button] pulsed\x1b[0m'); + }).catch(() => term.writeln('\r\n\x1b[31m[button failed]\x1b[0m')); + } + let holdOn = false; + function toggleHold() { + fetch('/api/button?latch=' + (holdOn ? 'off' : 'on')).then(r => r.json()).then(s => { + updateButtonUi(s); + term.writeln(`\r\n\x1b[33m[button] hold ${holdOn ? 'ON' : 'off'}\x1b[0m`); + }).catch(() => term.writeln('\r\n\x1b[31m[hold failed]\x1b[0m')); + } + function updateButtonUi(s) { + if (typeof s.buttonLatch === 'undefined') return; + holdOn = !!s.buttonLatch; + const b = document.getElementById('holdBtn'); + b.textContent = 'Hold: ' + (holdOn ? 'ON' : 'off'); + b.classList.toggle('danger', holdOn); + } // ---- SD logging controls ---- let logOn = false; diff --git a/platformio.ini b/platformio.ini index 6c40413..51b6841 100644 --- a/platformio.ini +++ b/platformio.ini @@ -84,10 +84,11 @@ build_flags = -DTARGET_RX_PIN=44 ; CONFIRM: dongle RX <- sensor TX -DTARGET_TX_PIN=43 ; CONFIRM: dongle TX -> sensor RX ; --- GPIO control lines to the target (reset out / button out) --- - ; GPIO4 + GPIO12 are free, broken-out pins on the T3-S3. (Earlier 2/1 were - ; wrong: GPIO2 is the SD MISO and GPIO1 is the battery-ADC pin.) - -DGPIO_RESET_PIN=4 ; -> sensor RST (active-low pulse) - -DGPIO_WAKE_PIN=12 ; -> sensor button/wake (active-low pulse) + ; GPIO38 + GPIO39 are free pins on the T3-S3 RIGHT header, right next to the + ; UART pins TXD=43/RXD=44 -- so reset/button/TX/RX/GND all come off one row. + ; (GPIO2=SD MISO and GPIO1=battery ADC; GPIO4/12 are not broken out at all.) + -DGPIO_RESET_PIN=38 ; -> sensor RST (active-low pulse) + -DGPIO_WAKE_PIN=39 ; -> sensor button/wake (active-low pulse) -DGPIO_CTRL_ACTIVE_LOW=1 ; --- microSD on FSPI/SPI2 (separate bus from LoRa, which uses HSPI/SPI3) --- -DT3S3_SD_SCK=14 diff --git a/src/main.cpp b/src/main.cpp index 5bdc9fb..6a6d978 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -125,6 +125,9 @@ size_t telnetLineLen[MAX_TELNET] = {0}; volatile uint32_t rxBytes = 0; // from target volatile uint32_t txBytes = 0; // to target +// Button line held active (latched) rather than momentary-pulsed. +bool buttonLatched = false; + // SD logging (T3-S3) bool sdAvailable = false; bool logEnabled = true; // ON by default once we have a date @@ -190,6 +193,17 @@ static void gpioPulse(int pin, uint32_t ms, Stream* reply, const char* name) { if (reply) reply->printf("[ctrl] %s pulsed pin %d for %lu ms\r\n", name, pin, (unsigned long)ms); } +// Hold a control line active (on) or release it (off) -- a latched "press". +static void gpioLatch(int pin, bool on, Stream* reply, const char* name) { + if (pin < 0) { if (reply) reply->printf("[ctrl] %s pin not configured\r\n", name); return; } + int active = GPIO_CTRL_ACTIVE_LOW ? LOW : HIGH; + int inactive = GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW; + pinMode(pin, OUTPUT); + digitalWrite(pin, on ? active : inactive); + if (reply) reply->printf("[ctrl] %s latch=%s (pin %d %s)\r\n", + name, on ? "on" : "off", pin, on ? "held" : "released"); +} + // ============================================================================ // SD logging (T3-S3) // ============================================================================ @@ -364,7 +378,7 @@ void handleCommand(const char* line, Stream* reply) { const char* args = line[n] ? line + n + 1 : line + n; if (!strcmp(verb, "help")) { - reply->print("[help] ~status ~reset [ms] ~button [ms] ~baud " + reply->print("[help] ~status ~reset [ms] ~button [ms|on|off] ~baud " "~port ~log on|off ~gpio <0|1>\r\n" " ~mesh [on|off] ~psk ~chan ~msg \r\n" " (any non-~ line is sent to the target UART)\r\n"); @@ -373,7 +387,9 @@ void handleCommand(const char* line, Stream* reply) { } else if (!strcmp(verb, "reset")) { gpioPulse(GPIO_RESET_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "reset"); } else if (!strcmp(verb, "wake") || !strcmp(verb, "button")) { - gpioPulse(GPIO_WAKE_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "button"); + if (!strncmp(args, "on", 2)) { gpioLatch(GPIO_WAKE_PIN, true, reply, "button"); buttonLatched = true; } + else if (!strncmp(args, "off", 3)) { gpioLatch(GPIO_WAKE_PIN, false, reply, "button"); buttonLatched = false; } + else { gpioPulse(GPIO_WAKE_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "button"); buttonLatched = false; } } else if (!strcmp(verb, "baud")) { uint32_t b = strtoul(args, nullptr, 10); if (b) { setBaudRate(b); reply->printf("[ctrl] baud=%lu\r\n", (unsigned long)b); } @@ -495,6 +511,7 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t r["baudSerial1"] = baudSerial1; r["rx"] = rxBytes; r["tx"] = txBytes; r["log"] = logEnabled; r["ntp"] = ntpSynced; r["sd"] = sdAvailable; + r["buttonLatch"] = buttonLatched; #if BOARD_T3S3 r["logfile"] = logFile ? logPath : ""; #endif @@ -539,6 +556,7 @@ static String statusJson() { doc["baudSerial1"] = baudSerial1; doc["rx"] = rxBytes; doc["tx"] = txBytes; doc["log"] = logEnabled; doc["ntp"] = ntpSynced; doc["sd"] = sdAvailable; + doc["buttonLatch"] = buttonLatched; #if BOARD_T3S3 doc["logfile"] = logFile ? logPath : ""; #endif @@ -563,13 +581,23 @@ void setupWebServer() { gpioPulse(GPIO_RESET_PIN, ms, nullptr, "reset"); req->send(200, "text/plain", "reset pulsed\n"); }); - auto pulseButton = [](AsyncWebServerRequest* req) { + // Momentary pulse (?ms=), or latch the line held (?latch=on|off|1|0). + auto doButton = [](AsyncWebServerRequest* req) { + if (req->hasParam("latch")) { + String v = req->getParam("latch")->value(); + bool on = (v == "1" || v.startsWith("on") || v == "true"); + gpioLatch(GPIO_WAKE_PIN, on, nullptr, "button"); + buttonLatched = on; + req->send(200, "application/json", statusJson()); + return; + } uint32_t ms = req->hasParam("ms") ? req->getParam("ms")->value().toInt() : 0; gpioPulse(GPIO_WAKE_PIN, ms, nullptr, "button"); - req->send(200, "text/plain", "button pulsed\n"); + buttonLatched = false; + req->send(200, "application/json", statusJson()); }; - server.on("/api/button", HTTP_GET, pulseButton); - server.on("/api/wake", HTTP_GET, pulseButton); // back-compat alias + server.on("/api/button", HTTP_GET, doButton); + server.on("/api/wake", HTTP_GET, doButton); // back-compat alias server.on("/api/baud", HTTP_GET, [](AsyncWebServerRequest* req) { if (req->hasParam("baud")) setBaudRate(req->getParam("baud")->value().toInt()); req->send(200, "application/json", statusJson());