Implementing a Finite State Machine for LoRa communication
You may have seen my previous post on this project, go read it here first otherwise. This is a continuation of that project, directly, and it's somewhat important to understand the context of this new stuff.
What's a finite state machine?
It's just a fancy term for "making choices," based on what the current situation is. Something that us humans are very familiar with, although you probably don't put it in terms like that. The most basic version of a FSM is a Turing Machine, but if you don't know what a FSM is, then that doesn't help explain anything either. So let's just have a contrived example instead!
Right now, you're reading this article. Your state
is reading
. And thus, there are no decisions to make, or actions to partake, except to continue reading through everything I'm writing. Except, something hopefully just happened! Your state
briefly changed, because you reached the end of the line. So you had to backtrack to the other side of the page, and find the next line down, and then resume reading again. That other procedure of actions doesn't fit under reading
, if you break it down enough, so we can call that state
something like find_next_line
. And when you are in that state
, you execute a different set of actions to reading
. Thus, when you have found the next line, you can transition back into reading
as your state
, and all is well!
Hopefully that made a little bit of sense, otherwise try keep reading, and perhaps a more concrete and less contrived example will aid your understanding...
Diagrams, woo!
If you've seen anything about networking, especially TCP/UDP, that probably has some familiar mechanisms. And, "if it ain't broke, don't fix it." But it's more that they're pretty sane and good ways of doing things, and trying to find a different way, "just because," isn't worth the effort and issues associated with it.
OK, explanation time, shall I?
We begin in the idle
state, and do two things:
- Check if a packet needs sending
- Check if a packet is being received
Let's go down the simple process of receiving
. This state is pretty simple, we just read everything from the LoRa radio integrated on the board, and transition to the verify
state. (I don't actually do anything in the verify
state for now, although that could be easily added in the future). So now that we've successfully got a packet, we'll tell the sender that we have actually got it. And for that, we transition to ack
, where we, well, ACKnowlege the packet. To do that, we just send a tiny packet back, which hopefully gets through.
Now that we've got that down, let's dissect the process of sending
a packet.
Things start off simple, just put it out there on the airwaves. Then we wait
for a receiver to ACK our packet. If that doesn't happen, we will retransmit
the packet again, which will trigger another wait
, etc., until we reach a point where there's just nothing coming back, and we drop the packet.
That's the whole thing, and it doesn't really account for anything very complicated or tricky situations, as it's only ever going to be used between two units. Shall we look at some code now?
How do I code this!?
Oh yes, this is amazing cool C++ stuff. I was absolutely trumped on how to do this, until I searched up and found that one YouTube video on how to do it. And while you could just watch it and learn everything I did, I like my example more, and you're already here.
Full code is always available on the relevant GitHub repository.
To get started with implementing a FSM in C++ (i.e. Arduino), we just plop in: void (*state)();
Easy, right? Fine, let's talk about it...
This is called a function pointer, as it's a type of pointer
, except it points to a whole function. Once you understand that, the syntax makes a whole lot more sense. I have already talked about this here, but a full explanation is also provided below.
*state
defines a pointer variable, called "state"
(*state)
then we wrap it in some parentheses
(*state)()
and add the typical function parentheses, to define what parameters we want to pass. We don't have any to pass through, so we leave it empty.
void (*state)()
we are defining something function-related, thus we need to tell it what type we will return, which is just void
in this case
The only thing left to do is to use it! To get started, we set the pointer to a suitable function, such as void idle() {...}
, but we just grab the function name (which is automatically a pointer to it!). Then we can 'call' the function pointer, as if it is a function, which will actually call the function we have set it to, i.e. idle()
.
void setup () {
state = idle;
}
void update () {
state();
}
Then we can implement our individual states as desired. I won't go over all that, because it's tedious and boring, so I'll just mention one last thing and be done with it: As we continuously call our state
in each update
, you have to take that in mind when writing your code. If you're more interested in the state transitions, or you want to differentiate between a transition and not, you can add another state variable.
I just called mine prev
, and update it whenever suitable. That way, at the start of a state, if you have code that you only want to run once, on transitioning to it, you can do something like:
void idle () {
if (prev != state) {
// this runs just once,
// immediately upon transitioning
...
prev = state;
}
// this will always run,
// and runs every time this state is updated
...
// and to transition:
prev = state;
state = next_state;
return;
}
So hopefully that helps explain the key stuff. Don't forget the GitHub repo, linked above, for my actual, up-to-date code.
If there's any niggling little things left, please see the home page for contact information.