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());