PingPong

Send a PING wireless packet from one of the two serially connected TWELITEs and receive a PONG wireless packet back from the other.

This ACT includes.

  • Sending a prompt response from the receipt of a wireless packet

  • Transmission with direct address of the peer

  • Input from serial port - Serial

  • Digital (button) input - Buttons

  • Analogue input - Analogue

how to use act

Required TWELITE

Two of any of the following.

Explanation of ACT

Include

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>

Include <TWELITE> in all ACTs. Here, the simple network <NWK_SIMPLE> should be included.

Declaration section

// 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";
  • Sample act common declarations

  • Prototype declarations for longer processes (sending and receiving), since they are made into functions

  • Variables for holding data in 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 of the program is the initial setup of each section and the start of each section.

the_twelite

This object is the core class object for manipulating 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)

the_twelite に設定を反映するには << を用います。

Use << to reflect the setting in the_twelite.

  • TWENET::appid(APP_ID) to specify the Application ID.

  • TWENET::channel(CHANNEL) to specify the channel.

  • TWENET::rx_when_idle() Specifies that the receive circuit is open.

The <<, >> operator is originally a bit shift operator, but it is used differently from its meaning.

// The following statements are 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 is written in the same way as the board registration, specifying <> as <NWK_SIMPLE>.

The second line specifies <NWK_SIMPLE>, specifying 0xFE (WK_SIMPLE is a Child Node with an unset ID).

The third line specifies the maximum number of relays. This explanation does not touch on relaying, but packets are relayed when operating with multiple units.

the_twelite.begin(); // start twelite!

Execute the_twelite.begin() at the end of the setup() function.

Analogue

Class object that handles ADCs (analog-to-digital converters).

Analogue.setup(true);

Initialization Analogue.setup(). The parameter true specifies to wait in place 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 pin to be ADC'd.

The pack_bits() function is used to specify the bitmap. It is a function with variable number of arguments, each argument specifies a bit position to be set to 1. For example, pack_bits(1,3,5) returns the binary value 101010. This function has the constexpr specification, so if the parameters are constants only, they are expanded to constants.

The parameters are specified as PIN_ANALOGUE::A1 (ADC0) and PIN_ANALOGUE::VCC (module supply voltage).

The second parameter is specified as 50, and the ADC operation is started by default with TickTimer, which is set to

Except for the first time, the ADC is started in an interrupt handler.

Buttons

Detects changes in DIO (digital input) values; Buttons only detect a change in value after the same value has been detected a certain number of times in order to reduce the effects of mechanical button chattering.

Buttons.setup(5);

Initialization is done with Buttons.setup(). The parameter 5 is the number of detections required to determine the value, but it is the maximum value that can be set. Internally, the internal memory is allocated based on this number.

Buttons.begin(pack_bits(PIN_BTN),
					5, 		// history count
					10);  	// tick delta

The start is done with Buttons.begin() The first parameter is the DIO to be detected. The second parameter is the number of detections required to determine the state. The third parameter is the detection interval. Since 10 is specified, the HIGH and LOW states are determined when the same value is detected five times in a row every 10 ms.

The detection of the DIO state in Buttons is done by an event handler. Event handlers are called in the application loop after an interrupt has occurred, so there is a delay compared to interrupt handlers.

Serial

Serial objects can be used without initialization or initiation procedures.

Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;

Outputs a string to the serial port. mwx::crlf is a newline character.

loop()

Loop function are called as callback functions from the main loop of the TWENET library. The basic description here is to wait for the object to be used to become available and then process it. This section describes the use of some objects used in ACT.

The main loop of the TWENET library processes incoming packets and interrupt information stored in the FIFO queue in advance as events, after which loop() is called. After exiting loop(), the CPU enters DOZE mode and waits until a new interrupt occurs with low current consumption.

Therefore, code that assumes the CPU is always running will not work well.

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 the internal FIFO queue, so there is some leeway, but it should be read out promptly. To read data, call Serial.read().

Here, the vTransmit() function is called to send a PING packet in response to a 't' key input.

Buttons

It becomes available at the timing when a change in DIO (digital IO) input is detected, and is read by 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 HIGH/LOW of the current DIO, ordered from bit0 to DIO0,1,2,... . and so on, starting from bit 0. For example, for DIO12, HIGH / LOW can be determined by evaluating btn_state & (1UL << 12). If the bit is set to 1, it is HIGH.

When the IO state is determined for the first time, MSB (bit31) is set to 1. The initial determination process is also performed when the device returns from sleep.

// 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);

The vTransmit() is called at the timing when the button is released except for the initial confirmation. To make the timing of the press (! (btn_state && (1UL << PIN_BTN))) to invert the condition logically.

transmit()

This function requests TWENET to send a wireless packet. At the end of this function, the wireless packet is not yet processed. The actual transmission will be completed in a few ms or later, depending on the transmission parameters. This section describes typical transmission request methods.

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();
	}
}

Getting Network and Packet Objects

	if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {

Get a network object with the_twelite.network.use<NWK_SIMPLE>(). Use that object to get a pkt object by .prepare_tx_packet().

Here it is declared in the conditional expression of the if statement. The declared pkt object is valid until the end of the if clause. pkt object gives a response of type bool, which here is true if there is a free space in TWENET's send request queue and the send request is accepted, or false if there is no space.

Settings for sending packets

		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)

