#include <WiFi.h>
#define NTP_SERVER "pool.ntp.org"
#define UTC_OFFSET 28800
#define UTC_OFFSET_DST 0
/* spiffs session */
#include "SPIFFS.h"
bool fsInit = false;
String opchannel_text = ""; //ouptut channel data for save to spiffs
/* input session */
#define ipTotal 4
struct ip_struct {
String type, id, valkey, value, unit, temp; //type,id,inputkey,value* ,unit,temp
} ipsensor[ipTotal] = {
{ "ph", "10", "ph", "0", "pH", "0"},
{ "cond", "11", "cond", "0", "us/cm", "0"},
{ "fcl", "12", "fcl", "0", "mg/L", "0"},
{ "orp", "13", "orp", "0", "mV", "0"}
} ;
/* ouptput session */
#define opTotal 8
struct op_struct {
String hoa, mode, key, lo, hi, db, DD, MM, hh, mm, wd, dur, to; //hoa,mode,inputkey,splow,sphigh,spdeadband,day,month,hour,minutes,weekday,duration,timeout
} opchannel[opTotal] = {
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
{ "auto", "manual", "*", "*", "*", "*", "*", "*", "12", "00", "*", "30", "60" },
};
unsigned long op_actiontimer[opTotal] = { 0, 0, 0, 0, 0, 0, 0, 0 }; //keep relay output turn-on time
uint16_t op_remainduration[opTotal] = { 0, 0, 0, 0, 0, 0, 0, 0 }; //keep remain duration when priority stop it
uint8_t op_setpointtype[opTotal] = { 0, 0, 0, 0, 0, 0, 0, 0 }; //type 0:setpoint low / 1:setpoint high
uint8_t op_status[opTotal] = { 0, 0, 0, 0, 0, 0, 0, 0 }; //0: not run, 1: mark need turn on 2: status on 3: status off timeout
int8_t priority = -1;
/* other session */
bool dir1 = 0;
bool dir2 = 1;
void setup() {
Serial.begin(115200);
loadOutputConfig();
/*
// WiFi.begin("Wokwi-GUEST", "", 6);
WiFi.begin("HighTalent", "abba123456", 6);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);
*/
//for test input config
ipsensor[0].value = "7";
ipsensor[1].value = "50";
//for test output config
opchannel[0].mode = "onoff";
opchannel[0].key = "ph";
opchannel[0].lo = "5.5";
opchannel[0].hi = "7.8";
opchannel[0].db = "0";
opchannel[0].dur = "120";
opchannel[2].mode = "onoff";
opchannel[2].key = "cond";
opchannel[2].lo = "10";
opchannel[2].hi = "70";
opchannel[2].db = "12";
opchannel[2].dur = "120";
//opchannel[0].DD = "";
//opchannel[0].MM = "";
//opchannel[0].hh = "16";
//opchannel[0].mm = "04";
//opchannel[0].wd = "";
//opchannel[0].dur = "120";
//opchannel[0].to = "60";
//
opchannel[1].mode = "timer";
//opchannel[1].key = "ph";
//opchannel[1].lo = "5.5";
//opchannel[1].hi = "8.5";
//opchannel[1].dur = "120";
//opchannel[1].to = "60";
//opchannel[1].DD = "";
//opchannel[1].MM = "";
opchannel[1].hh = "17";
opchannel[1].mm = "52";
//opchannel[1].wd = "";
opchannel[1].dur = "120";
opchannel[1].to = "60";
saveOutputConfig();
Serial.println("-------- setup done --------");
}
void loop() {
if (ipsensor[0].value.toFloat() > 9) dir1 = 0;
if (ipsensor[0].value.toFloat() < 5 ) dir1 = 1;
if (dir1) ipsensor[0].value = String((ipsensor[0].value.toFloat() + 0.5), 2); else ipsensor[0].value = String((ipsensor[0].value.toFloat() - 0.5), 2);
Serial.printf("%s : %s %s \n", ipsensor[0].valkey.c_str(), ipsensor[0].value.c_str(), ipsensor[0].unit.c_str());
if (ipsensor[1].value.toFloat() > 100) dir2 = 0;
if (ipsensor[1].value.toFloat() < 20 ) dir2 = 1;
if (dir2) ipsensor[1].value = String((ipsensor[1].value.toFloat() + 15), 2); else ipsensor[1].value = String((ipsensor[1].value.toFloat() - 15), 2);
Serial.printf("%s : %s %s \n", ipsensor[1].valkey.c_str(), ipsensor[1].value.c_str(), ipsensor[1].unit.c_str());
handleRelayTask();
delay(3000);
}
void handleRelayTask() {
bool debug = true;
Serial.println("----------------------------\n Handle Ouptut Channel Task\n----------------------------");
verfiyTimer() ;
for (int i = 0; i < opTotal; i++) { //Do all channels
if (debug) Serial.print( i == priority ? "[ *" : "[ " );
if (debug) Serial.printf("Channel #%02d ] hoa %4s | %6s mode ", i, opchannel[i].hoa.c_str(), opchannel[i].mode.c_str() );
if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "onoff")) { //mode : onoff mode
//default
if ( opchannel[i].dur == "*") opchannel[i].dur = "30"; //default druration 30s
if ( opchannel[i].to == "*") opchannel[i].to = "60"; //default cd time 60s
if (debug) Serial.printf("| %s ( %s - %s )\n", opchannel[i].key, opchannel[i].lo, opchannel[i].hi);
if (op_status[i] == 0) { //onoff status 0 : normal
if (checkSetpoint(i)) op_status[i] = 1;
}
if (op_status[i] == 1) {
if (op_status[priority] == 2) { //when active, priority running so do nothing
Serial.printf("\n!! relay(%02d) terminate because first priority are running...\n", i);
op_status[i] = 0;
} else { //when active, no priority running so go to switch relay on
Serial.printf("need to switch on relay(%02d) duration %s s\n", i, opchannel[i].dur.c_str());
if (i == priority) { //this is priority, try to switch of other relay
Serial.println("!! priority lock & terminate other channel...");
for (int i = 0; i < opTotal; i++) { //search all channel
if (i != priority) { //non priority so check run status
if ((op_status[i] == 2) || (op_status[i] == 5)) { //when relay running stop it
if (opchannel[i].mode == "timer") {
op_remainduration[i] = opchannel[i].dur.toInt() - (millis() - op_actiontimer[i]) / 1000 ;
Serial.printf("!! relay(%02d) are running... turn it off... remain duration : %d s\n", i, op_remainduration[i]);
op_status[i] = 4;
} else {
Serial.printf("!! relay(%02d) are running... turn it off...\n", i);
op_status[i] = 0;
}
//switch off relay
}
}
}
}
//switch on relay
op_status[i] = 2;
op_actiontimer[i] = millis();
}
}
if (op_status[i] == 2) {
Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
if ( millis() - op_actiontimer[i] > opchannel[i].dur.toInt() * 1000 ) { //turn off relay after duration
Serial.printf("!! time's up... switch off relay(%02d) and pause status for %s s\n", i, opchannel[i].to.c_str());
//switch off relay
op_status[i] = 3;
op_actiontimer[i] = millis();
}
if (((millis() - op_actiontimer[i]) > 1000 ) && checkDeadband(i)) {
Serial.printf("!! switch off relay(%02d) and pause status for %s s\n", i, opchannel[i].to.c_str());
//switch off relay
op_status[i] = 3;
op_actiontimer[i] = millis();
}
}
if (op_status[i] == 3) {
Serial.printf("!! waiting for relay(%02d) timeout...\n", i);
if ( millis() - op_actiontimer[i] > opchannel[i].to.toInt() * 1000 ) { //clear status after duration time
Serial.printf("!! time's up... free relay(%02d) status...\n", i);
op_status[i] = 0;
}
}
}
else if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "timer")) { //mode : timer mode
//default
if ( opchannel[i].mm == "*") opchannel[i].mm = "00"; //default second 00
if ( opchannel[i].dur == "*") opchannel[i].dur = "30"; //default druration 30s
if ( opchannel[i].to == "*") opchannel[i].to = "60"; //default cd time 60s
if (debug) Serial.printf("| %s/%s %s:%s (%s)\n", opchannel[i].DD.c_str(), opchannel[i].MM.c_str(), opchannel[i].hh.c_str(), opchannel[i].mm.c_str(), opchannel[i].wd.c_str());
if (op_status[i] == 1) {
if (op_status[priority] == 2) { //when active, priority running so do nothing
Serial.printf("!! relay(%02d) terminate because first priority are running...\n", i);
op_status[i] = 0;
} else { //when active, no priority running so go to switch relay on
Serial.printf("!! work according to schedule... switch on relay(%02d) duration : %s s\n", i, opchannel[i].dur.c_str());
if (i == priority) { //this is priority, try to switch of other relay
Serial.println("!! priority lock & terminate other channel...");
for (int i = 0; i < opTotal; i++) { //search all channel
if (i != priority) { //non priority so check run status
if ((op_status[i] == 2) || (op_status[i] == 5)) { //when relay running stop it
if (opchannel[i].mode == "timer") {
op_remainduration[i] = opchannel[i].dur.toInt() - (millis() - op_actiontimer[i]) / 1000 ;
Serial.printf("!! relay(%02d) are running... turn it off... remain duration : %d s\n", i, op_remainduration[i]);
op_status[i] = 4;
} else {
Serial.printf("!! relay(%02d) are running... turn it off...\n", i);
op_status[i] = 0;
}
//switch off relay
}
}
}
}
//switch on relay
op_status[i] = 2;
op_actiontimer[i] = millis();
}
}
if (op_status[i] == 2) {
Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
if ( millis() - op_actiontimer[i] > opchannel[i].dur.toInt() * 1000 ) { //turn off relay after duration
Serial.printf("!! time's up... switch off relay(%02d) pause status for a while\n", i);
//switch off relay
op_status[i] = 3;
}
}
if (op_status[i] == 3) {
Serial.printf("!! waiting for relay(%02d) timeout...\n", i);
if ( millis() - op_actiontimer[i] > 60000 ) { //clear status after least 1 minutes
Serial.printf("!! time's up... free relay(%02d) status...\n", i);
op_status[i] = 0;
}
}
if (op_status[i] == 4) {
if (op_status[priority] != 2) { //when active, priority running so do nothing
Serial.printf("!! turn on relay(%02d) for remain duration : %d s\n", i, op_remainduration[i]);
//turn on relay
op_status[i] = 5;
op_actiontimer[i] = millis();
}
}
if (op_status[i] == 5) {
Serial.printf("!! relay(%02d) status : [ ON ]\n", i);
if ( millis() - op_actiontimer[i] > op_remainduration[i] * 1000 ) { //turn off relay after duration
Serial.printf("!! time's up... switch off relay(%02d) pause status for a while\n", i);
//switch off relay
op_status[i] = 3;
}
}
}
else if ((opchannel[i].hoa == "auto") && (opchannel[i].mode == "manual")) {
if (debug) Serial.println("| manual");
}
else if (opchannel[i].hoa == "hand") {
if (debug) Serial.println("| hand on");
if (op_status[i] != 2) {
//switch on relay
op_status[i] = 2;
}
Serial.printf("!! relay(%02d) status : [ HOA ON ]\n", i);
}
else if (opchannel[i].hoa == "off") {
if (debug) Serial.println("");
if (op_status[i] != 0) {
//switch off relay
op_status[i] = 0;
Serial.printf("!! relay(%02d) status : [ HOA OFF ]\n", i);
}
}
}
Serial.println("");
}
bool initSPIFFS() {
bool succ = true;
if (!SPIFFS.begin(true)) {
Serial.println("An Error has occurred while mounting SPIFFS");
succ = false;
}
//if (SPIFFS.format()) Serial.println("Success formatting"); else Serial.println("Error formatting");
return succ;
}
void saveOutputConfig() {
bool debug = false;
opchannel_text = "";
if (debug) Serial.println("----------------------------\n save output config to spiffs...\n----------------------------");
for (int i = 0; i < opTotal; i++) {
opchannel_text += opchannel[i].hoa + "," + opchannel[i].mode + "," + opchannel[i].key + "," + opchannel[i].lo + "," + opchannel[i].hi + "," + opchannel[i].db + "," + opchannel[i].DD + "," + opchannel[i].MM + "," + opchannel[i].hh + "," + opchannel[i].mm + "," + opchannel[i].wd + "," + opchannel[i].dur + "," + opchannel[i].to + "\n" ;
}
if (debug) Serial.println("content of output.cfg : ");
if (debug) Serial.println(opchannel_text);
if (!fsInit) fsInit = initSPIFFS();
if ( fsInit ) {
String rcfilename = "/output.cfg";
Serial.print("writing output config to spiffs ");
File file = SPIFFS.open(rcfilename, FILE_WRITE);
if (!file) {
Serial.println("failed to open file for writing"); return;
}
if (file.print(opchannel_text)) Serial.println("− success!!"); else Serial.println("− failed!!");
} else Serial.println("spiffs init fails...");
}
void loadOutputConfig() {
bool debug = false;
int ptr = 0;
int row = 0;
if (debug) Serial.println("----------------------------\n load output config from spiffs...\n----------------------------");
if (!fsInit) fsInit = initSPIFFS();
if ( fsInit ) {
String rcfilename = "/output.cfg";
Serial.print("reading output config from spiffs ");
File file = SPIFFS.open(rcfilename);
if (!file) {
Serial.println("failed to open file for reading");
return;
} else {
char string[file.size()];
file.read((uint8_t *)string, sizeof(string));
string[file.size()] = '\0';
file.close();
if (sizeof(string) > 0) {
Serial.println("- success!!");
opchannel_text = String(string);
} else {
Serial.println("- failed!! try init. output config file...");
saveOutputConfig();
}
}
} else {
Serial.println("spiffs init fails...");
}
if (debug) Serial.println("content of output.cfg : ");
if (debug) Serial.println(opchannel_text);
if (debug) Serial.println("apply ouptut config to system...");
Serial.println("relay output : hoa | mode | key | lo | hi | db | DD | MM | hh | mm | wd | dur | to");
for (int i = 0; i < opchannel_text.length(); i++) {
if (opchannel_text.charAt(i) == '\n') {
String inputString = opchannel_text.substring(ptr, i);
char *token = strtok((char *)inputString.c_str(), ",");
opchannel[row].hoa = String(token); token = strtok(NULL, ",");
opchannel[row].mode = String(token); token = strtok(NULL, ",");
opchannel[row].key = String(token); token = strtok(NULL, ",");
opchannel[row].lo = String(token); token = strtok(NULL, ",");
opchannel[row].hi = String(token); token = strtok(NULL, ",");
opchannel[row].db = String(token); token = strtok(NULL, ",");
opchannel[row].DD = String(token); token = strtok(NULL, ",");
opchannel[row].MM = String(token); token = strtok(NULL, ",");
opchannel[row].hh = String(token); token = strtok(NULL, ",");
opchannel[row].mm = String(token); token = strtok(NULL, ",");
opchannel[row].wd = String(token); token = strtok(NULL, ",");
opchannel[row].dur = String(token); token = strtok(NULL, ",");
opchannel[row].to = String(token);
Serial.printf("opchannel[%d] : %4s | %6s | %6s | %6s | %6s | %5s | %2s | %2s | %2s | %2s | %3s | %5s | %5s\n", row, opchannel[row].hoa, opchannel[row].mode, opchannel[row].key, opchannel[row].lo, opchannel[row].hi, opchannel[row].db, opchannel[row].DD, opchannel[row].MM, opchannel[row].hh, opchannel[row].mm, opchannel[row].wd, opchannel[row].dur, opchannel[row].to );
ptr = i + 1 ;
row++;
}
}
}
/*
bool checkSetpoint(int idx, String setpointLo, String setpointHi ) {
bool succ = false;
if (opchannel[idx].key == "ph") {
if ((setpointLo != "*") && (ph <= setpointLo.toFloat())) {
Serial.printf("!! ph : %.2f <= %s | setpoint low trigger ", ph, setpointLo.c_str()); succ = true; op_setpointtype[idx] = 0;
}
if ((setpointHi != "*") && (ph >= setpointHi.toFloat())) {
Serial.printf("!! ph : %.2f >= %s | setpoint high trigger ", ph, setpointHi.c_str()); succ = true; op_setpointtype[idx] = 1;
}
} else if (opchannel[idx].key == "ec") {
if ((setpointLo != "*") && (ec <= setpointLo.toFloat())) {
Serial.printf("!! ec : %.2f <= %s | setpoint low trigger ", ec, setpointLo.c_str()); succ = true; op_setpointtype[idx] = 0;
}
if ((setpointHi != "*") && (ec >= setpointHi.toFloat())) {
Serial.printf("!! ec : %.2f >= %s | setpoint high trigger ", ec, setpointHi.c_str()); succ = true; op_setpointtype[idx] = 1;
}
}
return succ;
}
*/
bool checkSetpoint(int idx) {
bool succ = false;
for (int i = 0; i < ipTotal; i++) {
if (opchannel[idx].key == ipsensor[i].valkey ) {
if ((opchannel[idx].lo != "*") && (ipsensor[i].value.toFloat() <= opchannel[idx].lo.toFloat())) {
Serial.printf("!! %s : %s <= %s | setpoint low trigger ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].lo.c_str()); succ = true; op_setpointtype[idx] = 0;
}
if ((opchannel[idx].hi != "*") && (ipsensor[i].value.toFloat() >= opchannel[idx].hi.toFloat())) {
Serial.printf("!! %s : %s >= %s | setpoint high trigger ", ipsensor[i].valkey.c_str(), ipsensor[i].value, opchannel[idx].hi.c_str()); succ = true; op_setpointtype[idx] = 1;
}
}
}
return succ;
}
/*
bool checkDeadband(int idx, String setpointLo, String setpointHi, String deadBand ) {
bool succ = false;
if ((deadBand == "*" ) || (deadBand == "0" )) return succ;
if (opchannel[idx].key == "ph") {
if (( op_setpointtype[idx] == 0) && (ph >= setpointLo.toFloat() + deadBand.toFloat())) {
Serial.printf("!! ph : %.2f >= %s + %s | deadband reached ", ph, setpointLo.c_str(), deadBand.c_str()); succ = true;
}
if (( op_setpointtype[idx] == 1) && (ph <= setpointHi.toFloat() - deadBand.toFloat())) {
Serial.printf("!! ph : %.2f <= %s - %s | deadband reached ", ph, setpointHi.c_str(), deadBand.c_str()); succ = true;
}
} else if (opchannel[idx].key == "ec") {
if ((setpointLo != "*") && (ec >= setpointLo.toFloat() + deadBand.toFloat())) {
Serial.printf("!! ec : %.2f >= %s + %s | deadband reached ", ec, setpointLo.c_str(), deadBand.c_str()); succ = true;
}
if ((setpointHi != "*") && (ec <= setpointHi.toFloat() - deadBand.toFloat())) {
Serial.printf("!! ec : %.2f <= %s - %s | deadband reached ", ec, setpointHi.c_str(), deadBand.c_str()); succ = true;
}
}
return succ;
}
*/
bool checkDeadband(int idx) {
bool succ = false;
if (opchannel[idx].db == "*" ) return succ;
for (int i = 0; i < ipTotal; i++) {
if (opchannel[idx].key == ipsensor[i].valkey ) {
if (( op_setpointtype[idx] == 0) && (ipsensor[i].value.toFloat() >= opchannel[idx].lo.toFloat() + opchannel[idx].db.toFloat())) {
Serial.printf("!! %s : %s >= %s + %s | deadband reached ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].lo.c_str(), opchannel[idx].db.c_str()); succ = true; return succ;
}
if (( op_setpointtype[idx] == 1) && (ipsensor[i].value.toFloat() <= opchannel[idx].hi.toFloat() - opchannel[idx].db.toFloat())) {
Serial.printf("!! %s : %s <= %s - %s | deadband reached ", ipsensor[i].valkey.c_str(), ipsensor[i].value.c_str(), opchannel[idx].hi.c_str(), opchannel[idx].db.c_str()); succ = true; return succ;
}
}
} return succ;
}
void verfiyTimer() {
bool debug = false;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time"); return;
}
Serial.println(&timeinfo, "System Clock : %d/%m/%Y %H:%M (%a)" );
char timeVar[16];
strftime( timeVar, sizeof(timeVar), "%d,%m,%H,%M,%a", &timeinfo ); //eg: 02,23,10,45,Fri
String timeVal[5];
char* token = strtok(timeVar, ",");
uint8_t i = 0;
while (token != NULL) {
timeVal[i] = String(token); token = strtok(NULL, ","); i++;
}
for (int i = 0; i < opTotal; i++) {
if (opchannel[i].mode != "onoff") {
bool succ = true;
if ((opchannel[i].DD != "*") && (opchannel[i].DD != timeVal[0])) succ = false;
if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[0], opchannel[i].DD, succ);
if ((opchannel[i].MM != "*") && (opchannel[i].MM != timeVal[1])) succ = false;
if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[1], opchannel[i].MM, succ);
if ((opchannel[i].hh != "*") && (opchannel[i].hh != timeVal[2])) succ = false;
if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[2], opchannel[i].hh, succ);
if ((opchannel[i].mm != "*") && (opchannel[i].mm != timeVal[3])) succ = false;
if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[3], opchannel[i].mm, succ);
if ((opchannel[i].wd != "*") && (opchannel[i].wd != timeVal[4])) succ = false;
if ( debug ) Serial.printf("%s - %s : %d\n", timeVal[4], opchannel[i].wd, succ);
if ( debug ) Serial.printf("timer rc[%d] - trigger : %d\n", i, succ );
if ( op_status[i] == 0 ) op_status[i] = succ;
}
}
}