/      日本語

PingPong

Send and receive packets
If you send a PING wireless packet from one of two serially connected TWELITE devices, the other will return a PONG wireless packet.

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 ID
  • TWENET::channel(CHANNEL) Specify the channel
  • TWENET::rx_when_idle() Open the receive circuit

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.

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.

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

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

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.

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