Finite State Machine Architecture Advice

Hi All,

I’m looking to get some general advice on best practice for designing a finite state machine based boron app. We have a long and complicated app that we have been using for the last two years. The app is quite unwieldy and would be difficult for anyone other than myself to develop in without bringing down the house of cards. So I am hoping to rebuild the app using a FSM pattern in hopes that the code will become a much more friendly place for other developers to contribute.

I am having trouble deciding on the scope of my states. In its simplest form I could probably draw my state diagram like below:

The data collection state shown in the diagram is where most of the action happens in our firmware. This would be a large state and I’m not sure if lumping it together is a good idea. This state is kind of like a session for our use case, we do not know how long it will last for, we do not know if or when we will capture a radio tag, etc. The fact that this is like one session makes me inclined to put it in it’s own state, as different timers and variables will be setup on entry to this state and then reset in the following state.

To complicate matters, I have a whole host of different particle functions that need to be able to be called regardless of what state the device is in, how do I fit that into this pattern? Would I just have a second FSM that is running just to check if a particle function has been called and if it has then perform an action and then continue with the FSM as per the diagram? If anyone has any pointers it would be greatly appreciated.

The first thing to adhere to with an FSM is that you should not keep the code flow trapped in a state.
Each state should be visited, dealt with and leave again (you can revisit the same state over and over, but just not keep the flow trapped). If you need to wait for something, that waiting should be its own state.
As long you obey that your Particle.function() handlers can be called between iterations of loop() without you needing to even think about them.

IMO, your "Data Collection" state should be broken up into multiple (sub)states (check for tag / collect data / analyse & summarize / check criteria and set follow-up state / handle follow-up state).
When doing things periodically, the "idle" state would be my choice of handling that although I'd then rather call it "scheduler" state.
If these (or some of them) periodic tasks are independent of the "business" FSM you could even consider putting them in their own Software Timer callbacks.

BTW, I'd also dedicate a set of states to the handling of the cloud connection.

2 Likes

Thanks for the feedback. In my current app (pre FSM redesign), there is no blocking code other than when a sensor is read for x number of seconds so we can analyse a group of readings.

  • I like your idea of changing idle to scheduler
  • I agree that the Data collection state should be broken up into smaller states. Some of these (sub)states should be able to be reached by calling particle functions. ie. a particle function to enter the read tag state, and then return to whatever state it was at before.

Could you elaborate on this a bit further, or could you give a simplified example?

Can I also ask for a bit more advice around cloud connection states? We are using the PublishQueueAsyncRK library to manage storing data during periods of disconnection.

2 Likes
Timer timEnvSensors(60000, timCheckEnvSensors); // every 60 seconds check sensors 

void timCheckEnvSensors() {
  // read environmental sensors
  // e.g. set global vars accordingly
  ... 
}

void setup() {
  ...
  timCheckEnvSensor.start();
}

For more info look here Device OS API | Reference Documentation | Particle
After that you can forget about the sensor acquisition as the function will be called in the background.

IIRC, PublishQueueAsyncRK does use a FSM internally. Hence you can have a look there :wink:

1 Like

Hi there,
my best advice here would be the following:

  • read the AN010 about FSMs here.
  • use the FSM library instead of building it with code. One of the advantages is that the library has entry/exit state functions that fire off and can help to write your code better (in the sense of more localized/focused on one task and more isolated of other code). This in turn can help to troubleshoot later.
  • go crazy with the number of states, don't try to limit the number just because.
    Of course here, if you have a variable that counts to 1000 do not create 1000 states for it (there are other mechanisms for that on FSMs - if I find a reference I'll post it here).

Your sensor probably has many more states than the 4 you have drawn above.
For example: the first draft of my garage project had maybe 3 states, but when I started really thinking about it, and wanting to run code for when the garage was open, when the code was triggering a pulse to open the garage, when it was mid way to open, when it finally closed, when it was mid way being closed, etc, it ended up with maybe 15 states.

There's this previous discussion where @chipmc talks about the states of his project.
There are many others as well.

Cheers!

3 Likes

@StngBo ,

Very timely topic given I am also rethinking my approach to states. Here are the states as I have defined them and this has worked well across a number of projects such that I have a “skeleton” FSM that is the starting point of many of my use cases.

States:

  • INITIALIZATION_STATE - this “state” is basically setup() but by making it a state I can keep track of where I came from. So, I may take a different tack in the ERROR_STATE based on where the program flow came from.
  • ERROR_STATE - Records the cause of the error and makes sure the device is reset gracefully. No system resets or power cycling is done outside of this state.
  • IDLE_STATE - this is where the device spends most of its waking time and is the jumping off point for most paths through the code.
  • MEASURING_STATE- This is where I collect data from sensors and - importantly - do this before turning on the cellular modem as some readings such as battery charge are impacted by the power draw of the modem
  • REPORTING_STATE - Here I construct the data payload and put into the publish queue - still not connected to Particle
  • CONNECTING_STATE - In this state I decide if I should connect to Particle or wait for another reporting period to save power - or because I am in a low-battery state.
  • RESPONSE_WAIT_STATE - This one may go away but, if I do connect and do send data to my back-end service (Ubidots) using a Particle webhook, this is the state I wait in to confirm that the package was received.
  • NAPPING_STATE - During the “operating hours” this is a low power state the device will go into and it can be awoken by either an sensor interrupt or the start of a new reporting period.
  • SLEEPING_STATE - During “non-operating hours” this is a low power state that will only wake on the next reporting period. sensor interrupts are disconnected and peripherals are powered down to save energy.
  • FIRMWARE_UPGRADE_STATE - I added this state recently to ensure that I get firmware updates adequate time to download, captured the result of firmware update attempts and prevented the firmware update from interfering with data transmission.

Note, this is all optimized for low power, low bandwidth use - this is my reality. Also, none of the states are blocking as @ScruffR has already pointed out above.

Hope this helps,

Chip

8 Likes

@StngBo All the advice above is spot on. Another point is that FSM can be multi-level and this is applicable particular to sensors or when reading from sensors that take a time to warm-up or reply. I have one product which is not power constrained (mains powered) but needs to maintain responsiveness to a UI whilst reading the status of 48 locks. In this case the interrogation of the lock controller board via RS485 has its own FSM, each lock/door is a FSM and the overall device has a FSM (STARTUP, RESTART, STANDBY, UPDATE, etc.)

3 Likes

@armor

i would be interested in further detail as to how you implement the multiple levels. I currently use a coarse structure (Idle, Initiating, Acquiring, Terminating) with logical flags to execute code conditionally within the Acquiring state.

If I understand you correctly these flags could be replaced by a second layer FSM.

Thanks

1 Like

@OziGreybeard

Assuming a switch() based selection of state,

switch(FSM_STATE_LEVEL1)
{
  case LEVEL1_STATE1:
    L1S1_controller();
    break;
...
}

Within L1S1_controller() another level can be implemented

void L1S1_controller()
{
  switch(FSM_STATE1_LEVEL2)
  {
    case LEVEL2_STATE1:
      L2S1_controller();
      break;
  ...
  }
}

Trust that clarifies.

1 Like

I love this forum. Every time I come here I learn something new. Thanks to everyone on this thread :slight_smile:

5 Likes

Thanks