Skip to content

apadevices/APADOSE

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

163 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

APADOSE

APA-Dose Library

Autonomous proportional chemical dosing for swimming pool automation
Part of the APA Devices product family.

Version 3.17.3  ·  AVR  ·  ESP  ·  STM32  ·  No required dependencies


Key Features

Proportional dosing control

  • True proportional output — both PWM speed and pulse duration scale continuously with error; never bang-bang on/off
  • Closed-loop feedback — 2 sensor readings averaged before each dose, 3 after; verifies the water actually moved toward setpoint
  • Adaptive dose correction — first failed dose gets +30 % PWM boost; second gets +50 % PWM and doubled pulse time (capped at 5 min); alarm fires only after three consecutive failures — no human intervention needed between attempts
  • Dose delivery health monitoring — EMA-learned baseline continuously tracks each pump's normal chemical delivery; getDoseEffectiveness() returns 0–100 (last dose as a % of the learned baseline; 100 until baseline established after 3 doses); ALARM_INEFFECTIVE fires automatically when delivery drops below setEfficiencyThreshold() (default 20, active out of the box; pass 0 to disable); getLastDoseSensorBefore() / getLastDoseSensorAfter() expose raw before/after sensor averages for display or logging; check hasDoseHistory() first
  • Adaptive proportional band — optional self-learning mode: after each feedback cycle the library nudges the effective band up when the sensor overshot, down when it undershot, converging toward the pool's true chemical response; disabled by default, enabled with enableAdaptivePB(nudgePct) (1–25% per cycle); learned value is EEPROM-persistent per pump
  • Pool volume scaling — one call (ApaDose::setPoolVolume(m3)) scales pulse duration, rest period, and feedback timing for pools between 10–90 m³; reference is 20 m³; pools above ~30 m³ require this to converge to setpoint; disabled by default for backward compatibility; survives factoryReset()
  • Dead-band — optional noise filter (ApaDose::setDeadbandPct(pct), 0–20% of proportional band) suppresses dosing when the error is small; asymmetric entry/exit hysteresis prevents oscillation at the boundary; same % value applies correctly to both pH and ORP; cleared by factoryReset()
  • Over-setpoint alarm — if the sensor stays on the wrong side of setpoint for more than 30 minutes, ALARM_OVER_SETPOINT fires to notify the operator that chemistry has drifted past target without intervention; when a dead-band is configured the alarm mirrors the same band width on the opposite side of the setpoint (with dead-band off, any persistent over-setpoint triggers it); auto-clears the moment the reading returns to the dosing zone — no acknowledge required
  • pH-first dosing priority (J)clPump.setPhPump(&phPump) links the CL pump to the pH pump; CL dosing is automatically suspended when pH exceeds 7.6 (CL_PH_MAX), where chlorine is mostly ineffective; resumes when pH returns below threshold; off by default
  • Cross-settle coupling (A)clPump.setCrossSettleMinutes(n) holds CL dosing for N minutes after each pH dose, preventing the pH/ORP see-saw caused by acid doses temporarily depressing ORP during mixing; off by default; requires setPhPump() first

Safety

  • Full alarm system — wrong direction, ineffective dose, safety band, daily dose limit, sensor fault (ALARM_SENSOR_FAULT) — all built-in and reported via callback or polling
  • Filtration interlock — dosing blocked the instant the filter stops; a running dose halts immediately; no chemical ever injected into stagnant water
  • External stop — optional callback from any external system (maintenance mode, backwash, cover) blocks all dosing immediately; a mandatory 5-minute settling time applies after the signal clears before dosing resumes
  • Chemical tank empty sensor — optional dry-contact callback (setTankEmptyCallback()) fires ALARM_TANK_EMPTY the instant the tank runs dry; blocks dosing and priming until the tank is refilled and acknowledged; zero SRAM cost if unused
  • Over-feed alarm (OFA) — protects against over-dosing by tracking how many minutes each pump runs each day; call setOFALimit(30) once in setup() to set a 30-minute reference for a 20 m³ pool — the library scales the limit automatically for other pool sizes if you called setPoolVolume(); at 70 % of the limit a status warning fires (dosing continues); at 90 % ALARM_OFA fires, dosing stops, and the ACK button is required — pressing ACK resets the counter immediately so dosing can resume straight away; the counter also resets automatically at midnight so unattended systems recover on their own without operator attention; getOFAPct() returns today's usage (0–100 %) for a dashboard row; disabled by default
  • Dynamic OFA (dOFA) — self-learning over-feed protection; always active, zero configuration needed; dOFA learns what a normal dosing day looks like for THIS pool and fires ALARM_OFA when today's proportional run time exceeds 2× the learned baseline (warning status at 1.5×); no limit to guess — the library builds it automatically from real daily usage; both dOFA and fixed OFA coexist independently, whichever fires first controls; baseline is EMA-averaged over the last N days (default 10, adjustable 3–14 via setDOFAAdaptDays()), persisted to EEPROM at midnight and survives power cycles; the baseline is ready from day 2 onwards — one qualifying day (≥ 5 min proportional run) is sufficient; during warm-up the pool is protected by ALARM_INEFFECTIVE, ALARM_WRONG_DIRECTION, ALARM_SAFETY_BAND, the daily dose limit, and optional fixed OFA; isDOFALearning() returns true during warm-up; getDOFAPct() returns today's proportional run as a % of the learned baseline; call resetDOFA() at spring opening to restart learning after a seasonal shutdown; sensor-less pumps are inert (proportional dosing never runs, counter stays 0)
  • Setpoint range enforcement — pH 6.8 – 7.8 and ORP 400 – 850 mV enforced on every write; out-of-range values rejected before reaching EEPROM
  • Inter-pump chemical lockout — 90-second enforced gap after any pump instance doses; prevents incompatible chemicals meeting at the same pipe inlet
  • Startup blackout — optional N-minute dosing hold after power-on (blackoutMinutes parameter in begin()); gives electrochemical sensors time to stabilize before the first dose decision; isInStartupBlackout() exposes the state for display

