diff --git a/Firmware/simulator/README.md b/Firmware/simulator/README.md index 9b16ae8..9bbd9ad 100644 --- a/Firmware/simulator/README.md +++ b/Firmware/simulator/README.md @@ -29,7 +29,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| -| 9 Nov 2026 | **Set up AgOpenWeb from your measures** (version 1.3): the turn clears the tractor's front corner, the drawbar meets the tyre at its height, every turn listed by headland, reasons behind arrows, legend per colour, fresh script ([the page](#set-up-agopenweb-from-your-measures)). | +| 9 Nov 2026 | **Set up AgOpenWeb from your measures** 1.3: the turn clears the front corner, the drawbar meets the tyre, turns listed by headland, reasons behind arrows ([the page](#set-up-agopenweb-from-your-measures)). The seed drill page shows its 19 sections before the program talks. | | 8 Nov 2026 | **Set up AgOpenWeb from your measures**: a calculator that turns the tractor and implement measures into the U-turn, headland and tramline settings, with the reasons, a drawing, examples and drawbars kept off the rear tyres ([the page](#set-up-agopenweb-from-your-measures)). | | 7 Nov 2026 | The seed drill page's four tabs and Firmware tab checked, with a made-up newer `.bin` "on GitHub"; `firmware.json` lists the seed drill `.bin` once it is on `main`; the modules' names on the network ([How it is built](#how-it-is-built)). | | 4 Nov 2026 | The Teensy's clock and temperature, the receivers' and the TM171's temperatures and the XIAO's in the pretend modules' answers. | diff --git a/Firmware/simulator/sim-seeder.js b/Firmware/simulator/sim-seeder.js index 23ae1bb..286dfb5 100644 --- a/Firmware/simulator/sim-seeder.js +++ b/Firmware/simulator/sim-seeder.js @@ -8,7 +8,8 @@ // The seed drill module's answers (zNet.ino: apiStatus, apiSettingsGet, apiWifiPost, apiScan, apiRelaysPost, apiRestartPost, the // program update on /api/update). The sections the program asks for come from the pretend tractor of sim-common.js, the // same the Teensy page's Machine tab shows: Renato's no-till seed drill, 19 sections on the Relay24_ESP32 board (24 relays, -// relay 1 follows section 1 and so on), on the network cable. No pressure or flow sensor. +// relay 1 follows section 1 and so on, "Sections on the seed drill" set to 19 on its Settings tab), on the network cable. +// No pressure or flow sensor. // The own-network password is the module's default, agopengps. "use strict"; (function () { @@ -19,7 +20,7 @@ const AP = "Seeder-4E7C"; // from the ESP3 // sta: on the tractor network (the cable); lost: was, until LOST_AT s (the cable unplugged); ap: the cable not plugged in const NET = S.opt.wifi, LOST_AT = 24; const X = S.load(KEY, () => ({t0: Date.now() - S.RUNNING * 1000, ap: AP, pass: "agopengps", staOn: false, sta: "", staPassSet: false, - host: "seeder", subnet: "192.168.5", ipLast: 123, first: 1, secTop: 0, ev: [], evAt: -1})); + host: "seeder", subnet: "192.168.5", ipLast: 123, first: 1, secs: IMPL.sections, secTop: 0, ev: [], evAt: -1})); const T = () => (Date.now() - X.t0) / 1000; S.bar("No board behind this page: the values are made up. The own-network password is agopengps. Park the tractor to see the sections closed." + " The Relay24_ESP32 board is on the network cable: \"tractor network lost\" = the cable unplugged.", @@ -70,8 +71,10 @@ function seeder() { return {req: req, on: on, kmh: sp.main ? M.tractor(Math.max(0, M.T())).kmh : 0}; } -// what relay i does (relayFn): with X.first, sections in a row from that one; else the program's pin config (sections 1-8) -const relayFn = i => X.first ? (X.first + i <= 64 ? X.first + i : 0) : i < 8 ? i + 1 : 0; +// what relay i does (relayFn): with X.first, sections in a row from that one; else the program's pin config (sections 1-8); +// nothing past the seed drill's last section (X.secs; 0 = no limit) +const top = () => X.secs || 64; +const relayFn = i => X.first ? (X.first + i <= top() ? X.first + i : 0) : i < 8 && i + 1 <= top() ? i + 1 : 0; const hex = v => v.toString(16); // sections as the module sends them (jsonHex) function status() { @@ -89,11 +92,11 @@ function status() { tram: 0, geo: 0, uturn: link && tr.moving && !tr.straight ? 1 : 0, req: hex(sd.req), secTop: X.secTop, ago229: !many ? -1 : link ? 0 : silent, n239: Math.floor(talking() * 10), n229: many ? Math.floor(talking() * 10) : 0, nHello: Math.floor(talking()), crcErr: 0, invert: 0, hydEn: 0, raise: 2, lower: 4}, - out: {bits: bits, on: hex(on), first: X.first, onHigh: true, fn: fn}, + out: {bits: bits, on: hex(on), first: X.first, secs: X.secs, onHigh: true, fn: fn}, log: X.ev.slice()}; } const settings = () => ({apSsid: X.ap, staOn: X.staOn, staSsid: X.sta, staPassSet: X.staPassSet, host: X.host, eth: true, ipLast: X.ipLast, - first: X.first, subnet: X.subnet}); + first: X.first, secs: X.secs, subnet: X.subnet}); const number = (v, lo, hi) => /^\d{1,5}$/.test(String(v)) && +v >= lo && +v <= hi; // argNumber const printable = (v, lo, hi) => typeof v === "string" && v.length >= lo && v.length <= hi && /^[\x20-\x7E]*$/.test(v); @@ -135,8 +138,11 @@ function scan() { function relaysPost(b) { if (b.pw !== X.pass) return S.err(403, "wrong password"); if (!number(b.first, 0, 64)) return S.err(400, "first section: 0 to 64"); + if (b.secs !== undefined && !number(b.secs, 0, 64)) return S.err(400, "sections: 0 to 64"); X.first = +b.first; - event(X.first ? "relays 1-" + RELAYS + " follow sections " + X.first + "-" + Math.min(64, X.first + RELAYS - 1) : "relays follow the program's pin config"); + if (b.secs !== undefined) X.secs = +b.secs; + event(X.first ? "relays 1-" + RELAYS + " follow sections " + X.first + "-" + Math.min(top(), X.first + RELAYS - 1) : "relays follow the program's pin config"); + if (X.secs) event("the seed drill has " + X.secs + " sections"); return S.ok(); } function restartPost(b) { diff --git a/Firmware/simulator/smoke-test.js b/Firmware/simulator/smoke-test.js index 09881f9..33bd067 100644 --- a/Firmware/simulator/smoke-test.js +++ b/Firmware/simulator/smoke-test.js @@ -300,7 +300,8 @@ async function teensy(browser, port, q, name) { 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); Settings: the relays set back to the program's pin config (the password asked in a prompt); + // 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); // Module: the search for Wi-Fi networks; Firmware: the program on GitHub (none until the .bin is on main), wrong files // refused, the report downloaded { @@ -322,8 +323,10 @@ async function teensy(browser, port, q, name) { if (!(await pg.locator("#t-settings").isVisible())) fail.push(what + ": tab Settings does not show"); if ((await pg.locator("#relays span").count()) !== 24) fail.push(what + ": expected the Relay24_ESP32's 24 relays"); if ((await pg.locator("#relays span.on").count()) !== 19) fail.push(what + ": expected 19 relays on while sowing"); - await expect(pg, what, "#relayNote", /^24 relays, 24 of them sections: relay 1 follows section 1/); + await expect(pg, what, "#relayNote", /^24 relays, 19 of them sections: relay 1 follows section 1/); await expect(pg, what, "#r18", /^19S19$/); + await expect(pg, what, "#r19", /^20–$/); + if ((await pg.locator("#drillSecs").inputValue()) !== "19") fail.push(what + ": the Settings tab should show 19 sections on the seed drill"); await pg.fill("#first", "0"); await pg.click("button[onclick='saveRelays()']"); await pg.waitForTimeout(1500); await expectSoon(pg, what, "#msgRelays", /done/); @@ -554,6 +557,18 @@ async function teensy(browser, port, q, name) { await shot(pg, "seeder-lost"); await pg.close(); } + // the seed drill's module before the program ever talked (the cable not plugged in): the 19 sections set on the Settings + // tab show, not the 16 of the program's pin config + { + const what = "Seed drill page, cable unplugged"; + const pg = await page(browser, "http://127.0.0.1:" + port + "/Boards/seeder/?wifi=ap", what); + await pg.waitForTimeout(2500); + await expect(pg, what, "#sub", /not on the tractor network · program silent/); + if ((await pg.locator("#secs span").count()) !== 19) fail.push(what + ": expected the 19 sections set, with the program silent"); + if ((await pg.locator("#secs span.on, #secs span.req").count()) !== 0) fail.push(what + ": a section is open or asked for with the program silent"); + await expect(pg, what, "#detail", /^0 of 19 open/); + await pg.close(); + } // the sprayer's module that lost the tractor's Wi-Fi { const what = "Sprayer page, network lost"; diff --git a/Seeder/Seeder_ESP32/README.md b/Seeder/Seeder_ESP32/README.md index 64c004d..2372886 100644 --- a/Seeder/Seeder_ESP32/README.md +++ b/Seeder/Seeder_ESP32/README.md @@ -20,6 +20,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| +| 9 Nov 2026 | **0.2.5**: *Sections on the seed drill* on the Settings tab: the Sowing tab shows them all before the program has asked for any (it showed 16), relays past them stay idle ([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)). | | 2 Nov 2026 | **0.2.1**: the Wi-Fi settings on the page as on the XIAO page, with a search for networks ([Setup](#setup)). | | 28 Oct 2026 | **0.2**: runs on the [Relay24_ESP32](#hardware-relay24_esp32) board of this repository, now the default; the [KC868-A32](#hardware-kc868-a32) stays as a choice. | @@ -46,6 +47,10 @@ Relay24_ESP32 (192.168.5.123) ──► DPDT relays 1–19 ──► 19 actuator KC868-A32). The page's *Relay 1 follows section* setting can start the row at another section; 0 there gives the relays back to the program's pin config, which only knows sections 1–16. +- **Sections on the seed drill** (Settings tab, 19 for this one): the Sowing tab draws that + many sections from the start, before the program has asked for any (it used to draw 16 until + section 19 was asked for), and a relay past the last section does nothing. 0 = as many as + the program asks for. - No message from the program for **2 seconds** (cable pulled, tablet frozen): every relay drops and every row closes. - No pressure or flow sensor: no PGN 160 (that is the sprayer's). @@ -138,6 +143,8 @@ other board. their widths. With more than 16 sections AgOpenWeb sends PGN 229 by itself, and the page shows *Sections 17-64 (PGN 229): now*. The actuators take a few seconds for their stroke, so set *Tool → Timing → Look ahead on / off* to that time. +5. **Page, Settings tab**: *Sections on the seed drill* = 19 (and *Relay 1 follows section* = 1), + then *Save*: the Sowing tab shows the 19 sections even before AgOpenWeb has opened one. ## Wiring: Relay24_ESP32 diff --git a/Seeder/Seeder_ESP32/Seeder_ESP32.ino b/Seeder/Seeder_ESP32/Seeder_ESP32.ino index 4452d2f..03bacfd 100644 --- a/Seeder/Seeder_ESP32/Seeder_ESP32.ino +++ b/Seeder/Seeder_ESP32/Seeder_ESP32.ino @@ -62,7 +62,7 @@ #include "dPage.h" #define FW_NAME "Seeder_ESP32" -#define FW_VERSION "0.2.4" +#define FW_VERSION "0.2.5" // ---------------------------------------------------------------- board and pins // BOARD says which board this is (change the line below, or compile with -DBOARD=...): @@ -143,6 +143,8 @@ struct Settings uint8_t subnet[3] = { 192, 168, 5 }; // the program's subnet (PGN 201); the module is . uint8_t ipLast = 123; // 123 = the machine module; another number for a second module uint8_t firstSection = 1; // relay 1 = this section, then in a row (0 = the program's pin config) + uint8_t numSections = 0; // sections on the seed drill: the page shows that many, a relay past them does + // nothing (0 = as many as the program asks for) String apSsid, apPass; // the module's own Wi-Fi network bool staOn = false; // the tractor's Wi-Fi network instead of the network cable String staSsid, staPass; diff --git a/Seeder/Seeder_ESP32/dPage.h b/Seeder/Seeder_ESP32/dPage.h index 51a9a35..2766b6a 100644 --- a/Seeder/Seeder_ESP32/dPage.h +++ b/Seeder/Seeder_ESP32/dPage.h @@ -103,6 +103,7 @@ a{color:var(--acc)}

