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 fromINIT
toWORK_JOB
: performs some processing (in this act, updates a counter every 1msTickCount
and proceeds toTX
state after a random count)loop()
stateTX
requests transmissionloop()
stateWAIT_TX
waits for transmission completionloop()
stateEXIT_NORMAL
goes to sleep (returns to 1.)
- If an error occurs,
loop()
stateEXIT_FATAL
resets 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
step
state machine - Initializes the
the_twelite
class 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_pair
is from the C++ standard library and creates astd::pair
object. 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_t
type writes 4 bytes in big-endian order. - The same applies for
uint16_t
data.
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.