Flexibility

  • 1 to 4 independent pumps — each instance is a full isolated state machine with its own dosing cycle, feedback loop, alarm state, and EEPROM block
  • Sensor-less pump support — pass nullptr as the sensor callback for flocculant or algaecide pumps; filtration interlock, daily limit, and priming all remain active
  • Solenoid valve support — set min == max in setPumpRange() for time-proportional on/off control; no other code changes needed
  • Manual dosingtriggerManualDose() for button or RTC-triggered doses; duration clamped to 5 minutes regardless of what is passed; blocked by most safety guards — exception: ALARM_OFA does not block manual doses so the operator can intervene and add chemical without acknowledging the alarm first
  • Scheduled dosingsetScheduledDose() arms a recurring dose at a configurable wall-clock time; optional interval (daily/weekly/fortnightly) and sensor threshold to skip when reading is already in range; all safety guards apply; requires RTC
  • Pipe primingtriggerPrime() fills dry pipes on installation or after a container swap; bypasses all safety guards so it works even under an active alarm — except ALARM_TANK_EMPTY (no point running a dry pump); no rest period is imposed after priming — consecutive primes are allowed immediately (useful for long pipe runs requiring multiple passes)
  • Shock / super-chlorinationtriggerShock() doses chlorine at full power until ORP reaches a target or a time ceiling; automatic early-stop margin, ORP rise check, inter-pump interlock, and post-shock safety band suppression all built-in; hobbyist and pro overloads available
  • Dosing window — restrict automatic dosing to a configurable daily hour range via setDosingWindow(); manual doses and priming are unaffected

Monitoring

  • Chemical volume trackinggetDailyVolumeMl() and getLastDoseVolumeMl() estimate consumption from actual pulse duration and PWM intensity; resets at midnight when an RTC is connected
  • Tank level estimation — two independent optional features, each usable alone: the software path (setTankCapacity(litres)) estimates remaining volume from dose pulses — no hardware sensor required; getTankRemainingPct() (0–100 %) and getTankDaysUntilEmpty() (rolling 7-day estimate in days; 255 = not enough data yet) update after every dose; ALARM_TANK_EMPTY fires when estimated consumption reaches capacity; the hardware path (setTankEmptyCallback()) fires ALARM_TANK_EMPTY instantly from a float switch or dry-contact sensor — see Safety Systems → Chemical tank empty sensor; combined, the hardware sensor is the sole alarm authority and the software estimation provides the percentage display — real-time alert from hardware, visual progress on a dashboard; acknowledgeAlarm() resets the consumed counter; disabled by default; persisted to EEPROM at midnight
  • Dose countergetDailyDoseCount() tracks combined automatic and manual doses per day; resets every 24h — at real midnight with an RTC, every 24h from boot without one
  • Rest period queriesgetSecondsUntilNextDose() returns seconds remaining in the current rest period (0 when ready to dose); getSecondsSinceLastDose() returns seconds elapsed since the last dose completed (0 if no dose yet this session) — both are useful for dashboards and LCD status rows
  • System status snapshotgetSystemStatus(buf, size) fills a caller-supplied buffer with a single-line summary of the current state (active alarms, dosing phase, sensor value, daily dose count); size APA_DOSE_STATUS_BUFFER_SIZE (96) is sufficient for the longest output

Engineering

  • EEPROM persistence — setpoint, band, and dosing type survive power loss; magic-number and checksum validation on every boot with automatic fallback to safe defaults
  • RTC scheduling — optional: daily counter reset at midnight, dosing window by hour; library works fully without an RTC
  • Non-blocking — pure millis() state machine; zero delay() calls; safe to call every loop() iteration alongside any other code
  • Universal hardware support — AVR (Uno through Mega), ESP8266, ESP32, STM32 — same source, no #ifdef in user code
  • Minimal footprint — two-pump sketch: ~22 KB flash / ~877 B RAM on Uno; ~303 B RAM per additional instance; 23 boolean flags packed into 3 bytes; dOFA adds 5 B SRAM + 2 B EEPROM per instance; tank estimation adds 4 B SRAM + 3 B EEPROM per instance; pool volume and dead-band add 2 bytes SRAM total (shared) and 3 bytes EEPROM; ConfigData 25 bytes, up to 4 instances from EEPROM address 192
  • No required dependencies — the library itself needs only <Arduino.h> and <EEPROM.h>; RTClib (+ Adafruit BusIO) is required only when using an RTC for scheduling — not needed without one

Installation

Arduino IDE — Download the repository as a .zip and install via Sketch → Include Library → Add .ZIP Library.

PlatformIO — Copy the library folder into your project's lib/ directory, then add to platformio.ini:

[env:your_board]
platform  = atmelavr          ; or espressif32, ststm32
board     = uno               ; your target board
framework = arduino
lib_extra_dirs = lib          ; folder containing the APA-DOSING_LIB directory

build_flags =
    ; -D APA_DOSE_DEBUG       ; uncomment for per-cycle diagnostic output on Serial

No additional dependencies — only the standard <Arduino.h> and <EEPROM.h> are required.


What It Does

APA-Dose controls dosing pumps to keep a pool's pH or ORP (chlorine) at a target setpoint. It reads the current sensor value, calculates how far it is from the target, and runs the pump for a proportional duration — short pulses for small deviations, longer pulses when the water is further off. After each dose it waits for the chemical to mix, reads the sensor again, and verifies the dose was effective before starting the next cycle.

The library is fully non-blocking. All timing uses millis(). Zero delay() calls. Multiple independent pump instances run side-by-side in a single loop().


How Proportional Dosing Works

The setpoint and proportional band

The user sets a setpoint (target value) and a proportional band (control range). The band defines the sensor range over which the pump output scales from minimum to maximum. Error is calculated as the distance from the setpoint, expressed as a percentage of the band.

pH-PLUS pump example  (setpoint 7.4,  band 1.0 pH)
Doses when pH falls below setpoint — raises pH toward target.

                                                          SP
          ──────────── Proportional band ───────────────►│
  pH  ────┼─────────────┬──────────┬─────────┬───────────┼────
         6.4           6.65       6.9       7.15         7.4
          │                                               │
        100 %          75 %      50 %      25 %       threshold
       max dose       long dose  mid dose  short dose   (idle)
          │
       ALARM─┘
     (safety band)

  Pump PWM:    pumpMaxPWM  ◄──────────────────────  min+10%   off
  Pulse time:  180 s       ◄──────────────────────  10 s       off
  Rest period: 20 min      ◄──────────────────────  5 min       —

  Optional dead-band  (setDeadbandPct, default off):
  ──── active dosing zone ─────────────────────────►│◄─10%─►│
                                                   7.30    7.40 (SP)
  Suppresses dosing within 10% of band from SP; exits at 5% (hysteresis).
pH-MINUS pump example  (setpoint 7.4,  band 1.0 pH)
Doses when pH rises above setpoint — lowers pH toward target.

  SP
  │◄─────────────────── Proportional band ─────────────────
  ┼───────────┬─────────┬──────────┬─────────────────────  pH
 7.4         7.65      7.9       8.15                     8.4
  │                                                        │
threshold    25 %      50 %      75 %                   100 %
  (idle)   short dose  mid dose  long dose              max dose
                                                           │
                                                        ALARM─┘
                                                      (safety band)

  Pump PWM:    off  min+10%  ──────────────────────►  pumpMaxPWM
  Pulse time:   —   10 s     ──────────────────────►  180 s
  Rest period:  —   5 min    ──────────────────────►  20 min

  Optional dead-band  (setDeadbandPct, default off):
  │◄─10%─►│◄──────────────────── active dosing zone ──────────────
  7.40    7.50
  (SP)  (SP+10%)
  Suppresses dosing within 10% of band from SP; exits at 5% (hysteresis).

Dead-band and over-setpoint alarm correlation

When a dead-band is configured, the same band width W is mirrored symmetrically on both sides of the setpoint. The dosing zone boundary (SP − W for a raising pump) is also the over-setpoint alarm threshold on the far side (SP + W). With dead-band disabled (W = 0), any persistent over-setpoint reading triggers the alarm.

  pH-PLUS pump example  (setpoint 7.4, band 1.0, dead-band 10 %)

  dead-band width W = 10% × 1.0 = 0.10 pH

  don't dose ◄──── W ────►│◄──── W ──── alarm fires after 30 min
                          7.4
           7.30          7.40          7.50
       (dead-band entry)  SP       ALARM_OVER_SP

ALARM_OVER_SETPOINT is non-latching and auto-clears the moment the sensor returns to the dosing zone. No acknowledgeAlarm() call is needed.

The chlorine (ORP) pump follows the pH-PLUS pattern — dosing starts when ORP falls below setpoint.

One direction per pump. Each ApaDose instance controls one chemical direction — either raising pH (PH_PLUS) or lowering it (PH_MINUS). Running both a pH+ and a pH- pump on the same pool at the same time is not supported and will cause the two pumps to fight each other. Choose the direction that matches your water — install only that one pump for pH control.

The dosing cycle

Every automatic dose passes through six phases:

  ┌──────────────────────────────────────────────────────────┐
  │                                                          │
  │  ① SAMPLE BEFORE     2 readings × 30 s apart             │
  │        │             averaged → before-dose value        │
  │        ▼                                                 │
  │  ② CALCULATE PULSE                                       │
  │        │   error %  =  |setpoint − reading| / band       │
  │        │   PWM      ∝  error %   (proportional)          │
  │        │   time     ∝  error %   (10 – 180 s)            │
  │        │   rest     ∝  error %   (5 – 20 min)            │
  │        ▼                                                 │
  │  ③ RUN PUMP          analogWrite(PWM) for pulse time     │
  │        │                                                 │
  │        ▼                                                 │
  │  ④ REST              chemical mixes into pool water      │
  │        │             (5 – 20 min, proportional to dose)  │
  │        ▼                                                 │
  │  ⑤ SAMPLE AFTER      3 readings × 30 s apart             │
  │        │             averaged → after-dose value         │
  │        ▼                                                 │
  │  ⑥ EVALUATE FEEDBACK                                     │
  │        │  update EMA delivery baseline                   │
  │        ├─ EMA ratio < threshold? (default 20 %)          │
  │        │       └──────────────► ALARM_INEFFECTIVE        │
  │        ├─ direction wrong ×3? ──► ALARM_WRONG_DIRECTION  │
  │        ├─ sensor moved enough?                           │
  │        │     yes ──► failedAttempts=0; adaptive PB nudge │
  │        └─     no  ──► failedAttempts++; boost next dose  │
  │                       alarm after 3 (ALARM_INEFFECTIVE)  │
  └────────────────────────┬─────────────────────────────────┘
                           │ repeat
                           ▼

Expected timing: A full cycle takes 8 – 26 minutes depending on how far the sensor is from setpoint — pre-sampling alone is 1 minute, pulse is 10 – 180 seconds, rest is 5 – 20 minutes, post-sampling is 1.5 minutes. Add any startup blackout on top. Seeing nothing on Serial for several minutes after boot is normal. Enable APA_DOSE_DEBUG in platformio.ini (build_flags = -D APA_DOSE_DEBUG) to print per-cycle progress and confirm the library is running.

OFA accumulation: each proportional dose pulse in phase ③ adds its run time to two independent daily counters. The fixed OFA counter (optional, setOFALimit()) triggers a warning at 70 % and stops dosing at 90 % of the configured limit. The dynamic OFA counter (dOFA, always active) builds a self-learned baseline and fires when today exceeds 2× normal. Manual doses and shock are excluded from the dOFA counter. See Safety Systems → Over-feed alarm and Dynamic OFA below.

Dosing zones

Error (% of band) PWM output Pulse duration Rest period
0 – 25 % proportional 10 – 30 s 5 min
25 – 50 % proportional 30 – 60 s 10 min
50 – 75 % proportional 60 – 120 s 15 min
75 – 100 % pumpMaxPWM 180 s 20 min

A 10 % minimum floor above pumpMinPWM is always applied to overcome pipe resistance.


Safety Systems

Most safety features are always active with no configuration required. Two features are opt-in by passing non-zero values to begin(): startup blackout and daily dose limit. The filtration interlock and filter-off notification are only active when a FilterCallback is supplied to begin() — omit it (use the no-filter overload) only when your hardware has no filter pump signal.

