One of the cardinal rules on this forum is, if you post your whole code, others will benefit. In this case, I'd really like to see what's 'behind the curtain' - so, would you please post your whole code? Thanks!
@Proietti please say your last posted code works.
I ask because your switch/case statement in D1_MainFunction() has an unusual structure. There was no compiler error or warning, but reduced so you can see, and I removed some unnecessary { braces }, the statement is
//DOSER1///////////////////////////////////////////////////////////
void D1_MainFunction()
{
switch (D1State)
{
case D1_IDLE:
break;
if (D1_SolutionFlag) //IF TRUE SOLUTION TO DOSE AVAILABLE
{
case D1_BOOST:
// code code code
break;
case D1_DOSETIME:
// code code code
break;
case D1_DOSERON:
// code code code
break;
case D1_DOSERTIMING:
// code code code
break;
case D1_DOSEROFF:
// code code code
break;
case D1_HOSECOUNT:
// code code code
break;
case D1_HOSECHECK:
// code code code
break;
}
}
}
Now you can see the line
if (D1_SolutionFlag) //IF TRUE SOLUTION TO DOSE AVAILABLE
is without any case. The case code before it ends with a break, so that line can never be executed.
Whatever that test is, it is neither being performed nor respected.
As I implied, I am surprised no warning or error - this is plainly unreachable code.
a7
The compiler did not flag any issues but i will adjust my code tomorrow, test and let you know. Thanks
The SolutionFlag is a boolean that allows the normal timing and dosing of the pump. If the SolutionFlag is false normal oparation will not happen. It is basically allowing normal function only if there is liquid solution to dose. If the solutions are finished the flag is turned to false and stop normal function, so that pumps dont run dry. If tge solutions are finished i get a notification on my phone to refill, and until i do this the flag renains false. This is taken care of elsewhere. The first 3 states outside of the SolutionFlag are used for setup and calibration of the dosing pump and can only be activated through the phone app. I will post the entire code tomorrow and some screen shots of the app.
Normally it is good to accompany a state machine solution with a state diagram. That is a diagram which indicates the states, the conditions for the transitions into the successor states (which could for example be the expiration of a timer) and any actions to be performed on those transitions (say generating a warning). Example State Machine Diagram - an overview | ScienceDirect Topics
Picking out the major states has to be done with some care. Looking at your original code fragment , the "states" such as D1_6DOSEDAY appear actually to be operating frequency parameters and not really major states. On the other hand, some of the flags, such as D1_SolutionFlag, hint at major states in the process, in this case that the solution tank could be empty to be handled as a (terminating) state. You also have flags which appear to duplicate some of the states such as the pair D1_BoostActive / D1_BOOSTMODE indicating that some actions belonging to the states are done outside the main state handling code.
Oh and ensure that you have compiler warnings switched on. "if" statements controlling cases within a switch statement does look odd, as has been pointed out.
Ok, here is my entire code and screenshots as promised above.
Home Screen
Settings Screen
D1_Function Pop Up
Firebase Server
Tab1 code
/*
AquArt DoseTec
WRITTEN BY MARIANO PROIETTI
16 CRANE AVENUE
ISLEWORTH
MIDDLESEX, TW7 7JL
TEL. 07771 528 969
*/
//LIBRARIES//
//WIFI//
#include <ESP8266WiFi.h>
//FIREBASE//
#include <FirebaseESP8266.h>
#include <addons/TokenHelper.h>
#include <addons/RTDBHelper.h>
//OTA//
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
//RTC//
#include <SNTPtime.h>
//OTHERS//
#include <EEPROM.h>
//#include "TickTwo.h"
#include <SimpleTimer.h>
#include <Simplepush.h>
//FIREBASE//
#define DATABASE_URL ""
#define DATABASE_SECRET ""
//WIFI GLOBALS//
//AquArt Setup//
#define WIFI_SSID ""
//#define WIFI_SSID "AquArt";
#define WIFI_PASSWORD ""
//Dom Setup//
//#define WIFI_SSID "DomReef"
//#define WIFI_PASSWORD "Negheth1"
//SETUP//
AsyncWebServer serverOTA(80);
SNTPtime NTPch("uk.pool.ntp.org");
Simplepush push;
SimpleTimer timer;
//STATE MACHINES VARIABLES//
enum D1_MainFunction {D1_IDLE, D1_BOOST, D1_DOSETIME, D1_DOSERON, D1_DOSERTIMING, D1_DOSEROFF, D1_HOSECOUNT, D1_HOSECHECK};
D1_MainFunction D1State = D1_IDLE;
//FIREBASE SETUP//
FirebaseAuth auth;
FirebaseConfig config;
FirebaseData dosetec;
String DOSETec_Parent = "/DOSETec"; //Parent path
String DOSETec_Child[9] = {"D1_Boost", "D1_CalibrateButton", "D1_CalibrationInput", "D1_DailyDosage", "D1_Function", "D1_SolutionInput", "D1_ALKATecDose", "D1_BoostDosage", "DOSETecActive"}; //Child path
size_t DOSETec_numChild = 9;
void streamCallback(MultiPathStreamData stream)
{
DOSETec_numChild = sizeof(DOSETec_Child) / sizeof(DOSETec_Child[0]);
for (size_t i = 0; i < DOSETec_numChild; i++)
{
if (stream.get(DOSETec_Child[i]))
{
DOSETecStateFunction(stream.dataPath.c_str(), stream.value.c_str());
}
}
}
void streamTimeoutCallback(bool timeout)
{
}
//TICKER INSTANCES//
//void ClockFunction();
//TickTwo T_ClockFunction(ClockFunction, 1000);
//PIN ASSIGNMENTS//
#define LEDON D4
#define DOS1 D1
#define DOS2 D2
#define DOS3 D5
#define DOS4 D6
#define DOS5 D7
#define DOS6 D8
//TIME//
strDateTime dateTime;
byte Hr;
byte Min;
byte Sec;
unsigned long TimeNow;
unsigned long BoostNow;
unsigned long BoostPrev;
//DOSER1 VARIABLES//
int D1_State;
int D1_Divisor;
unsigned long D1_Time;
unsigned long D1_ModDevisor;
unsigned long D1_SolutionInputInt;
float D1_DoseTime;
float D1_CalibrationInputFloat;
float D1_DailyDosageFloat;
float D1_BoostDosageFloat;
boolean D1_BoostActive = false;
boolean D1_VolFlag = false;
boolean D1_SolutionFlag = false;
boolean D1_EmptyAlertFlag = false;
boolean D1_CalibrateActive = false;
boolean D1_ALKADoseActive = false;
boolean D1_HoseFlagAlert = false;
boolean D1_ALKATecFlag = false;
boolean D1_RemainVolFlag = false;
boolean D1_2D_Flag = true;
boolean D1_5D_Flag = false;
//GENERAL TIMING VARIABLES//
float Round = 0.5; //variable used for rounding up or down
unsigned long D1_DoserTime;
//GENERAL DOSER VARIABLES//
boolean LedOnFlag = true;
boolean DOSETecActive = false;
//TIME VARIABLES//
unsigned long Mod1 = 86400; //Modulo 1 in secs
unsigned long Mod2 = 43200; //Modulo 2 in secs
unsigned long Mod6 = 14400; //Modulo 6 in secs
unsigned long Mod12 = 7200; //Modulo 12 in secs
unsigned long Mod24 = 3600; //Modulo 24 in secs
unsigned long Mod48 = 1800; //Modulo 48 in secs
unsigned long T2 = 300; //5 mins in secs offset
unsigned long T3 = 600; //10 mins in secs offset
unsigned long T4 = 900; //15 mins in secs offset
unsigned long T5 = 1200; //20 mins in secs offset
unsigned long T6 = 1500; //25 mins in secs offset
//VOLUME UPDATE VARIABLES
int D1_MLperiods;
int D2_MLperiods;
int D3_MLperiods;
int D4_MLperiods;
int D5_MLperiods;
int D6_MLperiods;
unsigned long D1_RemainVolume;
unsigned long D1_RemainVolTemp;
//DAYS REMAINING VARIABLES
int D1_DaysRemaining;
int D2_DaysRemaining;
int D3_DaysRemaining;
int D4_DaysRemaining;
int D5_DaysRemaining;
int D6_DaysRemaining;
//HOSE CHANGE VARIABLES
int D1_Hose = 0;
int D2_Hose = 0;
int D3_Hose = 0;
int D4_Hose = 0;
int D5_Hose = 0;
int D6_Hose = 0;
int MaxHose = 30000;
void setup()
{
Serial.begin(115200);
EEPROM.begin(512);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED)
{
LedOnFlag = false;
delay(500);
}
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
if (LedOnFlag)
{
digitalWrite(LEDON, HIGH);
}
else
{
digitalWrite (LEDON, LOW);
}
config.token_status_callback = tokenStatusCallback;
config.database_url = DATABASE_URL;
config.signer.tokens.legacy_token = DATABASE_SECRET;
Firebase.begin(DATABASE_URL, DATABASE_SECRET);
dosetec.setBSSLBufferSize(1024, 1024);
dosetec.setResponseSize(1024);
if (!Firebase.beginMultiPathStream(dosetec, DOSETec_Parent))
{
Serial.println("DOSETec Stream error");
}
Firebase.setMultiPathStreamCallback(dosetec, streamCallback, streamTimeoutCallback);
NTPch.setSNTPtime();
delay(500);
//OTA////////////////////////////////////////////////////////
serverOTA.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "AquArt DOSETec");
});
AsyncElegantOTA.begin(&serverOTA);
serverOTA.begin();
//Firebase.setString(", "WiFi.localIP().toString(),"/update"");
/////////////////////////////////////////////////////////////
pinMode(DOS1, OUTPUT);
pinMode(DOS2, OUTPUT);
pinMode(DOS3, OUTPUT);
pinMode(DOS4, OUTPUT);
pinMode(DOS5, OUTPUT);
pinMode(DOS6, OUTPUT);
pinMode(LEDON, OUTPUT);
timer.setInterval(1000L, ClockFunction); //SNTP CLOCK
D1_StateCheck();
}
void loop()
{
timer.run();
D1_MainFunction();
if (D1_ALKATecFlag)
{
if (Firebase.ready())
{
Firebase.setStringAsync(dosetec, "/DOSETec/ALKATecDose1", "false");
D1_ALKATecFlag = false;
}
}
else if (D1_RemainVolFlag)
{
EEPROM.get(10, D1_RemainVolume);
if (Firebase.ready())
{
Firebase.setIntAsync(dosetec, "/DOSETec/D1_RemainVolume", D1_RemainVolume);
D1_RemainVolFlag = false;
}
}
else if (DOSETecActive)
{
if (Firebase.ready())
{
Firebase.setStringAsync(dosetec, "/DOSETec/DOSETec_Online", "0");
DOSETecActive = false;
}
}
}
void DOSETecStateFunction(String path, String value) //FIREBASE CALLBACK FUNCTION WHICH ONLY TRIGGERS WHEN A CHANGE TO A CHILD IS DETECTED
{
if (path == "/D1_Boost")
{
if (value == "true")
{
D1_BoostActive = true;
}
else
{
D1_BoostActive = false;
}
}
else if (path == "/D1_CalibrateButton")
{
if (value == "true")
{
D1_CalibrateActive = true;
}
else
{
D1_CalibrateActive = false;
}
}
else if (path == "/D1_ALKATecDose")
{
if (value == "true")
{
D1_ALKADoseActive = true;
}
else
{
D1_ALKADoseActive = false;
}
}
else if (path == "/DOSETecActive")
{
if (value == "true")
{
DOSETecActive = true;
}
}
//PARAMETERS//
else if (path == "/D1_CalibrationInput")
{
D1_CalibrationInputFloat = value.toFloat();
D1_DoseTime = 30000/D1_CalibrationInputFloat;
EEPROM.put(5, D1_DoseTime);
EEPROM.commit();
}
else if (path == "/D1_DailyDosage")
{
D1_DailyDosageFloat = value.toFloat();
EEPROM.put(0, D1_DailyDosageFloat);
EEPROM.commit();
}
else if (path == "/D1_SolutionInput")
{
D1_SolutionInputInt = value.toInt();
D1_RemainVolume = D1_SolutionInputInt;
EEPROM.put(10, D1_RemainVolume);
EEPROM.commit();
D1_SolutionFlag = true;
D1_RemainVolFlag = true;
}
else if (path == "/D1_BoostDosage")
{
D1_BoostDosageFloat = value.toFloat();
EEPROM.put(25, D1_BoostDosageFloat);
EEPROM.commit();
//FUNCTIONS//////////////////////////////////////
else if (path == "/D1_Function")
{
if (value == "0") //OFF/BLEED
{
D1State = D1_IDLE;
D1_State = 0;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "1") //CALIBRATE
{
D1State = D1_IDLE;
D1_State = 0;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "2") //HOSECHANGE
{
D1State = D1_IDLE;
D1_Hose = 0;
D1_State = 0;
EEPROM.put(20, D1_State);
EEPROM.put(15, D1_Hose);
EEPROM.commit();
D1_HoseFlagAlert = true;
}
else if (value == "3") //BOOSTMODE
{
D1State = D1_BOOST;
D1_State = 1;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "4") //1DOSEDAY
{
D1_Divisor = 1;
D1_ModDevisor = Mod1;
D1State = D1_DOSETIME;
D1_State = 2;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "5") //2DOSEDAY
{
D1_Divisor= 2;
D1_ModDevisor = Mod2;
D1State = D1_DOSETIME;
D1_State = 3;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "6") //6DOSEDAY
{
D1_Divisor = 6;
D1_ModDevisor = Mod6;
D1State = D1_DOSETIME;
D1_State = 4;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "7") //12DOSEDAY
{
D1_Divisor = 12;
D1_ModDevisor = Mod12;
D1State = D1_DOSETIME;
D1_State = 5;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else if (value == "8") //24DOSEDAY
{
D1_Divisor = 24;
D1_ModDevisor = Mod24;
D1State = D1_DOSETIME;
D1_State = 6;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
else //48DOSEDAY
{
D1_Divisor = 48;
D1_ModDevisor = Mod48;
D1State = D1_DOSETIME;
D1_State = 7;
EEPROM.put(20, D1_State);
EEPROM.commit();
}
}
}
void D1_StateCheck() //FUNCTION TO SAVE CURRENT SETTINGS IN CASE OF POWER FAILURE OR REBOOT. FUNCTION IS TRIGGERED IN SETUP
{
EEPROM.get(20, D1_State);
if (D1_State == 0)
{
D1State = D1_IDLE;
}
else if (D1_State == 2)
{
D1_Divisor = 1;
D1_ModDevisor = Mod1;
D1State = D1_DOSETIME;
}
else if (D1_State == 3)
{
D1_Divisor= 2;
D1_ModDevisor = Mod2;
D1State = D1_DOSETIME;
}
else if (D1_State == 4)
{
D1_Divisor = 6;
D1_ModDevisor = Mod6;
D1State = D1_DOSETIME;
}
else if (D1_State == 5)
{
D1_Divisor = 12;
D1_ModDevisor = Mod12;
D1State = D1_DOSETIME;
}
else if (D1_State == 6)
{
D1_Divisor = 24;
D1_ModDevisor = Mod24;
D1State = D1_DOSETIME;
}
else if (D1_State == 7)
{
D1_Divisor = 48;
D1_ModDevisor = Mod48;
D1State = D1_DOSETIME;
}
else
{
D1State = D1_IDLE;
}
}
//END//
Tab 2 code
//DOSER1///////////////////////////////////////////////////////////
void D1_MainFunction()
{
switch (D1State)
{
case D1_IDLE:
{
if (D1_CalibrateActive)
{
digitalWrite(DOS1, HIGH);
}
else
{
digitalWrite(DOS1, LOW);
}
break;
}
case D1_BOOST:
{
if (D1_SolutionFlag) //IF TRUE SOLUTION TO DOSE AVAILABLE
{
if (D1_ALKADoseActive)
{
//Put code here for the ALKATec Device
}
}
break;
}
case D1_DOSETIME:
{
if (D1_SolutionFlag) //IF TRUE SOLUTION TO DOSE AVAILABLE
{
EEPROM.get(5, D1_DoseTime);
EEPROM.get(0, D1_DailyDosageFloat);
EEPROM.get(25, D1_BoostDosageFloat);
D1_Time = ((D1_DailyDosageFloat / D1_Divisor) * D1_DoseTime) + Round;
if (TimeNow % D1_ModDevisor == 0)
{
D1_VolUpdateFunction(D1_Divisor, 0, false);
D1_VolFlag = true;
D1State = D1_DOSERON;
}
else if (D1_BoostActive)
{
digitalWrite(DOS1, HIGH);
}
else if (!D1_BoostActive)
{
digitalWrite(DOS1, LOW);
D1_VolUpdateFunction(0, D1_BoostDosageFloat, true);
}
else
{
}
}
else
{
D1State = D1_IDLE;
}
break;
}
case D1_DOSERON:
{
D1_DoserTime = millis();
digitalWrite(DOS1, HIGH);
D1State = D1_DOSERTIMING;
break;
}
case D1_DOSERTIMING:
{
if (millis() - D1_DoserTime >= D1_Time)
{
D1State = D1_DOSEROFF;
}
break;
}
case D1_DOSEROFF:
{
digitalWrite(DOS1, LOW);
EEPROM.get(15, D1_Hose);
D1State = D1_HOSECOUNT;
break;
}
case D1_HOSECOUNT:
{
D1_Hose = D1_Hose + 1;
EEPROM.put(15, D1_Hose);
EEPROM.commit();
D1State = D1_HOSECHECK;
break;
}
case D1_HOSECHECK:
{
if (D1_Hose >= MaxHose && D1_HoseFlagAlert == true)
{
D1_HoseAlert();
}
D1State = D1_DOSETIME;
break;
}
}
}
//VOLUME UPDATE//
void D1_VolUpdateFunction(int periods, int ml, bool boost)
{
EEPROM.get(0, D1_DailyDosageFloat);
EEPROM.get(10, D1_RemainVolume);
if (D1_VolFlag) //FLAG TO STOP REPEAT SESSIONS
{
if (boost) //BOOST FUNCTION
{
D1_RemainVolTemp = D1_RemainVolume - ml; //VOLUME CALCULATION USING THE BOOST
if (D1_RemainVolTemp <= 0)
{
D1_RemainVolume = 0;
D1_SolutionFlag = false;
D1_EmptyAlertFlag = true;
D1_EmptyAlert();
}
else
{
D1_RemainVolume = D1_RemainVolTemp; //UPDATE D1_RemainVolTemp FOR NEXT TIME ROUND
}
}
else //NORMAL FUNCTION
{
D1_MLperiods = D1_DailyDosageFloat / periods; //SINGLE DOSE CALCULATION IN ML
D1_RemainVolTemp = D1_RemainVolume - D1_MLperiods; //REMAINING VOLUME MINUS A SINGLE DOSE
if (D1_RemainVolTemp <= 0)
{
D1_RemainVolume = 0;
D1_SolutionFlag = false;
D1_EmptyAlertFlag = true;
D1_EmptyAlert();
}
else
{
D1_RemainVolume = D1_RemainVolTemp; //UPDATE D1_RemainVolTemp FOR NEXT TIME ROUND
}
}
EEPROM.put(10, D1_RemainVolume);
EEPROM.commit();
D1_VolFlag = false; //FLAG TO STOP REPEAT SESSIONS
D1_DaysRemaining = D1_RemainVolume / D1_DailyDosageFloat; //UPDATE DAYS REMAINING
D1_DaysRemainCheckFunction(); //CALL FUNCTION TO CHECK VOLUME STATUS
D1_RemainVolFlag = true; //UPDATE FIREBASE
}
}
void D1_DaysRemainCheckFunction()
{
if (D1_DaysRemaining <= 5 && D1_DaysRemaining > 2)
{
if (D1_5D_Flag)
{
D1_5D_Flag = false;
D1_2D_Flag = true;
D1_5DSolutionAlert();
}
}
else if (D1_DaysRemaining <=2)
{
if (D1_2D_Flag)
{
D1_2D_Flag = false;
D1_2DSolutionAlert();
}
}
else if (D1_DaysRemaining > 5)
{
D1_5D_Flag = true;
}
else
{
}
}
//ALERTS//
void D1_EmptyAlert()
{
if (D1_EmptyAlertFlag)
{
push.send("qxrt6i", "AquArt DOSETec Alert", "D1 Empty Solution. Doser Disabled", "Alert");
D1_EmptyAlertFlag = false;
}
}
void D1_5DSolutionAlert()
{
push.send("qxrt6i", "AquArt DOSETec Alert", "D1 5 Days Solution Remaining", "Alert");
}
void D1_2DSolutionAlert()
{
push.send("qxrt6i", "AquArt DOSETec Alert", "D1 2 Days Solution Remaining", "Alert");
}
void D1_HoseAlert()
{
push.send("qxrt6i", "AquArt DOSETec Alert", "D1 Hose Pipe Change Required", "Alert");
D1_HoseFlagAlert = false;
}
void ClockFunction()
{
dateTime = NTPch.getTime(0, 1); //Get time from internal clock
Hr = dateTime.hour;
Min = dateTime.minute;
Sec = dateTime.second;
TimeNow = ((Hr*3600) + (Min*60) + Sec);
if (TimeNow % Mod2 == 0)
{
NTPch.setSNTPtime(); //SYNC TIME TWICE/DAY
}
}
It looks like you have made significant changes to your state machine since your first post so does the original problem still exist?
The smartphone configuration interface looks cool, however, that is simply setting configuration parameters. Once these parameters have been defined, the Aquarium software will continue to run autonomously according to the state machine you have defined. Ideally, there would be a clear separation between the code to update the configuration parameters and the code which regularly runs (your state machine) to perform the actions on the aquarium. Maybe you have already got that far.
I have addressed the update from my phone to the esp8266 using the firebase library and in particular the stream function, which only activates when a change to a Child is detected. If it is parameter which is changed ie. dosing amount it is emmediatly saved to EEPROM for use by the main function of the Esp8266. I had this entire set up running for years using the Blynk interface but since Blynk now have totally changed their system (which i do not like) i decided to create my own app using Kodular and Firebase as a server
I guess there is always a mixture of it, isn't it? Matter of fact I use this kind of approach:
For every state my project may take I ask myself following questions:
Is there an activity
-
to properly INITatilize a given state X?
-
to properly TERMinate that state X?
-
to properly RUN that state X, and
-
what are the conditions to leave state X to identify which INIT-Function (of next state Y, Z, etc.) I have to call
Not all functions may be needed, however it helps not to miss something.
And, yes, there is the assumption behind that INIT and TERM can acutually fully excecuted within one function call because they called just once at the edge. Otherwise a state of, e.g, INIT_X might be needed that ends on its own, ie. without "external conditions" once finished, and then calls state then RUN_X.
![]()
i assume there can be hybrids, but sticking to one concept or the other should be simple
sounds like a mode of operation that may be described by a state machine



