Skip to content

Repository files navigation

Pool Cover Firmware

An Arduino Nano ESP32 replacement for a failed automatic pool cover controller. It drives the cover's two motor relays, stops it when it meets resistance, reports what it is doing over Wi-Fi, and logs three water/air temperature probes.

Current-based obstacle and end-of-travel detection is the heart of it. There are no working limit switches, so the end of travel is detected as an obstruction — to a safety controller they are the same event.

Replacing a Latham / Coverstar 1601 controller?

This is a working replacement for a dead 1601 Electronic Controller (board marked E0301 Rev A / LPE0301RC) — provided your cover is driven by a two-way electric motor. That is the case this was built for and runs on daily.

If you are here because you searched that part number: this is the repository I wanted to find and didn't. Start with Is this right for your cover?, and read the safety disclaimer before building anything — this is a personal project, not a certified product, and it is not equivalent in safety to the controller it replaces.

This is the live firmware. Older versions are read-only under archive/; from here on, versions are git history rather than folders.

Is this right for your cover?

You need a reversible electric motor — the common three-wire arrangement, where the controller energises one conductor to open and another to close:

Motor terminal Function
H1/D1 Direction 1
H2/N Neutral
D2 Direction 2
EARTH Ground

If that matches what your 1601 was wired to, the electrical job is straightforward: two relays driving mechanically interlocked reversing contactors, plus a current transducer on the motor line. Full detail in docs/HARDWARE.md.

It will not work if your cover is hydraulic. The 1601 also drove hydraulic power packs through its S1/S2 solenoid outputs. This firmware has no equivalent — it switches motor direction, nothing else. If S1/S2 were in use on your board, stop here.

What you give up, and what you gain

The 1601 offered a Motor Int. mode that stops the motor from its limit sensors. This has no limit-sensor input at all. Instead it watches motor current and stops when the load says the cover has arrived or hit something — which is why it works on an installation where the limit sensors were dead.

That is a genuine trade. Current sensing detected the end of travel on every recorded run with zero false positives, but it has a documented blind spot: an obstruction that raises motor load by less than ~45 % above normal travel cannot be separated from the cover's own mechanical variation. If your limit sensors work and you like them, a commercial controller keeps them.

The commercial alternative, and what it costs

The 1601 is discontinued. The replacement quote came to about $800 in parts before labour, and I could not get a clear answer on whether the proposed alternative would actually work with my motor.

That is less a complaint than a description of the market. Pool service is a narrow speciality with few competitors in any given area, and like the solar industry it is built around standard installations and standard service contracts. Step outside that and there is often nowhere else to go — you take what is offered, at the price offered, or you deal with it yourself.

For the record, the answer I could not get: yes, it would have worked. The Coverstar A5270 touch controller can drive a three-wire motor directly on high voltage with no control board at all. That is a supported path for a failed 1601 on an electric system, and if you want a warranty and someone else's liability, it is the right call. Ask a Latham or Coverstar dealer:

"I have a failed 1601 Electronic Controller (E0301 Rev A / LPE0301RC) on a 3-wire electric motor system. What is the current replacement or retrofit kit?"

This build came to a bit over $500 — enclosure, contactors, relays, 24 V supply, current transducer, operator controls, temperature probes and the Nano. So the honest comparison is roughly $500 of parts and a lot of weekends against $800 of parts plus labour.

That is not a big enough saving to be the reason. The reasons were being unable to confirm the proposed fix would work, and — as it turned out — a board swap never being what the cover actually needed.

Safety and disclaimer

Read this before deciding to build one.

Safety is the governing constraint on this project, not a consideration added at the end. Every place a shortcut was available it was refused, and those refusals are listed concretely in What actually keeps it safe — including the ones that cost real capability. The limits below are stated plainly for the same reason.

This is not equivalent to the controller it replaces

A commercial pool cover controller is designed, tested, manufactured and supported by a company that carries liability for it. This is a personal project, published in case the approach is useful to someone else in the same situation.