Feature Behaviour
Filtration interlock Dosing is blocked when the filter pump is off. A running dose stops immediately if the filter cuts out mid-dose. Requires a FilterCallback passed to begin(); inactive when the no-filter overload is used.
External stop An optional callback registered via setExternalStopCallback() can block all dosing from any external system — maintenance mode, backwash cycle, pool cover, or a signal from a filtration controller. A running dose stops the instant the callback returns true. After the signal clears, a mandatory 5-minute settling time (EXTERNAL_STOP_RESUME_MS) must pass before the next dose is allowed — this prevents a brief dose from firing while an operator is still toggling between filtration modes or water is still flowing through a diverted outlet. Priming is exempt.
Startup blackout Optional delay (0 – 60 min) after boot before the first dose. Prevents overdosing after a warm restart when the water chemistry is still in transition. Enabled by passing a non-zero blackoutMinutes to begin(); default is 0 (disabled).
Safety band If the sensor drifts beyond min(band × 1.5, hardCap) from setpoint, ALARM_SAFETY_BAND fires and dosing stops. The check runs every 10 s via the sensor read cycle — not only when a dose is about to start. Clears automatically when the sensor recovers. Hard caps: ±1.0 pH · ±150 mV ORP.
Wrong direction detection If the sensor moves the wrong way on 3 consecutive cycles, ALARM_WRONG_DIRECTION fires. Catches wrong chemical installed or reversed pump wiring before significant harm occurs.
Ineffective dose detection ALARM_INEFFECTIVE fires when the EMA delivery ratio drops below the configured threshold (default 20%, active out of the box), when the sensor shows no response after 3 consecutive dose attempts, or when ORP fails to rise during shock. Catches empty container, blocked tube, or failed pump.
Manual dose ceiling triggerManualDose() clamps duration to 5 minutes regardless of what is passed. Prevents runaway from automation code errors.
Chemical tank empty sensor An optional callback registered via setTankEmptyCallback() fires ALARM_TANK_EMPTY (latching) the moment it returns true — e.g. a float switch or capacitive sensor wired to a dry-contact input. Blocks dosing and priming until the tank is refilled and acknowledgeAlarm() is called. Checked at dose-start time, not continuously, so there is no overhead during the rest period. Zero SRAM cost if unused.
Tank level estimation setTankCapacity(litres) enables software-only tank tracking without any hardware sensor. The library accumulates consumed volume (mL) from every dose and compares it to the configured capacity. ALARM_TANK_EMPTY fires when consumed ≥ capacity. If setTankEmptyCallback() is also registered, the hardware sensor is the sole alarm authority and estimation is suppressed — percentage display still works. acknowledgeAlarm() resets consumed to zero. Consumed counter and capacity are persisted to EEPROM at midnight and survive power cycles. Cost: 4 bytes SRAM + 3 bytes EEPROM per instance. Disabled by default — call setTankCapacity() to enable.
Daily dose limit Optional maximum doses per day. Enabled by passing a non-zero maxDailyDoses to begin(); default is 0 (no limit). ALARM_DAILY_LIMIT fires when reached and clears automatically when the counter resets — at real midnight when an RTC callback is registered, every 24 h from boot without one. No acknowledgment required; dosing resumes on its own the next day.
Stale sensor / sensor fault If the sensor callback returns invalid or out-of-range values continuously for 2 minutes, or returns no valid value at all for 30 minutes, ALARM_SENSOR_FAULT fires and dosing stops. Clears automatically when the sensor recovers — no acknowledgment required. Prevents dosing against a frozen or disconnected sensor.
NaN / infinity guard Every sensor reading is validated before use. A single bad value sends one status message but never corrupts averaging, never triggers a false alarm, and never crashes the state machine.
Filter-off notification If the filter stays off for 30 minutes, a single "Filter off>30min" status message fires. The operator is reminded that circulation has stopped. Active only when a FilterCallback is provided to begin().
Setpoint range enforcement Both pH and ORP setpoints are rejected if outside safe operating bounds. pH: 6.8 – 7.8 (floor prevents dangerously acidic water; ceiling prevents scaling and chlorine inefficiency). ORP: 400 – 850 mV (floor prevents under-chlorination; ceiling prevents harmful free-chlorine levels for bathers). Out-of-range values are rejected silently and reported via ALARM_INVALID_PARAM.
Inter-pump lockout After any pump instance completes a dose, all other instances wait 90 s before starting. Prevents back-to-back injection of incompatible chemicals at the same inlet (acid + chlorine → chlorine gas).
Shock interlock While triggerShock() is running on a chlorine pump, all other ApaDose instances are held immediately — "Held:shock active" fires once per held instance. When shock ends all instances resume automatically. This prevents pH acid from being dosed into a high-ORP pool mid-shock.
Post-shock safety band suppression After shock completes, the safety band alarm is suppressed for the cooldown window (default 24 h, max 48 h) while elevated ORP normalizes. Without suppression, the expected post-shock ORP level would trigger a false ALARM_SAFETY_BAND within minutes of stopping. "Post-shock normal" fires when the window expires.
Over-setpoint protection If the sensor stays on the wrong side of setpoint for more than 30 minutes, ALARM_OVER_SETPOINT fires. The threshold mirrors the dead-band width on the opposite side of the setpoint — the same W that defines the dosing entry boundary. With dead-band disabled (W = 0) any persistent over-setpoint triggers it. Auto-clears when the reading returns to the dosing zone — no ACK needed. Cost: 4 bytes SRAM per instance.

Alarm System

Alarms stop the pump immediately. Each alarm is reported through the onAlarmTriggered callback and can also be polled at any time.

Alarm Trigger Recovery
ALARM_WRONG_DIRECTION Sensor moved wrong way 3× in a row Fix chemical or wiring → acknowledgeAlarm()
ALARM_INEFFECTIVE EMA delivery ratio below threshold, no sensor response after 3 doses, or no ORP rise during shock Fix pump or supply → acknowledgeAlarm()
ALARM_SAFETY_BAND Sensor beyond safety band Automatic when sensor returns to safe range
ALARM_DAILY_LIMIT Max daily doses reached Automatic at midnight (RTC) or after 24 h (millis) — no acknowledgment needed
ALARM_SENSOR_FAULT Invalid/out-of-range readings for 2 min, or no valid reading for 30 min Automatic when sensor recovers — no acknowledgment needed
ALARM_TANK_EMPTY Tank empty callback returned true at dose-start time Refill tank → acknowledgeAlarm()
ALARM_OFA Daily proportional run-time reached 90 % of the setOFALimit() ceiling or 2× the dOFA learned baseline acknowledgeAlarm() — resets both daily counters immediately; counters also reset automatically at midnight
ALARM_OVER_SETPOINT Sensor on wrong side of setpoint for >30 min Automatic when sensor returns to dosing zone — no ACK needed
ALARM_INVALID_PARAM Bad configuration value Rejected silently — no alarm stays active

Receiving alarms via callback

Register an alarm callback before calling begin():

void onAlarm(ApaDoseAlarm alarm, const char* message) {
  // message is max 19 chars — fits one row of a 16×2 LCD
  Serial.println(message);
  digitalWrite(PIN_BUZZER, HIGH);
}

void onAlarmCleared(ApaDoseAlarm alarm, const char* message) {
  digitalWrite(PIN_BUZZER, LOW);
}

void setup() {
  phPump.setCallbacks(onAlarm, onAlarmCleared, onStatus);
  phPump.begin(getpH, filterRunning, DOSE_PH, PH_MINUS, 20);
}

Polling alarm state

Alarm state can also be polled without callbacks — useful for display code:

void loop() {
  phPump.update();

  if (phPump.isAlarmActive()) {
    lcd.print(phPump.getAlarmMessage());  // same text as the callback
    if (buttonPressed())
      phPump.acknowledgeAlarm();
  }
}

