diff --git a/Firmware/simulator/smoke-test.js b/Firmware/simulator/smoke-test.js index 4796997..b4631e1 100644 --- a/Firmware/simulator/smoke-test.js +++ b/Firmware/simulator/smoke-test.js @@ -14,6 +14,7 @@ // seed drill's relays follow and the sprayer's calibration (the pages' own password question is answered). Also opens the sprayer // and Teensy pages inside a frame with the sandbox of an AgOpenWeb web card, where the browser's confirm() and prompt() do // nothing: the pages' own question box must show there, fit the card and answer. A browser dialog on any page fails the check. +// Enter in a password box does what its button does; an empty box or a held Enter does nothing. "use strict"; const fs = require("fs"), path = require("path"), http = require("http"); const {chromium} = require("playwright"); @@ -374,6 +375,40 @@ async function teensy(browser, port, q, name) { if (await pg.locator("#ghInstall").isDisabled()) fail.push(what + ": Install from GitHub is not offered"); await pg.close(); } + // Enter in the XIAO page's password boxes: it does what the card's button does (here every action asks first, and the check + // says no); an empty box does nothing, and a held Enter does not also answer the question it opened + { + const what = "XIAO page, Enter in the password boxes"; + const pg = await page(browser, "http://127.0.0.1:" + port + "/Boards/xiao/", what, j => { if (j.xiao.C3) j.xiao.C3.built = LATER; }); + const askOpen = async (re, how) => { + await pg.waitForTimeout(300); + if (!(await pg.locator("#ask").isVisible())) { fail.push(what + ": " + how + " did not ask anything"); return; } + await expect(pg, what, "#askText", re); + await pg.keyboard.press("Escape"); await pg.waitForTimeout(200); + if (await pg.locator("#ask").isVisible()) fail.push(what + ": Escape does not close the question"); + }; + await pg.waitForTimeout(2500); + await pg.click('nav button[data-t="wifi"]'); await pg.waitForTimeout(300); + await pg.fill("#subnet", "192.168.7"); await pg.locator("#pwN").press("Enter"); await pg.waitForTimeout(300); + if (await pg.locator("#ask").isVisible()) fail.push(what + ": Enter in the empty Module network password asked something"); + await pg.fill("#pwN", "agopengps"); await pg.locator("#pwN").press("Enter"); + await askOpen(/^Change the module's subnet to 192\.168\.7 and restart it\?/, "Enter in the Module network password"); + await pg.click('nav button[data-t="prog"]'); await pg.waitForTimeout(300); + if (await pg.locator("#ghInstall").isDisabled()) fail.push(what + ": Install from GitHub is not offered"); + await pg.click("#ghInstall"); await pg.waitForTimeout(200); + await expect(pg, what, "#fwMsg", /^Type the XIAO network password\.$/); + await pg.locator("#pwP").press("Enter"); await pg.waitForTimeout(300); + if (await pg.locator("#ask").isVisible()) fail.push(what + ": Enter in the empty password box asked something"); + await pg.fill("#pwP", "agopengps"); await pg.locator("#pwP").press("Enter"); // the password typed after Install from GitHub asked for it + await askOpen(/^Install [\d.]+, built 31 Dec 2099 23:59 from GitHub \(v[\d.]+_XIAO_ESP32C3\.bin\) on the XIAO\?$/, "Enter after Install from GitHub asked for the password"); + const bin = Buffer.alloc(4096); bin[0] = 0xE9; bin[12] = 5; // an ESP32-C3 program's header (chip id 5) + await pg.setInputFiles("#binFile", {name: "v4.5_XIAO_ESP32C3.bin", mimeType: "application/octet-stream", buffer: bin}); await pg.waitForTimeout(300); + await pg.focus("#pwP"); + await pg.keyboard.down("Enter"); await pg.keyboard.down("Enter"); await pg.keyboard.down("Enter"); await pg.keyboard.up("Enter"); // held: repeats + await askOpen(/^Install v4\.5_XIAO_ESP32C3\.bin on the XIAO\?$/, "a held Enter in the password box"); + if (await pg.locator("#prog").isVisible()) fail.push(what + ": the installation started"); + await pg.close(); + } // the seed drill module's page: the Relay24_ESP32 on the network cable sowing a pass with 19 of its 24 relays (relay 1 = // section 1 and so on, 19 sections set); Settings: the relays set back to the program's pin config (the password asked in a // prompt); @@ -631,6 +666,9 @@ async function teensy(browser, port, q, name) { await shot(pg, "sprayer-card-ask"); await fr.locator("#askYes").press("Escape"); await askClosed(fr, what, "Escape"); await expect(fr, what, "#msgWifi", /^$/); + await fr.locator("#pwW").press("Enter"); // Enter in the password: the same question + await askShown(fr, what, /^Save and restart the module\? Its Wi-Fi goes off/, false); + await fr.locator("#askYes").press("Escape"); await askClosed(fr, what, "Escape"); await fr.click('nav button[data-t="firmware"]'); await fr.waitForTimeout(300); await fr.click("button[onclick*='/api/restart']"); await askShown(fr, what, /^Password of the own Wi-Fi network$/, true); @@ -638,6 +676,8 @@ async function teensy(browser, port, q, name) { await fr.click("#askNo"); await askClosed(fr, what, "Cancel"); await expect(fr, what, "#msgProg", /^$/); await fr.click("button[onclick*='/api/restart']"); + await fr.locator("#askIn").press("Enter"); await fr.waitForTimeout(200); + if (!(await fr.locator("#ask").isVisible())) { fail.push(what + ": Enter with no password typed answers the question"); await fr.click("button[onclick*='/api/restart']"); } await fr.fill("#askIn", "agopengps"); await fr.locator("#askIn").press("Enter"); await askClosed(fr, what, "Enter"); await expectSoon(fr, what, "#msgProg", /^done, restarting/); } @@ -654,6 +694,11 @@ async function teensy(browser, port, q, name) { await shot(pg, "teensy-card-ask"); await fr.click("#askNo"); await askClosed(fr, what, "Cancel"); await expect(fr, what, "#fwMsg", /^$/); + await fr.focus("#reboot"); // a held Enter on Restart Teensy: one question, not also its answer + await pg.keyboard.down("Enter"); await pg.keyboard.down("Enter"); await pg.keyboard.down("Enter"); await pg.keyboard.up("Enter"); + await askShown(fr, what, /^Restart the Teensy\?/, false); + if (await fr.locator("#ask").isVisible()) { await fr.click("#askNo"); await askClosed(fr, what, "Cancel"); } + await expect(fr, what, "#fwMsg", /^$/); await fr.click("#reboot"); await fr.click("#askYes"); await askClosed(fr, what, "OK"); await expectSoon(fr, what, "#fwMsg", /^Restarting/); diff --git a/Firmware/v4.5_AiO_WEB/README.md b/Firmware/v4.5_AiO_WEB/README.md index e2a51b4..705cf72 100644 --- a/Firmware/v4.5_AiO_WEB/README.md +++ b/Firmware/v4.5_AiO_WEB/README.md @@ -31,6 +31,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| +| 13 Nov 2026 | A held Enter no longer answers the page's question too: Enter on a button such as *Restart Teensy* asks once and waits for the answer ([Web page](#web-page)). | | 12 Nov 2026 | A WAS fault or a silent Keya is told on the program's screen again when its AutoSteer button goes on: on a bench with no WAS the button only went red, with no reason ([WAS check](#was-check)). | | 11 Nov 2026 | **Keya**: holding the wheel cuts autosteer, by the motor's own current against the program's Current Turn limit ([Keya kickout](#keya-kickout)). After an update, an old page a browser kept fetches the new one by itself ([Web page](#web-page)). | | 10 Nov 2026 | The UM982's temperature is read (`HWSTATUSA`); one a device does not give reads *not reported* ([Identification](#identification)). Tabs and light/dark button 44 px high, 8 px corners; network name `AiO-xxxx`, with a hyphen ([Web page](#web-page)). | @@ -77,7 +78,7 @@ Open `http:///`, normally `http://192.168.5.126/` (the third number f Dark or light: the ☀ / ☾ button at the top (remembered by the browser), otherwise the device's setting. -Every question the page asks (write to the TM171, load the receiver's settings, switch the WAS check off, install firmware, restart…) opens in a box of the page itself, not a browser pop-up: inside an AgOpenWeb web card the browser's pop-ups show nothing, so those buttons did nothing there (official `Docs/ADD_ON_CARDS.md`: the card's frame has no `allow-modals`). The tabs and the light/dark button at the top are 44 px high, with the same 8 px corners as that box's buttons: a size a finger hits on the cab's screen. +Every question the page asks (write to the TM171, load the receiver's settings, switch the WAS check off, install firmware, restart…) opens in a box of the page itself, not a browser pop-up: inside an AgOpenWeb web card the browser's pop-ups show nothing, so those buttons did nothing there (official `Docs/ADD_ON_CARDS.md`: the card's frame has no `allow-modals`). Enter is its OK, but a held Enter answers nothing: the key that pressed a button cannot also confirm it. The tabs and the light/dark button at the top are 44 px high, with the same 8 px corners as that box's buttons: a size a finger hits on the cab's screen. The page carries the date its own firmware was compiled, and compares it with the one the module reports every second. When they differ, the page on the screen is an old one (seen on 11 Nov 2026 in Firefox after an update: the cards showed the new firmware's data, the top bar was still the old one until a refresh). It then fetches the page again at an address the browser has never seen (`/?b=`), so no kept copy can answer. If the page that comes is still old, a notice at the top says to reload it by hand (Ctrl+F5). After an update or *Go back* asked from the page, the page takes the module as restarted when its time on went back or is behind where it would be had it kept running: a *Go back* a few seconds after start-up used to leave the page waiting for ever. diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/README.md b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/README.md index c5ee8a8..c7bf956 100644 --- a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/README.md +++ b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/README.md @@ -23,7 +23,7 @@ Version 4.5, the same number as the Teensy firmware it goes with (v4.5_AiO_WEB). | Date | What changed | |---|---| -| 13 Nov 2026 | Loading the page no longer stops the XIAO on a weak WiFi link (the Teensy saw "XIAO silent for 10 s"), and the browser keeps the page: a new visit only asks whether it changed ([What it does](#what-it-does)). | +| 13 Nov 2026 | Loading the page no longer stops the XIAO on weak WiFi ("XIAO silent for 10 s"); the browser keeps the page. Enter in a password field does what its button does ([What it does](#what-it-does)). | | 11 Nov 2026 | The page checks that it is the page of the program running: after an update an old page a browser kept (old buttons, new data) fetches the new one by itself ([What it does](#what-it-does)). | | 10 Nov 2026 | Tabs and the light/dark button 44 px high with 8 px corners; the WiFi tab's password to confirm in a card of its own, no longer a field across the page ([What it does](#what-it-does)). Messages give the size of an updated program, not 0 bytes. | | 9 Nov 2026 | The page's questions (save and restart, install, the module's firmware, subnet) in its own box: the browser's pop-ups show nothing inside an AgOpenWeb web card, so those buttons did nothing there ([What it does](#what-it-does)). | @@ -69,6 +69,7 @@ Version 4.5, the same number as the Teensy firmware it goes with (v4.5_AiO_WEB). - Changes (WiFi, update, restart) **ask for the password of the XIAO's own network**, to confirm. - The question before each one (save and restart, install, the module's firmware, the subnet) opens in a box of the page itself, not a browser pop-up: inside an AgOpenWeb web card the browser's pop-ups show nothing, so those buttons did nothing there. + - **Enter** in a password field does what its button does, and the question still comes first. On the Firmware tab: the action that asked for the password (*Type the XIAO network password*), else **Install** with a `.bin` chosen, else **Install from GitHub**; never **Restart**. An empty field does nothing, and a held Enter does not also answer the question it opened. - The tabs and the light/dark button at the top are 44 px high, with the same 8 px corners as that box's buttons: a size a finger hits on the cab's screen. - On the WiFi tab the password and **Save and restart the XIAO** have a card of their own, *Confirm and save*, as wide as the other two. - The page carries the date its own program was compiled and compares it with the one the XIAO reports. When they differ (an old page the browser kept after an update), it fetches the page again at an address the browser has never seen (`/?b=`); if that page is still old, a notice says to reload it by hand. diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/xPage.h b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/xPage.h index c66049c..63a5894 100644 --- a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/xPage.h +++ b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32/xPage.h @@ -221,7 +221,11 @@ const ask=text=>askShow(text,false); const askText=text=>askShow(text,true).then(ok=>ok?$("askIn").value:null); $("askYes").addEventListener("click",()=>askEnd(true));$("askNo").addEventListener("click",()=>askEnd(false)); $("ask").addEventListener("click",e=>{if(e.target===$("ask"))askEnd(false);}); -$("ask").addEventListener("keydown",e=>{if(e.key==="Escape")askEnd(false);else if(e.key==="Enter"&&e.target===$("askIn"))askEnd(true);}); +// Enter is OK (on the OK button, or in the field once something is typed); a held Enter answers nothing, so the key press +// that opened the question cannot also answer it +$("ask").addEventListener("keydown",e=>{if(e.key==="Escape")askEnd(false); + else if(e.key==="Enter"){if(e.repeat){e.preventDefault();return;} + if(e.target===$("askIn")){e.preventDefault();if($("askIn").value&&!e.isComposing)askEnd(true);}}}); // ---- light / dark: the phone's setting until the header button is used, then this phone's choice function themeNow(){return document.documentElement.dataset.theme||(matchMedia("(prefers-color-scheme: light)").matches?"light":"dark");} function themeShow(){$("theme").textContent=themeNow()==="light"?"☾":"☀";} @@ -384,6 +388,7 @@ async function loadSettings(){try{const s=await getJ("/api/settings");if(s.error f.apPass.value="";f.staPass.value="";f.staPass.placeholder=s.staPassSet?"(saved)":"";}catch(e){}} $("fs").addEventListener("submit",async e=>{e.preventDefault();const f=$("fs"),m=$("setMsg"); if(f.apPass.value&&f.apPass.value.length<8){m.textContent="The password must have 8 characters or more.";m.className="note bad";return;} + if(!f.pw.value){m.textContent="Type the current password of the XIAO network.";m.className="note bad";f.pw.focus();return;} if(!await ask("Save and restart the XIAO? The XIAO WiFi goes off for a few seconds."))return; const p=new URLSearchParams();for(const k of ["apSsid","apPass","staSsid","staPass","host","pw"])p.set(k,f[k].value);p.set("staOn",f.staOn.checked?"1":"0"); try{const j=await getJ("/api/settings",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:p.toString()}); @@ -394,6 +399,7 @@ async function loadSettings(){try{const s=await getJ("/api/settings");if(s.error $("subnetBtn").addEventListener("click",async()=>{ const v=$("subnet").value.trim(),m=$("subnetMsg"),ok=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(v); if(!ok||ok.slice(1).some(x=>+x>255)){m.textContent="Three numbers, like 192.168.5.";m.className="note bad";return;} + if(!$("pwN").value){m.textContent="Type the XIAO network password.";m.className="note bad";$("pwN").focus();return;} const cur=lastTeensy&&lastTeensy.s&&lastTeensy.s.ip?lastTeensy.s.ip:"",last=cur?cur.split(".")[3]:"126"; if(cur&&cur.split(".").slice(0,3).join(".")===v){m.textContent="The module is on that subnet already.";m.className="note";return;} if(!await ask("Change the module's subnet to "+v+" and restart it?\n\nThe module will be at "+v+"."+last+"; its page opens at that IP from a device on that network, and AgOpenWeb / AgOpenGPS must use subnet "+v+"."))return; @@ -420,6 +426,19 @@ function netsMsg(list){const n=list.length;$("scanMsg").textContent=n?(n===1?"1 m.textContent="no answer";}); // the password typed once is used in both tabs (only in this page, never saved) for(const id of ["pwW","pwP"])$(id).addEventListener("input",e=>{for(const o of ["pwW","pwP"])if(o!==id)$(o).value=e.target.value;}); +// Enter in a password box does what its card's button does (13 Nov 2026): these boxes are not in a form (the Save one is, +// and the browser submits it), so on their own they did nothing. An empty box or a held key does nothing; every action asks +// before it starts. +function onEnter(id,act){$(id).addEventListener("keydown",e=>{if(e.key!=="Enter"||e.isComposing)return;e.preventDefault(); + if(!e.repeat&&$(id).value)act();});} +onEnter("pwN",()=>$("subnetBtn").click()); +// The «Update or restart» password: the action that asked for it ("Type the XIAO network password"), else Install with a +// .bin chosen, else Install from GitHub with a newer version there. Never Restart. +let pwFor=null; +function pwAsk(again){pwFor=again;$("pwP").focus();} +onEnter("pwP",()=>{if(busy)return;const a=pwFor;pwFor=null; + if(a)a();else if(!$("install").disabled)$("install").click();else if(!$("ghInstall").disabled)ghInstall(); + else fwMsg("Nothing to install: choose a .bin first.","warn");}); // ---- program function fwMsg(t,c){const m=$("fwMsg");m.textContent=t;m.className="note "+(c||"");} @@ -502,7 +521,8 @@ async function ghInstall(){ if(!ghNewer()||busy)return; const chip=myChip(); if(!chip){fwMsg("It is not known whether this XIAO is a C3 or an S3.","bad");return;} - if(!$("pwP").value){fwMsg("Type the XIAO network password.","bad");$("pwP").focus();return;} + if(!$("pwP").value){fwMsg("Type the XIAO network password.","bad");pwAsk(ghInstall);return;} + pwFor=null; const bdir="v"+gh.ver+"_XIAO_ESP32_bin",name="v"+gh.ver+"_XIAO_ESP32"+chip+".bin"; if(!await ask(("Install "+verText(gh.ver,ghBuilt())+" from GitHub ("+name+") on the XIAO?")))return; busy=true;$("install").disabled=true;$("ghInstall").disabled=true;const p=$("prog");p.classList.remove("hidden");p.value=0; @@ -565,15 +585,16 @@ function renderTeensyFw(){ r.onerror=()=>{inf.textContent="The file could not be read.";inf.className="note bad";}; r.readAsText(f); }); -function tPassword(){if($("pwP").value)return true;tMsg("Type the XIAO network password (Update or restart card).","bad");$("pwP").focus();return false;} -$("tInstall").addEventListener("click",async()=>{ +function tPassword(again){if($("pwP").value){pwFor=null;return true;}tMsg("Type the XIAO network password (Update or restart card).","bad");pwAsk(again);return false;} +async function tInstall(){ const f=$("tHexFile").files[0];if(!f||tBusy)return; if(!f.name.toLowerCase().endsWith(".hex")){tMsg("Choose a .hex file.","bad");return;} - if(!tPassword())return; + if(!tPassword(tInstall))return; const b=tHex&&tHex.fw?" ("+tHex.fw+", built "+tHex.built+")":""; if(!await ask(("Install "+f.name+b+" on the module (Teensy) over the link?\n\nAutosteer must be off and the tractor stopped. Autosteer is off during the update, about a minute.")))return; sendTeensyHex(f,f.name,$("tForce").checked,0); -}); +} +$("tInstall").addEventListener("click",tInstall); // The .hex to the XIAO and on to the Teensy; progress from p0 to 95 %, then the Teensy's answer function sendTeensyHex(blob,name,force,p0){ tBusy=true;busy=true;renderTeensyFw();const p=$("tProg");p.classList.remove("hidden");p.value=p0; @@ -600,7 +621,7 @@ function tWaitCheck(t){ } async function tGhInstall(){ const g=gh&&gh.teensy;if(!g||tBusy)return; - if(!tPassword())return; + if(!tPassword(tGhInstall))return; const names=[g.file,gh.dir+".hex",gh.dir+".ino.hex"].filter(Boolean); // the Arduino IDE exports vX.Y_AiO_WEB.ino.hex if(!await ask(("Install "+verText(gh.ver,g.built)+" from GitHub on the module (Teensy) over the link?\n\nAutosteer must be off and the tractor stopped. Autosteer is off during the update, about a minute.")))return; tBusy=true;busy=true;renderTeensyFw();const p=$("tProg");p.classList.remove("hidden");p.value=0;tMsg("Downloading from GitHub…"); @@ -616,16 +637,17 @@ async function tGhInstall(){ }catch(e){tBusy=false;busy=false;p.classList.add("hidden");renderTeensyFw();tMsg("Not downloaded: "+e.message+". The module's current firmware keeps running.","bad");} } $("tGhInstall").addEventListener("click",tGhInstall); -$("tRevert").addEventListener("click",async()=>{ +async function tRevert(){ const s=lastTeensy&&lastTeensy.s,p=s&&s.prev;if(!p||tBusy)return; - if(!tPassword())return; + if(!tPassword(tRevert))return; if(!await ask(("Put the module (Teensy) back to its previous firmware, "+tName(p.fw)+" built "+(builtText(p.built)||p.built)+"?\n\nAutosteer must be off and the tractor stopped. The firmware running now is replaced; the settings are kept.")))return; tBusy=true;renderTeensyFw();tMsg("Asking the module…"); try{const j=await getJ("/api/teensy-revert",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:"pw="+encodeURIComponent($("pwP").value)}); if(j.ok)tWaitStart("The module is going back to its previous firmware and restarting — do not switch the power off…"); else{tBusy=false;renderTeensyFw();tMsg("Not done: "+j.error+". The module's current firmware keeps running.","bad");} }catch(e){tBusy=false;renderTeensyFw();tMsg("No connection to the XIAO.","bad");} -}); +} +$("tRevert").addEventListener("click",tRevert); $("reboot").addEventListener("click",async()=>{if(busy||!await ask("Restart the XIAO?"))return; try{const j=await getJ("/api/restart",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:"pw="+encodeURIComponent($("pwP").value)}); fwMsg(j.ok?"Restarting…":"Error: "+j.error,j.ok?"warn":"bad");}catch(e){fwMsg("No connection to the XIAO.","bad");}}); diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/README.md b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/README.md index 6c24a69..d837538 100644 --- a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/README.md +++ b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/README.md @@ -4,7 +4,7 @@ The same program (`v4.5_XIAO_ESP32`, for the Teensy firmware v4.5_AiO_WEB), comp The ESP32C3 and the ESP32S3 are different chips: each needs its own file. -**Built:** 13 Nov 2026 (C3 13:25, S3 13:25), from the program as it is after the pull request that keeps the link to the Teensy going while the page loads on a weak WiFi link. Compiled in the cloud with the ESP32 core 3.3.12 (arduino-cli, the board's default settings, the same as in the program's README). +**Built:** 13 Nov 2026 (C3 16:14, S3 16:14), from the program as it is after the pull request that makes Enter in the page's password fields do what their buttons do. Compiled in the cloud with the ESP32 core 3.3.12 (arduino-cli, the board's default settings, the same as in the program's README). When the program in `v4.5_XIAO_ESP32` changes, compile these files again and upload them over the old ones, with the same names. **Install from GitHub** on the XIAO page offers a file when it was compiled later than the program running on the XIAO (the date is read from the file). | File | What for | diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3.bin b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3.bin index 4b66f7e..2a3daa9 100644 Binary files a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3.bin and b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3.bin differ diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3_USB.bin b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3_USB.bin index ff3fc11..0f970c5 100644 Binary files a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3_USB.bin and b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32C3_USB.bin differ diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3.bin b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3.bin index a379c52..c436d05 100644 Binary files a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3.bin and b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3.bin differ diff --git a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3_USB.bin b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3_USB.bin index 6abb4bb..749d6fd 100644 Binary files a/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3_USB.bin and b/Firmware/v4.5_AiO_WEB/v4.5_XIAO_ESP32_bin/v4.5_XIAO_ESP32S3_USB.bin differ diff --git a/Firmware/v4.5_AiO_WEB/zWebPage.h b/Firmware/v4.5_AiO_WEB/zWebPage.h index 96f6e55..42b5551 100644 --- a/Firmware/v4.5_AiO_WEB/zWebPage.h +++ b/Firmware/v4.5_AiO_WEB/zWebPage.h @@ -434,7 +434,11 @@ const ask=text=>askShow(text,false); const askText=text=>askShow(text,true).then(ok=>ok?$("askIn").value:null); $("askYes").addEventListener("click",()=>askEnd(true));$("askNo").addEventListener("click",()=>askEnd(false)); $("ask").addEventListener("click",e=>{if(e.target===$("ask"))askEnd(false);}); -$("ask").addEventListener("keydown",e=>{if(e.key==="Escape")askEnd(false);else if(e.key==="Enter"&&e.target===$("askIn"))askEnd(true);}); +// Enter is OK (on the OK button, or in the field once something is typed); a held Enter answers nothing, so the key press +// that opened the question cannot also answer it +$("ask").addEventListener("keydown",e=>{if(e.key==="Escape")askEnd(false); + else if(e.key==="Enter"){if(e.repeat){e.preventDefault();return;} + if(e.target===$("askIn")){e.preventDefault();if($("askIn").value&&!e.isComposing)askEnd(true);}}}); const PAGE_BUILT=")WEBPAGE" __DATE__ " " __TIME__ R"WEBPAGE("; let lastS=null,lastSet=null,tab="status",online=false,logAfter=0,logPaused=false,umAfter=0,umUntil=0,lastUp=0,upBefore=0,waitingBoot=false,busy=false; const fmt=(v,d,u)=>(v===null||v===undefined||Number.isNaN(v))?"–":Number(v).toFixed(d)+(u||""); diff --git a/Seeder/Seeder_ESP32/README.md b/Seeder/Seeder_ESP32/README.md index 429bc1b..5ec4c47 100644 --- a/Seeder/Seeder_ESP32/README.md +++ b/Seeder/Seeder_ESP32/README.md @@ -10,7 +10,7 @@ by a 12 V DPDT relay. Two boards: - a **KinCony KC868-A32** (ESP32, 32 relays, network socket), each relay switching an external DPDT relay. -Version 0.2.7, made from [Sprayer_ESP32](../../Sprayer/Sprayer_ESP32) v0.8 when the seed drill +Version 0.2.8, made from [Sprayer_ESP32](../../Sprayer/Sprayer_ESP32) v0.8 when the seed drill got its own folder. Written from the Relay24_ESP32 design and KinCony's pin list: it has not run on either board yet. @@ -20,6 +20,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| +| 13 Nov 2026 | **0.2.8**: Enter in the password at the end of *Network settings* saves, as its button does; in the page's question box an empty password or a held Enter answers nothing ([How it works](#how-it-works)). | | 10 Nov 2026 | **0.2.7**: the tabs at the top of the page are 44 px high with 8 px corners, like the buttons of the page's question box: easier to hit on the cab's screen ([How it works](#how-it-works)). | | 9 Nov 2026 | **0.2.5**: *Sections on the seed drill* (Settings): the Sowing tab shows all before the program asks, relays past them idle. **0.2.6**: questions in the page's own box: the browser's pop-ups show nothing in an AgOpenWeb web card ([How it works](#how-it-works)). | | 7 Nov 2026 | **0.2.2**: add-on card (`_aowcard._tcp`) under its Wi-Fi name; `_aow-ui` and `/aow/status` gone. **0.2.3**: four tabs as the sprayer's ([How it works](#how-it-works)). **0.2.4**: Module tab: Module card, one Network settings card. Code shared with the sprayer marked ([Notes](#notes)). | @@ -67,7 +68,10 @@ Relay24_ESP32 (192.168.5.123) ──► DPDT relays 1–19 ──► 19 actuator in the [simulator](https://renatorgr.github.io/Boards/seeder/), rebuilt from this code on every change. Every question the page asks (save and restart, install a program, the password) opens in a box of the page itself, not a browser pop-up: inside an AgOpenWeb web - card the browser's pop-ups show nothing, so those buttons did nothing there. The tabs at the + card the browser's pop-ups show nothing, so those buttons did nothing there. Enter is its OK, + but not in an empty password field, and a held Enter answers nothing (the key that pressed a + button cannot also confirm it). Enter in the password at the end of *Network settings* saves, + as its button does. The tabs at the top are 44 px high, with the same 8 px corners as that box's buttons: a size a finger hits on the cab's screen. @@ -124,7 +128,7 @@ The relays close when their PCF8574 pin is **LOW** (`RELAY_ON_HIGH 0`). 4. Relay24_ESP32: plug the DevKitC's USB socket into the computer (the board can be powered or not). KC868-A32: power it with 12 V and plug its USB socket in, as in KinCony's guide. *Tools → Port*: its COM port. Click *Upload*. -5. Serial Monitor at **115200**: it prints `Seeder_ESP32 v0.2.7` and `ready`. +5. Serial Monitor at **115200**: it prints `Seeder_ESP32 v0.2.8` and `ready`. Later program updates: *Sketch → Export compiled binary* in the Arduino IDE, then on the page's *Firmware* tab *Choose .bin* (`Seeder_ESP32.ino.bin` from this folder, not the diff --git a/Seeder/Seeder_ESP32/Seeder_ESP32.ino b/Seeder/Seeder_ESP32/Seeder_ESP32.ino index 8f4e5bf..04e399a 100644 --- a/Seeder/Seeder_ESP32/Seeder_ESP32.ino +++ b/Seeder/Seeder_ESP32/Seeder_ESP32.ino @@ -9,7 +9,7 @@ Licence: GNU General Public License v3 or later (Firmware/v4.5_AiO_WEB/COPYING), WITHOUT ANY WARRANTY. SPDX-License-Identifier: GPL-3.0-or-later */ -// Seeder_ESP32.ino v0.2.7 (2026-11-10), not yet run on a board. +// Seeder_ESP32.ino v0.2.8 (2026-11-13), not yet run on a board. // // Boards (BOARD below): // - Relay24_ESP32 (folder Relay24_ESP32 of this repository): an ESP32 DevKitC V4 plugged into a board with @@ -62,7 +62,7 @@ #include "dPage.h" #define FW_NAME "Seeder_ESP32" -#define FW_VERSION "0.2.7" +#define FW_VERSION "0.2.8" // ---------------------------------------------------------------- board and pins // BOARD says which board this is (change the line below, or compile with -DBOARD=...): diff --git a/Seeder/Seeder_ESP32/Seeder_ESP32.ino.bin b/Seeder/Seeder_ESP32/Seeder_ESP32.ino.bin index 4f5bc00..07e6d2b 100644 Binary files a/Seeder/Seeder_ESP32/Seeder_ESP32.ino.bin and b/Seeder/Seeder_ESP32/Seeder_ESP32.ino.bin differ diff --git a/Seeder/Seeder_ESP32/Seeder_ESP32.ino.merged.bin b/Seeder/Seeder_ESP32/Seeder_ESP32.ino.merged.bin index 08ffb39..635aa48 100644 Binary files a/Seeder/Seeder_ESP32/Seeder_ESP32.ino.merged.bin and b/Seeder/Seeder_ESP32/Seeder_ESP32.ino.merged.bin differ diff --git a/Seeder/Seeder_ESP32/dPage.h b/Seeder/Seeder_ESP32/dPage.h index 9bf4fb3..7a8ee49 100644 --- a/Seeder/Seeder_ESP32/dPage.h +++ b/Seeder/Seeder_ESP32/dPage.h @@ -228,7 +228,11 @@ const ask=text=>askShow(text,false); const askText=text=>askShow(text,true).then(ok=>ok?$('askIn').value:null); $('askYes').onclick=()=>askEnd(true);$('askNo').onclick=()=>askEnd(false); $('ask').onclick=e=>{if(e.target===$('ask'))askEnd(false)}; -$('ask').onkeydown=e=>{if(e.key==='Escape')askEnd(false);else if(e.key==='Enter'&&e.target===$('askIn'))askEnd(true)}; +// Enter is OK (on the OK button, or in the field once something is typed); a held Enter answers nothing, so the key press +// that opened the question cannot also answer it +$('ask').onkeydown=e=>{if(e.key==='Escape')askEnd(false); + else if(e.key==='Enter'){if(e.repeat){e.preventDefault();return} + if(e.target===$('askIn')){e.preventDefault();if($('askIn').value&&!e.isComposing)askEnd(true)}}}; // relay functions as the module sends them: 1-64 a section, 101-105 the program's other functions const FN={0:'–',101:'Lift ↓',102:'Lift ↑',103:'Tram R',104:'Tram L',105:'Geo'}; function fnName(f){return f>=1&&f<=64?'S'+f:(FN[f]||'?')} @@ -314,6 +318,9 @@ async function saveWifi(){const m=$('msgWifi'); if(!$('pwW').value){m.innerHTML='Type the password of the own Wi-Fi network.';$('pwW').focus();return} if(!await ask('Save and restart the module? Its Wi-Fi goes off for a few seconds and every section closes.'))return; post('/api/wifi',{apSsid:$('apSsid').value,apPass:$('apPass').value,staOn:$('staOn').checked?'1':'0',staSsid:$('staSsid').value,staPass:$('staPass').value,host:$('host').value,subnet:$('subnetIn').value.trim(),ipLast:$('ipLast').value},'msgWifi',$('pwW').value)} +// Enter in the Save card's password saves, as its button does (the box is not in a form, so on its own it did nothing); +// an empty box or a held key does nothing +$('pwW').onkeydown=e=>{if(e.key!=='Enter'||e.isComposing)return;e.preventDefault();if(!e.repeat&&$('pwW').value)saveWifi()}; // "Search for networks": the networks the module hears (GET /api/scan, asked again until the list comes), one per // line, strongest first; a tap chooses one (as on the XIAO page) function sig(r){return r>=-60?'strong':r>=-72?'medium':'weak'} diff --git a/Sprayer/Sprayer_ESP32/README.md b/Sprayer/Sprayer_ESP32/README.md index 52a6713..97b8ce1 100644 --- a/Sprayer/Sprayer_ESP32/README.md +++ b/Sprayer/Sprayer_ESP32/README.md @@ -4,7 +4,7 @@ Section control for an 8 m sprayer boom with 8 sections of 1 m, driven by **AgOp **AgOpenGPS** over Wi-Fi, on an **ESP32 8-relay board**. It also measures boom pressure and flow, and has its own web page. -Version 0.10.6 (successor of [Sprayer_Nano](../Sprayer_Nano) v0.3): runs on the bench, not yet +Version 0.10.7 (successor of [Sprayer_Nano](../Sprayer_Nano) v0.3): runs on the bench, not yet tested on the sprayer. ## Changelog @@ -13,6 +13,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| +| 13 Nov 2026 | **0.10.7**: Enter in the password at the end of *Network settings* saves, as its button does; in the page's question box an empty password or a held Enter answers nothing ([How it works](#how-it-works)). | | 10 Nov 2026 | **0.10.6**: the tabs at the top of the page are 44 px high with 8 px corners, like the buttons of the page's question box: easier to hit on the cab's screen ([How it works](#how-it-works)). | | 9 Nov 2026 | **0.10.5**: the page's questions (save and restart, install, password) in its own box: the browser's pop-ups show nothing inside an AgOpenWeb web card, so those buttons did nothing there ([How it works](#how-it-works)). | | 7 Nov 2026 | **0.10.2**: add-on card (`_aowcard._tcp`) under its Wi-Fi name; `_aow-ui` and `/aow/status` gone. **0.10.3**: alarms also as PGN 221 ([Messages](#messages)). **0.10.4**: Module tab: Module card, one Network settings card. Code shared with the seed drill marked ([Notes](#notes)). | @@ -81,7 +82,10 @@ ESP32_Relay_X8 board ──► 8 relays ──► 8 solenoid valves (NC), one pe A `.bin` for another board, another module's program or the `.merged.bin` is refused. Every question the page asks (save and restart, install a program, the password) opens in a box of the page itself, not a browser pop-up: inside an AgOpenWeb web card the browser's - pop-ups show nothing, so those buttons did nothing there. The tabs at the top are 44 px high, + pop-ups show nothing, so those buttons did nothing there. Enter is its OK, but not in an empty + password field, and a held Enter answers nothing (the key that pressed a button cannot also + confirm it). Enter in the password at the end of *Network settings* saves, as its button does. + The tabs at the top are 44 px high, with the same 8 px corners as that box's buttons: a size a finger hits on the cab's screen. Without the board, the page can be tried in the [simulator](https://renatorgr.github.io/Boards/sprayer/), rebuilt from this code on every change. @@ -150,7 +154,7 @@ divider (to-GND ÷ total) in the page's calibration. 4. *Tools → Port*: the adapter's COM port. 5. Hold **IO0**, press and release **EN**, release IO0. Click *Upload*. When it says "Hard resetting", press **EN** once. -6. Serial Monitor at **115200**: it prints `Sprayer_ESP32 v0.10.6` and `ready`. +6. Serial Monitor at **115200**: it prints `Sprayer_ESP32 v0.10.7` and `ready`. "Failed to connect": TX and RX are probably swapped, or step 5 was skipped. diff --git a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino index 3fba738..82a5392 100644 --- a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino +++ b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino @@ -7,7 +7,7 @@ Licence: GNU General Public License v3 or later (file COPYING), WITHOUT ANY WARRANTY. SPDX-License-Identifier: GPL-3.0-or-later */ -// Sprayer_ESP32.ino v0.10.6 (2026-11-10), not yet tested on the sprayer. +// Sprayer_ESP32.ino v0.10.7 (2026-11-13), not yet tested on the sprayer. // // Board: "ESP32_Relay_X8" (303E32DC810): ESP32-WROOM-32E, 8 relays, 5-30 V DC in, LM2596 on board. // Or any ESP32 dev module with relay modules, up to 16 relays (BOARD_DEVKIT below). @@ -55,7 +55,7 @@ #include "sPage.h" #define FW_NAME "Sprayer_ESP32" -#define FW_VERSION "0.10.6" +#define FW_VERSION "0.10.7" // ---------------------------------------------------------------- board and pins // BOARD says which board this is (change the line below, or compile with -DBOARD=...): diff --git a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.bin b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.bin index 64c41c6..7a2fa20 100644 Binary files a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.bin and b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.bin differ diff --git a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.merged.bin b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.merged.bin index c6494df..9f4344b 100644 Binary files a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.merged.bin and b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino.merged.bin differ diff --git a/Sprayer/Sprayer_ESP32/sPage.h b/Sprayer/Sprayer_ESP32/sPage.h index e2ec365..bbcb23c 100644 --- a/Sprayer/Sprayer_ESP32/sPage.h +++ b/Sprayer/Sprayer_ESP32/sPage.h @@ -272,7 +272,11 @@ const ask=text=>askShow(text,false); const askText=text=>askShow(text,true).then(ok=>ok?$('askIn').value:null); $('askYes').onclick=()=>askEnd(true);$('askNo').onclick=()=>askEnd(false); $('ask').onclick=e=>{if(e.target===$('ask'))askEnd(false)}; -$('ask').onkeydown=e=>{if(e.key==='Escape')askEnd(false);else if(e.key==='Enter'&&e.target===$('askIn'))askEnd(true)}; +// Enter is OK (on the OK button, or in the field once something is typed); a held Enter answers nothing, so the key press +// that opened the question cannot also answer it +$('ask').onkeydown=e=>{if(e.key==='Escape')askEnd(false); + else if(e.key==='Enter'){if(e.repeat){e.preventDefault();return} + if(e.target===$('askIn')){e.preventDefault();if($('askIn').value&&!e.isComposing)askEnd(true)}}}; // relay functions as the module sends them: 1-64 a section, 101-105 the program's other functions const FN={0:'–',101:'Lift ↓',102:'Lift ↑',103:'Tram R',104:'Tram L',105:'Geo'}; function fnName(f){return f>=1&&f<=64?'S'+f:(FN[f]||'?')} @@ -399,6 +403,9 @@ async function saveWifi(){const m=$('msgWifi'); if(!$('pwW').value){m.innerHTML='Type the password of the own Wi-Fi network.';$('pwW').focus();return} if(!await ask('Save and restart the module? Its Wi-Fi goes off for a few seconds and every section closes.'))return; post('/api/wifi',{apSsid:$('apSsid').value,apPass:$('apPass').value,staOn:$('staOn').checked?'1':'0',staSsid:$('staSsid').value,staPass:$('staPass').value,host:$('host').value,subnet:$('subnetIn').value.trim(),ipLast:$('ipLast').value},'msgWifi',$('pwW').value)} +// Enter in the Save card's password saves, as its button does (the box is not in a form, so on its own it did nothing); +// an empty box or a held key does nothing +$('pwW').onkeydown=e=>{if(e.key!=='Enter'||e.isComposing)return;e.preventDefault();if(!e.repeat&&$('pwW').value)saveWifi()}; // "Search for networks": the networks the module hears (GET /api/scan, asked again until the list comes), one per // line, strongest first; a tap chooses one (as on the XIAO page) function sig(r){return r>=-60?'strong':r>=-72?'medium':'weak'}