{"id":50996634,"url":"https://github.com/apadevices/apapump","last_synced_at":"2026-06-20T10:03:11.471Z","repository":{"id":362717361,"uuid":"1260269784","full_name":"apadevices/APAPUMP","owner":"apadevices","description":"Autonomous pool filtration pump controller for Arduino — PCF8574 relay expander, solar heating, freeze protection, dry-run and overcurrent detection · APA Devices","archived":false,"fork":false,"pushed_at":"2026-06-05T14:57:58.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T16:26:25.274Z","etag":null,"topics":["apadevices","ardui","arduino-library","dryrun-protection","esp32","esp8266","freeze-protection","pcf8574","pool-automation","pump-controller","relay-control","solar-heating","st","stm32","water-treatment"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apadevices.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T10:13:47.000Z","updated_at":"2026-06-05T15:21:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/apadevices/APAPUMP","commit_stats":null,"previous_names":["apadevices/apapump"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/apadevices/APAPUMP","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apadevices%2FAPAPUMP","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apadevices%2FAPAPUMP/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apadevices%2FAPAPUMP/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apadevices%2FAPAPUMP/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apadevices","download_url":"https://codeload.github.com/apadevices/APAPUMP/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apadevices%2FAPAPUMP/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34565244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apadevices","ardui","arduino-library","dryrun-protection","esp32","esp8266","freeze-protection","pcf8574","pool-automation","pump-controller","relay-control","solar-heating","st","stm32","water-treatment"],"created_at":"2026-06-20T10:03:10.697Z","updated_at":"2026-06-20T10:03:11.462Z","avatar_url":"https://github.com/apadevices.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# APAPUMP\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"extras/apapump-logo.png\" width=\"600\" alt=\"APAPUMP\"\u003e\n\u003c/p\u003e\n\n**Autonomous filtration pump controller for APA Devices pool automation**\n· ![v1.0.0](https://img.shields.io/badge/version-1.0.0-blue)\n· ![Platforms](https://img.shields.io/badge/platforms-AVR%20ESP8266%20ESP32%20STM32-brightgreen)\n\n---\n\n## Key Features\n\n### Pump control\n- PCF8574AT I2C relay expander (default) or direct Arduino GPIO — one constructor covers both\n- Non-blocking state machine: IDLE → STARTING → RUNNING → STOPPING\n- Safe boot: all relays start OFF regardless of previous relay or pin state\n- Manual override: `FORCE_ON`, `FORCE_OFF`, `AUTO` — with optional timeout or midnight auto-return\n- Extension relay outputs on PCF P4–P7 for lights or extra equipment\n\n### Scheduling and daily tracking\n- Schedule callback drives pump on/off without any external library\n- Daily runtime target with catch-up logic and optional time window\n- Midnight rollover via RTC epoch callback or 24-hour millis() fallback\n\n### Follower devices\n- **UVC sanitizer** — ON before pump, OFF after (configurable pre/post delay)\n- **AUX device** (heat pump, ozone, etc.) — ON after pump, OFF before pump stops\n- **Solar valve** — opens when solar drives the pump; instantly closes in manual mode\n\n### Solar heating\n- Absorber–pool temperature dead-band hysteresis (configurable start/stop deltas)\n- Pool cooling: runs in reverse when pool temperature exceeds maximum\n- Day/night boundary gate via RTC epoch callback\n- **Solar safety (Priority 0):** absorber too hot → pump ON + valve OPEN, overrides even `FORCE_OFF`\n\n### Safety alarms\n- **Overcurrent** — EMA-learned baseline × 1.5 threshold; cold-start gate of 5 samples\n- **Dry-run** — pressure below EMA baseline × 40% (or absolute minimum before EMA builds)\n- **Overpressure** — absolute hard limit at `maxPressure` AND EMA-relative dirty-filter detection\n- **Freeze protection** — pool temp below 4.5 °C → cyclic run (5 min ON / 10 min rest); suppressed if dry-run detected\n- **No-flow stub** — flow switch confirmed after settle; hardware-ready for future sensor\n- All alarms are latching — explicit `acknowledgeAlarm()` required\n\n### Engineering\n- **564 B SRAM** on Arduino Uno with all features registered — fits comfortably in 2 KB\n- 23 boolean flags packed into 3 bytes; all string literals in flash (`F()` macro)\n- EEPROM: 12 bytes, magic + version + checksum validation, persists target and clean pressure\n- Zero `delay()` calls — every path returns within one `loop()` iteration\n- Designed to integrate with APADOSE and APALCDGUI via callback bridges — no coupling\n\n---\n\n## Installation\n\n**Arduino IDE:** Sketch → Include Library → Add .ZIP Library → select the APAPUMP folder.\n\n**PlatformIO:**\n```ini\nlib_deps =\n    https://github.com/apadevices/APAPUMP\n```\n\n---\n\n## What It Does\n\nAPAPUMP controls a pool filtration pump and up to three follower devices (UVC lamp, AUX device, solar valve) through a PCF8574AT I2C relay expander or direct Arduino output pins. Each call to `update()` in your `loop()` advances the state machine, checks every safety guard in priority order, and fires alarm callbacks when something is wrong — all without blocking.\n\nOut of the box, the zero-arg constructor targets the APA Devices HMI board v1.0: PCF8574AT at 0x3C with pump on P0, UVC on P1, AUX on P2, and solar valve on P3. Every optional feature is **disabled by default** and activated with one method call.\n\n---\n\n## How It Works\n\n### State machine\n\n```\n                                ┌──────────────────────────────────┐\n                                │           pump.begin()           │\n                                ▼                                  │\n                             [ IDLE ]                              │\n                                │                                  │\n                   _shouldPumpRun() = true                         │\n                   min OFF time OK (60 s)                          │\n                                │                                  │\n                          [ STARTING ]                             │\n                                │                                  │\n                   UVC pre-delay (default 5 s)                     │\n                                │                                  │\n                          [ RUNNING ] ◄── safety alarms checked:   │\n                                │         current · pressure ·     │\n                                │         flow · freeze            │\n                   _shouldPumpRun() = false                        │\n                   min run time OK (300 s)                         │\n                                │                                  │\n                         [ STOPPING ]                              │\n                                │                                  │\n          AUX lead ──► pump OFF ──► UVC post-delay ────────────────┘\n```\n\n**Guards that bypass min-run time:** `FORCE_ON` and `FORCE_OFF` — manual mode suspends timing guards.\n\n### Follower timing\n\n```\nTime →    0         5s         15s       [pump runs…]   Ts-30s    Ts      Ts+30s\n          │          │          │                │        │         │        │\nUVC:      ├──ON──────┼──────────┼────────────────┼────────┼─────────┼──OFF───┤\nPump:               ON──────────────────────────────────OFF\nAUX:                           ON───────────────OFF\n          ◄─ pre ──►◄──────────────── RUNNING ──────────►◄─ lead ─►◄─ post─►\n```\n\n`Ts` = moment the stop sequence begins. `pre` = `enableUVC(preDelayS, …)`. `lead` = `enableAux(…, stopLeadS)`. `post` = `enableUVC(…, postDelayS)`.\n\n### Priority engine\n\nEvery `update()` tick calls the priority engine. The first rule that matches wins:\n\n| Priority | Condition | Result |\n|----------|-----------|--------|\n| **P0** | Solar absorber \u003e safety temp (default 50 °C) | Pump ON + valve OPEN — overrides FORCE_OFF |\n| **P1** | `FORCE_OFF` manual mode | Pump OFF |\n| **P2** | `FORCE_ON` manual mode | Pump ON |\n| **P3** | External request callback returns `true` | Pump ON |\n| **P4** | Freeze: pool temp \u003c threshold AND water present | Pump ON |\n| **P5a** | Solar: absorber \u003e pool + startDelta | Pump ON (hysteresis) |\n| **P5b** | Catch-up: daily runtime \u003c daily target (window OK) | Pump ON |\n| **P5c** | Schedule callback returns `true` | Pump ON |\n| — | None of the above | Pump OFF |\n\n### PCF relay layout (APA HMI board v1.0 defaults)\n\n```\nPCF8574AT at I2C address 0x3C\n│\n├─ P0 ── Pump relay              (always required)\n├─ P1 ── UVC relay               (optional — enableUVC())\n├─ P2 ── AUX relay               (optional — enableAux())\n└─ P3 ── Solar valve relay       (optional — enableSolar(…, useValve=true))\n│\n├─ P4 ── Extension relay         (optional — setExtraOutput(4, on/off))\n├─ P5 ── Extension relay         (optional — setExtraOutput(5, on/off))\n├─ P6 ── Extension relay         (optional — setExtraOutput(6, on/off))\n└─ P7 ── Extension relay         (optional — setExtraOutput(7, on/off))\n```\n\nPCF HIGH = relay ON, LOW = relay OFF. `begin()` writes `0x00` immediately — all relays start OFF.\n\n---\n\n## Quick Start\n\n### Minimal — APA HMI board defaults (zero wiring, zero config)\n\n```cpp\n#include \u003cWire.h\u003e\n#include \u003cAPAPUMP.h\u003e\n\nApaPump pump;              // PCF at 0x3C — pump=P0, uvc=P1, aux=P2, valve=P3\n\nvoid setup() {\n    Wire.begin();          // required for PCF mode — call before pump.begin()\n    pump.begin();          // all relays OFF, EEPROM loaded\n}\n\nvoid loop() {\n    pump.update();         // non-blocking — call every loop()\n}\n```\n\n### Alternative: direct GPIO\n\n```cpp\n// Active-high relay (most modules)\nApaPump pump(RELAY_DIRECT, 7);               // pump on pin 7\n\n// Active-low relay (some modules)\nApaPump pump(RELAY_DIRECT, 7);\npump.setActiveLow();             // call BEFORE pump.begin()\n\n// Multiple relays, direct GPIO\nApaPump pump(RELAY_DIRECT, 7, 8, 9, 10);    // pump=7, uvc=8, aux=9, valve=10\n```\n\n### Scheduled pump with alarm feedback\n\n```cpp\n#include \u003cWire.h\u003e\n#include \u003cAPAPUMP.h\u003e\n\nApaPump pump;\n\nbool scheduleActive() {\n    // return true when your RTC time falls inside a timer slot\n    return false;  // replace with real logic\n}\n\nvoid onAlarm(PumpAlarm alarm) {\n    if (alarm == PUMP_ALARM_NONE) return;   // alarm was cleared\n    Serial.print(F(\"Pump alarm: \"));\n    switch (alarm) {\n        case PUMP_ALARM_OVERCURRENT:   Serial.println(F(\"overcurrent\"));   break;\n        case PUMP_ALARM_LOW_PRESSURE:  Serial.println(F(\"dry run\"));       break;\n        case PUMP_ALARM_HIGH_PRESSURE: Serial.println(F(\"high pressure\")); break;\n        case PUMP_ALARM_NO_FLOW:       Serial.println(F(\"no flow\"));       break;\n        default: break;\n    }\n    // pump continues running — call pump.acknowledgeAlarm() when investigated\n}\n\nvoid setup() {\n    Wire.begin();\n    pump.begin(scheduleActive);             // schedule drives on/off\n    pump.enableUVC(5, 30);                  // UVC: 5 s pre, 30 s post\n    pump.enableAux(10, 30);                 // AUX: 10 s delay, 30 s lead\n    pump.setPumpAlarmCallback(onAlarm);\n}\n\nvoid loop() { pump.update(); }\n```\n\n---\n\n## Manual Override\n\nForce the pump on or off at any time. All timing guards and schedules are suspended:\n\n```cpp\npump.setManualMode(FORCE_ON);   // force pump ON  (e.g. vacuuming, winterising)\npump.setManualMode(FORCE_OFF);  // force pump OFF (e.g. plumbing, service)\npump.setManualMode(AUTO);       // return to automatic control\n```\n\n**The solar valve closes immediately when entering FORCE_ON** — operator gets full system pressure.\n\n**Auto-return options** — pick one or both:\n\n```cpp\n// Timeout: returns to AUTO after 60 minutes\nApaPump pump(RELAY_PCF, 0x3C, 0, 1, 2, 3, 60);   // 6th constructor parameter\npump.setManualTimeout(60);                           // or set at any time\n\n// Midnight reset (requires setMidnightCallback())\npump.setManualAutoReset(true);\n```\n\n**Note:** Solar safety (P0) still applies even in `FORCE_OFF`. If the absorber reaches the safety temperature, the pump runs to prevent heat damage regardless of manual mode.\n\n---\n\n## Followers: UVC, AUX, Solar Valve\n\nEach follower is independent and optional. Wire the relay to the corresponding PCF bit (or GPIO pin), then call the enable method:\n\n```cpp\n// UVC lamp: ON 5 s before pump, OFF 30 s after pump stops\npump.enableUVC(5, 30);\n\n// AUX device (heat pump, ozone): ON 10 s after pump starts, OFF 30 s before pump stops\npump.enableAux(10, 30);\n\n// Solar valve: open when solar logic drives the pump\n// useValve=true enables the valve relay on P3 (or direct pin d)\npump.enableSolar(absorberTempCb, poolTempCb, /*useValve=*/true, ...);\n```\n\n**UVC pre-delay** — when a pre-delay is configured, the pump enters STARTING state while UVC warms up. If the reason to start disappears during STARTING (e.g. schedule ends), the sequence is cancelled cleanly — UVC turns off and the pump never fires.\n\n---\n\n## Solar Heating\n\nSolar logic becomes the **sole on/off driver** when enabled — the schedule callback is only used for the daily runtime target.\n\n```cpp\npump.enableSolar(\n    []() { return adc.getSolarTemp(); },   // absorber temperature callback\n    []() { return adc.getPoolTemp(); },    // pool temperature callback (optional)\n    true,                                  // useValve — open valve when solar running\n    8.0f,                                  // startDelta: start when absorber \u003e pool + 8°C\n    3.0f,                                  // stopDelta:  stop  when absorber \u003c pool + 3°C\n    32.0f,                                 // maxPoolTemp: pool cooling if pool \u003e 32°C\n    2.0f,                                  // coolDelta: cool when absorber \u003c pool - 2°C\n    50.0f                                  // safetyTemp: force pump ON if absorber \u003e 50°C\n);\n\n// Optional: restrict solar heating to daytime hours\npump.setSolarDayNight(\n    []() { return rtc.getEpoch(); },       // Unix epoch callback\n    7,                                     // daytime starts at 07:00\n    20                                     // daytime ends at 20:00\n);\n```\n\n### Solar hysteresis\n\n```\nAbsorber temp\n     │\n     │                     START threshold = pool + startDelta\n─────┼──────────────────────────────────────────────────────\n     │   ← pump ON                               pump ON →\n     │\n─────┼──────────────────────────────────────────────────────\n     │                     STOP threshold = pool + stopDelta\n     │\n```\n\nThe gap between start and stop thresholds prevents rapid on/off cycling when temperature hovers near the boundary. Increase `startDelta − stopDelta` for wider hysteresis.\n\n---\n\n## Daily Runtime Target and Catch-Up\n\n```cpp\n// Option A: fixed target (user sets it once)\npump.setDailyTarget(360);                   // run 6 hours per day\n\n// Option B: use the APALCDGUI timer sum as the target\npump.begin(scheduleActive, nullptr,\n           []() { return gui.getTimerTotalMinutes(); });\n\n// Query progress\nuint16_t ran = pump.getDailyRuntimeMinutes();\nbool     met = pump.isDailyTargetMet();\n```\n\nWhen solar is enabled and the daily target is not met, catch-up runs until the target is reached:\n\n```cpp\n// Optional: limit catch-up to daytime hours (requires setMidnightCallback)\npump.setCatchupWindow(8, 20);   // catch-up only between 08:00 and 20:00\n```\n\n---\n\n## Safety Alarms\n\nAll alarms are **latching** — the pump does not stop automatically (your `alarmCb` decides the action). Call `acknowledgeAlarm()` after investigation to clear.\n\n| Alarm | Triggers when | Requires |\n|-------|--------------|----------|\n| `PUMP_ALARM_OVERCURRENT` | current \u003e learned baseline × 1.5 | `setCurrentCallback()` |\n| `PUMP_ALARM_LOW_PRESSURE` | pressure below 40% of EMA baseline (dry-run) | `enablePressure()` |\n| `PUMP_ALARM_HIGH_PRESSURE` | pressure \u003e `maxPressure` OR \u003e EMA × (1+peakPct%) | `enablePressure()` + `setPressurePeakAlarm()` |\n| `PUMP_ALARM_NO_FLOW` | flow switch reports no flow after 30 s settle | `setFlowCallback()` |\n\n**Alarm callback pattern:**\n\n```cpp\npump.setPumpAlarmCallback([](PumpAlarm alarm) {\n    if (alarm == PUMP_ALARM_NONE) {\n        // alarm was acknowledged — update LCD, clear indicator\n        gui.cancelActiveAlert();\n        return;\n    }\n    // alarm fired — notify operator\n    gui.postActiveAlert(F(\"Pump alarm!\"), F(\"Check equipment\"),\n                        ALERT_CRITICAL, []() { pump.acknowledgeAlarm(); });\n});\n```\n\n### Wiring a buzzer or alarm output\n\nThe alarm callback is the right place to drive any physical indicator — buzzer, LED, relay. No extra library method needed.\n\n**Direct GPIO (no APASENSE):**\n```cpp\npump.setPumpAlarmCallback([](PumpAlarm alarm) {\n    bool active = (alarm != PUMP_ALARM_NONE);\n    digitalWrite(5, active ? HIGH : LOW);   // buzzer on pin 5\n});\n```\n\n**With APASENSE** (v1.1.0+, recommended for APA hardware):\n```cpp\npump.setPumpAlarmCallback([](PumpAlarm alarm) {\n    bool active = (alarm != PUMP_ALARM_NONE);\n    adc.setLed(0, active);                      // LED0 = PCF P4\n    if (active) adc.alert(BUZZER_ALARM, true);  // repeating alarm pattern\n    else         adc.stopAlert();               // silence when alarm clears\n});\n```\n\n\u003e **Tip:** APASENSE owns the PCF expander LEDs and buzzer. `alert(BUZZER_ALARM, true)` plays a repeating triple-beep until `stopAlert()` is called — no delay() or manual timer needed.\n\n### Dry-run protection\n\nWhen `enablePressure()` is registered, APAPUMP builds a dual pressure baseline (EMA): one for normal running, one for when the solar valve is open (higher pressure expected due to absorber resistance). After 5 pump runs, if pressure stays near zero after 30 s, `PUMP_ALARM_LOW_PRESSURE` fires.\n\nBefore the EMA baseline builds (first 5 runs), an absolute minimum of 0.1 bar is used.\n\n### Freeze protection\n\n```cpp\npump.enableFreezeProtection(\n    []() { return adc.getPoolTemp(); },   // pool/pipe temperature\n    4.5f                                  // threshold in °C (default)\n);\n```\n\nWhen pool temperature drops below the threshold, the pump **cycles** to prevent pipes from freezing: runs for `APAPUMP_FREEZE_ON_SEC` (default 5 min), rests for `APAPUMP_FREEZE_OFF_SEC` (default 10 min), and repeats until temperature rises. Both durations are overridable via `build_flags`. **Dry-run interlock:** if the pressure sensor is enabled, calibrated, and indicates no water in the pipes (pressure near zero), freeze protection is suppressed — forcing the pump without water would burn the motor.\n\n---\n\n## Integrating with APADOSE and APALCDGUI\n\nAPAPUMP, APADOSE, and APALCDGUI are independent libraries — your sketch is the bridge. No library knows about the others.\n\n### Bridge: APALCDGUI timer → APAPUMP schedule\n\n```cpp\n// In begin(): pass a lambda that reads the timer state from the GUI\npump.begin(\n    []() {\n        // return true when current time falls inside any timer slot\n        uint16_t nowMin = rtcHour * 60 + rtcMinute;\n        for (uint8_t i = 0; i \u003c APA_LCD_MAX_TIMERS; i++) {\n            if (gui.isTimerEnabled(i) \u0026\u0026\n                nowMin \u003e= gui.getTimerStart(i) \u0026\u0026\n                nowMin \u003c  gui.getTimerEnd(i)) return true;\n        }\n        return false;\n    },\n    nullptr,\n    []() { return gui.getTimerTotalMinutes(); }  // daily target from timer sum\n);\n```\n\n### Bridge: APADOSE post-shock → APAPUMP\n\n```cpp\n// After a shock dose, run the pump for 4 hours to circulate the chlorine.\n// Uses the externalRequestCb — no new API needed.\n\nstatic uint32_t postShockUntil = 0;\n\npump.begin(scheduleCb,\n    []() {\n        // Shock is running: extend the post-shock window\n        if (dose1.isShockActive()) {\n            postShockUntil = millis() + 4UL * 3600UL * 1000UL;\n            return true;\n        }\n        // Pump on until post-shock window expires\n        return (millis() \u003c postShockUntil);\n    }\n);\n```\n\n### Bridge: pump manual mode → APALCDGUI status indicator\n\n```cpp\n// Show [M] in the LCD corner whenever the pump is in manual mode\npump.setStatusCallback([](const __FlashStringHelper* msg) {\n    if (pump.getManualMode() != AUTO) gui.setStatusIndicator('M');\n    else                              gui.clearStatusIndicator();\n    // Also forward the status text to a scrolling message area\n    Serial.println(msg);\n});\n```\n\n### Bridge: APASENSE → APAPUMP (pressure and current)\n\n```cpp\n// Order: adc.begin() BEFORE pump.begin() — pressure auto-zeros on first pump stop.\n// pressureCb returns -1.0f while APASENSE is uncalibrated — APAPUMP skips\n// all pressure logic until a valid reading arrives.\n\nadc.begin();    // zero-cal requires pump off — call first\n\npump.begin();\npump.enablePressure(\n    []() { return adc.getPressure(); },    // -1.0f until calibrated\n    4.0f                                   // absolute max pressure (bar)\n);\npump.setCurrentCallback([]() { return adc.getCurrent(); });\n\n// Bridge: tell APASENSE when pump state changes so it can re-zero on pump stop\npump.setPumpStateCallback([](bool on) { adc.onPumpState(on); });\n```\n\n---\n\n## Examples\n\n| Sketch | Level | Description |\n|--------|-------|-------------|\n| `examples/01_minimal/` | Basic | PCF or GPIO, manual override via Serial, all three constructor options |\n| `examples/02_scheduler/` | Intermediate | APALCDGUI timer schedule, UVC + AUX followers, alarm → active alert bridge |\n| `examples/03_solar_freeze/` | Advanced | Solar heater + solar valve, freeze protection, pressure safety, APADOSE post-shock bridge |\n\n---\n\n## Platform Verification\n\nCompiled with the `01_minimal` example. Zero errors, zero library warnings on all platforms.\n\n| Platform | Board | RAM used | RAM total | Flash used | Flash total |\n|----------|-------|----------|-----------|------------|-------------|\n| Arduino Mega 2560 | ATmega2560 | 564 B | 8 192 B (6.9%) | 11 860 B | 253 952 B (4.7%) |\n| Arduino Uno | ATmega328P | 564 B | 2 048 B (27.5%) | 11 094 B | 32 256 B (34.4%) |\n| ESP32 DevKit | ESP32 | 22 024 B | 327 680 B (6.7%) | 290 493 B | 1 310 720 B (22.2%) |\n| ESP8266 D1 Mini | ESP8266 | 28 852 B | 81 920 B (35.2%) | 274 147 B | 1 044 464 B (26.2%) |\n| STM32 Bluepill | STM32F103C8 | 2 628 B | 20 480 B (12.8%) | 25 984 B | 65 536 B (39.6%) |\n\n\u003e The Uno row uses 27.5% RAM — that is the library with all Phase 2 features **registered** in the example. A minimal sketch (no solar, no pressure, no freeze) sits below 20%. ESP32 and ESP8266 totals include the full Arduino framework regardless of use.\n\n---\n\n## EEPROM Layout\n\nAPAPUMP reserves **12 bytes** starting at `APAPUMP_EEPROM_ADDR` (default 520).\n\n### Field map\n\n| Address | Bytes | Field | Saved when |\n|---------|-------|-------|-----------|\n| 520 | 2 | Magic `0xA55A` | — validity marker |\n| 522 | 1 | Config version | — mismatch resets all fields to defaults |\n| 523 | 2 | Daily target (minutes) | `setDailyTarget()` |\n| 525 | 2 | Yesterday's runtime (minutes) | Midnight rollover (once per day) |\n| 527 | 2 | Minimum run time (seconds) | `setMinRunTime()` |\n| 529 | 2 | Clean pressure × 100 (bar; 0 = not learned) | `learnCleanPressure()` / `setCleanPressure()` |\n| 531 | 1 | Checksum (byte sum of bytes 520–530) | — corruption guard |\n\n### Write protection and EEPROM lifespan\n\n`_saveEEPROM()` writes the full 12-byte struct, but a physical write only occurs when a byte has actually changed:\n\n- **AVR (Uno/Mega):** `EEPROM.put()` calls `EEPROM.update()` per byte — reads first, writes only if value differs.\n- **ESP32/ESP8266:** `EEPROM.put()` updates a RAM buffer; `EEPROM.commit()` compares the buffer to flash before writing — no flash write if content unchanged.\n- **STM32:** EEPROM emulation with compare-before-write.\n\n**Practical write frequency** (worst case on a running system):\n\n| Event | Max frequency | Cycles used at 100 k limit |\n|-------|--------------|---------------------------|\n| Midnight rollover | 1 per day | 100 k days ≈ 273 years |\n| `learnCleanPressure()` | Operator action | Negligible |\n| `setMinRunTime()` from `setup()` | 0 physical writes if value unchanged | None |\n| First boot / corruption recovery | Once | 1 cycle |\n\n**Corruption recovery:** the checksum is verified on every `begin()`. If power fails mid-write and data is corrupted, the mismatch is detected on the next boot, factory defaults are loaded, and a clean struct is written.\n\n### Override base address\n\n```cpp\n// Define before #include if the default conflicts with another library in your project:\n#define APAPUMP_EEPROM_ADDR  532\n#include \u003cAPAPUMP.h\u003e\n```\n\n### APA EEPROM address map\n\nAll ranges across the APA library suite — free blocks shown explicitly:\n\n| Range | Bytes | Status | Owner |\n|-------|-------|--------|-------|\n| 0–127 | 128 | **free** | — |\n| 128–177 | 50 | used | APAPHX2_ADS1115 (sensor calibration) |\n| 178–188 | 11 | **free** | — |\n| 189–191 | 3 | used | APADOSE global (pool volume, dead-band) |\n| 192–279 | 88 | used | APADOSE per-pump config (22 bytes × up to 4 instances) |\n| 280–499 | 220 | **free** | — |\n| 500–501 | 2 | used | APALCDGUI brightness |\n| 502–508 | 7 | used | APALCDGUI timer slots (default 3 slots) |\n| 509–519 | 11 | **free** | — gap before APAPUMP ¹ |\n| **520–531** | **12** | **used** | **APAPUMP** |\n| 532– | — | **free** | — |\n\n\u003e ¹ When `APA_LCD_MAX_TIMERS=6` the timer block extends to 514, leaving a 5-byte gap (515–519) before APAPUMP. This gap is intentional — do not place anything in 515–519 to keep the gap safe regardless of timer configuration.\n\n---\n\n## License\n\n**Dual license — see LICENSE file for full terms.**\n\n| Use | License |\n|-----|---------|\n| Personal, private, educational, hobby | Free — no charge, no paperwork |\n| Commercial (products, services, OEM) | Separate written license required |\n\nCommercial use includes selling or distributing hardware with this software, providing paid pool automation services, or integrating it into any revenue-generating product or system.\n\nTo obtain a commercial license: [kecup@vazac.eu](mailto:kecup@vazac.eu)\n\n---\n\n*APAPUMP — APA Devices · [kecup@vazac.eu](mailto:kecup@vazac.eu)*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapadevices%2Fapapump","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapadevices%2Fapapump","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapadevices%2Fapapump/lists"}