Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Firmware/simulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
20 changes: 13 additions & 7 deletions Firmware/simulator/sim-seeder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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.",
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
19 changes: 17 additions & 2 deletions Firmware/simulator/smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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/);
Expand Down Expand Up @@ -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";
Expand Down
7 changes: 7 additions & 0 deletions Seeder/Seeder_ESP32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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).
Expand Down Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion Seeder/Seeder_ESP32/Seeder_ESP32.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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=...):
Expand Down Expand Up @@ -143,6 +143,8 @@ struct Settings
uint8_t subnet[3] = { 192, 168, 5 }; // the program's subnet (PGN 201); the module is <subnet>.<ipLast>
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;
Expand Down
10 changes: 6 additions & 4 deletions Seeder/Seeder_ESP32/dPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ a{color:var(--acc)}
<div class="secs rl" id="relays"></div>
<p class="note" id="relayNote">What each relay does comes from the program (Tool → Machine → Pin config).</p>
<div class="field"><span>Relay 1 follows section (the next relays the next sections). 0 = what the program's pin config says (sections 1-16 only)</span><input type="number" id="first" min="0" max="64" step="1"></div>
<div class="field"><span>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</span><input type="number" id="drillSecs" min="0" max="64" step="1"></div>
<div class="row"><button class="act" onclick="saveRelays()">Save</button></div>
<div class="msg" id="msgRelays"></div>
<p class="note">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.</p>
Expand Down Expand Up @@ -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;i<nSecs;i++){const e=$('s'+i),on=bit(d.out.on,i);if(on)open++;e.className=(on?'on':'')+(bit(d.aog.req,i)&&!on?' req':'')}
Expand Down Expand Up @@ -267,7 +269,7 @@ async function refresh(){
async function loadSettings(){
const d=await (await fetch('/api/settings',{cache:'no-store'})).json();
$('apSsid').value=d.apSsid;$('staOn').checked=d.staOn;$('staSsid').value=d.staSsid;$('host').value=d.host;$('subnetIn').value=d.subnet;
$('hostShow').textContent=d.host;$('ipLast').value=d.ipLast||123;$('first').value=d.first||0;
$('hostShow').textContent=d.host;$('ipLast').value=d.ipLast||123;$('first').value=d.first||0;$('drillSecs').value=d.secs||0;
$('apPass').value='';$('staPass').value='';$('staPass').placeholder=d.staPassSet?'(saved)':'';
}
async function post(url,fields,msgId,pass){
Expand Down Expand Up @@ -306,7 +308,7 @@ async function scanNets(){const m=$('scanMsg'),b=$('scan');m.textContent='search
await new Promise(r=>setTimeout(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…';
Expand Down
13 changes: 10 additions & 3 deletions Seeder/Seeder_ESP32/zNet.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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 += "]}";
Expand All @@ -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);
}
Expand Down Expand Up @@ -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}");
}
Expand Down
7 changes: 5 additions & 2 deletions Seeder/Seeder_ESP32/zOutputs.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading