diff --git a/.github/scripts/check_shared_blocks.py b/.github/scripts/check_shared_blocks.py new file mode 100644 index 0000000..086b41d --- /dev/null +++ b/.github/scripts/check_shared_blocks.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Shared code check: the blocks the sprayer and the seed drill modules have in common must stay the same. + +Sprayer/Sprayer_ESP32 and Seeder/Seeder_ESP32 are two programs with one origin: the Wi-Fi, the web helpers, the +settings for the network, the PGN handling and the relay driving are the same code in both. Those parts are +marked in the sources, with the same name in both programs: + + // ==== shared "web-helpers": ... + ... the code ... + // ==== end shared "web-helpers" + +This script reads every .ino of both programs, pairs the blocks by name and fails when a block differs, is +missing from one program, is nested, or is not closed. Run it from the repository root: no arguments, no +packages, exit code 1 when something is wrong. The "Shared code" GitHub Action runs it on every pull request +that touches either program. + +Licence: GNU General Public License v3 or later (Firmware/v4.5_AiO_WEB/COPYING). +SPDX-License-Identifier: GPL-3.0-or-later +""" +import difflib +import glob +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +PROGRAMS = {"Sprayer_ESP32": "Sprayer/Sprayer_ESP32", "Seeder_ESP32": "Seeder/Seeder_ESP32"} +START = re.compile(r'^\s*// ==== shared "([^"]+)"') +END = re.compile(r'^\s*// ==== end shared "([^"]+)"\s*$') + + +def read_blocks(folder, errors): + """{name: (file, first line number, lines)} of one program; problems go to errors""" + found = {} + for path in sorted(glob.glob(os.path.join(ROOT, folder, "*.ino"))): + rel = os.path.relpath(path, ROOT) + with open(path, encoding="utf-8") as f: + lines = f.read().split("\n") + open_name, open_at, body = None, 0, [] + for n, line in enumerate(lines, 1): + m = START.match(line) + if m: + if open_name: + errors.append(f'{rel}:{n}: shared "{m.group(1)}" starts inside "{open_name}" (opened at line {open_at})') + continue + open_name, open_at, body = m.group(1), n, [] + continue + m = END.match(line) + if m: + if not open_name: + errors.append(f'{rel}:{n}: end of shared "{m.group(1)}" without a start') + elif m.group(1) != open_name: + errors.append(f'{rel}:{n}: end of shared "{m.group(1)}" closes "{open_name}" (opened at line {open_at})') + else: + if open_name in found: + errors.append(f'{rel}:{open_at}: shared "{open_name}" is already in {found[open_name][0]}:{found[open_name][1]}') + else: + found[open_name] = (rel, open_at + 1, body) + open_name = None + continue + if open_name: + body.append(line.rstrip()) + if open_name: + errors.append(f'{rel}:{open_at}: shared "{open_name}" is never closed') + return found + + +def main(): + errors = [] + blocks = {name: read_blocks(folder, errors) for name, folder in PROGRAMS.items()} + (a, blocks_a), (b, blocks_b) = blocks.items() + for name in sorted(set(blocks_a) ^ set(blocks_b)): + where = blocks_a.get(name) or blocks_b.get(name) + other = b if name in blocks_a else a + errors.append(f'{where[0]}:{where[1]}: shared "{name}" has no block of that name in {other}') + same_lines = 0 + for name in sorted(set(blocks_a) & set(blocks_b)): + fa, la, ba = blocks_a[name] + fb, lb, bb = blocks_b[name] + if ba == bb: + same_lines += len(ba) + continue + diff = difflib.unified_diff(ba, bb, fromfile=f"{fa}:{la}", tofile=f"{fb}:{lb}", lineterm="", n=1) + errors.append(f'shared "{name}" differs between {a} and {b}: change both the same way, or move the line out of the block\n' + "\n".join(diff)) + for e in errors: + print(e) + if errors: + print(f"{len(errors)} problem(s) in the shared blocks") + return 1 + n = len(set(blocks_a) & set(blocks_b)) + print(f"OK: {n} shared blocks, {same_lines} lines the same in {a} and {b}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/shared-code.yml b/.github/workflows/shared-code.yml new file mode 100644 index 0000000..f661cdd --- /dev/null +++ b/.github/workflows/shared-code.yml @@ -0,0 +1,38 @@ +# Shared code check. +# The sprayer and the seed drill modules (Sprayer/Sprayer_ESP32, Seeder/Seeder_ESP32) have the same code in the +# blocks marked `// ==== shared "name"` ... `// ==== end shared "name"`. A change to one that is not made to the +# other fails this check, so the pull request shows it before it is merged. .github/scripts/check_shared_blocks.py +# does the work (plain Python 3, no packages). It runs only when either program, the script or this file changes. +# Licence: GNU General Public License v3 or later (Firmware/v4.5_AiO_WEB/COPYING). +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Shared code + +on: + pull_request: + paths: + - 'Sprayer/Sprayer_ESP32/**' + - 'Seeder/Seeder_ESP32/**' + - '.github/scripts/check_shared_blocks.py' + - '.github/workflows/shared-code.yml' + push: + branches: [main] + paths: + - 'Sprayer/Sprayer_ESP32/**' + - 'Seeder/Seeder_ESP32/**' + - '.github/scripts/check_shared_blocks.py' + - '.github/workflows/shared-code.yml' + +permissions: + contents: read + +jobs: + blocks: + name: blocks + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Compare the shared blocks of the sprayer and the seed drill programs + run: python3 .github/scripts/check_shared_blocks.py diff --git a/CLAUDE.md b/CLAUDE.md index f3e548d..5019604 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,10 @@ text to paste into GitHub as a raw markdown code block. page find new versions on GitHub by those names (see the firmware `CLAUDE.md`, "Common Gotchas"). - The firmware steers a tractor: safety code (WAS check, cut-offs, Keya heartbeat, watchdog) must keep working. - Pull requests: one topic each, a short plain description of what changes for Renato and how to test it. +- `Sprayer/Sprayer_ESP32` and `Seeder/Seeder_ESP32` have the same code in the blocks marked `// ==== shared "name"` … + `// ==== end shared "name"` (Wi-Fi, web helpers, network settings, PGN handling, relays). Change both the same way, + or move a line out of the block: `.github/scripts/check_shared_blocks.py` compares them on every pull request + (the "Shared code" check). What is only one module's (sensors, rate, the cable, the relays setting) stays outside. - README changelogs: one short row per day (a summary of up to 240 characters, with links to the sections that hold the details). `.github/scripts/check_changelog.py` checks them on every pull request (the "Docs" check). diff --git a/Firmware/simulator/sim-sprayer.js b/Firmware/simulator/sim-sprayer.js index a3d3bc4..46071c2 100644 --- a/Firmware/simulator/sim-sprayer.js +++ b/Firmware/simulator/sim-sprayer.js @@ -164,7 +164,7 @@ function wifiPost(b) { if (b.staPass || staSsid !== X.sta) X.staPassSet = !!b.staPass; // a new network without a password: an open one Object.assign(X, {ap: b.apSsid, staOn: staOn, sta: staSsid, host: b.host, subnet: net, ipLast: b.ipLast === undefined ? X.ipLast : +b.ipLast}); if (b.apPass) X.pass = b.apPass; - event("Wi-Fi settings saved from the page, restarting"); + event("network settings saved from the page, restarting"); restart(3000); return S.ok({ok: true, restart: true}); } diff --git a/Seeder/Seeder_ESP32/README.md b/Seeder/Seeder_ESP32/README.md index 0254d2f..64c004d 100644 --- a/Seeder/Seeder_ESP32/README.md +++ b/Seeder/Seeder_ESP32/README.md @@ -20,7 +20,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| -| 7 Nov 2026 | **0.2.2**: add-on card (`_aowcard._tcp`) under its Wi-Fi name; `_aow-ui` and `/aow/status` gone ([Notes](#notes)). **0.2.3**: four tabs as the sprayer page's, with Firmware ([How it works](#how-it-works)). **0.2.4**: Module tab: a Module card, one Network settings card. | +| 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. | | 27 Oct 2026 | **0.1**: the seed drill part of Sprayer_ESP32 0.8 in its own folder: KC868-A32, 19 sections, no sensors, no PGN 160. **0.1.1**: PGN 237 in the AiO v26 layout, the sections above 16 after it, so the [AiO module page](#what-the-aio-module-page-shows) shows them all. | @@ -270,5 +270,10 @@ messages with a right checksum from source `0x7F` (the program). announcement (`Docs/ADD_ON_CARDS.md`), both under the module's own Wi-Fi name (`Seeder-xxxx`, "Name on the network" on the page), unique to each module. The earlier `_aow-ui._tcp` and `GET /aow/status` (0.1 to 0.2.1) are gone. +- This program grew out of the sprayer module (`Sprayer/Sprayer_ESP32`): the Wi-Fi, the web helpers, the + network settings, the PGN handling and the relay driving are the same code in both, marked in the sources + as `// ==== shared "name"` … `// ==== end shared "name"`. A change to one of those blocks goes in both + programs: the "Shared code" check (`.github/scripts/check_shared_blocks.py`) compares them on every pull + request. The network cable, the sections above 16 and the relays setting are this program's own. - Licence: GNU General Public License v3 or later (`Firmware/v4.5_AiO_WEB/COPYING`). Written 2026 by Claude (Anthropic) for renatorgr. diff --git a/Seeder/Seeder_ESP32/Seeder_ESP32.ino b/Seeder/Seeder_ESP32/Seeder_ESP32.ino index ca3cec8..4452d2f 100644 --- a/Seeder/Seeder_ESP32/Seeder_ESP32.ino +++ b/Seeder/Seeder_ESP32/Seeder_ESP32.ino @@ -172,6 +172,7 @@ bool isRaise = false, isLower = false; uint32_t last239Ms = 0, last229Ms = 0, lastHelloMs = 0; bool have239 = false, have229 = false, haveHello = false; bool linkOk = false; // a PGN 239 or 229 arrived in the last NO_DATA_TIMEOUT_MS +bool masterOn = true; // the seed drill has no master switch (the sprayer's follows its switch): always on uint32_t count239 = 0, count229 = 0, countHello = 0, countCrcErr = 0, countOther = 0; bool staWasUp = false, ethWasUp = false; @@ -203,6 +204,7 @@ String defaultApSsid(); String moduleName(); // ---------------------------------------------------------------- small helpers +// ==== shared "pgn-checksum": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // AgOpenGPS checksum: sum of the bytes from the 3rd up to the one before the checksum uint8_t pgnChecksum(const uint8_t* msg, size_t length) { @@ -212,8 +214,10 @@ uint8_t pgnChecksum(const uint8_t* msg, size_t length) } bool staUp() { return cfg.staOn && WiFi.status() == WL_CONNECTED; } +// ==== end shared "pgn-checksum" bool netUp() { return staUp() || ethUp(); } // on the tractor network, over the cable or Wi-Fi +// ==== shared "log-event": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) void logEvent(const char* fmt, ...) { char* b = eventLog[eventNext]; @@ -226,6 +230,7 @@ void logEvent(const char* fmt, ...) Serial.println(b); eventNext = (eventNext + 1) % 12; } +// ==== end shared "log-event" static void printStatus() { diff --git a/Seeder/Seeder_ESP32/zNet.ino b/Seeder/Seeder_ESP32/zNet.ino index 82136d5..b11fb59 100644 --- a/Seeder/Seeder_ESP32/zNet.ino +++ b/Seeder/Seeder_ESP32/zNet.ino @@ -102,6 +102,7 @@ void wifiWatch() else if (cfg.staOn) logEvent("lost the tractor network '%s'", cfg.staSsid.c_str()); } +// ==== shared "web-helpers": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // ---------------------------------------------------------------- web helpers static void sendJson(const String& body, int code = 200) { @@ -142,7 +143,9 @@ static String jsonHex(uint64_t v) else snprintf(b, sizeof(b), "\"%lx\"", (unsigned long)v); return String(b); } +// ==== end shared "web-helpers" +// ==== shared "request-helpers": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // A whole number lo..hi in the request (only digits), else false static bool argNumber(const char* name, long lo, long hi, long& out) { @@ -171,6 +174,7 @@ static bool captiveRedirect() server.send(302, "text/plain", ""); return true; } +// ==== end shared "request-helpers" // ---------------------------------------------------------------- web answers static void apiStatus() @@ -210,6 +214,7 @@ static void apiStatus() for (uint8_t i = 0; i < NUM_RELAYS; i++) { if (i) j += ","; j += String(relayFn(i)); } j += "]}"; + // ==== shared "status-log": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) j += ",\"log\":["; bool first = true; for (uint8_t k = 0; k < 12; k++) @@ -222,6 +227,7 @@ static void apiStatus() } j += "]}"; sendJson(j); + // ==== end shared "status-log" } static void apiSettingsGet() @@ -233,6 +239,7 @@ static void apiSettingsGet() sendJson(j); } +// ==== shared "network-settings-post": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // "192.168.1" -> the three numbers; false when it is not three numbers 0-255 static bool parseSubnet(const String& text, uint8_t out[3]) { @@ -310,6 +317,7 @@ static void apiScan() scanState = -1; sendJson("{\"busy\":true}"); } +// ==== end shared "network-settings-post" // Relay 1 follows section "first", the next relays the next sections (0 = the program's pin config) static void apiRelaysPost() @@ -325,6 +333,7 @@ static void apiRelaysPost() sendJson("{\"ok\":true}"); } +// ==== shared "restart-update": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) static void apiRestartPost() { if (!passwordOk()) return; @@ -369,6 +378,7 @@ static void notFound() if (captiveRedirect()) return; server.send(404, "text/plain", "not found"); } +// ==== end shared "restart-update" void webStart() { diff --git a/Seeder/Seeder_ESP32/zOutputs.ino b/Seeder/Seeder_ESP32/zOutputs.ino index cdb0312..3611cd2 100644 --- a/Seeder/Seeder_ESP32/zOutputs.ino +++ b/Seeder/Seeder_ESP32/zOutputs.ino @@ -18,6 +18,7 @@ // Relay24_ESP32, on a LOW PCF8574 pin on the KC868-A32. // cfg.invertOutputs (PGN 238, byte 8 bit 0; the official module calls it isRelayActiveHigh) turns that // round: 0 = as built, 1 = inverted. +// ==== shared "relay-level": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // The program sends this setting at start-up and when the machine profile is loaded, so the // "relay active high" box in its machine settings normally stays unticked. @@ -27,6 +28,7 @@ static int relayLevel(bool on) const bool drive = on != (cfg.invertOutputs != 0); return (drive == (RELAY_ON_HIGH != 0)) ? HIGH : LOW; } +// ==== end shared "relay-level" static uint8_t raiseTimer = 0, lowerTimer = 0, lastTrigger = 0; @@ -112,6 +114,7 @@ void outputsInit() } #endif +// ==== shared "outputs": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // Everything off, whatever the polarity setting says (used before a restart or a program update) void outputsAllOff() { @@ -160,7 +163,7 @@ void applyOutputs() if (top > secTop) secTop = top; } - const uint64_t req = linkOk ? sectionsReq : 0; + const uint64_t req = (linkOk && masterOn) ? sectionsReq : 0; const bool other[6] = { false, // other[f - FN_OTHER] linkOk && isLower, linkOk && isRaise, linkOk && (tramline & 1), linkOk && (tramline & 2), @@ -180,4 +183,5 @@ void applyOutputs() relaysWrite(bits); relayBits = bits; sectionsOn = open; +// ==== end shared "outputs" } diff --git a/Seeder/Seeder_ESP32/zPgn.ino b/Seeder/Seeder_ESP32/zPgn.ino index d647548..af2e5e0 100644 --- a/Seeder/Seeder_ESP32/zPgn.ino +++ b/Seeder/Seeder_ESP32/zPgn.ino @@ -57,6 +57,7 @@ void sendSignOfLife() sendPgn(msg, sizeof(msg), broadcastIp()); } +// ==== shared "pgn-common-cases": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddress from) { switch (pgn) @@ -113,6 +114,7 @@ static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddre logEvent("machine config from the program: lift %s, raise %u s, lower %u s, outputs %s", cfg.hydEnable ? "on" : "off", cfg.raiseTime, cfg.lowerTime, cfg.invertOutputs ? "INVERTED (relay active high ticked)" : "normal"); break; +// ==== end shared "pgn-common-cases" case 236: // output functions, 24 entries if (dataLength < 24) return; @@ -123,6 +125,7 @@ static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddre cfg.firstSection ? " (not used: the relays follow the sections in a row)" : ""); break; + // ==== shared "pgn-subnet-receive": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) case 201: // subnet: save and restart with the new IP if (dataLength == 5 && d[5] == 201 && d[6] == 201) { @@ -164,3 +167,4 @@ void udpPoll() handlePgn(pgn, rx, dataLength, udp.remoteIP()); } } + // ==== end shared "pgn-subnet-receive" diff --git a/Seeder/Seeder_ESP32/zSettings.ino b/Seeder/Seeder_ESP32/zSettings.ino index 24cc8cf..bc449f7 100644 --- a/Seeder/Seeder_ESP32/zSettings.ino +++ b/Seeder/Seeder_ESP32/zSettings.ino @@ -26,6 +26,7 @@ void settingsLoad() { Settings d; // the defaults prefs.begin("seeder", true); + // ==== shared "settings-wifi-load": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) cfg.apSsid = prefs.getString("apSsid", defaultApSsid()); cfg.apPass = prefs.getString("apPass", DEFAULT_AP_PASS); cfg.staOn = prefs.getBool("staOn", false); @@ -34,6 +35,7 @@ void settingsLoad() cfg.host = prefs.getString("host", DEFAULT_HOST); if (prefs.getBytesLength("subnet") == 3) prefs.getBytes("subnet", cfg.subnet, 3); cfg.ipLast = prefs.getUChar("ipLast", d.ipLast); + // ==== end shared "settings-wifi-load" cfg.firstSection = prefs.getUChar("first", d.firstSection); cfg.raiseTime = prefs.getUChar("raise", d.raiseTime); cfg.lowerTime = prefs.getUChar("lower", d.lowerTime); @@ -58,6 +60,7 @@ void settingsLoad() void settingsSave() { prefs.begin("seeder", false); + // ==== shared "settings-wifi-save": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) prefs.putString("apSsid", cfg.apSsid); prefs.putString("apPass", cfg.apPass); prefs.putBool("staOn", cfg.staOn); @@ -66,6 +69,7 @@ void settingsSave() prefs.putString("host", cfg.host); prefs.putBytes("subnet", cfg.subnet, 3); prefs.putUChar("ipLast", cfg.ipLast); + // ==== end shared "settings-wifi-save" prefs.putUChar("first", cfg.firstSection); prefs.putUChar("raise", cfg.raiseTime); prefs.putUChar("lower", cfg.lowerTime); @@ -81,6 +85,7 @@ void settingsSave() // Only the Wi-Fi settings and the host name go back to the defaults (a forgotten password); the // program's config, the output functions, the module IP and the relays setting stay +// ==== shared "settings-reset-wifi": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) void settingsResetWifi() { cfg.apSsid = defaultApSsid(); @@ -91,3 +96,4 @@ void settingsResetWifi() cfg.host = DEFAULT_HOST; settingsSave(); } +// ==== end shared "settings-reset-wifi" diff --git a/Sprayer/Sprayer_ESP32/README.md b/Sprayer/Sprayer_ESP32/README.md index 6e5f542..cbb0a7a 100644 --- a/Sprayer/Sprayer_ESP32/README.md +++ b/Sprayer/Sprayer_ESP32/README.md @@ -13,7 +13,7 @@ One row per day, in short; the details are in the sections of this file (the lin | Date | What changed | |---|---| -| 7 Nov 2026 | **0.10.2**: add-on card (`_aowcard._tcp`) under its Wi-Fi name; `_aow-ui` and `/aow/status` gone ([Notes](#notes)). **0.10.3**: alarms also as PGN 221, on the program's screen ([Messages](#messages)). **0.10.4**: Module tab: a Module card, one Network settings card. | +| 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)). | | 2 Nov 2026 | **0.10**: rate (L/ha), target pressure and alarms worked out on the module and sent to the program (PGN 161); the page gets Spraying, [Settings](#setup), Module and Firmware tabs, with Install from GitHub. **0.10.1**: Wi-Fi settings as on the XIAO page. | | 27 Oct 2026 | **0.7.1** sensor dividers from a common resistor kit. **0.7.2** the page says which sensors there are. **0.8** up to 64 sections, relays from any section. **0.9** sprayer boards only (the seed drill got its own folder). **0.9.1** PGN 237 in the AiO v26 layout. | | 26 Oct 2026 | **0.6**: all 8 relays are sections by default. **0.7**: also a plain ESP32 with [relay modules](#a-separate-esp32-with-relay-modules), up to 16 relays. The relay number above its function on the page. | @@ -306,6 +306,11 @@ level the module is using. it to the right one or to −1. - The FPD-270A valves and the YF-S201 are plastic and made for water. Rinse with clean water after each job. Fit a filter before the flow meter. +- The seed drill module (`Seeder/Seeder_ESP32`) grew out of this program: the Wi-Fi, the web helpers, the + network settings, the PGN handling and the relay driving are the same code in both, marked in the sources + as `// ==== shared "name"` … `// ==== end shared "name"`. A change to one of those blocks goes in both + programs: the "Shared code" check (`.github/scripts/check_shared_blocks.py`) compares them on every pull + request. The sensors, the rate and the alarms are this program's own. - The ISOBUS group of AgOpenGPS is working on a standard way for modules to send such readings (ISOBUS DDIs). PGN 161 follows that proposal; the PGN number itself is this project's choice until the group picks one. The module keeps sending PGN 160 for the AiO diff --git a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino index 20135ab..52380b7 100644 --- a/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino +++ b/Sprayer/Sprayer_ESP32/Sprayer_ESP32.ino @@ -229,6 +229,7 @@ String defaultApSsid(); String moduleName(); // ---------------------------------------------------------------- small helpers +// ==== shared "pgn-checksum": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // AgOpenGPS checksum: sum of the bytes from the 3rd up to the one before the checksum uint8_t pgnChecksum(const uint8_t* msg, size_t length) { @@ -238,7 +239,9 @@ uint8_t pgnChecksum(const uint8_t* msg, size_t length) } bool staUp() { return cfg.staOn && WiFi.status() == WL_CONNECTED; } +// ==== end shared "pgn-checksum" +// ==== shared "log-event": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) void logEvent(const char* fmt, ...) { char* b = eventLog[eventNext]; @@ -251,6 +254,7 @@ void logEvent(const char* fmt, ...) Serial.println(b); eventNext = (eventNext + 1) % 12; } +// ==== end shared "log-event" static void ledSet(bool on) { diff --git a/Sprayer/Sprayer_ESP32/zNet.ino b/Sprayer/Sprayer_ESP32/zNet.ino index 5227b1f..81c096e 100644 --- a/Sprayer/Sprayer_ESP32/zNet.ino +++ b/Sprayer/Sprayer_ESP32/zNet.ino @@ -62,6 +62,7 @@ void wifiWatch() else if (cfg.staOn) logEvent("lost the tractor network '%s'", cfg.staSsid.c_str()); } +// ==== shared "web-helpers": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // ---------------------------------------------------------------- web helpers static void sendJson(const String& body, int code = 200) { @@ -102,6 +103,7 @@ static String jsonHex(uint64_t v) else snprintf(b, sizeof(b), "\"%lx\"", (unsigned long)v); return String(b); } +// ==== end shared "web-helpers" // A number for the JSON with d decimals; -1 for "none" (a NaN would break the JSON) static String jsonNum(float v, uint8_t d) @@ -124,6 +126,7 @@ static bool argFloat(const char* name, float lo, float hi, float& out) return out >= lo && out <= hi; } +// ==== shared "request-helpers": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // A whole number lo..hi in the request (only digits), else false static bool argNumber(const char* name, long lo, long hi, long& out) { @@ -152,6 +155,7 @@ static bool captiveRedirect() server.send(302, "text/plain", ""); return true; } +// ==== end shared "request-helpers" // ---------------------------------------------------------------- web answers static void apiStatus() @@ -204,6 +208,7 @@ static void apiStatus() for (uint8_t i = 0; i < MAX_TOOL_SECTIONS && i < cfg.toolSections; i++) { if (i) j += ","; j += String(cfg.sectionCm[i]); } j += "]}"; + // ==== shared "status-log": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) j += ",\"log\":["; bool first = true; for (uint8_t k = 0; k < 12; k++) @@ -216,6 +221,7 @@ static void apiStatus() } j += "]}"; sendJson(j); + // ==== end shared "status-log" } static void apiSettingsGet() @@ -232,6 +238,7 @@ static void apiSettingsGet() sendJson(j); } +// ==== shared "network-settings-post": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // "192.168.1" -> the three numbers; false when it is not three numbers 0-255 static bool parseSubnet(const String& text, uint8_t out[3]) { @@ -269,7 +276,7 @@ static void apiWifiPost() s.ipLast = ipLast; cfg = s; settingsSave(); - logEvent("Wi-Fi settings saved from the page, restarting"); + logEvent("network settings saved from the page, restarting"); sendJson("{\"ok\":true,\"restart\":true}"); restartAtMs = millis() + 500; } @@ -309,6 +316,7 @@ static void apiScan() scanState = -1; sendJson("{\"busy\":true}"); } +// ==== end shared "network-settings-post" static void apiCalibPost() { @@ -366,6 +374,7 @@ static void apiTotalPost() sendJson("{\"ok\":true}"); } +// ==== shared "restart-update": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) static void apiRestartPost() { if (!passwordOk()) return; @@ -410,6 +419,7 @@ static void notFound() if (captiveRedirect()) return; server.send(404, "text/plain", "not found"); } +// ==== end shared "restart-update" void webStart() { diff --git a/Sprayer/Sprayer_ESP32/zOutputs.ino b/Sprayer/Sprayer_ESP32/zOutputs.ino index 5689d7b..3ef280d 100644 --- a/Sprayer/Sprayer_ESP32/zOutputs.ino +++ b/Sprayer/Sprayer_ESP32/zOutputs.ino @@ -13,6 +13,7 @@ // RELAY_ON_HIGH (Sprayer_ESP32.ino) says how the relays of the chosen board close: on a HIGH pin // (ESP32_Relay_X8) or on a LOW pin (the usual relay modules). cfg.invertOutputs (PGN 238, byte 8 // bit 0; the official module calls it isRelayActiveHigh) turns that round: 0 = as built, 1 = inverted. +// ==== shared "relay-level": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // The program sends this setting at start-up and when the machine profile is loaded, so the // "relay active high" box in its machine settings normally stays unticked. @@ -22,6 +23,7 @@ static int relayLevel(bool on) const bool drive = on != (cfg.invertOutputs != 0); return (drive == (RELAY_ON_HIGH != 0)) ? HIGH : LOW; } +// ==== end shared "relay-level" static uint8_t raiseTimer = 0, lowerTimer = 0, lastTrigger = 0; @@ -49,6 +51,7 @@ void outputsInit() } } +// ==== shared "outputs": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) // Everything off, whatever the polarity setting says (used before a restart or a program update) void outputsAllOff() { @@ -117,4 +120,5 @@ void applyOutputs() relaysWrite(bits); relayBits = bits; sectionsOn = open; +// ==== end shared "outputs" } diff --git a/Sprayer/Sprayer_ESP32/zPgn.ino b/Sprayer/Sprayer_ESP32/zPgn.ino index 4c4fae6..c2d2f2f 100644 --- a/Sprayer/Sprayer_ESP32/zPgn.ino +++ b/Sprayer/Sprayer_ESP32/zPgn.ino @@ -178,6 +178,7 @@ void sendAlarmMessage() lastMs = now; } +// ==== shared "pgn-common-cases": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddress from) { switch (pgn) @@ -234,6 +235,7 @@ static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddre logEvent("machine config from the program: lift %s, raise %u s, lower %u s, outputs %s", cfg.hydEnable ? "on" : "off", cfg.raiseTime, cfg.lowerTime, cfg.invertOutputs ? "INVERTED (relay active high ticked)" : "normal"); break; +// ==== end shared "pgn-common-cases" case 236: // output functions, 24 entries if (dataLength < 24) return; @@ -263,6 +265,7 @@ static void handlePgn(uint8_t pgn, const uint8_t* d, uint8_t dataLength, IPAddre break; } + // ==== shared "pgn-subnet-receive": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) case 201: // subnet: save and restart with the new IP if (dataLength == 5 && d[5] == 201 && d[6] == 201) { @@ -304,3 +307,4 @@ void udpPoll() handlePgn(pgn, rx, dataLength, udp.remoteIP()); } } + // ==== end shared "pgn-subnet-receive" diff --git a/Sprayer/Sprayer_ESP32/zSettings.ino b/Sprayer/Sprayer_ESP32/zSettings.ino index ea8d9c5..7f931df 100644 --- a/Sprayer/Sprayer_ESP32/zSettings.ino +++ b/Sprayer/Sprayer_ESP32/zSettings.ino @@ -26,6 +26,7 @@ void settingsLoad() { Settings d; // the defaults prefs.begin("sprayer", true); + // ==== shared "settings-wifi-load": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) cfg.apSsid = prefs.getString("apSsid", defaultApSsid()); cfg.apPass = prefs.getString("apPass", DEFAULT_AP_PASS); cfg.staOn = prefs.getBool("staOn", false); @@ -34,6 +35,7 @@ void settingsLoad() cfg.host = prefs.getString("host", DEFAULT_HOST); if (prefs.getBytesLength("subnet") == 3) prefs.getBytes("subnet", cfg.subnet, 3); cfg.ipLast = prefs.getUChar("ipLast", d.ipLast); + // ==== end shared "settings-wifi-load" cfg.raiseTime = prefs.getUChar("raise", d.raiseTime); cfg.lowerTime = prefs.getUChar("lower", d.lowerTime); cfg.hydEnable = prefs.getUChar("hyd", d.hydEnable); @@ -80,6 +82,7 @@ void settingsLoad() void settingsSave() { prefs.begin("sprayer", false); + // ==== shared "settings-wifi-save": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) prefs.putString("apSsid", cfg.apSsid); prefs.putString("apPass", cfg.apPass); prefs.putBool("staOn", cfg.staOn); @@ -88,6 +91,7 @@ void settingsSave() prefs.putString("host", cfg.host); prefs.putBytes("subnet", cfg.subnet, 3); prefs.putUChar("ipLast", cfg.ipLast); + // ==== end shared "settings-wifi-save" prefs.putUChar("raise", cfg.raiseTime); prefs.putUChar("lower", cfg.lowerTime); prefs.putUChar("hyd", cfg.hydEnable); @@ -115,6 +119,7 @@ void settingsSave() // Only the Wi-Fi settings and the host name go back to the defaults (a forgotten password); the // program's config, the output functions and the calibration stay +// ==== shared "settings-reset-wifi": the same in Sprayer_ESP32 and Seeder_ESP32 (change both; check_shared_blocks.py compares them) void settingsResetWifi() { cfg.apSsid = defaultApSsid(); @@ -125,3 +130,4 @@ void settingsResetWifi() cfg.host = DEFAULT_HOST; settingsSave(); } +// ==== end shared "settings-reset-wifi"