This act includes the following:
- Immediate response transmission upon receiving a wireless packet
- Transmission specifying the recipient’s address directly
- Input from serial port -
Serial
- Digital (button) input -
Buttons
- Analog input -
Analogue
How to Use the Act
Required TWELITE
Prepare two units of any of the following:
- MONOSTICK BLUE / RED
- TWELITE R Series with UART-connected TWELITE DIP, etc.
Explanation of the Act
Declarations
Includes
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
Include <TWELITE>
in all acts. Here, we also include the simple network <NWK_SIMPLE>
.
Others
// application ID
const uint32_t APP_ID = 0x1234abcd;
// channel
const uint8_t CHANNEL = 13;
// DIO pins
const uint8_t PIN_BTN = 12;
/*** function prototype */
void vTransmit(const char* msg, uint32_t addr);
/*** application defs */
// packet message
const int MSG_LEN = 4;
const char MSG_PING[] = "PING";
const char MSG_PONG[] = "PONG";
- Common declarations for the sample act
- Function prototypes for longer processing (transmit and receive)
- Variables for holding data within the application
setup()
void setup() {
/*** SETUP section */
Buttons.setup(5); // init button manager with 5 history table.
Analogue.setup(true, 50); // setup analogue read (check every 50ms)
// 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(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE) // set Logical ID. (0xFE means a child device with no ID)
<< NWK_SIMPLE::repeat_max(3); // can repeat a packet up to three times. (being kind of a router)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // check every 10ms, a change is reported by 5 consequent values.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
}
The general flow is initial setup for each part, then starting each part.
the_twelite
This object is the core class for operating TWENET.
// 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(); // open receive circuit (if not set, it can't listen packts from others)
To apply settings to the_twelite
, use <<
.
TWENET::appid(APP_ID)
Specify the application IDTWENET::channel(CHANNEL)
Specify the channelTWENET::rx_when_idle()
Open the receive circuit
The <<
and >>
operators are bit shift operators in C, but here they are used as stream insertion operators, different from their original meaning. In the MWX library, these are used for settings and serial port I/O, similar to the C++ standard library.
However, the following usage is not available in the MWX library:
#include <iostream>
std::cout << "hello world" << std::endl;
Next, register the network.
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE);
<< NWK_SIMPLE::repeat_max(3);
The first line registers the board, specifying <NWK_SIMPLE>
in <>
.
The second line sets <NWK_SIMPLE>
to 0xFE
(child device with no ID).
The third line specifies the maximum number of repeats. Although not covered in this explanation, when operating with multiple devices, packets can be relayed.
the_twelite.begin(); // start twelite!
At the end of the setup()
function, the_twelite.begin()
is executed.
Analogue
This class object handles the ADC (Analog-to-Digital Converter).
Analogue.setup(true);
Initialization is done with Analogue.setup()
. The parameter true
means to wait until the ADC circuit is stable.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC), 50);
To start the ADC, call Analogue.begin()
. The parameter is a bitmap corresponding to the ADC target pins.
Use the pack_bits()
function to specify the bitmap. It’s a variadic function, and each argument specifies the bit position to set to 1. For example, pack_bits(1,3,5)
returns the value 101010
in binary. Since this function is constexpr
, if only constants are used as parameters, it will be expanded at compile time.
The parameters specify PIN_ANALOGUE::A1
(ADC0) and PIN_ANALOGUE::VCC
(module supply voltage).
The second parameter is 50
. By default, ADC operation starts with TickTimer, and except for the first time, ADC starts in the interrupt handler.
Buttons
Detects changes in DIO (digital input) values. Buttons
reduces the effects of mechanical button chattering by only considering a value change after the same value has been detected for a certain number of times.
Buttons.setup(5);
Initialization is done with Buttons.setup()
. The parameter 5
is the maximum number of detections required to confirm a value. Internally, memory is allocated based on this number.
Buttons.begin(pack_bits(PIN_BTN),
5, // history count
10); // tick delta
Start with Buttons.begin()
. The first parameter is the DIO to detect. Here, PIN_BTN
(12) defined in BRD_APPTWELITE::
is specified. The second parameter is the number of detections needed to confirm the state. The third is the detection interval. With 10
specified, if the same value is detected 5 times every 10ms, the state is confirmed as HIGH
or LOW
.
Buttons
is done in the event handler. The event handler is called in the application loop after an interrupt occurs, so there is more delay compared to the interrupt handler.Serial
The Serial
object can be used without any initialization or start procedure.
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
Outputs a string to the serial port. mwx::crlf
is a newline character.
loop()
The loop function is called as a callback from the main loop of the TWENET library. Basically, you wait until the object you want to use becomes available and then process it. Here, we explain the usage of some objects used in this act.
The main loop of the TWENET library processes received packets and interrupt information stored in the FIFO queue as events, and then calls loop()
. After exiting loop()
, the CPU enters DOZE mode and waits in low power until a new interrupt occurs.
Therefore, code that assumes the CPU is always running will not work correctly.
void loop() {
// read from serial
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
// Button press
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
// Serial << fmt("<BTN %b:%b>", btn_state, change_mask);
if (!(change_mask & 0x80000000) && (btn_state && (1UL << PIN_BTN))) {
// PIN_BTN pressed
vTransmit(MSG_PING, 0xFF);
}
}
}
Serial
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
While Serial.available()
is true
, there is input from the serial port. The data is stored in an internal FIFO queue, so there is some buffer, but you should read it promptly. Read the data with Serial.read()
.
Here, when the 't'
key is input, the vTransmit()
function is called to send a PING packet.
Buttons
When a change in DIO (digital IO) input is detected, it becomes available, and you can read it with Buttons.read()
.
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
The first parameter is a bitmap of the current DIO HIGH/LOW states, with DIO0,1,2,… in order from bit0. For example, for DIO12, you can check if it’s HIGH/LOW by evaluating btn_state & (1UL << 12)
. Bits set to 1 are HIGH.
Except for the first determination, vTransmit()
is called when the PIN_BTN
button is released. To trigger on the press timing, invert the condition like (!(btn_state && (1UL << PIN_BTN)))
.
transmit()
This function requests TWENET to send a wireless packet. When this function ends, the wireless packet has not been sent yet. The actual transmission will complete a few milliseconds later, depending on the parameters. Here, typical methods for requesting transmission are explained.
void vTransmit(const char* msg, uint32_t addr) {
Serial << "vTransmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
// do transmit
pkt.transmit();
}
}
Obtaining the Network Object and Packet Object
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
Obtain the network object with the_twelite.network.use<NWK_SIMPLE>()
. Use that object to get the pkt
object with .prepare_tx_packet()
.
Here, the pkt
object is declared within the condition of the if
statement and is valid until the end of the if
block. The pkt
object returns a bool
response: true
if there is space in the TWENET transmit request queue and the request is accepted, false
if there is no space.
Packet Transmission Settings
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
Packet settings are done using the <<
operator, just like initializing the_twelite
.
tx_addr()
Specify the destination address as a parameter.0x00
means send to parent if you are a child device;0xFE
means broadcast to any child device if you are a parent.tx_retry()
Specify the number of retries. For example, 3 means retry 3 times, so a total of 4 transmissions. Even under good conditions, a single wireless packet transmission can fail a few percent of the time.tx_packet_delay()
Set transmission delay. The first parameter is the minimum wait time before transmission, the second is the maximum wait time. In this case, after issuing the send request, transmission starts after a random delay between 100ms and 200ms. The third parameter is the retry interval. After the first packet is sent, retries are done every 20ms.
Packet Data Payload
Payload refers to the contents being carried. In wireless packets, it usually means the main data you want to send. Besides the main data, wireless packets also contain address and other auxiliary information.
To send and receive correctly, pay attention to the order of data in the payload. Here, we use the following data order. Build the payload according to this order.
# Index of first byte: Data type : Byte count : Contents
00: uint8_t[4] : 4 : 4-character identifier
08: uint16_t : 2 : ADC value of AI1 (0..1023)
06: uint16_t : 2 : Vcc voltage value (2000..3600)
10: uint32_t : 4 : millis() system time
The data payload can store 90 bytes (actually, a few more bytes can be stored).
A single byte in an IEEE802.15.4 wireless packet is valuable. It is recommended to use as little as possible. There is a limit to the amount of data that can be sent in one packet. If you split packets, you must consider the possibility of packet loss, which increases cost. Also, sending one extra byte consumes about 16μs × transmission current worth of energy, which especially affects battery-operated applications.
Let’s actually build the data payload structure as above. The payload can be accessed as a simplbuf<uint8_t>
container via pkt.get_payload()
. Build the data in this container according to the above specification.
You can write it as above, but the MWX library provides a helper function pack_bytes()
for building data payloads.
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
pack_bytes
の最初のパラメータはコンテナを指定します。この場合はpkt.get_payload()
です。
そのあとのパラメータは可変数引数でpack_bytes
で対応する型の値を必要な数だけ指定します。pack_bytes
は内部で.push_back()
メソッドを呼び出して末尾に指定した値を追記していきます。
On the third line, make_pair()
is a standard library function that generates a std::pair
. This avoids confusion with string types (specifically, whether to include the null character in the payload). The first parameter of make_pair()
is the string type (char*
, uint8_t*
, uint8_t[]
, etc). The second parameter is the number of bytes to store in the payload.
The 4th, 5th, and 6th lines store numeric values (uint8_t
, uint16_t
, uint32_t
). Even if you have signed numbers or char
types, cast them to one of these three types before storing.
analogRead()
and analogRead_mv()
get the ADC results. The former returns the ADC value (0..1023
), the latter returns the voltage (mV, 0..2470
). The module’s supply voltage is measured internally using a resistor divider, and analogRead_mv()
does the conversion.
This completes the packet preparation. Finally, request transmission.
pkt.transmit();
To send the packet, use the pkt.transmit()
method of the pkt
object.
on_rx_packet()
This is the process when a received packet is available.
void on_rx_packet(packet_rx& rx, bool_t &handled) {
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
// transmit a PONG packet with specifying the address.
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
// display the packet
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
}
First, the received packet data is passed as the parameter rx
. Access the wireless packet’s address information and data payload from rx
.
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
The next line refers to information such as the sender’s address (32-bit long address and 8-bit logical address) in the received packet data.
Serial << format("..receive(%08x/%d) : ",
rx.get_addr_src_long(), rx.get_addr_src_lid());
<NWK_SIMPLE>
, both an 8-bit logical ID and a 32-bit long address are always exchanged. When specifying a destination, you can use either the long address or logical address. Both addresses are included when receiving.The MWX library provides a function expand_bytes()
, which is the counterpart to pack_bytes()
used in transmit()
.
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
The first to third lines specify variables to store data.
On the sixth line, expand_bytes()
stores the packet payload data into variables. The first parameter is the container’s begin iterator (uint8_t*
pointer), obtained with .begin()
. The second parameter is the end iterator, obtained with .end()
, to prevent reading beyond the end of the container.
List variables as the third and subsequent parameters. The payload is read and data is stored in the listed variables in order.
This act omits error checking, such as for incorrect packet length. If you want strict checking, check the return value of expand_bytes()
.
The return value of expand_bytes()
is a uint8_t*
, but if reading goes beyond the end, it returns nullptr
.
If the 4-byte string identifier read into msg
is "PING"
, a PONG message is sent.
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
Next, display the received packet information.
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
Number formatting output is needed, so format()
is used. This is a helper class that allows the same syntax as printf()
for the >>
operator, but the number of arguments is limited to 8 (for 32-bit parameters). (If you exceed the limit, a compile error occurs. Note that Serial.printfmt()
does not have this limitation.)
mwx::crlf
is a newline (CRLF), and mwx::flush
waits until the output is complete (you can also write Serial.flush()
instead of mwx::flush
).