The line-voltage side was not improvised. During a mandatory one-year practicum in college I built closet-sized control enclosures for hydraulic robots and similar plant hardware in car manufacturing — built to someone else's plans, not designed by me, and with none of the software mine. That is where the wiring discipline, the contactor work and the enclosure practice come from.

What is different this time is that the design and the firmware are also mine, which is what the degree and the working years since are for. Both halves of the job in one pair of hands is the new part, not the electrical craft.

That background certifies nothing, and is not offered as a substitute for certification. This has not been evaluated, tested or certified by any safety body, against UL 325 or any other standard for powered covers. It has no independent watchdog hardware, no redundant sensing, no fail-safe brake, and no professional review. It works reliably on one pool. That is the entire extent of the evidence.

Do not read "zero false positives across 20 runs" as a safety rating. It is a measurement on one installation, on one cover, in one set of conditions.

The obstacle detection has a known blind spot

An obstruction that raises motor load by less than ~45 % above normal travel cannot be separated from the cover's own mechanical variation and will not be detected. This is a property of the mechanism, not a bug to be tuned away — normal travel on this cover already varies by 25 % from repeatable mechanical humps.

A soft obstruction — a person, a pet, an inflatable, an arm — may fall well under that threshold. This system cannot be relied on to detect a person.

Pool covers are dangerous independently of any of this

A powered cover is heavy, is a drowning and entrapment hazard, and can injure someone regardless of what is controlling the motor. Never let anyone be on, in, or near the cover or the water while it moves. Keep children away from the controls. Watch the cover through its entire travel, exactly as you would with the manufacturer's controller — hold-to-run operation exists for that reason and this firmware keeps it.

Mains voltage

This switches line voltage to a heavy motor through contactors. Incorrect wiring can cause fire, electrocution or death. If you are not comfortable wiring line-voltage reversing contactors, have a licensed electrician do that part. Check whether your work needs a permit or inspection where you live.

Building this may void warranties on your cover, and may affect your home or liability insurance. Some jurisdictions require listed and certified equipment for powered covers. Those are your responsibility to check.

No warranty

This is provided as-is, with no warranty of any kind, express or implied, including fitness for any particular purpose. Anyone who builds, installs or operates it does so entirely at their own risk and assumes full responsibility for any injury, death or property damage that results. The author accepts no liability of any kind.

If you are not prepared to own that risk, buy a commercial controller. That is a completely reasonable choice, and for most people it is the right one.

What actually keeps it safe

Not the clever part. The obstacle model is the smart layer, not the load-bearing one. The layers that matter are the mechanically interlocked contactors that physically cannot energise both directions, the firmware dead time on direction changes, the hard runtime timeouts that work with the model compiled out entirely — and a person watching the cover move.

The shortcuts that were available and not taken, since they say more than any claim about intent:

  • The old limit sensors were abandoned, not guessed at. Three unknown conductors carrying an undocumented excitation voltage could have been probed into a 3.3 V GPIO. Working endpoint detection was the prize. It was not worth risking the board or trusting a sensor whose behaviour nobody could describe.
  • The interlocks do not depend on the software being right. The contactors are mechanically interlocked and the runtime timeouts still fire with MODEL_ENABLED false. Disable every detector and the cover is still protected.
  • Hold-to-run was kept. Automating the cover so it closes on a button press or a schedule would be a few lines. A person watching it move is a safety layer, and it was not automated away.
  • The blind spot is documented rather than tuned out. Thresholds could have been tightened until the detection rate looked perfect on paper. That buys false trips and false confidence, so the ~45 % limit is published instead.
  • Every threshold traces to a measurement. ModelConfig.h carries the recorded value each number came from, so nothing rests on a figure that seemed about right.
  • The enclosure was rebuilt above water level rather than reusing a location already proven to flood.
  • Detection acts on trustworthy data only. Windows with too few ADC samples are discarded rather than judged, so a starved loop cannot produce a confident wrong answer.

Why this exists

In July 2026 water got into the pool cover's control box and tripped the breaker. The original controller — a Latham/Coverstar 1601 Electronic Controller, board marked E0301 Rev A / LPE0301RC — did not survive. It was a 2015-generation board, long discontinued, and the quote to replace it came to $800 in parts before labour.