See examples/advanced/06_alarm_management/ for a complete alarm UI with acknowledge button — documents both ALARM_INEFFECTIVE trigger paths (EMA efficiency and 3-strike) and shows getDoseEffectiveness() alongside the alarm state for every periodic status print.
See examples/intermediate/03_serial_diagnostics/ for interactive serial diagnostics including per-dose delivery health output: getDoseEffectiveness(), getLastDoseSensorBefore() / getLastDoseSensorAfter(), and EMA warm-up state.
See examples/expert/07_apa_serial/ for a serial interface with full alarm reporting.


Quick Start

Single pH pump

On first boot (blank EEPROM) the library starts with these built-in defaults. They are saved to EEPROM and survive all subsequent power cycles until changed via setProbeSetpoint() / setProportionalBand().

Parameter pH default ORP / CL default
Setpoint 7.4 700 mV
Band 1.0 pH 100 mV

Hardware note: PIN_PH_PUMP must be a PWM-capable pin (marked ~ on Arduino boards). Connect the pump through an N-channel MOSFET or a relay module — the library drives it with analogWrite(). A plain relay works too; set setPumpRange(255, 255) for on/off mode.

#include <APADOSE.h>

ApaDose phPump(PIN_PH_PUMP);  // must be a PWM-capable pin (~)

// Replace with your pH sensor library's read call.
// Must return a float. Called by the library every ~30 s during sampling.
float getpH()         { return phSensor.getPH(); }
bool  filterRunning() { return digitalRead(PIN_FILTER_RELAY) == HIGH; }

void onAlarm(ApaDoseAlarm alarm, const char* msg) {
  Serial.println(msg);
}

void setup() {
  phPump.setPumpRange(65, 255);  // 65 = PWM where YOUR pump starts — measure it
  phPump.setCallbacks(onAlarm);
  // PH_MINUS = acid pump (lowers pH); use PH_PLUS for a base pump (raises pH)
  // 20 min startup blackout · max 6 doses/day (pass 0 for either to disable)
  // Normal on first install — returns false only when no valid config exists yet or EEPROM is corrupt
  if (!phPump.begin(getpH, filterRunning, DOSE_PH, PH_MINUS, 20, 6))
    Serial.println("No saved config — defaults loaded");
}

void loop() {
  // update() is non-blocking — call every iteration; tolerates occasional ms-level loop delays
  phPump.update();
}

Temperature compensation: pH sensor readings are temperature-dependent (~0.003 pH/°C). The APAPHX and APAPHX2 libraries apply the Passco 2001 formula to deliver a stable, temperature-compensated value automatically. If using a different sensor library, apply temperature compensation inside your getSensorValue() callback so APA-Dose always receives a corrected reading.

pH + chlorine — two independent pumps

#include <APADOSE.h>

ApaDose phPump(PIN_PH_PUMP);
ApaDose clPump(PIN_CL_PUMP, APA_DOSE_EEPROM_ADDRESS + sizeof(ConfigData));  // each instance uses sizeof(ConfigData) bytes

float getpH()         { return phSensor.getPH(); }
float getORP()        { return orpSensor.getORP(); }
bool  filterRunning() { return digitalRead(PIN_FILTER_RELAY) == HIGH; }

void onAlarm(ApaDoseAlarm alarm, const char* msg) { Serial.println(msg); }

void setup() {
  phPump.setPumpRange(65, 255);
  phPump.setCallbacks(onAlarm);
  // PH_MINUS = acid pump (lowers pH); use PH_PLUS for a base pump (raises pH)
  if (!phPump.begin(getpH, filterRunning, DOSE_PH, PH_MINUS, 20))
    Serial.println("No saved config — defaults loaded");

  clPump.setPumpRange(65, 255);
  clPump.setCallbacks(onAlarm);
  if (!clPump.begin(getORP, filterRunning, DOSE_CL, CL_PLUS, 20))
    Serial.println("No saved config — defaults loaded");
}

void loop() {
  phPump.update();
  clPump.update();
}

Two pumps: After one pump doses, the other waits 90 seconds before it can start — see Inter-pump lockout below.


Multi-Pump Setup — Up to Four Pumps

Each ApaDose instance is a fully independent state machine with its own dosing cycle, feedback loop, alarm state, and EEPROM block. Instances never interfere with each other.

#include <APADOSE.h>

ApaDose phPump  (PIN_PH,   APA_DOSE_EEPROM_ADDRESS);
ApaDose clPump  (PIN_CL,   APA_DOSE_EEPROM_ADDRESS +     sizeof(ConfigData));  // EEPROM 212
ApaDose flocPump(PIN_FLOC, APA_DOSE_EEPROM_ADDRESS + 2 * sizeof(ConfigData));  // EEPROM 232
ApaDose algiPump(PIN_ALGI, APA_DOSE_EEPROM_ADDRESS + 3 * sizeof(ConfigData));  // EEPROM 252

void loop() {
  phPump.update();
  clPump.update();
  flocPump.update();  // sensor-less — manual / scheduled dosing only
  algiPump.update();  // sensor-less — manual / scheduled dosing only
}

Each pump's EEPROM configuration block is 20 bytes (sizeof(ConfigData)). The four-pump layout above occupies addresses 192 – 271, safely clear of the APAPHX2 sensor library (128 – 177).

Inter-pump lockout: After any pump instance completes a dose, all other instances wait 90 seconds before starting their next dose. This prevents acid and chlorine from being injected back-to-back at the same pipe inlet — mixing them there produces chlorine gas. If your second pump seems slow to react after the first one doses, this is why — it is working as intended.

Sensor-less pumps (flocculant / algaecide)

Pumps with no sensor are initialised by passing nullptr as the sensor callback. Proportional dosing never fires — control these pumps entirely with triggerManualDose():

// type and dir are required by the API signature but have no effect here:
// proportional dosing never runs without a sensor, so the library never consults them.
// Any valid combination compiles and works — DOSE_PH / PH_PLUS is the conventional placeholder.
flocPump.begin(nullptr, filterRunning, DOSE_PH, PH_PLUS, 0, 1);  // no sensor · no blackout · max 1 dose/day
algiPump.begin(nullptr, filterRunning, DOSE_PH, PH_PLUS, 0, 1);

The filtration interlock and daily dose limit remain active even without a sensor.

Manual and scheduled dosing

triggerManualDose(durationMs, restMs) triggers a single dose at pumpMaxPWM. Duration is in milliseconds and is clamped to 5 minutes — requests above this ceiling are silently reduced and a "Dose capped:5min" status message is sent. The optional restMs (default 20 min) sets the mixing wait before the next proportional cycle resumes. Each call increments the daily dose counter.

