Slp_Wk_and_Tx is a template source code intended for applications that perform some execution (such as sensor data acquisition) after periodic wake-up, and transmit the result as a wireless packet.In the form of setup() and loop(), complex conditional branches tend to occur in loop(), making it difficult to read. In this act, the code readability is improved by using the SM_SIMPLE state machine with a simple _switch_ syntax for state transitions inside loop().
This act includes the following:
- Typical intermittent operation control structure (sleep → wake up → measurement → wireless transmission → sleep)
- Packet generation, transmission procedures, and completion waiting
Functionality of the Act
- After startup, go through initialization and then sleep once
- Initialize in
setup() - Execute sleep in
begin()
- Initialize in
- After waking from sleep, initialize state variables and perform the following steps in order:
wakeup()wakes from sleep and performs initializationloop()transitions state fromINITtoWORK_JOB: performs some processing (in this act, updates a counter every 1msTickCountand proceeds toTXstate after a random count)loop()stateTXrequests transmissionloop()stateWAIT_TXwaits for transmission completionloop()stateEXIT_NORMALgoes to sleep (returns to 1.)
- If an error occurs,
loop()stateEXIT_FATALresets the module
Explanation of the Act
Declaration Section
Includes
#include <TWELITE>
#include <NWK_SIMPLE>
#include <SM_SIMPLE>
#include "Common.h"
<NWK_SIMPLE> is included to perform packet transmission. Basic definitions such as application ID are described in "Common.h".
State Definitions
To describe sequential processing inside loop(), this sample uses the concept of a state machine (state transitions). It uses <SM_SIMPLE>, which summarizes very simple state transitions.
The enumeration STATE corresponding to the following states is defined in Common.h.
enum class STATE {
INIT = 0, // INIT STATE
WORK_JOB, // do some job (e.g sensor capture)
TX, // reuest transmit
WAIT_TX, // wait its completion
EXIT_NORMAL, // normal exiting.
EXIT_FATAL // has a fatal error (will do system reset)
};
Declare the SM_SIMPLE state machine (state transitions) using the enumeration STATE that represents states.
SM_SIMPLE<STATE> step;
The declared step includes functions for state management, timeouts, and waiting for processing.
Sensor Data
This sample does not process sensor data but prepares dummy data.
struct {
uint16_t dummy_work_ct_now;
uint16_t dummy_work_ct_max; // counter for dummy work job.
} sensor;
setup()
void setup() {
/*** SETUP section */
step.setup(); // init state machine
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(false); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(DEVICE_ID); // set Logical ID.
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Sleep an Tx Act ---" << crlf;
}
Initializes variables and class objects.
- Initializes the
stepstate machine - Initializes the
the_tweliteclass object - Registers and initializes the network
<NWK_SIMPLE>(registersDEVICE_ID)
Next, starts the class object and hardware.
the_twelite.begin(); // start twelite!
This procedure starts the_twelite. Although not shown in act0..4, when setting configurations or registering behaviors of the_twelite, always call this.
begin()
void begin() {
Serial << "..begin (run once at boot)" << crlf;
SleepNow();
}
Called once immediately after setup(). Calls SleepNow() to perform the initial sleep procedure.
wakeup()
void wakeup() {
memset(&sensor, 0, sizeof(sensor));
Serial << crlf << int(millis()) << ":wake up!" << crlf;
}
Called immediately after waking up. Here, it initializes the sensor data area and outputs a wake-up message.
loop()
void loop() {
do {
switch(step.state()) {
case STATE::INIT:
sensor.dummy_work_ct_now = 0;
sensor.dummy_work_ct_max = random(10,1000);
step.next(STATE::WORK_JOB);
break;
...
}
} while (step.b_more_loop());
}
The above code is a simplified version of the actual code.
This control structure uses the SM_SIMPLE state machine. It is a do..while() loop. Inside the loop is a switch case statement that branches processing based on the state obtained from .state(). State transitions are done by calling .next() which writes a new state value to an internal variable in the state machine.
step.b_more_loop() is set to true when a state transition occurs by .next(). This is to execute the next state’s code (case clause) without exiting loop() when a state transition occurs.
The following explains each state.
STATE::INIT
sensor.dummy_work_ct_now = 0;
sensor.dummy_work_ct_max = random(10,1000);
step.next(STATE::WORK_JOB);
Initializes dummy sensor values. One is an increment counter, the other is a randomly determined stop count.
STATE::WORK_JOB
if (TickTimer.available()) {
Serial << '.';
sensor.dummy_work_ct_now++;
if (sensor.dummy_work_ct_now >= sensor.dummy_work_ct_max) {
Serial << crlf;
step.next(STATE::TX);
}
}
In the WORK_JOB state, processing is done at 1ms timer intervals. TickTimer.available() becomes true at each tick timer. The counter is incremented at each tick timer, and when it reaches dummy_work_ct_max, it transitions to the next state STATE::TX.
STATE::TX
if (Transmit()) {
Serial << int(millis()) << ":tx request success!" << crlf;
step.set_timeout(100);
step.clear_flag();
step.next(STATE::WAIT_TX);
} else {
// normall it should not be here.
Serial << int(millis()) << "!FATAL: tx request failed." << crlf;
step.next(STATE::EXIT_FATAL);
}
Calls the Transmit() function to request packet transmission. If the request succeeds, it transitions to STATE::WAIT_TXEVENT to wait for transmission completion. Here, the timeout and flag functions of the SM_SIMPLE state machine are used for the wait loop (a simple judgment based on variable changes during the wait loop).
A single transmission request failure is usually not expected, but if it fails, it transitions to the exceptional state STATE::EXIT_FATAL.
Transmit() function returns an MWX_APIRET object, which holds a boolean success/failure and up to 31 bits of data. It can be evaluated as a bool, so the if statement returns true if the transmission request succeeded, and false if it failed.STATE::WAIT_TX
if (step.is_flag_ready()) {
Serial << int(millis()) << ":tx completed!" << crlf;
step.next(STATE::EXIT_NORMAL);
} else if (step.is_timeout()) {
Serial << int(millis()) << "!FATAL: tx timeout." << crlf;
step.next(STATE::EXIT_FATAL);
}
Waiting for transmission completion is judged by setting the flag in the state machine function by on_tx_comp() described later. Timeout is judged by calling .is_timeout(), which checks the elapsed time since .set_timeout() was called.
Whether transmission succeeds or fails, a completion notification usually exists, but a timeout is set to transition to the exceptional state STATE::EXIT_FATAL.
STATE::EXIT_NORMAL
SleepNow();
Calls SleepNow() to enter sleep processing.
STATE::EXIT_FATAL
Serial << crlf << "!FATAL: RESET THE SYSTEM.";
delay(1000); // wait a while.
the_twelite.reset_system();
Performs a system reset as a critical error.
SleepNow()
void SleepNow() {
uint16_t u16dur = SLEEP_DUR;
u16dur = random(SLEEP_DUR - SLEEP_DUR_TERMOR, SLEEP_DUR + SLEEP_DUR_TERMOR);
Serial << int(millis()) << ":sleeping for " << int(u16dur) << "ms" << crlf;
Serial.flush();
step.on_sleep(); // reset status of statemachine to INIT state.
the_twelite.sleep(u16dur, false);
}
Performs periodic sleep. The sleep duration is randomized using the random() function to create some jitter. This is because if multiple devices synchronize their transmission cycles, the failure rate may increase significantly.
Before sleeping, the SM_SIMPLE state machine’s state is set to INIT by calling .on_sleep().
Transmit()
MWX_APIRET vTransmit() {
Serial << int(millis()) << ":vTransmit()" << crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x3 send four times in total)
<< tx_packet_delay(0,0,2); // send packet w/ delay (send first packet with randomized delay from 0 to 0ms, repeat every 2ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCC, 4) // string should be paired with length explicitly.
, uint32_t(millis()) // put timestamp here.
, uint16_t(sensor.dummy_work_ct_now) // put dummy sensor information.
);
// do transmit
//return nwksmpl.transmit(pkt);
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
Requests wireless packet transmission to the parent device with ID=0x00. The stored data includes the 4-character identifier (FOURCC) commonly used in Act samples, the system time [ms], and the dummy sensor value (sensor.dummy_work_ct_now).
First, obtains an object to store the transmission packet. Operates this object to set transmission data and conditions.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
In the MWX library, the object is obtained inside an if statement, and the object’s bool evaluation is used to proceed if true.
Here, the board object is obtained by the_twelite.network.use<NWK_SIMPLE>(), and the packet object is obtained by calling .prepare_tx_packet() on the board object. Failure to obtain the packet object is usually unexpected but occurs if the transmission queue is full and cannot accept transmission requests. This sample only sends a single transmission, so errors are limited to serious unexpected problems.
pkt << tx_addr(0x00) // Destination
<< tx_retry(0x1) // Retry count
<< tx_packet_delay(0,0,2); // Transmission delay
Sets transmission conditions (destination, retry, etc.) using the << operator on the obtained pkt object.
tx_addr specifies the packet destination. tx_retry is the retry count. tx_packet_delay specifies transmission delay.
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCC, 4) // string should be paired with length explicitly.
, uint32_t(millis()) // put timestamp here.
, uint16_t(sensor.dummy_work_ct_now) // put dummy sensor information.
);
The packet payload (data part) is an array derived from smblbuf<uint8_t> obtained by pkt.get_payload(). You can directly set values to this array, but here the pack_bytes() function is used to set values.
NWK_SIMPLE packet structure and maximum length.This function takes a variable number of arguments. The first parameter is the array object obtained from .get_payload().
make_pair(FOURCC,4):make_pairis from the C++ standard library and creates astd::pairobject. For a string type, it means writing 4 bytes from the beginning explicitly. (Strings can be confusing regarding including or excluding the terminator, so here the number of bytes to write is explicitly specified.)- Specifying a
uint32_ttype writes 4 bytes in big-endian order. - The same applies for
uint16_tdata.
You can also write data using a uint8_t pointer.
auto&& pay = pkt.get_payload(); // get buffer object.
// the following code will write data directly to internal buffer of `pay' object.
uint8_t *p = pay.begin(); // get the pointer of buffer head.
S_OCTET(p, FOURCC[0]); // store byte at pointer `p' and increment the pointer.
S_OCTET(p, FOURCC[1]);
S_OCTET(p, FOURCC[2]);
S_OCTET(p, FOURCC[3]);
S_DWORD(p, millis()); // store uint32_t data.
S_WORD(p, sensor.dummy_work_ct_now); // store uint16_t data.
pay.redim(p - pay.begin());
The array object obtained from .get_payload() is an array of size zero initially. Writing data to this array extends its size (actually writes data to the internal fixed-length buffer and updates the internal data size), and the final size is the payload data size.
Here, .begin() is used to get a uint8_t* pointer, which is used to write data, and .redim() is called with the written size at the end.
S_OCTET(), S_WORD(), S_DWORD() are functions used for writing data; for example, S_OCTET(p, 'H') is equivalent to *p = 'H'; p++;.
The final .redim() changes the array size without initializing the buffer. Calling .resize() would clear all data to zero.
Finally, calls .transmit() to request transmission. The return type is MWX_APIRET. After the request, actual transmission occurs, which may take several ms to tens of ms depending on parameters and size. on_tx_comp() is called upon completion.
return pkt.transmit();
MWX_APIRET is a class wrapping uint32_t, using the MSB as a success/failure flag and the remaining 31 bits for data. It is the return type of pkt.transmit(), holding transmission request success/failure (cast to bool) and transmission ID in the data part (.get_value()).on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
This system event is called upon transmission completion. Here, .set_flag() is called to mark completion.