Pulling the system apart to understand it turned up three things that made replacing-like-for-like unattractive:

The motor is simple. A 115/230 V three-wire motor: energise one conductor to open, another to close. No hydraulics, no solenoid, none of the feature relays the old board provided. Almost everything the 1601 did was unused.

The limit switches were already dead. Two magnetic limit sensors were installed, one broken long ago. The survivor terminated in a three-wire connector — supply, common, signal — meaning it was an active sensor excited by the old controller, at a voltage nobody documented. And the 1601's Motor Int. setting had been left disabled, so the controller ignored the sensors anyway. That is why the cover had always been operated hold-to-run, watching it and letting go at the end.

So the expensive replacement would have inherited the same blindness. A new controller still would not have known where the cover was.

That reframed the problem. The cover did not need a new controller so much as it needed a way to know it had arrived — and the motor was already broadcasting that, in current draw. Hitting the end of travel and hitting a child's inflatable look identical to the motor: something is resisting, stop. An Arduino Nano ESP32 with a current transducer could read that signal, and it could log every run while doing it.

The old limit wires were left disconnected, insulated, and labelled OLD LIMIT SENSOR rather than cut out, in case the sensor specification ever turns up.

It was never really about the board

Replacing the controller turned out to be the smallest part of the job.

The old control box sat low enough that water got into it — which is what killed the 1601 in the first place. The new enclosure is mounted high above water level, so the original failure mode is designed out rather than repeated.

Then the mechanism itself. The cover housing was full of leaves, and the motor had been dragging that load on every single run. Underneath were ripped limit switches and screws left half driven in — the accumulated workmanship of previous installers and maintainers, in the one part of the installation a customer never looks at. Swapping the board would have left all of it in place, and the new controller would have inherited a mechanism quietly working harder than it should.

Cleaned out, properly fastened, and with the electronics somewhere water cannot reach them, the cover should comfortably manage another few years.

If you build one of these, service the mechanism before you record training data. The obstacle model learns what "normal" load looks like. Do that on a cover full of leaves and you teach it that dragging is normal — and the extra baseline load eats directly into the margin the detectors have to work with. Every threshold in ModelConfig.h was measured on a cleaned, serviced mechanism. Yours will differ.

There is a nice side effect. Because every run's current profile is logged, the telemetry doubles as a mechanical health monitor: if the baseline load starts creeping up across runs, something is binding, dragging or wearing. That is a maintenance signal the original controller never offered.

How the detection was built

Not by guessing thresholds. The controller was wired up, run, and told to record everything: 20 full open and close cycles, alternating directions with a minute or two between them, logging RMS current every 100 ms. That produced the training set still kept in archive/data/.

The runs showed that normal travel varies by about 25 % all on its own, from repeatable mechanical humps at fixed points in the cover's travel. That number set the whole design: any threshold tight enough to catch a gentle obstruction would also fire on a hump. So instead of one rule there are eleven independent detectors — absolute limit, rate of rise, ratio to a rolling baseline, ratio to a latched baseline, waveform asymmetry, supply-rail sag, and so on — that vote. Every threshold in ModelConfig.h carries the measurement it came from.

The honest limit is documented rather than hidden: an obstruction that raises load less than ~45 % above normal travel cannot be separated from the cover's own mechanical variation, and will not be detected. Anything at or above 1.45× is.

The model is validated by compiling the real ObstacleModel.cpp for a desktop and replaying those recorded runs through it — 9 trips, zero false positives. That harness is in tools/ and still runs today.

Then the pool got interesting

One end of the pool falls into shade well before the other, which raises a question the cover cannot answer: is it worth running the pump to mix the warm water into the cool? Answering it needs temperature data, so three DS18B20 probes went onto a spare pin — currently reading deep water, shallow water and outside air, logged alongside every run.

Nothing acts on that data yet. It is being collected first, the same way the current data was collected before any detector was written.