Packets are configured using the << operator as in the_twelite initialization setup.

  • Specify the destination address in the tx_addr() parameter. If it is 0x00, it means that you are the Child Node and broadcast to the Parent Node, and if it is 0xFE, it means that you are the Parent Node and broadcast to any Child Node.

  • The tx_retry() parameter specifies the number of retransmissions. In the example 3 means that the number of retransmissions is 3, i.e., the packet is sent 4 times in total. Sending only one wireless packet may fail a few percent of the time even under good conditions.

  • tx_packet_delay() Sets the transmission delay. The first parameter is the minimum wait time to start sending and the second is the maximum wait time. The third is the retransmission interval. The third is the retransmission interval, meaning that a retransmission is performed every 20 ms after the first packet is sent.

Data Payload in a Packet

Payload means a loaded item, but in wireless packets it is often used to mean "the main body of data to be sent". In addition to the main body of data, the data in a wireless packet also contains some auxiliary information, such as address information.

For correct transmission and reception, please be aware of the data order of the data payload. In this example, the data order is as follows. Construct the data payload according to this data order.

# Index of first byte: Data type : Number of bytes : Contents

00: uint8_t[4] : 4 : four-character identifier
08: uint16_t   : 2 : ADC value of AI1 (0..1023)
06: uint16_t   : 2 : Voltage value of Vcc (2000..3600)
10: uint32_t   : 4 : millis() system time

The data payload can contain 90 bytes (actually a few more bytes).

Every byte in an IEEE802.15.4 wireless packet is precious. There is a limit to the amount of data that can be sent in a single packet. If a packet is split, the cost of the split packet is high because it must take into account transmission failures. Also, sending one extra byte consumes energy equivalent to approximately 16 µs x current during transmission, which can be significant, especially for battery-powered applications. {endhint %}

Let's actually build the data structure of the above data payload. The data payload can be referenced as a container of type simplbuf<uint8_t> via pkt.get_payload(). In this container, we build the data based on the above specification.

It can be written as above, but the MWX library provides an auxiliary function pack_bytes() for data payload construction.

// 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.
);

The first parameter of pack_bytes specifies the container. In this case, it is pkt.get_payload().

The parameters after that are variable arguments, specifying as many values of the corresponding type in pack_bytes as needed. The pack_bytes internally calls the .push_back() method to append the specified value at the end.

The third line, make_pair(), is a standard library function to generate std::pair. This is specified to avoid confusion of string types (specifically, whether or not to include null characters when storing payloads). 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.

Lines 4, 5, and 6 store values of numeric types (uint8_t, uint16_t, uint32_t). Numeric types such as signed, or even the same numeric type such as char are cast to the three types listed on the left and submitted.

analogRead() and analogRead_mv() get the result of ADC. The former is the ADC value (0..1023) and the latter is the voltage[mv](0..2470). The supply voltage of the module is read internally from the value of the voltage divider resistor, so we use adalogRead_mv() to perform that conversion.

This completes the packet preparation. Now all that remains is to make a request for transmission.

pkt.transmit();

Packets are sent using the pkt.transmit() method of the pkt object.

Although not used in this ACT, the return value contains information about the success or failure of the request and the number corresponding to the request. Use this return value if the process waits until the transmission is complete.

on_rx_packet()

This is the process when there is an incoming packet.

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 data of the incoming packet is passed as parameter rx. From rx, the address information and data payload of the wireless packet is accessed.

while (the_twelite.receiver.available()) {
		auto&& rx = the_twelite.receiver.read();

In the next line, the received packet data refers to the source address (32-bit long address and 8-bit logical address) and other information.

Serial << format("..receive(%08x/%d) : ",
   rx.get_addr_src_long(), rx.get_addr_src_lid());

In <NWK_SIMPLE>, two types of addresses are always exchanged: an 8-bit logical ID and a 32-bit long address. When specifying the destination, either the long address or the logical address is specified. When receiving, both addresses are included.

The MWX library provides a function expand_bytes() as a 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
    );

Lines 1 through 3 specify variables to store data.

The first parameter specifies the first iterator of the container (a uint8_t* pointer), which can be retrieved by the .begin() method. The second parameter is the next iterator after the end of the container and can be retrieved with the .end() method.

The third and subsequent parameters enumerate variables. The payloads are read and stored in the order in which they are listed.

This ACT omits error checking, such as if the packet length is wrong. If you want to make the check strict, judge by the return value of expand_bytes().

The return value of expand_bytes() is uint8_t*, but returns nullptr (null pointer) in case of access beyond the end.

The process sends a PONG message if the identifier of the 4-byte string read in msg is "PING".

if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
    vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}

It then displays information on packets that have arrived.

		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;

The format() is used because numeric formatting output is required. helper class that allows the same syntax as printf() for >> operators, but limits the number of arguments to a maximum of 8 (for 32-bit parameters). (A compile error will occur if the limit is exceeded. Note that Serial.printfmt() has no limit on the number of arguments.)

The mwx::crlf specifies a newline character (CR LF), and mwx::flush waits for completion of output. (mxw::flush may be written as Serial.flush())

最終更新