What each relay does comes from the program (Tool → Machine → Pin config).

Relay 1 follows section (the next relays the next sections). 0 = what the program's pin config says (sections 1-16 only)
+
Sections on the seed drill: the Sowing tab shows that many, a relay past them does nothing. 0 = as many as the program asks for

A seed drill with more than 16 sections needs the relays in a row from section 1: AgOpenWeb's pin config only knows sections 1-16, and sends sections 17-64 in PGN 229.

@@ -229,10 +230,11 @@ async function refresh(){ const r=await fetch('/api/status',{cache:'no-store'});const d=await r.json(); $('dot').className='dot'+(d.aog.link?' on':''); $('sub').textContent=(d.wifi.up?'on '+(d.wifi.staUp?d.wifi.sta:'the cable')+' as '+d.wifi.ip:'not on the tractor network')+(d.aog.link?' · program talking':' · program silent'); - // Sowing tab: as many sections as the program has asked for (secTop), at least those the relays follow (up to 16) + // Sowing tab: the sections set on the Settings tab (secs), else as many as the program has asked for (secTop), at + // least those the relays follow (up to 16) $('silent').hidden=d.aog.link; let mapped=0,nSec=0;for(const f of d.out.fn)if(f>=1&&f<=64){nSec++;mapped=Math.max(mapped,f)} - const n=Math.max(8,d.aog.secTop||0,Math.min(16,mapped)); + const n=Math.max(8,d.out.secs||0,d.aog.secTop||0,Math.min(16,mapped)); if(nSecs!==n){nSecs=n;$('secs').innerHTML=cells('s',n,0);$('secs').style.gridTemplateColumns='repeat('+Math.min(n,8)+',1fr)'} let open=0; for(let i=0;isetTimeout(r,800))} m.textContent='no answer'} finally{b.disabled=false}} -function saveRelays(){post('/api/relays',{first:$('first').value},'msgRelays')} +function saveRelays(){post('/api/relays',{first:$('first').value,secs:$('drillSecs').value},'msgRelays')} // the report for an analysis: every answer of the module in one .json file, downloaded by this browser async function report(){ const b=$('dumpBtn'),m=$('dumpMsg');b.disabled=true;m.className='note';m.textContent='Gathering…'; diff --git a/Seeder/Seeder_ESP32/zNet.ino b/Seeder/Seeder_ESP32/zNet.ino index b11fb59..0da9f7a 100644 --- a/Seeder/Seeder_ESP32/zNet.ino +++ b/Seeder/Seeder_ESP32/zNet.ino @@ -210,6 +210,7 @@ static void apiStatus() j += ",\"raise\":" + String(cfg.raiseTime) + ",\"lower\":" + String(cfg.lowerTime) + "}"; j += ",\"out\":{\"bits\":" + String(relayBits) + ",\"on\":" + jsonHex(sectionsOn) + ",\"first\":" + String(cfg.firstSection); + j += ",\"secs\":" + String(cfg.numSections); j += ",\"onHigh\":" + String(RELAY_ON_HIGH ? "true" : "false") + ",\"fn\":["; for (uint8_t i = 0; i < NUM_RELAYS; i++) { if (i) j += ","; j += String(relayFn(i)); } j += "]}"; @@ -235,6 +236,7 @@ static void apiSettingsGet() String j = "{\"apSsid\":" + jsonStr(cfg.apSsid) + ",\"staOn\":" + String(cfg.staOn ? "true" : "false"); j += ",\"staSsid\":" + jsonStr(cfg.staSsid) + ",\"staPassSet\":" + String(cfg.staPass.length() ? "true" : "false"); j += ",\"host\":" + jsonStr(cfg.host) + ",\"eth\":true,\"ipLast\":" + String(cfg.ipLast) + ",\"first\":" + String(cfg.firstSection); + j += ",\"secs\":" + String(cfg.numSections); j += ",\"subnet\":\"" + String(cfg.subnet[0]) + "." + String(cfg.subnet[1]) + "." + String(cfg.subnet[2]) + "\"}"; sendJson(j); } @@ -319,16 +321,21 @@ static void apiScan() } // ==== end shared "network-settings-post" -// Relay 1 follows section "first", the next relays the next sections (0 = the program's pin config) +// Relay 1 follows section "first", the next relays the next sections (0 = the program's pin config); the seed drill has +// "secs" sections (0 = as many as the program asks for) static void apiRelaysPost() { if (!passwordOk()) return; - long first = 0; + long first = 0, secs = cfg.numSections; if (!argNumber("first", 0, MAX_SECTIONS, first)) { sendError(400, "first section: 0 to 64"); return; } + if (server.hasArg("secs") && !argNumber("secs", 0, MAX_SECTIONS, secs)) { sendError(400, "sections: 0 to 64"); return; } cfg.firstSection = first; + cfg.numSections = secs; settingsSave(); - if (first) logEvent("relays 1-%u follow sections %ld-%ld", (unsigned)NUM_RELAYS, first, min((long)MAX_SECTIONS, first + NUM_RELAYS - 1)); + const long top = secs ? secs : MAX_SECTIONS; + if (first) logEvent("relays 1-%u follow sections %ld-%ld", (unsigned)NUM_RELAYS, first, min(top, first + NUM_RELAYS - 1)); else logEvent("relays follow the program's pin config"); + if (secs) logEvent("the seed drill has %ld sections", secs); applyOutputs(); sendJson("{\"ok\":true}"); } diff --git a/Seeder/Seeder_ESP32/zOutputs.ino b/Seeder/Seeder_ESP32/zOutputs.ino index 3611cd2..b63bce8 100644 --- a/Seeder/Seeder_ESP32/zOutputs.ino +++ b/Seeder/Seeder_ESP32/zOutputs.ino @@ -12,6 +12,8 @@ // (the 19-section seed drill has more sections than the program's pin config knows). // - cfg.firstSection 0: what the program gives each relay (PGN 236, kept in cfg.pin): sections 1-16 and // the other functions, for the first 24 relays. +// - cfg.numSections 1-64: the seed drill has that many sections, so a relay that would follow a section past +// them does nothing (0: no limit). The page draws that many sections before the program has asked for any. // Everything is off while the program is silent. // // RELAY_ON_HIGH (Seeder_ESP32.ino) says how the board's relays close: on a HIGH 74HC595 output on the @@ -36,15 +38,16 @@ static uint8_t raiseTimer = 0, lowerTimer = 0, lastTrigger = 0; // tramline right, tramline left, geo stop uint8_t relayFn(uint8_t i) { + const uint8_t top = cfg.numSections ? cfg.numSections : MAX_SECTIONS; // the last section the drill has if (cfg.firstSection) { const uint16_t s = cfg.firstSection + i; - return s <= MAX_SECTIONS ? s : 0; + return s <= top ? s : 0; } if (i >= 24) return 0; const uint8_t p = cfg.pin[i]; if (p >= PIN_HYD_LOWER && p <= PIN_GEO_STOP) return FN_OTHER + (p - PIN_HYD_LOWER + 1); - return p <= 16 ? p : 0; + return p <= 16 && p <= top ? p : 0; } #if BOARD == BOARD_RELAY24 diff --git a/Seeder/Seeder_ESP32/zSettings.ino b/Seeder/Seeder_ESP32/zSettings.ino index bc449f7..56332b8 100644 --- a/Seeder/Seeder_ESP32/zSettings.ino +++ b/Seeder/Seeder_ESP32/zSettings.ino @@ -6,8 +6,8 @@ SPDX-License-Identifier: GPL-3.0-or-later */ // What is kept: the two Wi-Fi networks (the own one and the tractor's), the host name, the program's subnet -// (PGN 201) and the module IP's last number, the first section the relays follow, the machine config -// (PGN 238) and the output functions (PGN 236). Anything missing in flash keeps the default from the +// (PGN 201) and the module IP's last number, the first section the relays follow and the seed drill's +// section count, the machine config (PGN 238) and the output functions (PGN 236). Anything missing in flash keeps the default from the // Settings struct in Seeder_ESP32.ino. String defaultApSsid() @@ -37,6 +37,7 @@ void settingsLoad() cfg.ipLast = prefs.getUChar("ipLast", d.ipLast); // ==== end shared "settings-wifi-load" cfg.firstSection = prefs.getUChar("first", d.firstSection); + cfg.numSections = prefs.getUChar("secs", d.numSections); cfg.raiseTime = prefs.getUChar("raise", d.raiseTime); cfg.lowerTime = prefs.getUChar("lower", d.lowerTime); cfg.hydEnable = prefs.getUChar("hyd", d.hydEnable); @@ -54,6 +55,7 @@ void settingsLoad() if (cfg.host.length() < 1 || cfg.host.length() > 24) cfg.host = DEFAULT_HOST; if (cfg.ipLast < 2 || cfg.ipLast > 254) cfg.ipLast = d.ipLast; if (cfg.firstSection > MAX_SECTIONS) cfg.firstSection = d.firstSection; + if (cfg.numSections > MAX_SECTIONS) cfg.numSections = d.numSections; for (uint8_t i = 0; i < 24; i++) if (cfg.pin[i] > PIN_GEO_STOP) cfg.pin[i] = 0; } @@ -71,6 +73,7 @@ void settingsSave() prefs.putUChar("ipLast", cfg.ipLast); // ==== end shared "settings-wifi-save" prefs.putUChar("first", cfg.firstSection); + prefs.putUChar("secs", cfg.numSections); prefs.putUChar("raise", cfg.raiseTime); prefs.putUChar("lower", cfg.lowerTime); prefs.putUChar("hyd", cfg.hydEnable);