What someone else might take from this

  • A worked example of replacing a discontinued proprietary pool-cover controller with a commodity microcontroller, a current transducer and some relays — roughly $500 in parts against an $800 quote before labour.
  • Obstacle detection from motor current alone, on hardware with no position feedback of any kind.
  • The measurements behind every threshold, and a replay harness that runs the real firmware on a laptop — so the model can be re-tuned against new data without a reflash, and without guessing.
  • A reason to look under your own cover housing. The leaves, the ripped limit switches and the half-driven screws were doing more damage than the dead board was.

Safety note. See the safety disclaimer above before acting on any of this. Short version: the obstacle detection has a documented blind spot, cannot be relied on to detect a person, and is not a certified safety device.

Hardware

Full text description in docs/HARDWARE.md. Diagrams are still to be drawn. Summary:

Cover motor Latham/Coverstar 115/230 V three-wire (H1/D1, H2/N, D2, earth)
Switching Two relays driving mechanically interlocked reversing contactors
Board Arduino Nano ESP32 (arduino:esp32:nano_nora)
Current sensor ACS712-20A through a 10k/20k divider → A0
Temperature 3 × DS18B20 on one 1-Wire bus → D6, 4.7 k pull-up to 3.3 V
Inputs D0 porch OPEN, D1 porch CLOSE, D2/D3 limit contacts (monitored, non-functional)
Outputs D4 OPEN relay, D5 CLOSE relay — both active-low

The board's GPIOs are not 5 V tolerant; the 1-Wire pull-up goes to 3.3 V, not 5 V. Probe placement: T0 deep water 4 ft, T1 shallow water 1 ft, T2 outside air.

Files

File Purpose
Pool_Cover_Firmware.ino Motor control, relays, Wi-Fi, telemetry, heartbeat
ObstacleModel.h / .cpp The detector framework and all 11 detectors
ModelConfig.h Every threshold, with the measurement it came from
TempSensors.h / .cpp Non-blocking DS18B20 reader (raw values only)
TempConfig.h Temperature tunables
secrets.example.h Template for secrets.h — Wi-Fi credentials, not committed
pool_cover_data_collector.py Laptop-side UDP collector, CSV logger, watchdog, temperature correction
calibrate_temps.py Derives probe offsets from logged raw readings
tools/ Host test harness — compiles the real firmware for your computer
docs/ Hardware description and the raw design-phase notes
libraries/ OneWire and DallasTemperature, vendored so a clone builds as-is
archive/ Superseded firmware versions and recorded telemetry

First time: flashing it

  1. Credentials. Copy the template and fill in the network:

    cp secrets.example.h secrets.h
    

    secrets.h is gitignored and never leaves the machine. The controller only uses Wi-Fi to broadcast telemetry — the cover works fine without it. If Wi-Fi fails, logging stops; motor control and obstacle detection do not.

  2. Libraries. OneWire and DallasTemperature, either from the IDE's Library Manager or from the copies in libraries/.

  3. Sketch folder. The Arduino IDE requires the folder name to match the .ino name. This repo's folder is whatever you cloned it as, so point the IDE at a correctly-named link rather than moving files:

    ln -s "$(pwd)" ~/Documents/Arduino/Pool_Cover_Firmware
    

    Then open Pool_Cover_Firmware from the sketchbook. The IDE compiles the extra .cpp files automatically.

  4. Flash to arduino:esp32:nano_nora. Last verified build: 23 % flash, 17 % RAM.

FIRMWARE_VERSION near the top of the .ino is reported in every heartbeat and on the serial banner — bump it when you flash a behaviour change, so the logs say what is actually on the board.

Day to day: watching it run

The Nano broadcasts JSON over UDP to 255.255.255.255:5005, whether or not anything is listening. To record it, run the collector on a machine on the same network:

python3 pool_cover_data_collector.py

It writes pool_cover_runs.csv (one row per 100 ms of a run) and pool_cover_heartbeats.csv (every 5 s, always). Both are gitignored — they are live logs, not source. It also applies the temperature calibration offsets, converts to °F, and warns when the Nano goes quiet.

Everything else is reachable over serial at 115200 — status, temps, models, open/close/stop, unblock. The full command list is in the header comment of the .ino.