// Button-triggered dose — safe to call every loop(); returns false immediately if blocked
if (buttonPressed())
  flocPump.triggerManualDose(30UL * 1000UL);          // 30 s at pumpMaxPWM

// RTC-scheduled weekly dose — call once when the condition is met
if (t.weekday == 1 && t.hour == 8 && !weeklyFlocDone) {
  if (flocPump.triggerManualDose(60UL * 1000UL))      // 60 s at pumpMaxPWM; returns false if blocked
    weeklyFlocDone = true;
}

triggerManualDose() returns false and does nothing when: a dose or prime is already running, an alarm is active, the filter is off (when a FilterCallback is provided), the external stop callback returns true or its 5-minute resume delay is still active, the daily dose limit is reached, or the 90-second inter-pump lockout is still in effect.

Scheduled dosing — setScheduledDose()

setScheduledDose(hour, minute, durationMs) arms a dose that fires automatically at the configured wall-clock time — no tracking code in loop() required. An RTC callback must be registered via setRTCCallback() for the schedule to work.

// Algaecide every day at 09:00 for 30 s (sensor-less — threshold ignored, always doses)
algiPump.setScheduledDose(9, 0, 30UL * 1000UL);

// Flocculant every 7 days at 08:30 for 60 s (sensor-less — threshold ignored, always doses)
flocPump.setScheduledDose(8, 30, 60UL * 1000UL, 7);

// pH acid daily at 07:00 — doses only if pH has drifted past setpoint (default threshold = setpoint)
phPump.setScheduledDose(7, 0, 10UL * 1000UL);

// pH acid daily — explicit threshold: dose only if pH > 7.4 regardless of setpoint
phPump.setScheduledDose(7, 0, 10UL * 1000UL, 1, 7.4f);

// pH acid daily — always dose at 07:00 no matter what the sensor reads
phPump.setScheduledDose(7, 0, 10UL * 1000UL, 1, 0.0f);

Optional parameters:

# Name Default Effect
4 intervalDays 1 Repeat every N days. 7 = weekly, 14 = fortnightly.
5 threshold setpoint Skip dose if sensor is already in the safe direction. Default NAN = use the pump's own setpoint. 0.0 = always dose. Any finite value = explicit override. Ignored for sensor-less pumps.

The threshold direction is resolved automatically: for a PH_MINUS pump, 7.4 means "skip if pH ≤ 7.4"; for a PH_PLUS or DOSE_CL pump, 7.4 means "skip if pH ≥ 7.4". No direction parameter needed.

All standard safety guards apply — filtration interlock, external stop, tank-empty check, daily dose limit, inter-pump lockout, and active alarm block all prevent the dose from firing, exactly as for triggerManualDose(). The scheduled dose is most useful for sensor-less pumps (algaecide, flocculant) where proportional control does not apply.

Priming — filling dry pipes

triggerPrime(durationMs, pwm) runs the pump for exactly durationMs milliseconds to fill a dry pipe after first installation or a chemical container swap. pwm is optional — pass 0 (default) to use pumpMaxPWM, or any value between pumpMinPWM and pumpMaxPWM for a slower fill.

Priming bypasses all safety guards — no filter check, no alarm state, no daily dose limit, no inter-pump lockout. Use it only during maintenance, never from automated scheduling code.

// Fill the pipe after swapping the acid container — 20 s at full speed
phPump.triggerPrime(20UL * 1000UL);

// Slower prime if pipe or fittings are fragile
phPump.triggerPrime(20UL * 1000UL, 160);              // 160 / 255 PWM

A "Prime done" status message fires when the duration elapses. Priming does not count toward the daily dose limit. No rest period is imposed after priming — priming fills dry pipe only and carries no chemical into the pool, so consecutive primes and immediate automatic dosing are both permitted.

triggerPrime() returns false if a dose or another prime is already running.

See examples/advanced/05_multi_pump/ for the complete four-pump example.

Shock / super-chlorination

triggerShock() doses the chlorine pump at full power until ORP reaches a target value or a time ceiling expires, then automatically returns to normal proportional control. Use it after heavy bather load, algae events, storms, or any situation where the pool is so far from target that proportional dosing would take many hours.

Two overloads:

// Hobbyist — use named presets, sensible defaults (4 h max, 24 h cooldown)
clPump.triggerShock(SHOCK_ORP_STANDARD, phPump.getProbeValue());

// Pro — full control
clPump.triggerShock(780, 3, phPump.getProbeValue(), 48);
//                  ^targetORP  ^maxHours  ^currentPH  ^cooldownHours

Named ORP presets (use instead of raw mV values):

Constant Value When
SHOCK_ORP_MILD 700 mV Post-rain, minor algae risk
SHOCK_ORP_STANDARD 750 mV Weekly maintenance shock
SHOCK_ORP_AGGRESSIVE 800 mV Heavy algae, high bather load

Key behaviours:

  • pH must be 7.0–7.6 before shock — chlorine is far more effective in this range (above 7.6, over half the free chlorine converts to the inactive OCl⁻ form)
  • Stops 10% before the target ORP — compensates for chlorine mixing lag; ORP continues rising after dosing stops
  • At 20 minutes: checks that ORP has risen ≥ 20 mV; if not, aborts with ALARM_INEFFECTIVE — catches an empty tank or failed pump before wasting hours
  • All other pumps are held during shock — "Held:shock active" fires; they resume automatically when shock ends
  • Safety band alarm suppressed for 24 h after shock (configurable up to 48 h) — prevents false ALARM_SAFETY_BAND while ORP normalizes
  • With RTC: post-shock cooldown is wall-clock based and survives power cycles
  • dailyDoseCount is not incremented; dailyVolumeMl is accumulated for cost tracking
// In loop() — check shock state for display or logging
if (clPump.isShockActive()) {
  Serial.print("Shock active, ");
  Serial.print(clPump.getShockRemainingSeconds() / 60);
  Serial.println(" min remaining");
}

See examples/basic/02_ph_and_cl/ for the hobbyist shock call with a dedicated button.
See examples/advanced/05_multi_pump/ for the pro call with RTC-backed cooldown and serial feedback.
Full entry guards, chemistry reference, and alarm table: docs/API.md.


Setup Order

Call setup methods in this order — order matters on first boot:

1. setPumpRange()               calibrate motor dead band (optional, default 50–255)
                                set min == max (e.g. 255, 255) for solenoid valves — see below
2. setPumpFlowRate()            optional — pump output at max PWM in mL/min (default 450)
                                enables getDailyVolumeMl() / getLastDoseVolumeMl() volume tracking
