// config.example.h — copy to config.h and edit. config.h is gitignored. // // cp config.example.h config.h // // Every value you need to change lives here. You should not have to edit the // .ino to get a working node. #pragma once // ─── Bench mode ───────────────────────────────────────────────────────────── // 1 = skip WiFi entirely and just print readings to the Serial Monitor, fast. // 0 = normal operation: read both sensors, and POST to the base station. // // START AT 1. Prove both probes read through the ESP8266 before you add WiFi to // the list of things that could be broken. One thing at a time is the whole // trick to bringing hardware up. #define BENCH_MODE 0 // ─── WiFi ─────────────────────────────────────────────────────────────────── // The hotspot from Module 4 (the Android base station). Ignored in bench mode. #define WIFI_SSID "FarmIoT" #define WIFI_PASSWORD "change-me" // ─── Where to send readings ───────────────────────────────────────────────── // LEAVE POST_HOST EMPTY and the node finds the base station by itself. // // The phone running the hotspot IS this node's gateway, by definition — so the // node already knows its address the moment it connects. You never have to look // up an IP. // // This is not just convenience. Android RANDOMISES the hotspot subnet: ours came // up on 10.215.63.55, not the 192.168.43.1 every guide quotes, and it can shuffle // again on any hotspot restart or reboot. A hardcoded IP goes stale SILENTLY — // the node keeps reading perfectly and publishes into the void, every node in the // field stops reporting, and the only symptom is absence. // // Set POST_HOST only to override: a real server on the LAN, a different host, etc. // #define POST_HOST "192.168.1.50" #define POST_HOST "" #define POST_PORT 1880 #define POST_PATH_WATER "/water" // Test the endpoints before you blame the firmware — from any device on the hotspot: // curl -X POST http://:1880/water -H 'Content-Type: application/json' \ // -d '{"node":"test","ok":true,"depth_mm":500}' // The node prints the phone's address on serial as `gateway :` when it connects. // Name for this node's water reading. If you run more than one water node, make // this unique — and make sure no OTHER node is already using the name. Two // devices publishing under one name is a real failure we have had: the dashboard // interleaves them and reports a healthy node as faulty. // // (The soil probe is not on this board — it has its own sketch in // firmware/soil-node-sleep/, with its own NODE_ID.) #define NODE_ID_WATER "water-tank-1" // How often to read and send, in seconds. Ignored in bench mode, which polls // every 2s so you get a fast feedback loop while wiring. #define REPORT_INTERVAL_S 60 // ─── Water probe calibration ──────────────────────────────────────────────── // THE DRY READING. Power the probe up sitting in air (NOT in the tank) and read // the raw value — see Step 4 of the Module 2 page on the site. It will NOT // be zero. Ours read 26. Put YOUR number here. // // This is per-probe and it drifts over months. Re-measure if readings look off. #define DRY_OFFSET_COUNTS 26 // Water depth above the probe when the tank is FULL, in mm. Measure this once. // Set to 0 if you don't know it yet — the node reports depth in mm and simply // omits the percentage. #define TANK_FULL_MM 0 // ─── Valve (Module 2 — MANUAL control for now) ────────────────────────────── // The valve is driven through a relay on this pin. Water sits on D7/D1, leaving // D2 (and, since the soil bus moved off this board, D6/D5) free. Don't move it // onto a boot-strap pin (D3/D4/D8) or D0. #define VALVE_PIN D2 // Most 1-channel relay boards are ACTIVE-LOW: IN LOW energises the relay. That // suits us — at boot the pin floats and the board's pull-up holds it de-energised, // so the valve is CLOSED before code runs. If your relay clicks the wrong way // (opens when it should close), set this to 0. #define VALVE_ACTIVE_LOW 1 // How often (seconds) to ask the base station for the desired valve state, so the // page button feels responsive. Sensors still report every REPORT_INTERVAL_S. #define VALVE_POLL_S 1 // Endpoint on the base station that returns "1" (open) or "0" (closed). #define VALVE_PATH "/valve" // Safety: shut an open valve after this many seconds WITHOUT hearing the base // station say "open". 0 = disabled — but think hard before you set that. // // This is the LAST line of defence, and the only one that lives on the node // itself. A failed valve poll deliberately HOLDS the last state (so a brief WiFi // blip doesn't cycle your valve) — which means an OPEN valve plus a dead base // station stays open until someone walks out to it. Nothing upstream can save // you, because the thing that failed is the link to upstream. // // It counts from the last poll that said "open", so a planned run of any length is // never cut while the base station is reachable — the run length is set there, by // the page's timed buttons. Default is 30 minutes of silence: long enough to ride out // WiFi trouble, far shorter than the time it takes to empty a tank onto one spot. // Don't set it to 0 on a node you leave alone. #define VALVE_MAX_OPEN_S 1800 // ─── Pack voltage sense (Module 3) ────────────────────────────────────────── // Two resistors across the pack put a quarter of it on A0: // // pack+ ──[100k]──┬──[33k]── P− (P−, never B−) // └── A0 // // BEFORE the A0 wire ever touches the board: meter that junction to P− with the // wire disconnected. Expect ~3.1 V on a full 12.6 V pack — the unloaded ratio is // 33/133. Under 3.3 V is the pass. If you read ~9.5 V the two resistors are the // wrong way round; that failure is loud on purpose. 12.6 V into a 3.3 V pin kills // an ESP8266 instantly, permanently, with no warning. // // Connected, the junction drops to ~2.9 V because the NodeMCU's own 220k/100k on // A0 loads the bottom leg. That is expected, not a fault. // // mV per ADC count. Do NOT calculate this — MEASURE it. Flash with BENCH_MODE 1, // read `pack_raw` off the serial line, meter the pack at the same moment: // // PACK_MV_PER_COUNT = (meter millivolts) / (pack_raw counts) // // Theory says ~13.7 for this node's as-built divider — R1 and R2 measured // standalone on the meter's 200k range at 99.5 kOhm and 32.41 kOhm, i.e. a // bang-on 100k/33k pair (2026-09-08). An earlier reading on the megaohm range // said 91k/26k and was simply wrong: that range reads ~20 % low down in the tens // of kilohms. Use the 200k range for these parts. But the onboard divider is not // held to 1 % on a clone board, so yours will still differ and it is per-board. Take a SECOND point at a different // pack voltage: one point sets the constant, the second proves the divider is // linear and that you haven't got a cold joint. #define PACK_MV_PER_COUNT 13.6f // Flip to 1 once you have taken both calibration points. Until then the node // publishes "pack_cal":false and says so on serial at boot, so nobody reads a // placeholder as a measurement. #define PACK_CALIBRATED 0 // ─── Phone charger duty cycle (Stage 2) ───────────────────────────────────── // Relay on the POSITIVE line, between the fuse and the phone buck's INPUT: // // pack+ ─[fuse]─► relay COM ─ NO ─► buck IN+ ─► OUT+ ─► USB +5V ─► phone // P− ──────────────────────────────► buck IN− ─► OUT− ─► USB GND // D+ ⎯ D− shorted // // ⚠ Break the RED only — the negative rail stays continuous. // ⚠ This buck powers the PHONE ONLY. If it also fed the ESP, the node would // switch off its own supply and never come back. Give the ESP its own feed // off the fuse, upstream of this relay. // ⚠ Set the buck's output to 5.0 V and confirm it with a meter BEFORE the USB // socket is wired. An adjustable buck ships at whatever the pot is at. // // Relay control side: VCC → the ESP's 5 V buck OUT+ (= ESP VIN), GND → ESP GND, // IN → below. ⚠ NOT ESP VBUS: it is only live on USB. On the pack it sits at ~3 V // and the relay never pulls in (metered 2026-09-10: VIN 5 V, VBUS 3 V). #define CHARGER_PIN D6 // GPIO12, free since the soil bus left this board // 0 = IN HIGH energises. This node's relay boards are active-HIGH (2026-07-17), // same as the valve. If the fresh relay clicks the wrong way, flip this to 1. #define CHARGER_ACTIVE_LOW 0 // Hysteresis, in millivolts. Between OFF and ON nothing happens — that dead band // is what stops the relay hunting when the phone's own charging load drags the // pack back across a single setpoint. // // ⚠ These are ESTIMATES until a week of logged pack curve says otherwise. That is // the whole reason Stage 1 shipped first. Set them from what THIS pack does in // THIS sun, not from these numbers. // ⚠ Do NOT read them as state of charge: while the MPPT is in CV the terminal sits // near 12.6 V regardless of how full the pack really is, so ON arrives earlier in // the morning than SoC alone would suggest. That is fine — it is daylight, which // is the point. #define PACK_ON_MV 12300 // there is surplus — charge the phone #define PACK_OFF_MV 11600 // pack is falling — phone coasts on its own battery // Sanity band on the RAW A0 counts. 12.6 V full lands near 920 and the ADC // saturates about 14 V, so a count outside this is an open divider, a lost ground // bond or a wedged ADC — a fault, not a flat battery. The charger holds OFF and the // node publishes "pack_ok":false so the dashboard can tell the two apart. #define PACK_RAW_MIN 100 #define PACK_RAW_MAX 1010 // ─── OTA — over-the-air firmware update ───────────────────────────────────── // Reflash this node over WiFi, no USB cable. This is what makes the node // maintainable once it's on battery, sealed in a box, or up a tank — you push a // new build from your laptop on the same hotspot instead of unwiring it. // // FIRST FLASH IS STILL USB. OTA can only receive an update once OTA-capable // firmware is ALREADY running. So flash this build over the cable one last time; // every update after that can go wireless. // // 1 = enable, 0 = disable. Auto-off in bench mode (no WiFi there anyway). #define OTA_ENABLE 1 // The name the node advertises. Appears as a network port in the Arduino IDE and // resolves as .local via mDNS, so you never chase its IP. Make it // unique per node if you run several. #define OTA_HOSTNAME "farm-node-1" // Password required to push an update. DO NOT ship a node with this empty — // anyone on the hotspot could otherwise overwrite your firmware. Leave "" only // on the bench. Change it before the node leaves your desk. #define OTA_PASSWORD "change-me-ota"