From 3c564df84998af960e395bd5da45e87ab7c2de64 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:00:12 +0000 Subject: [PATCH] Seeder 0.2.5: "Sections on the seed drill" setting, the page shows them before the program talks The seed drill page drew only the sections the program had asked for since the start (secTop), at least 16, so a module that had not heard AgOpenWeb yet (or just restarted) showed 16 sections instead of the drill's 19. A new setting on the Settings tab, "Sections on the seed drill" (0-64, 0 = as many as the program asks for, kept in flash as "secs"), tells the page how many sections to draw from the start; a relay that would follow a section past the last one does nothing, so the relay card now reads "24 relays, 19 of them sections". /api/settings and /api/status carry "secs"; /api/relays takes it with "first". Simulator: the pretend module has the setting at 19; the smoke test checks the Settings value, relay 20 idle and the 19 sections with the cable never plugged in (?wifi=ap). READMEs: changelog rows and the setup step. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VKEBDHD5gzEhTiu3KLnBSx --- Firmware/simulator/README.md | 2 +- Firmware/simulator/sim-seeder.js | 20 +++++++++++++------- Firmware/simulator/smoke-test.js | 19 +++++++++++++++++-- Seeder/Seeder_ESP32/README.md | 7 +++++++ Seeder/Seeder_ESP32/Seeder_ESP32.ino | 4 +++- Seeder/Seeder_ESP32/dPage.h | 10 ++++++---- Seeder/Seeder_ESP32/zNet.ino | 13 ++++++++++--- Seeder/Seeder_ESP32/zOutputs.ino | 7 +++++-- Seeder/Seeder_ESP32/zSettings.ino | 7 +++++-- 9 files changed, 67 insertions(+), 22 deletions(-) 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);