How the model works

Each 100 ms window is turned into a fully-derived ModelContext (baseline, slope, swing ratio, rail sag, clipping, sample validity), then handed to every detector. Detectors are independent, know nothing about each other, and know nothing about motor control.

Each has a severity:

Severity Effect
ADVISORY Reported in telemetry only. Use to trial a new detector safely.
VOTE Counts toward MODEL_REQUIRED_VOTES (default 1).
HARD Stops the motor on its own.

The detectors

Name Severity Fires on
abs vote RMS above a direction-specific ceiling (700 OPEN / 600 CLOSE)
slope vote Rising more than 120 counts per 300 ms
rel vote 1.25× a rolling 3 s baseline — self-calibrating
drift vote 1.45× a baseline latched at 2–5 s — catches slow climbs
sat hard Reading pinned at the measurable limit
swing vote Waveform lopsided; positive half clipping
sag vote Sensor 5 V rail sagging under load
slip hard Load built up then collapsed — the drive is slipping
nostart hard No motor current after the relay closed
timeout hard Past the hard travel limit (36 s / 51 s)
overrun advisory Past expected travel time

rel and drift are deliberately redundant. The rolling baseline adapts to a heavier cover or a cold morning; the latched one cannot be dragged upward by the very problem it is watching for.

Adding or removing a model

Add — write a class in ObstacleModel.cpp, make one static instance, and add it to DETECTORS[]. Nothing else changes.

class MyDetector : public ObstacleDetector {
 public:
  MyDetector() : ObstacleDetector("mine", "what it does",
                                  DetectorSeverity::ADVISORY, true) {}
  bool evaluate(const ModelContext& c) override { return c.sample.rms > 900; }
  const char* reason() const override { return "my_reason"; }
};
static MyDetector detMine;
// then add &detMine to DETECTORS[]

Start new detectors as ADVISORY. They appear in telemetry and the CSV without ever stopping the cover, so you can check them against real runs first.

Remove — delete its line from DETECTORS[], or leave it and turn it off at runtime.

Disable everything — set MODEL_ENABLED false in ModelConfig.h. Motor control, timeouts and telemetry are unaffected.

Runtime control over serial (115200)

models                      list every detector, its state and whether it tripped
model slope off             disable one
model swing advisory        keep it reporting but stop it acting
model drift vote            let it act again

Useful for isolating a nuisance trip without reflashing.

Temperature probes

Three DS18B20 on a shared 1-Wire bus on D6, with one 4.7 k pull-up to 3.3 V — this board's GPIOs are not 5 V tolerant.

Probe Placement
T0 deep water, 4 ft down
T1 shallow water, 1 ft down
T2 outside air, middle of the pool

Probes are indexed by 1-Wire address, which is stable across reboots, so these hold unless a probe is physically swapped. temps over serial prints each ROM address next to its label — worth writing on the cables.

Raw on the Nano, corrected in Python

The firmware reports raw degrees C, exactly as the sensor returned, to 4 decimals so the 12-bit 0.0625 °C step survives. Everything else happens in the collector:

Job Where
Reading the bus firmware
Calibration offsets TEMP_OFFSETS_C in the collector
°C → °F collector
Plausibility, staleness, fault values collector
Probe agreement checks collector

That keeps the recorded data as ground truth: recalibrating is an edit to the collector, never a reflash, and a wrong offset can always be undone because the raw value is what got logged. temp{i}_raw_c and temp{i}_c sit side by side in the CSV so a column name can never lie about which one it holds.

The 0.5 °C tolerance

TEMP_TOLERANCE_C = 0.5 is the DS18B20 datasheet accuracy. Two probes can each be off by that much in opposite directions, so disagreement is only flagged past twice the spec.

It applies only while calibrating, when every probe is in one bucket and should therefore agree. Once installed the check is meaningless — deep water, shallow water and outside air are supposed to differ, often by 20 °F. Set TEMP_REFERENCE_F to a value to enter calibration mode, None when installed.

