When to read sensors whilst running a state machine?

Just a little embellishment.

Actually / sadly its more like knew it, forgot it, knew it, forgot it.

That's what happens when we forget to take our medication.

Tom.... :smiley: :+1: :coffee: :australia:

Medication ? :confused:

Thank you all so much, really appreciate the support!!

My program hasn’t been finished yet and I need to revise it following your advice here. I’ll look up how I can post code correctly and show where I’m at later. Lots to think about :slight_smile: Cheers, Rene

#include <Servo.h>
#include <FastPwmPin.h>
#include <LibPrintf.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>

// Initialize the LCD (address 0x27, 20 columns, 4 rows)
LiquidCrystal_I2C lcd(0x27, 20, 4);

// Pin connected to the DS18B20 data line
#define ONE_WIRE_BUS 7 // Input pin from the DS18B20 temp sensors; recommended to connect with a 4.7kΩ pull-up resistor between DQ and VDD.
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

const int voltPin = A0;         //Used for voltage reading from voltage-devider
const int ampPin = A1;          //Used for current reading and alarm/protection setting, signal from WCS1700 module
const int irSensorPin = A2;     //Used for part detection
const int glowPin = A3;         //Used for detection of light emission when part is at temperature
const int annealRelayPin = 9;   //ZVS operation servo, connected to SSR
const int servoPin = 11;        //Trapdoor operation servo

// Entries related to averaging
const int numReadings = 10;
float readings[numReadings];      
int readIndex = 0;                
float total = 0;                  
float average = 0;               
unsigned long previousMillis = 0; 
const long interval = 100;  

Servo myServo;

enum State { Waiting, Annealing, Soaking, Trapdoor };
State currentState = Waiting;

unsigned long annealStartTime = 0;
unsigned long soakStartTime = 0;
unsigned long trapdoorStartTime = 0;
const unsigned long annealTime = 1000; // 8 seconds ; now 1 sec for testing
const unsigned long soakDuration = 1100; // 1.1 seconds
const unsigned long trapdoorDuration = 500; // 0.5 seconds
const int dutyCycle = 64; // 25% of 255

void setup() {
    Serial.begin(115200);
    sensors.begin();

    myServo.attach(servoPin);
    myServo.write(90); // Trapdoor closed in correct position
    digitalRead(irSensorPin) == HIGH;
    pinMode(LED_BUILTIN, OUTPUT);

    lcd.init();             // Initialize the LCD
    lcd.backlight();        // Turn on the backlight
    lcd.setCursor(0, 0);    // Set cursor to the first row, first column
    lcd.print("Dashboard"); // Display static text
    //Start up fan (need pwm tied in with temp), cooling water pump, radiator fan (needs pwm to be tied in with temp) - All separate functions called from Setup?
    Serial.print("Setup completed.\n");
    delay(2000);            //Delay entered to have the system settled, will be removed later once proven. Need to understand how long it takes for the machine to get into steady state
}

void loop() {
   unsigned long currentMillis = millis();



    switch (currentState) {
        case Waiting:
            Serial.print("Current State: Waiting\n");
            //Waiting for a part to be entered (detected) for the machine to start
            if (digitalRead(irSensorPin) == LOW) {
                previousMillis = currentMillis;
                currentState = Annealing;
            }
            break;

        case Annealing:
            Serial.print("Current State: Annealing\n");
            //After part detected to have been placed in the coil switch on machine for pre-determined time or until glow value reached
            Serial.print("Relay: High\n");
            while (millis() - previousMillis <= annealTime || analogRead(glowPin) < 50) {
                digitalWrite(annealRelayPin, HIGH);
                digitalWrite(LED_BUILTIN, HIGH);
            }
            
            digitalWrite(annealRelayPin, LOW);
            Serial.print("Relay: Low\n");
            digitalWrite(LED_BUILTIN, LOW);
            previousMillis = millis();
            
            currentState = Soaking;
            break;

        case Soaking:
            Serial.print("Current State: Soaking\n");
            //Set up to allow the part to remain at the temperature reached during annealing
            soakStartTime = millis();
            while (millis() - soakStartTime <= soakDuration) {
            FastPwmPin::enablePwmPin(3, 1000L, 25); // Pin 3, 1 KHz, 25% duty cycle //switching on PWM for relay pin
            digitalWrite(LED_BUILTIN, HIGH);
            }

            FastPwmPin::disablePwmPin(3); //switching off PWM for relay pin
            digitalWrite(LED_BUILTIN, LOW);

            currentState = Trapdoor;
            break;

        case Trapdoor:
            Serial.print("Current State: Trapdoor\n");
            //Leave trapdoor open to clear the part long enough
            trapdoorStartTime = millis();
            while (millis() - trapdoorStartTime <= trapdoorDuration);{
                myServo.write(135);
                delay(1000); //Delay to be replaced by millis once proven.
            }
            myServo.write(90);
            
            currentState = Waiting;
            break;

        default:
        break;
    }
}

void Current() {
    for (int i = 0; i < numReadings; i++) {
    readings[i] = 0;
  }
    // Needs max amp alarm/shutdown
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    total -= readings[readIndex];
    readings[readIndex] = analogRead(ampPin) * (5.0 / 1023.0); 
    total += readings[readIndex];
    readIndex = (readIndex + 1) % numReadings;
    
    float averageAmp = total / numReadings;
    Serial.println(averageAmp);
  }
}

void Voltage() {
    float readVoltage();
    const int numReadings = 10;
    float readings[numReadings];
    float total = 0;
    int index = 0;
    float averageVolt = 0;

    for (int i = 0; i < numReadings; i++) {
        readings[i] = analogRead(A2) * (5.0 / 1023.0); // Convert to voltage
        total += readings[i];
        delay(100); // Delay for stability //Delay to be removed later once proven.
    }

    averageVolt = total / numReadings;
    return averageVolt;
}

void Glow() {
    int glowValue = analogRead(glowPin); // Read potentiometer value
    float glow = glowValue * (5.0 / 1023.0); // 
    // Needs min glow alarm/shutdown
    lcd.setCursor(0, 1); // Set cursor to the second row
    lcd.print("Glow: ");
    lcd.print(glow, 0); // Display voltage without decimal places
    lcd.print(" G  ");    // Add unit and clear extra characters

    delay(500); // Update every 500ms //Delay to be removed later once proven.
}

void Temperature() {
        sensors.requestTemperatures(); // Request temperature readings

        float tempC = sensors.getTempCByIndex(0); // Get temperature in Celsius
        // Needs max temp alarm/shutdown
        Serial.print("Temp: ");
        Serial.print(tempC);
        Serial.print(" °C");
        
        delay(1000); //Delay to be removed later once proven.

}

void Display() {
        // Set initial 'template' for dashboard with text and sensor values updating once every 0.1 second
        //lcd.setCursor(0, 0); // top left
        //lcd.setCursor(19, 0); // top right
        //lcd.setCursor(0, 3); // bottom left
        //lcd.setCursor(19, 3); // bottom right
        lcd.setCursor(0,3);
        Serial.print("Temp:      ");
        Serial.print(" °C");


}

Please note I have still a lot to include to finalise the code and set-up. Hope it is clear what I’m trying to do and appreciate your comments. BTW Took me a long time to find out how to post code - searching for “how to post code” doesn’t get you there :slight_smile:

Thanks, Rene

Hmm, I thought it was obvious but maybe not.

OR

Prepare a separate sketch for each Input (read sensor, I2C, etc) task to start. Those should be pretty small. When you get those done, they can be added together as the final sketch. The small ones will debug quicker.

I and others here can convert delay code to non-blocking at least for some parts but for you to learn, you got to do it too once you’ve seen examples to go by.

Hello Rene

If you need help designing tasks, here is a link that shows you the first steps.

This allows you to program the tasks as functions that can be tested individually and then linked to the overall project using control structures with a state machine.

here's a table driven approach to implementing a state machine. Overkill for the often trivial state machines discussed on this forum, but a sanity saver for ones more complicated.

for complex state machines, the challenge is verifying the transitions and actions, and is easier just looking at the data, not logic

there are separate tables for the transitions to the next state and the actions for that transition

sm.cpp

#include <Arduino.h>
#include "sm.h"

extern int turnOff (void *arg);
extern int turnOn  (void *arg);

// -----------------------------------------------------------------------------
typedef int (*Action_t)(void*) ;

// ---------------------------------------------------------
int __ (void *arg)
{
    return OK;
}

// ---------------------------------------------------------
State_t smTransitionTbl [S_Last] [St_Last] = {
//      St_Null St_Sensor1 St_ThrshHi  St_ThrsLo St_Tmr
    {   S_Idle, S_On,      S_Idle,     S_Idle,   S_Idle,  }, // S_Idle
    {   S_Off,  S_Off,     S_On,       S_Off,    S_Idle, },  // S_Off
    {   S_On,   S_On,      S_On,       S_Off,    S_Idle, },  // S_On
};


Action_t smActionTbl [S_Last] [St_Last] = {
//      St_Null St_Sensor1 St_ThrshHi  St_ThrsLo St_Tmr
     {   __,    __,        turnOff,     __,      turnOff },    // S_Idle
     {   __,    __,        turnOn,      __,      turnOff },    // S_Off
     {   __,    __,        __,          turnOff, turnOff },    // S_On
};

// ---------------------------------------------------------
static State_t _smState  = S_Idle;

int
smEngine (Stim_t stim)
{
    if (St_Last <= stim)
        return ERROR;

    State_t last = _smState;

    Action_t   func = smActionTbl [_smState] [stim];
    _smState        = smTransitionTbl [_smState] [stim];

    char s [90];
    sprintf (s, "    stim %d, state %d -> %d, %s",
        stim, last, _smState, __ == func ? "" : "exec func");
    Serial.println (s);
 
    return (*func) (NULL);
}

sm.h

#ifndef _SM_H_
# define _SM_H_

enum Stim_t  { St_Nul, St_Sensor1, St_ThrshHi, St_ThrshLo, St_Tmr, St_Last };
enum State_t { S_Idle, S_Off, S_On, S_Last };
enum         { ERROR = -1, OK };

int smEngine (Stim_t stim);

#endif
#include "Arduino.h"

#include "sm.h"

const int  PinSensor1 = A1;
const int  PinSensor2 = A0;
const int  PinOutput  = 10;

int  butState;

const int ThreshHigh = 100;
const int ThreshLow  =  90;

bool sensor1;
int  sensor2;

// -----------------------------------------------------------------------------
unsigned long msecPeriod;
unsigned long msec0;
unsigned long msec;

bool          tmr;

// -------------------------------------
void timerSet (
    int  _msecPeriod )
{
    msecPeriod = _msecPeriod;
    msec0      = msec;
    tmr        = true;
}

// ---------------------------------------------------------
int turnOff (void *arg)
{
    Serial.println (__func__);
    digitalWrite (PinOutput, LOW);
    return OK;
}

// -------------------------------------
int turnOn (void *arg)
{
    Serial.println (__func__);
    digitalWrite (PinOutput, HIGH);
    timerSet (4000);
    return OK;
}

// -----------------------------------------------------------------------------
void loop ()
{
    msec = millis ();

    if (tmr && msec - msec0 >= msecPeriod)  {
        tmr     = false;
        smEngine (St_Tmr);
    }

    sensor2 = analogRead  (PinSensor2);
    if (ThreshLow > sensor2)
        smEngine (St_ThrshLo);
    else if (ThreshHigh < sensor2)
        smEngine (St_ThrshHi);

    char but = digitalRead (PinSensor1);
    if (butState != but)  {
        butState  = but;
        delay (30);

        if (LOW == but)
            smEngine (St_Sensor1);
    }

#if (1)
    Serial.println (sensor2);
    delay (500);
#endif

}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    pinMode (PinOutput,  OUTPUT);

    pinMode (PinSensor1, INPUT_PULLUP);
    butState = digitalRead (PinSensor1);
}

Thanks but tasks are not the best solution. If the user already implements a state machine for one task, she can implement another state machine for the sensors, and both machines are called in loop().

EDIT see post 35 for correction.
Just FYI, at IBM we had software that took a table as input and created the state machine.

as you can see, there's not much to do once you have the tables correct

would be more intiutive to take a graphviz file as the input to create the tables

digraph G { 
  graph [
    label="Finite State Machine"
    labelloc="t"
    fontname="sans-serif"
  ]
  node [
    style=filled
    fillcolor=gray95
    fontname="sans-serif"
  ]
  
  idle -> short [label="25-499" fontsize=10]; 
  idle -> medium [label="500-1999" fontsize=10]; 
  idle -> long [label="2000+" fontsize=10]; 
  short -> idle;  
  medium -> idle;  
  long -> idle;  
}

Actually now that I think more about it the output was not a state machine, it was just complex if then else.

Which in non-blocking code I call Tasks since they run virtually parallel.

When I use that word for functions containing not just a state machine (or multiple) but a timeout at the start to let a single timer cover all wait periods of the state machine… you didn’t object.

but Task in computer science has a very specific meaning.

why not say several functions are executed within loop()

You go to trouble to write a state machine, but compromise it with case code that blocks:

            soakStartTime = millis();
            while (millis() - soakStartTime <= soakDuration) {
              FastPwmPin::enablePwmPin(3, 1000L, 25); // Pin 3, 1 KHz, 25% duty cycle //switching on PWM for relay pin
              digitalWrite(LED_BUILTIN, HIGH);
            }

which may be fine. But if you are OK with blocking code, you could just write the entire thing as a series of tests, actions and delays.

In a bob-blocking sketch, the expectation is that loop() runs free, cycling very rapidly and often doing nothing but checking that time has (not yet) elapsed or inputs have changed.

millis() can be called once at the beginning. This is an early thing I look at: calls to millis(), to get an idea of what is happening flow-wise and as far as blocking is concerned.

This would allow other functionality to be added, which itself would politely not block and not take much time each loop pass, again maybe mostly not doing anything. Yet.

a7

Good idea. Next comes the state switch with only non-blocking code. Other task related/managing functions are not required in such state machines.

There are as many ways to handle a timed state as there are to skin a cat.

Here I added an additional state which intiates soaking, and changed the soaking state to just wait out the period of soaking then shut it down.

/// in annealing

           currentState = StartSoaking;

           break;

       case StartSoaking :
           FastPwmPin::enablePwmPin(3, 1000L, 25); // Pin 3, 1 KHz, 25% duty cycle //switching on PWM for relay pin
           digitalWrite(LED_BUILTIN, HIGH);

           soakStartTime = now;   // unsigned long now = millis(); at top of loop!
           currentState = Soaking;

           break;

       case Soaking :
           if (now - soakStartTime > soakDuration) {
             FastPwmPin::disablePwmPin(3); //switching off PWM for relay pin
             digitalWrite(LED_BUILTIN, LOW);

             currentState = Trapdoor;
           }

           break;

I could not test this. But the idea is sound, and the same thing can be done to de-block any other cases that are currently blocking whilst marking time.

a7

Because I’m not part of the priesthood who don’t own the dictionary and I can change lightbulbs.

Find yourself a more general definition for the word Task in computing, it’s not that obscure nor pwned.

“The Functions run as Tasks” is a descriptive phrase, not a pedant’s perfect phrase.