Skip to main content

Main State Machine Logic

Now all the things are in place for the main state machine logic which is in the loop() function. Before diving in, let's stop for a moment and think about how things should be designed. A common way to handle the actual states and events in state machines is by event-state propagation. That is if an event has been raised, check which state that is relevant for, and if we are in that state, then handle the event.

In code this might become a bit clearer:

if (event_flags & NETWORK_CONN_FLAG) {
switch (state) {
case NOT_CONNECTED:
// Do the transition
state = CONNECTED_TO_NETWORK;

// Do other things

break;
default:
break;
}

// Mark that the event has been processed
event_flags &= ~NETWORK_CONN_FLAG;
}
...

The beauty of this is that it creates clear boundaries for our states and our system. As in this case - if a connection event is received - the system only wants to act on it if it isn't connected. A connection event might be raised when the device is already connected due to changing bands or some other network configuration, but that doesn't matter as long as we are connected. The system should only act on this event if it is not connected, and this event-state propagation ensures that.

Now, for the first four events, this will become:

void loop() {
if (event_flags & NETWORK_CONN_FLAG) {
switch (state) {
case NOT_CONNECTED:
state = CONNECTED_TO_NETWORK;
LedCtrl.on(Led::CELL);
Log.infof("Connected to operator: %s\r\n",
Lte.getOperator().c_str());
Log.info("Connecting to MQTT broker...");
connectMqtt();

Log.infof(
F("Connected to MQTT broker, subscribing to topic: %s!\r\n"),
mqtt_sub_topic);
MqttClient.subscribe(mqtt_sub_topic, AT_LEAST_ONCE);
state = CONNECTED_TO_BROKER;
break;
default:
break;
}

event_flags &= ~NETWORK_CONN_FLAG;
} else if (event_flags & NETWORK_DISCONN_FLAG) {
switch (state) {
default:
state = NOT_CONNECTED;
LedCtrl.off(Led::CELL);
LedCtrl.off(Led::CON);
LedCtrl.off(Led::DATA);
LedCtrl.off(Led::USER);
LedCtrl.off(Led::ERROR);

Log.info("Network disconnection, attempting to reconnect...");

Lte.end();
connectLTE();
break;
}

event_flags &= ~NETWORK_DISCONN_FLAG;
} else if (event_flags & BROKER_DISCONN_FLAG) {

switch (state) {
case CONNECTED_TO_BROKER:
state = CONNECTED_TO_NETWORK;

Log.info("Lost connection to broker, attempting to reconnect...");

MqttClient.end();
connectMqtt();

break;

case STREAMING_DATA:
state = CONNECTED_TO_NETWORK;

Log.info("Lost connection to broker, attempting to reconnect...");

stopStreamTimer();
MqttClient.end();
connectMqtt();

break;

default:
break;
}

event_flags &= ~BROKER_DISCONN_FLAG;
}
//...
}

This can be examined with the state diagram shown previously in the structure chapter. One can trace the events, and see which state they should interact on.

Note that in the event of receiving messages there isn't a specific flag as the system is built around the capability of processing messages later when there is heavy load and a lot of messages incoming. Thus, the event trigger will simply be if the circular buffer is not empty.


// ...
else if (received_message_identifiers_head != received_message_identifiers_tail) {

switch (state) {
case CONNECTED_TO_BROKER:
case STREAMING_DATA: {

// Arbitrary number, but must be enough for the message
char message[400] = "";

// Update the circular buffer tail index before read. Since these values are modified in a callback
// called by an ISR, interrupts are temporarily disabled during the update.
cli();
received_message_identifiers_tail = (received_message_identifiers_tail + 1) & RECEIVE_MESSAGE_ID_BUFFER_MASK;
const uint32_t message_id = received_message_identifiers[received_message_identifiers_tail];
sei();

const bool message_read_successfully = MqttClient.readMessage(
mqtt_sub_topic, (uint8_t *)message, sizeof(message), message_id);

if (message_read_successfully) {
decodeMessage(message);
} else {
Log.error("Failed to read message\r\n");
}

} break;

default:
break;
}
}

Source code

This example doesn't include all the minor aspects of the sandbox application, but should work as a starting point for diving into the source code. The main logic is the same, but the sandbox application also includes timers for streaming data, command handling from the serial line and decoding the received messages.

The source code can be found here.