Readings print to one decimal in °F because ±0.5 °C is ±0.9 °F; more digits would be inventing precision the sensor does not have.

Installed:

[TEMP] deep 4ft=79.0F  shallow 1ft=83.1F  outside air=89.1F  | spread=10.1F

Calibrating:

[TEMP] deep 4ft=75.3F  shallow 1ft=75.0F  outside air=75.5F  | spread=0.56F  | ref=75.2F  mean=75.26F  err=+0.06F

Markers: ! the probe didn't answer, ? it answered with an implausible value (e.g. the −127 fault code), ~ the reading is stale.

Calibrating

Put all three probes in one bucket, let the collector run a few minutes, then:

python3 calibrate_temps.py 75.2 F --minutes 10

It reads the raw values already in pool_cover_heartbeats.csv and prints a TEMP_OFFSETS_C block to paste into the collector. Because it works off logged raw data, it can be re-run later against readings recorded before you thought to calibrate.

Offsets correct fixed bias, not scale error, so calibrate near the temperature you care about.

Where temperatures are logged

No separate file. The heartbeat CSV already writes a row every 5 s — about 3.5 MB/day — so temperatures ride along at no structural cost. The runs CSV keeps only the raw values (temp{i}_raw_c) for correlating load with water temperature.

Why it doesn't disturb obstacle detection

A DS18B20 conversion takes 750 ms at 12 bits and each scratchpad read blocks the loop ~11 ms. Done naively that starves the current sampler, and the obstacle model refuses to act on any window with fewer than 150 ADC samples — so careless temperature code would silently disable the safety system.

Three things prevent it:

  1. Conversion is started and never waited on (setWaitForConversion(false)).
  2. Reads are spaced a full 100 ms window apart, so at most one ~11 ms transaction lands in any window. One read per loop pass would not work — loop passes are microseconds apart, so all three would land together.
  3. The bus is left completely alone while the motor runs.

Measured with tools/temp_cost_test, which runs the real sketch with 1-Wire calls that consume realistic bus time:

ADC samples per window
Idle, probes reading min 176, median 179 (limit is 150)
Motor running 200 — no bus traffic at all

Achieved cycle: 1063 ms. ~1 Hz is the hardware ceiling — 750 ms of that is the conversion, and going faster would mean 9-bit resolution (0.5 °C steps).

Pin remapping — the trap that cost a debugging round

On the Nano ESP32 the Arduino pin names are logical, not hardware GPIOs: D6 is 6 to the API but is physically GPIO 9. The core translates inside pinMode/digitalWrite/analogRead, so ordinary sketch code never notices.

OneWire does not go through that translation — on ESP32 it defines #define PIN_TO_BITMASK(pin) (pin) and pokes the GPIO registers directly. Passing D6 straight to it drives GPIO 6, which on this board is the D3 header pin, and nothing appears on the bus.

The pin is therefore translated explicitly with digitalPinToGPIONumber() before the bus is opened. Any other library doing direct register access needs the same treatment.

The bus is also re-scanned every 5 s while no probes are found, so a wiring fix is picked up without a reboot.

Direction lockout

When the model stops the cover, that direction is refused until the cover has been driven the other way. Cycling the key off and on is not enough — the cover must actually move in reverse for DIRECTION_UNBLOCK_MS (1200 ms).

One rule covers both cases:

  • at an end stop, repeated presses cannot keep driving into it
  • at a mid-travel obstruction, you must back off before going forward again

Only one direction is ever blocked at a time. Blocking a direction clears any block on the other, so the cover can never end up refusing to move at all — that is an invariant of the design, not a case that has to be handled.

A trip locks the direction only if the detector says it should. nostart is the exception: no motor current is an electrical fault that says nothing about position, so it does not demand a reverse move. Timeouts do lock, because a run that outlasted its limit leaves the position unknown.

unblock over serial clears it by hand for bench work.

Validation

The model was validated by compiling ObstacleModel.cpp unchanged for the host and replaying all 20 recorded runs through it, plus synthetic scenarios for paths no recorded run covers.