3. setRTCCallback()             optional — enables time-based scheduling
4. setDosingWindow()            optional — restrict dosing to specific hours
5. setExternalStopCallback()    optional — block dosing from external conditions (maintenance, backwash…)
6. setCallbacks()               register alarm/status callbacks BEFORE begin()
7. begin()                      connect sensor, set type + direction, load EEPROM, start library
                                returns false if EEPROM data was corrupt — defaults are used, safe to continue
8. ApaDose::setPoolVolume()     optional — system-wide pool size scaling (static, affects all instances)
   ApaDose::setDeadbandPct()    optional — system-wide dosing dead-band (static, affects all instances)
                                both must come AFTER begin() — EEPROM must be initialised first
                                can also be called at runtime from loop() — takes effect immediately

begin() parameters

Two overloads are available. Use the full form when a filter pump signal is wired; use the no-filter shorthand when it is not:

// Full form — filtration interlock and filter-off notification active
pump.begin(sensorReader, filter, type, dir, blackoutMinutes, maxDailyDoses);

// No-filter shorthand — interlock disabled
pump.begin(sensorReader, type, dir, blackoutMinutes, maxDailyDoses);
# Parameter Type Required Default Description
1 sensorReader SensorReadCallback Yes Callback returning the current sensor value as float. Pass nullptr for sensor-less pumps (flocculant, algaecide) — proportional dosing is then disabled.
2 filter FilterCallback No (omitted) Callback returning true when the filter pump is running. Present only in the full form; omit it by using the no-filter overload.
3 type ApaDoseType Yes Chemical type: DOSE_PH (pH pump) or DOSE_CL (chlorine/ORP pump). Always overwrites the EEPROM-stored value — hardware type is authoritative.
4 dir ApaDoseDirection Yes pH dosing direction: PH_MINUS (acid, lowers pH) or PH_PLUS (base, raises pH). For DOSE_CL pumps use CL_PLUS — a named alias for PH_PLUS that makes intent clear; direction is not used in chlorine control logic.
5 blackoutMinutes uint8_t No 0 Startup delay before the first dose, in minutes. Accepted range 0 – 60; 0 disables the blackout.
6 maxDailyDoses uint8_t No 0 Maximum combined automatic and manual doses per day. 0 = no limit.

Parameters 5 and 6 are positional and must be supplied in order when used. Trailing defaults can be omitted — pump.begin(getSensor, filterRunning, DOSE_PH, PH_MINUS, 20) sets a 20-minute blackout and leaves the daily limit unrestricted.

Sensor smoothing required. The SensorReadCallback must return a pre-smoothed value. APA-Dose averages 2 readings before each dose and 3 readings after, but each individual sample is used as received. High reading-to-reading deviation — caused by electrical noise, unstable sensor electronics, or missing RC filtering on the ADC input — leads to incorrect proportional pulse calculation, wrong-direction detection false positives, and premature ALARM_SENSOR_FAULT or ALARM_WRONG_DIRECTION triggers. Apply a moving average, exponential smoothing (IIR), or median filter inside the callback before returning. The APAPHX-Board v2 hardware delivers inherently stable, high-resolution readings through its dedicated ADC and signal conditioning circuit, making software smoothing largely unnecessary when using that board; bare ADC solutions wired directly to a microcontroller analog input typically require it.

EEPROM writes happen on every call to setProbeSetpoint(), setProportionalBand(), or setDosingType(), and once at begin() if no valid config was found. On AVR, EEPROM.put() uses EEPROM.update() internally and skips bytes that already match, limiting wear. On ESP8266/ESP32, EEPROM.commit() is called per save — flash writes are more expensive; call forceConfigurationSave() after a batch of changes to guarantee persistence before the next power cycle rather than calling setters one at a time in a configuration flow.

Diagnostic output — define APA_DOSE_DEBUG before building to print per-cycle messages on Serial (sensor readings, PWM value, pulse duration, feedback result).

PlatformIO — add to platformio.ini:

build_flags = -D APA_DOSE_DEBUG

Arduino IDE — open APADOSE.h in the library folder and uncomment the prepared line near the top of the file:

// #define APA_DOSE_DEBUG   ← remove the leading // to enable

Remember to comment it back out before releasing production firmware — the define adds ~200 bytes of flash and runtime snprintf overhead on every dosing cycle.

Peristaltic pumps (default)

setPumpRange(minPWM, maxPWM) with min < max — both PWM speed and pulse duration scale with error. This is the primary use case.

Peristaltic pump calibration — two steps:

Step 1 — PWM start threshold (required): Run examples/calibration/00_pump_calibration/ before writing your main sketch. It is an interactive Serial utility: type a PWM value, the pump runs for 3 seconds, repeat until the shaft turns — then type done and it prints the exact setPumpRange() line to copy. Run it once per pump; the threshold is specific to each motor and supply voltage.

Step 2 — Flow rate (optional, for volume tracking): Run examples/calibration/01_flow_rate_calibration/ to measure your pump's output at max PWM in mL/min. Fill the container with 500 mL of the actual chemical, type run — the pump starts. Type stop when the container empties. The sketch prints the exact setPumpFlowRate() line to copy. Once set, getDailyVolumeMl() and getLastDoseVolumeMl() return accurate consumption figures.

Solenoid valves — time-proportional mode

Solenoids are binary devices and cannot be speed-controlled by PWM. Set min == max to lock PWM at a fixed level and let pulse duration carry all the proportionality:

pump.setPumpRange(255, 255);  // solenoid: always full-on; time varies with error

With min == max, the dosing zones table still applies — the solenoid opens for 10–180 s proportional to the error percentage. Feedback, safety, and alarm systems work identically.

Volume tracking

Call setPumpFlowRate() with your pump's measured output at max PWM to enable chemical consumption monitoring:

Measuring your pump's flow rate: Run examples/calibration/01_flow_rate_calibration/ to measure this value. Fill the container with exactly 500 mL of the actual chemical, type run — the pump starts at full speed. Type stop the moment the container empties. The sketch prints the flow rate and the exact setPumpFlowRate() line to copy. Run once per pump using the real chemical — water has a different viscosity and gives inaccurate results.

phPump.setPumpFlowRate(420.0);  // measured 420 mL/min for this specific pump

The default is 450 mL/min if not called. Volume per dose is estimated from actual pulse duration and PWM intensity relative to maximum. Read back with:

Serial.print(phPump.getDailyVolumeMl(), 1);    // total mL dosed today
Serial.print(phPump.getLastDoseVolumeMl(), 1);  // mL in the last dose
Serial.print(phPump.getDailyDoseCount());        // number of doses today

getDailyVolumeMl() resets at midnight when an RTC callback is registered, in sync with getDailyDoseCount(). Without an RTC both counters reset every 24 h from boot.

