The code supports the Atmega2560 microcontroller (eg. Arduino Mega2560 or CONTROLLINO MAXI boards) and it was ported to the ESP32 platform by @Gixy31
Four main metrics are measured and periodically reported over MQTT and a 3.5" Nextion touch screen: water temperature and pressure, pH and ORP values.
Pumps states, tank-levels estimates and other parameters are also periodically reported
Two PID regulation loops are running in parallel: one for PH, one for ORP
An additional simple (on/off) regulation loop is handling the water temperature (it starts/stops the house-heating system circulator which brings heat to a heat exchanger mounted on the pool water pipes)
pH is regulated by injecting Acid from a tank into the pool water (a relay starts/stops the Acid peristaltic pump)
ORP is regulated by injecting Chlorine from a tank into the pool water (a relay starts/stops the Chlorine peristaltic pump)
Defined time-slots and water temperature are used to start/stop the filtration pump for a daily given amount of time (a relay starts/stops the filtration pump)
Tank-levels are estimated based on the running-time and flow-rate of each pump.
Ethernet connectivity parameters can be set through a webpage accessible from the LAN at http://PoolMaster.local.
If an ethernet connection is available, the internal clock (RTC) is synchronized with a time-server every day at midnight.
An API function enables telling the system what the outside air temperature is. In case it is below -2.0°C, filtration is started until it rises back above +2.0°C
Communication with the system is performed using the MQTT protocol over an Ethernet connection to the local network/MQTT broker.
Every 30 seconds (by default), the system will publish on the "PoolTopicMeas1" and "PoolTopicMeas2"(see in code below) the following payloads in Json format:
{"Tmp":818,"pH":321,"PSI":56,"Orp":583,"FilUpT":8995,"PhUpT":0,"ChlUpT":0}
{"AcidF":100,"ChlF":100,"IO":11,"IO2":0}
Tmp: measured Water temperature value in °C x100 (8.18°C in the above example payload)
pH: measured pH value x100 (3.21 in the above example payload)
Orp: measured Orp (aka Redox) value in mV (583mV in the above example payload)
PSI: measured Water pressure value in bar x100 (0.56bar in the above example payload)
FiltUpT: current running time of Filtration pump in seconds (reset every 24h. 8995secs in the above example payload)
PhUpT: current running time of Ph pump in seconds (reset every 24h. 0secs in the above example payload)
ChlUpT: current running time of Chl pump in seconds (reset every 24h. 0secs in the above example payload)
AcidF: percentage fill estimate of acid tank ("pHTank" command must have been called when a new acid tank was set in place in order to have accurate value)
ChlF: percentage fill estimate of Chlorine tank ("ChlTank" command must have been called when a new Chlorine tank was set in place in order to have accurate value)
IO: a variable of type BYTE where each individual bit is the state of a digital input on the Arduino. These are :
- FiltPump: current state of Filtration Pump (0=on, 1=off)
- PhPump: current state of Ph Pump (0=on, 1=off)
- ChlPump: current state of Chl Pump (0=on, 1=off)
- PhlLevel: current state of Acid tank level (0=empty, 1=ok)
- ChlLevel: current state of Chl tank level (0=empty, 1=ok)
- PSIError: over-pressure error
- pHErr: pH pump overtime error flag
- ChlErr: Chl pump overtime error flag
IO2: a variable of type BYTE where each individual bit is the state of a digital input on the Arduino. These are :
- pHPID: current state of pH PID regulation loop (1=on, 0=off)
- OrpPID: current state of Orp PID regulation loop (1=on, 0=off)
- Mode: state of pH and Orp regulation mode (0=manual, 1=auto)
- Heat: state of water heat command (0=off, 1=on)
- R1: state of Relay1 (0=off, 1=on)
- R2: state of Relay2 (0=off, 1=on)
- R6: state of Relay6 (0=off, 1=on)
- R7: state of Relay7 (0=off, 1=on)
- this code was developped for two main hardware configurations (see list in the hardware section below):
- Controllino-Maxi or
- Arduino Mega 2560 + Ethernet shield + relay shield + RTC module
For this sketch to work on your setup you must change the following in the code (in the "Config.h" file):
- possibly the pinout definitions depending on your wiring
- the unique address of the DS18b20 water temperature sensor
- MAC and IP address of the Ethernet shield
- MQTT broker IP address and login credentials
- possibly the topic names on the MQTT broker to subscribe and publish to
- the Kp,Ki,Kd parameters for both PID loops in case your peristaltic pumps have a different throughput than 1.5Liters/hour for the pH pump and 3.0Liters/hour for the Chlorine pump. Also the default Kp values were adjusted for a 50m3 pool volume. You might have to adjust the Kp values in case of a different pool volume and/or peristaltic pumps throughput (start by adjusting it proportionally). In any case these parameters are likely to require adjustments for every pool
1- testing your water quality (using liquid kits and/or test strips for instance) and balancing it properly (pH, Chlorine, Alkalinity, Hardness). Proper water balancing will greatly ease the pH stability and regulation
2- calibrating the pH probe using calibrated buffer solutions (pay attention to the water temperature which plays a big role in pH readings)
3- adjusting pH to 7.4
4- once above steps 1 to 3 are ok, you can start regulating ORP
Notes:
a/ the ORP sensor should theoretically not be calibrated nore temperature compensated (by nature its 0mV pivot point cannot shift)
b/ the ORP reading is strongly affected by the pH value and the water temperature. Make sure pH is adjusted at 7.4
c/ prefer platinium ORP probes for setpoints >500mV (ie. Pools and spas)
e/ the response time of ORP sensors can be fast in reference buffer solutions (10 secs) and yet very slow in pool water (minutes or more) as it depends on the water composition
In this project the Arduino PID library is used to start/stop the chemicals pumps in a cyclic manner, similar to a "PWM" signal.
The period of the cycle is defined by the WINDOW SIZE parameter which is fixed (in Milliseconds). What the PID library does is vary the duty cycle of the cycle.
If the computed error in the PID loop is null or negative, the duty cycle is set to zero (the ouput of the function PID.Compute()) and the pump is never actuated.
If the error is positive, the duty cycle is set to a value between 0 and a max value (equal to the WINDOW SIZE, ie. the pump is running full time).
So in practice the ouput of the PID.Compute() function is a duration in milliseconds during which the pump will be activated for every cycle.
If for instance, the WINDOW SIZE is set to 3600000ms (ie. one hour) and the output of the PID is 600000ms (ie. 10mins), the pump will be activated for 10mins at the begining of every hour cycle.
On the default Kp,Ki,Kd parameter values of the PID:
By default in this project, Ki and Kd are null for stability reasons and so the PID loop is only a P loop, ie. a proportional loop.
Adding some Ki and Kd to the PID loop may theoretically increase regulation performance but is also more complex to adjust and could result in instabilities. Since a P-only loop worked well enough and that safety considerations should be taken seriously in this project, I left it as is.
For my 50m3 pool the Kp default values are 2000000 for the pH loop and 4500 for the Orp loop. They were chosen experimentally in the following way:
I experimentally checked how much chemical was required to change the measured parameter (pH or Orp) by a certain amount. For instance I determined that 83ml of acid changed the pH by 0.1 for my 50m3 pool. The flow rate of the acid pump being 1.5L/hour, we can then determine for how many minutes the pump should be activated if the pH error is 0.1, which are (0.083*60/1.5) = 3.3minutes or roughly 200000ms.
And so for an error of 1 in the pH PID loop, the pump needs to be activated 10 times longer, ie. during 2000000ms, which should be taken as the Kp value. The same reasoning goes for the Kp value of the Orp PID loop.
On the WINDOW SIZE:
Various parameters influence the speed at which an injected chemical in the pool water will result in a variation in the measured pH or Orp.
Experimentally I measured that in my case it can take up to 30minutes and therefore the injection cycle period should be at least 30mins or longer in order not to inject more chemical over the following cycles thinking that it required more when in fact the chemical reactions simply needed more time to take effect, which would eventually result in overshooting.
So in my case I setlled for a safe one hour WINDOW SIZE (ie. 3600000ms)
Below are the Payloads/commands to publish on the "PoolTopicAPI" topic (see hardcoded in code) in Json format in order to launch actions on the Arduino:
- {"Mode":1} or {"Mode":0} -> set "Mode" to manual (0) or Auto (1). In Auto, filtration starts/stops at set times of the day and pH and Orp are regulated
- {"Heat":1} or {"Heat":0} -> start/stop the regulation of the pool water temperature
- {"FiltPump":1} or {"FiltPump":0} -> manually start/stop the filtration pump
- {"ChlPump":1} or {"ChlPump":0} -> manually start/stop the Chl pump to add more Chlorine
- {"PhPump":1} or {"PhPump":0} -> manually start/stop the Acid pump to lower the Ph
- {"PhPID":1} or {"PhPID":0} -> start/stop the Ph PID regulation loop
- {"OrpPID":1} or {"OrpPID":0} -> start/stop the Orp PID regulation loop
- {"PhCalib":[4.02,3.8,9.0,9.11]} -> multi-point linear regression calibration (minimum 1 point-couple, 6 max.) in the form [ProbeReading_0, BufferRating_0, xx, xx, ProbeReading_n, BufferRating_n]
- {"OrpCalib":[450,465,750,784]} -> multi-point linear regression calibration (minimum 1 point-couple, 6 max.) in the form [ProbeReading_0, BufferRating_0, xx, xx, ProbeReading_n, BufferRating_n]
- {"PhSetPoint":7.4} -> set the Ph setpoint, 7.4 in this example
- {"OrpSetPoint":750.0} -> set the Orp setpoint, 750mV in this example
- {"WSetPoint":27.0} -> set the water temperature setpoint, 27.0deg in this example
- {"WTempLow":10.0} -> set the water low-temperature threshold below which there is no need to regulate Orp and Ph (ie. in winter)
- {"OrpPIDParams":[2857,0,0]} -> respectively set Kp,Ki,Kd parameters of the Orp PID loop. In this example they are set to 2857, 0 and 0
- {"PhPIDParams":[1330000,0,0.0]} -> respectively set Kp,Ki,Kd parameters of the Ph PID loop. In this example they are set to 1330000, 0 and 0
- {"OrpPIDWSize":3600000} -> set the window size of the Orp PID loop in msec, 60mins in this example
- {"PhPIDWSize":1200000} -> set the window size of the Ph PID loop in msec, 20mins in this example
- {"Date":[1,1,1,18,13,32,0]} -> set date/time of RTC module in the following format: (Day of the month, Day of the week, Month, Year, Hour, Minute, Seconds), in this example: Monday 1st January 2018 - 13h32mn00secs
- {"FiltT0":9} -> set the earliest hour (9:00 in this example) to run filtration pump. Filtration pump will not run beofre that hour
- {"FiltT1":20} -> set the latest hour (20:00 in this example) to run filtration pump. Filtration pump will not run after that hour
- {"PubPeriod":30} -> set the periodicity (in seconds) at which the system info (pumps states, tank levels states, measured values, etc) will be published to the MQTT broker
- {"PumpsMaxUp":1800} -> set the Max Uptime (in secs) for the Ph and Chl pumps over a 24h period. If over, PID regulation is stopped and a warning flag is raised
- {"Clear":1} -> reset the pH and Orp pumps overtime error flags in order to let the regulation loops continue. "Mode" also needs to be switched back to Auto (1) after an error flag was raised
- {"DelayPID":30} -> Delay (in mins) after FiltT0 before the PID regulation loops will start. This is to let the Orp and pH readings stabilize first. 30mins in this example. Should not be > 59mins
- {"TempExt":4.2} -> Provide the system with the external temperature. Should be updated regularly and will be used to start filtration when temperature is less than 2°C. 4.2deg in this example
- {"PSIHigh":1.0} -> set the water high-pressure threshold (1.0bar in this example). When water pressure is over that threshold, an error flag is set
- {"pHTank":[20,100]} -> call this function when the Acid tank is replaced or refilled. First parameter is the tank volume in Liters, second parameter is its percentage fill (100% when full)
- {"ChlTank":[20,100]} -> call this function when the Chlorine tank is replaced or refilled. First parameter is the tank volume in Liters, second parameter is its percentage fill (100% when full)
- {"Relay":[1,1]} -> call this generic command to actuate spare relays. Parameter 1 is the relay number (R1 in this example), parameter 2 is the relay state (ON in this example). This command is useful to use spare relays for additional features (lighting, etc). Available relay numbers are 1,2,6,7,8,9
- {"Reboot":1} -> call this command to reboot the controller (after 8 seconds from calling this command)
- {"pHPumpFR":1.5} -> call this command to set pH pump flow rate un L/s. In this example 1.5L/s
- {"ChlPumpFR":3} -> call this command to set Chl pump flow rate un L/s. In this example 3L/s
- {"RstpHCal":1} -> call this command to reset the calibration coefficients of the pH probe
- {"RstOrpCal":1} -> call this command to reset the calibration coefficients of the Orp probe
- {"RstPSICal":1} -> call this command to reset the calibration coefficients of the pressure sensor
- x1 CONTROLLINO MAXI (ATmega2560) or Arduino Mega 2560 + Ethernet shield + relay shield + RTC module
- x1 pH_Orp_Board (digital interface to the pH and Orp probes with galavanic isolation)
- x2 Peristaltic pumps, suction lances for tanks, pH and Orp probes
- x1 Water grounding
- x1 Water temperature probe (DS18B20)
- x1 Pressure sensor
- x1 Nextion Enhanced NX4832K035 - Generic 3.5'' HMI Touch Display
Below is a (quick and dirty) wiring diagram with the Controllino MAXI. Right click and display image in full screen to see the details.
Cloud integration example (BLYNK)
See NodeRed folder for more info and code
See the NodeRed folder for more info and this tutorial on how to create a Grafana dashboard from MQTT data.
Non-cloud home automation integration example (JEEDOM)