Against real data — 9 trips, zero false positives:

  • Tripped on every run that reached a stop and was held long enough
  • Never tripped on runs 15/16/17/19, which were released mid-travel
  • Detection lag after onset: 0–200 ms
  • Run 18 tripped 600 ms before the drive first slipped

Synthetic scenarios:

Scenario Result
Mid-travel obstruction at 15 s stop at 15.10 s (abs)
Slow drift to 1.50× early travel stop at 22.70 s (swing)
Slow drift to 1.30× no stop — known blind spot
Motor never starts stop at 0.30 s (nostart)
Runs forever at normal load stop at 36.00 s (timeout)
Startup inrush incl. rail hit no stop (blanked)
Starved loop, 40-sample windows no stop (untrusted data)

Known blind spot

Normal travel already varies by 25 % because of repeatable mechanical humps (OPEN at 10–14 s and 19–23 s, CLOSE at 9–14 s and 24–31 s). An obstruction that raises load by less than ~45 % above early travel therefore cannot be separated from normal variation and will not be detected. Anything at or above 1.45× is.

Measurement limits worth remembering

  • analogRead() saturates near 3080, not 4095.
  • Below ~550 counts RMS the waveform is symmetric and the reading is honest. Above that the positive half clips and RMS under-reports.
  • Every trip threshold sits inside the linear region deliberately, so the decision is always made on clean data. Clipping is used only as its own separate hard-trip signal.
  • 1 count RMS = 0.01294 A. Normal travel is 5.5–5.8 A; the end stop is 13.7 A (OPEN) and 16 A or more (CLOSE).

Re-tuning after new data

Every derived value the model computed is in the CSV (baseline_rms, ratio_base, latched_baseline, ratio_latched, slope_300, swing_ratio, mean_sag, clipped, model_flags). Thresholds can be re-checked offline against real runs without reflashing.

tools/ contains the host replay harness used above:

The 20 runs the model was built and validated against are kept at archive/data/csv_archive_2026-08-13/Pool_Cover_Firmware_V3/pool_cover_runs.csv — the live pool_cover_runs.csv at the root only holds whatever the collector has recorded since you last cleared it.

cd tools && make
RUNS=../archive/data/csv_archive_2026-08-13/Pool_Cover_Firmware_V3/pool_cover_runs.csv

./replay $RUNS               # all detectors
./replay $RUNS --only slip   # one detector in isolation
./synth                      # synthetic scenarios

It links the real ObstacleModel.cpp, so what it tests is what runs on the Nano.

lockout_test goes further: it compiles the whole sketch against Arduino and WiFi shims in tools/shim/ and drives it through the lockout scenario with a virtual clock and a synthesised 60 Hz current waveform. 17 checks, covering the trip, the key-cycle refusal, a too-brief reverse tap, a real reverse, and the both-directions-blocked deadlock that the design makes impossible.

make test     # synth + lockout_test

Where this could go

The cover was the hard part, and it left behind a foundation: a controller that already measures, logs and broadcasts everything it knows.

The intention is to extend the same approach to the rest of the pool — a second Nano driving the pump and heater, deciding from water and air temperatures, cover position, and the weather forecast. Pump speed and evaporation losses are where a pool actually spends money, and both are controllable. Commercial pool automation costs several times what the parts do, and none of it adapts to one particular pool.

The same method applies: instrument first, log for a season, derive the rules from measurements rather than guessing.

Two boundaries are deliberate. It would be a separate controller, not an extension of this one — the cover firmware stays small and single-purpose. And it would listen to the cover's telemetry, never drive it: closing a cover unattended is exactly what the blind spot makes unsafe. Hold-to-run with someone watching is not a limitation to automate away.

License

MIT — use it, change it, sell it, no permission needed and no attribution required beyond keeping the copyright notice.

The licence includes the standard disclaimer of warranty and limitation of liability. That is not a formality here: see Safety and disclaimer. If you build one, it is yours, and so is the risk.

About

Arduino Nano ESP32 pool cover controller with current-based obstacle detection — a working replacement for a failed Latham/Coverstar 1601 (E0301 / LPE0301RC) board on a two-way electric motor

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages