Create Event Driven Code for Serial Input

I am using a Nextion touchscreen connected to an Arduino Nano to control my project. I am using the SoftwareSerial.h library to connect the screen to the Arduino so that I can keep the primary serial ports free for uploading my sketches. I have had zero success using any of the existing Nextion libraries out there so I am creating my own functions to handle communication between the screen and Nano. I am trying to use an Event-Driven approach to create my code. I stumbled upon the Eventually.h library but it only handles pin inputs and timer events. If possible, how can I add to this functionality to the library to handle a Serial input? Is there any other event-driven libraries out there that will execute an event when data is received by the serial port?

are you using software serial at 115200 bauds? with lots of communication?

don't look further... use a real hardware port.

To me events are something that happens in almost "no time", like a status change of a pin. The reception of serial data involves events like falling/rising edges, but I would consider the reception itself as a process.

If what you want is to control your program depending on data/commands received you could just write a function that polls Serial and reacts when a valid command was received.

I am not sure if this is what you are looking for, but I have written a comprehensive example of a state machine controlled by Serial commands on Wokwi:

https://wokwi.com/projects/322586637175358035

If you start it and type "Help" or "?" in the Serial command line it will print the possible commands.

The sketch is quite long due to the different functions/states but the core is quite easy to understand and to adopt to other needs

Here is the sketch:

/*
  Example of a State Machine                      2022-01-31       ec2021

  The states can change on different criteria:

  Time:
       The state WORKING changes after a given time
       using the function "TimeToChange"
  Counter:
       The state COUNTING changes after an EndCount was reached
       using the function "EndCounterReached"
  Direct:
      The states WORK, WAIT, COUNT and BATCH are used to prepare the corresponding
      states WORKING, WAITING, COUNTING and RUNBATCH. Therefore they are passed only
      once and link directly to their corresponding "main" state.
  Externally:
      All repetitive states can be quit by Serial command.
      The state WAITING (entered via WAIT) can only be quit by Serial command.

  If two states shall be performed one after another this can be initiated
  by State.ActState = <FirstState> and State.NextState = <NextState> without changing
  the state machine. However this way it is also possible to create new state that
  calls two or more States one after another (it must keep track of the sequence
  by its own of course).

  The enum State_Type lists names for different states:

  enum {
    UNKNOWN,                  Kind of a "Null Pointer" telling that no legal state is used
    WAIT,                     Prepares for WAITING
    WAITING,                  Idles until quit by a Serial command
    WORK,                     Prepares for WORKING
    WORKING,                  In this example: Pretends to work for a certain time
    COUNT,                    Prepares for COUNTING
    COUNTING                  Counts until the counter reaches the limit "EndCount"
    BATCH,                    Prepares RUNBATCH
    RUNBATCH                  Example how to automatically control the state machine
  } State_Type;

  The structure State_Struct organizes the relevant data for the actual State and points
  to the following (next) State:

  typedef struct {
   int  ActState;               Actual State to be performed (value from enum State_Type)
   long ActDuration;            Time how long the State shall be performed
   int  NextState;              The State to change to when the ActState is done
   int StateCount;              Counter used for "Batch processing" of States
   bool GetStarttime;           Boolean to control the acquisition of Starttime
   long Starttime;              Time in millis() when the ActState started
   long Worktime;               Time in millis() used to control an internal function of "WORKING"
   String WorkText;             Text written during the state "WORKING"
   long TimePerCount;           Interval in millis() for counting
   long Count;                  The variable that counts upwards until EndCount
   long DefaultEndCount;        Used as default for EndCount (can be changed via Serial)
   bool DefaultEndCountHasChanged Forces EndCount to change when "COUNT" is called
   long EndCount;               End value for COUNTING (default or temporarily changed via Serial)
  } State_Struct;

  Input "?" or "Help" via Serial to get a list of the available commands.

  You may play around with the State Machine, e.g. by command "Count 40" and then command "WAIT"
  during the progress of COUNTING to see what happens. You can always quit states by "WAIT".
  Up to now there is no "Stop" and "Resume" command available ... Of course this is possible:
  It would require to store different data and restoring them after "Resume", including
  - where appropriate - the consideration of the "remaining time-to-go", to finalize the
  stopped state. Still something to do for the future ...


*/
enum {
  UNKNOWN,
  WAIT,
  WAITING,
  WORK,
  WORKING,
  COUNT,
  COUNTING,
  BATCH,
  RUNBATCH
} State_Type;


typedef struct {
  int ActState        = WAIT;
  long ActDuration   = 0;
  int NextState       = UNKNOWN;
  int StateCount      = 0;
  bool GetStarttime  = true ;
  long Starttime     = 0;
  long Worktime      = 0;
  String WorkText    = "Work ";
  long TimePerCount  = 0;
  long Count         = 0;
  long DefaultEndCount = 10;
  bool DefaultEndCountHasChanged = false;
  long EndCount        = 10;
} State_Struct;

// Global Variables
State_Struct State;
String Serialcurrentline = "";

// State Functions
boolean TimeToChange() {
  return (millis() - State.Starttime >= State.ActDuration);
}

boolean TimeToWork(long wTime) {
  if (millis() - State.Worktime >= wTime) {
    State.Worktime = millis();
    return true;
  } else return false;
}

boolean EndCounterReached(long Criteria) {
  return (Criteria >= State.EndCount);
}

void PrintActState() {
  String sState = "UNKNOWN";
  switch (State.ActState) {
    case WAIT     : sState = "WAIT"; break;
    case WAITING  : sState = "WAITING"; break;
    case WORK     : sState = "WORK"; break;
    case WORKING  : sState = "WORKING"; break;
    case COUNT    : sState = "COUNT"; break;
    case COUNTING : sState = "COUNTING"; break;
    case BATCH    : sState = "BATCH"; break;
    case RUNBATCH : sState = "RUNBATCH"; break;
  }
  Serial.println();
  Serial.println(sState);
}

void GetStarttime() {
  if (State.GetStarttime) {
    State.GetStarttime = false;
    State.Starttime = millis();
    State.Worktime  = State.Starttime;
  }
}

void ChangeState() {
  if (!State.NextState == UNKNOWN) {
    State.ActState = State.NextState;
    State.NextState = UNKNOWN;
  } else State.ActState = WAIT;

}

void InitStateMachine() {
  State.ActState = "WAIT";
  State.NextState = UNKNOWN;
}

void HandleStateBatch() {
  State.NextState = RUNBATCH;
  switch (State.StateCount) {
    case 0 : State.ActState  = COUNT;          // Using the default entry to COUNT/COUNTING
      State.EndCount = 3;                      // But change the EndCount which is not
      break;                                   // reset by COUNT
    case 1 :  State.ActDuration = 3200;        // By setting the data like in the state "WORK"
      State.ActState     = WORKING;            // we can jump to "WORKING" directly
      State.GetStarttime = true;               // and overwrite the defaults from "WORK"
      State.WorkText = "Batch in Progress.. ";
      State.ActState = WORKING;
      PrintActState();
      break;
    case 2 : State.ActState = COUNT;           // see above
      State.EndCount = 7;
      break;
    default: State.ActState = WAIT;            // Finally back to WAIT
      break;
  }
  State.StateCount++;
}

void ResetCounter() {
  State.EndCount = State.DefaultEndCount;
  State.Count    = 0;

}

void HandleStates() {
  switch (State.ActState) {
    case WAIT :    State.ActDuration = 0;
      State.ActState     = WAITING;
      State.GetStarttime = true;
      PrintActState();
      break;
    case WAITING : // Wait forever or until Serial Cmd
      break;
    case WORK  :   State.ActDuration = 5200;
      State.GetStarttime = true;
      State.WorkText = "Work in Progress.. ";
      State.ActState     = WORKING;
      PrintActState();
      break;
    case WORKING : GetStarttime();
      if (TimeToWork(1000)) Serial.print(State.WorkText);
      if (TimeToChange()) {
        Serial.println();
        ChangeState();
      };
      break;
    case COUNT  :  State.Count        = 0;
      State.TimePerCount = 500;
      State.ActState      = COUNTING;
      State.GetStarttime = true;
      if (State.DefaultEndCountHasChanged) ResetCounter();
      PrintActState();
      break;
    case COUNTING: GetStarttime();
      if (TimeToWork(State.TimePerCount)) {
        Serial.print(++State.Count);
        if (State.Count < State.EndCount) Serial.print(F(","));
      }
      if (EndCounterReached(State.Count)) {
        ResetCounter();
        Serial.println();
        ChangeState();
      };
      break;
    case BATCH:   State.StateCount    = 0;
      State.ActState      = RUNBATCH;
      break;
    case RUNBATCH : HandleStateBatch();
      break;

    default :      State.ActState = WAIT;
      break;
  }

}

// Help

void PrintHelp() {
  Serial.println(F("Example State Machine"));
  Serial.println(F("---------------------"));
  Serial.println(F("Commands:"));
  Serial.println(F("Help or ?           : Print this help text"));
  Serial.println(F("Wait                : Do nothing but wait for next serial command"));
  Serial.println(F("Work                : Pretend to work ..."));
  Serial.println(F("Count               : Count to EndCount (default = 10, 2 counts per second)"));
  Serial.println(F("Count <value>       : Count to <value> = {1,2,3, ...}"));
  Serial.println(F("Both                : Do Work followed by Count "));
  Serial.println(F("Batch               : Perform a batch of states "));
  Serial.println(F("DefEndCount         : Prints value of default EndCount"));
  Serial.println(F("SetEndCount <value> : Sets default EndCount to <value> = {1,2,3, ..."));
  Serial.println();
}


// Serial Functions

void HandleSerial() {
  while (Serial.available()) {
    char c = Serial.read();
    bool CR = (c == char(10));
    if (c >= ' ') {
      Serialcurrentline += c;
    };
    if (CR)      {
      HandleCommands(Serialcurrentline);
      Serialcurrentline = "";
    };
  }
}

void HandleCounts(String Cmd) {
  if (Cmd.length() > 5) {
    long counts = Cmd.substring(6).toInt();
    if (counts <= 0) counts = 1;
    State.EndCount = counts;
  } else State.EndCount = State.DefaultEndCount;
  State.ActState = COUNT;
}

void SetDefEndCount(String Cmd) {
  if (Cmd.length() > sizeof(F("SetEndCount"))) {
    long counts = Cmd.substring(12).toInt();
    if (counts <= 0) counts = 1;
    State.DefaultEndCount = counts;
    State.DefaultEndCountHasChanged = true;
    Serial.print(F(" > Default EndCount set to "));
    Serial.print(counts);
    Serial.println(F(" < "));
  };
}

void HandleCommands(String Cmd) {
  bool unknown = true;
  if (Cmd.startsWith(F("Help")) || Cmd.startsWith(F("?")) )  {
    PrintHelp();
    unknown = false;
  };
  if (Cmd.startsWith(F("Wait")))  {
    State.ActState = WAIT;
    unknown = false;
  };
  if (Cmd.startsWith(F("Work")))  {
    State.ActState = WORK;
    unknown = false;
  };
  if (Cmd.startsWith(F("Count"))) {
    HandleCounts(Cmd);
    unknown = false;
  };
  if (Cmd.startsWith(F("Both")))  {
    State.ActState = WORK;
    State.NextState  = COUNT;
    unknown = false;
  };
  if (Cmd.startsWith(F("Batch"))) {
    State.ActState = BATCH;
    State.NextState  = UNKNOWN;
    unknown = false;
  };
  if (Cmd.startsWith(F("DefEndCount"))) {
    Serial.print(F("Default EndCount = "));
    Serial.println(State.DefaultEndCount);
    unknown = false;
  };
  if (Cmd.startsWith(F("SetEndCount"))) {
    SetDefEndCount(Cmd);
    unknown = false;
  };
  if (unknown) {
    Serial.print (Cmd);
    Serial.println(F(" is unknown! For Help input \"Help\" or \"?\" "));
  };
}

// Setup

void setup() {
  Serial.begin(115200);
  Serial.println(F("Example State Machine"));
  InitStateMachine();  // Not really necessary ;-)
}

// Loop

void loop() {
  HandleSerial();
  HandleStates();
}

Instead of using "if (Serial.available())" in your loop() you can define a function named:

void serialEvent()
{
  //statements
}

The Arduino core will call your serialEvent() function, right after your loop() returns, if a character is available.

Also works for other hardware serial ports:

void serialEvent1()
void serialEvent2()
void serialEvent3()

Hi @johnwasser , thanks for this information ... There is always something to learn :wink:

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.