Adaptive proportional band

Call enableAdaptivePB(nudgePct) after begin() to switch from a fixed control band to a self-learning one. After each successful feedback cycle the library nudges the effective band toward the pool's true chemical response — wider when the sensor overshot, narrower when it undershot.

phPump.enableAdaptivePB(5);  // learn at 5% per feedback cycle (1–25 allowed)

// Read back the current effective band at any time:
Serial.print(F("Band: "));
Serial.println(phPump.getAdaptedPB());  // learned or fixed, whichever is active
Call Behaviour
enableAdaptivePB(5) Enable at 5% nudge rate; seeds from fixed band on first enable
enableAdaptivePB(0) Disable; learned value is discarded; reverts to fixed band
getAdaptedPB() Current effective band (learned when on, fixed when off)
isAdaptivePBEnabled() true when nudgePct > 0

The learned value is saved to EEPROM after every adjustment and restored on reboot. It is per-instance — each pump learns independently.

Full API reference: docs/API.md


Platform Support

Platform Tested boards Notes
AVR Uno, Mega, Nano, Pro Mini Full support; fits on Uno (32 KB flash / 2 KB SRAM)
ESP8266 NodeMCU, Wemos D1 Mini EEPROM begin() / commit() handled automatically
ESP32 ESP32-DevKit, S2, S3, C3 EEPROM flash emulation handled automatically
STM32 Nucleo, Blue Pill (STM32duino) Verified on Blue Pill F103C8 (STM32duino)

Verified build sizes (examples/basic/02_ph_and_cl — two-pump sketch, release mode, clean build):

Board Flash RAM
Arduino Uno (ATmega328P) 22,424 B / 32,256 B (70%) 877 B / 2,048 B (43%)
Arduino Mega 2560 23,476 B / 253,952 B (9%) 877 B / 8,192 B (11%)
ESP32-DevKit 296,533 B / 1,310,720 B (23%) 22,208 B / 327,680 B (7%)
NodeMCU v2 (ESP8266) 282,747 B / 1,044,464 B (27%) 28,968 B / 81,920 B (35%)
Blue Pill (STM32F103C8T6) 32,424 B / 65,536 B (49%) 2,764 B / 20,480 B (14%)

ESP flash totals include the full Arduino framework (WiFi stack, OS); the library itself adds a few KB on top of a bare sketch.

Dependencies: <Arduino.h> and <EEPROM.h> only.
No APA libraries. No sensor libraries. No communication libraries. No other third-party code.


APA Ecosystem

APA-Dose works standalone with any sensor that returns a float. For a complete household pool automation solution, it pairs with dedicated APA Devices hardware and libraries to cover every layer of the control stack.

Build a complete pool controller — no proprietary black box required

Professional pool controllers cost hundreds to thousands of euros, lock you into a closed system, and offer no visibility into what they actually do. The APA Devices ecosystem gives you the same closed-loop proportional dosing, safety interlocks, and alarm logic on hardware you own and software you can read — for a fraction of the price.

Everything you need, layer by layer:

Layer Component What it provides
Sensor hardware APAPHX-Board v2 Ready-made dual-channel pH + ORP measurement board based on ADS1115; I²C, calibrated, temperature-compensated
Sensor firmware APAPHX2_ADS1115 Non-blocking driver for the APAPHX-Board v2; applies Passco 2001 formula for stable temperature-compensated readings; returns a single float
Dosing control APA-Dose (this library) Proportional pump control, closed-loop feedback, full alarm system, safety interlocks — all the intelligence
Temperature DS2482 DS2482-800 I²C-to-1-Wire bridge for DS18B20 water temperature sensors; or use any DS18B20 library or raw sensor of your choice
Microcontroller Your choice Any Arduino-compatible board — Uno, Mega, ESP32, STM32

Pair an APAPHX-Board v2 with the APAPHX2_ADS1115 library and this APA-Dose library, add a microcontroller, wire your dosing pumps — and you have a complete, calibrated, safe pool dosing system. Temperature monitoring is optional but straightforward with the DS2482 library or any DS18B20 solution you already have.

No subscriptions. No cloud dependency. No proprietary protocol. Full source code. You control everything.

See examples/expert/ for complete sketches combining all three libraries.


Files

APA-DOSING_LIB/
├── src/
│   ├── APADOSE.h            Main header — all public types and API
│   └── APADOSE.cpp          Implementation
├── docs/
│   └── API.md               Full API reference
├── examples/
│   ├── calibration/         00_pump_calibration  (run first — finds setPumpRange() value)
│   │                        01_flow_rate_calibration  (optional — measures mL/min for volume tracking)
│   ├── basic/               01_single_ph · 02_ph_and_cl  (includes shock button)
│   ├── intermediate/        03_serial_diagnostics · 04_lcd_display
│   ├── advanced/            05_multi_pump  (RTC + shock pro API) · 06_alarm_management
│   └── expert/              07–10  combined APAPHX2 + DS2482 sketches
├── extras/
│   └── apadose-banner.png   Repository banner image
├── LICENSE
├── keywords.txt
├── library.properties
└── CHANGELOG.md

Disclaimer

This library was developed for private household pool automation only.

It is not designed, tested, or approved for commercial pools, public swimming facilities, spas, or any installation subject to health and safety regulation.

The library is provided as-is, without warranty of any kind, and without any support.

Chemical dosing systems carry real safety risks. Incorrect configuration, wiring errors, or sensor failure can result in severe over-dosing that may harm bathers, damage equipment, or create hazardous conditions. The user assumes full responsibility for installation, configuration, safe operation, and compliance with all applicable local regulations.


License

APA-Dose is released under a dual license:

Non-commercial use — free
Personal, private, educational, and hobby use is permitted free of charge. See the LICENSE file for full terms.

Commercial use — license required
Using this library in commercial products or services is strictly prohibited without a separate written Commercial License. This includes — but is not limited to:

  • Selling hardware with APA-Dose pre-installed or bundled
  • Commercial pool maintenance or chemical dosing services that rely on this library
  • Integrating this library into products or systems sold to third parties

For commercial licensing, custom integration, volume pricing, or OEM arrangements, please contact:

jaroslav@vazac.eu


Author: kecup@vazac.eu
© APA Devices

About

Arduino library for autonomous pool chemical dosing. Proportional closed-loop control for pH+, pH− and chlorine/ORP; scheduled/manual dosing for flocculant and algaecide — up to 4 pumps. Non-blocking, full alarm and safety system, filtration interlock, EEPROM persistence. AVR, ESP8266, ESP32, STM32. No external dependencies.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages