BRD_APPTWELITE

IO communication (basic function of standard application App_Twelite)

This is a sample using board support <BRD_APPTWELITE>, which assumes the same wiring as required by App_TweLite.

This ACT includes:

  • Transmission and reception of wireless packets

  • Interactive settings mode - <STG_STD>

  • Digital (button) input - Buttons

  • analog input - Analogue

This sample cannot communicate with App_TweLite.

functions of ACT

  • Read M1 to determine the parent or child unit.

  • Reads the value of DI1-DI4; the Buttons class reduces the effect of chattering, so that changes are notified only when the values are the same consecutively. Communication is performed when there is a change.

  • Reads the value of AI1-AI4.

  • Sends the values of DI1-4, AI1-4, and VCC to a child unit if it is the parent unit, or to the parent unit if it is a child unit, every DI change or every 1 second.

  • Set to DO1-4 or PWM1-4 depending on the value of the received packet.

How to use ACT

Required TWELITE and wiring example

Explanation of ACT

include part

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

Include <TWELITE> in all ACTs. Here is a simple network <NWK_SIMPLE> and board support <BRD_APPTWELITE> should be included.

It also includes <STG_STD> to add Interactive settings mode.

declaration part

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
// logical id
uint8_t LID = 0;

/*** function prototype */
MWX_APIRET transmit();
void receive();

/*** application defs */
const char APP_FOURCHAR[] = "BAT1";

// sensor values
uint16_t au16AI[5];
uint8_t u8DI_BM;
  • sample-act common declaration

  • Its prototype declarations (send and receive), since the longer process is functionalized

  • Variables for holding data in the application

setup()

void setup() {
	/*** SETUP section */	
	// init vars
	for(auto&& x : au16AI) x = 0xFFFF;
	u8DI_BM = 0xFF;

	// load board and settings
	auto&& set = the_twelite.settings.use<STG_STD>();
	auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();

	// settings: configure items
	set << SETTINGS::appname("BRD_APPTWELITE");
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.
	LID = set.u8devid(); // logical ID

	// the twelite main class
	the_twelite
		<< set                      // apply settings (appid, ch, power)
		<< TWENET::rx_when_idle();  // open receive circuit (if not set, it can't listen packts from others)

	if (brd.get_M1()) { LID = 0; }

	// Register Network
	nwk << set // apply settings (LID and retry)
			;

	// if M1 pin is set, force parent device (LID=0)
	nwk << NWK_SIMPLE::logical_id(LID); // write logical id again.
	
	/*** BEGIN section */
	// start ADC capture
	Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0); // setup analogue read (check every 16ms)
	Analogue.begin(pack_bits(
						BRD_APPTWELITE::PIN_AI1,
						BRD_APPTWELITE::PIN_AI2,
						BRD_APPTWELITE::PIN_AI3,
						BRD_APPTWELITE::PIN_AI4,
				   		PIN_ANALOGUE::VCC)); // _start continuous adc capture.

	// Timer setup
	Timer0.begin(32, true); // 32hz timer

	// start button check
	Buttons.setup(5); // init button manager with 5 history table.
	Buttons.begin(pack_bits(
						BRD_APPTWELITE::PIN_DI1,
						BRD_APPTWELITE::PIN_DI2,
						BRD_APPTWELITE::PIN_DI3,
						BRD_APPTWELITE::PIN_DI4),
					5, 		// history count
					4);  	// tick delta (change is detected by 5*4=20ms consequtive same values)	


	the_twelite.begin(); // start twelite!

	/*** INIT message */
	Serial 	<< "--- BRD_APPTWELITE ---" << mwx::crlf;
	Serial	<< format("-- app:x%08x/ch:%d/lid:%d"
					, the_twelite.get_appid()
					, the_twelite.get_channel()
					, nwk.get_config().u8Lid
				)
			<< mwx::crlf;
	Serial 	<< format("-- pw:%d/retry:%d/opt:x%08x"
					, the_twelite.get_tx_power()
					, nwk.get_config().u8RetryDefault
					, OPT_BITS
			)
			<< mwx::crlf;
}

The general flow of the program is the initial setup of each section and the start of each section.

Registration of various BEHAVIOR objects

	auto&& set = the_twelite.settings.use<STG_STD>();
	auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();

Register BEHAVIOR objects to determine system behavior. It can be an Interactive settings mode, board support, or a network description of wireless packets.

It will not work unless registered within setup().

Configure the Interactive settings mode

// Initialize Interactive settings mode
auto&& set = the_twelite.settings.use<STG_STD>();

set << SETTINGS::appname("BRD_APPTWELITE");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
LID = set.u8devid(); // logical ID;

Initialize the Interactive settings mode. First, a set object is obtained. Then, the following process is performed.

  • Set the application name to "BRD_APPTWELITE" (used in the menu)

  • Rewrite default Application ID and CHANNEL values

  • Delete unnecessary items

  • reads configuration values saved by set.reload().

  • Copy the values of OPT_BITS and LID into the variables

Reflection of the read Interactive settings mode settings is described below.

Below is an example screen. + + + and + three times with a pause of 0.2 to 1 second to bring up the Interactive settings mode screen.

[CONFIG/BRD_APPTWELITE:0/SID=8XXYYYYY]
a: (0x1234ABCD) Application ID [HEX:32bit]
i: (        13) Device ID [1-100,etc]
c: (        13) Channel [11-26]
x: (      0x03) RF Power/Retry [HEX:8bit]
o: (0x00000000) Option Bits [HEX:32bit]

 [ESC]:Back [!]:Reset System [M]:Extr Menu    

Option Bits are not used in this sampler, although they are displayed in the menu.

the_twelite

This object behaves as the core of TWENET.

auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();

Register a board (this ACT registers <BRD_APPTWELITE>). Specify the name of the board you want to register with <> after use as follows.

The return value obtained by universal reference (auto&&) is a board object of reference type. This object contains board-specific operations and definitions. The following example uses the board object to check the status of the M1 pin: if the M1 pin is LO, set LID = 0, i.e., the Parent Node address.

	if (brd.get_M1()) { LID = 0; }

Initial configuration is required to make the the_twelite work. Application ID and wireless CHANNEL settings are mandatory.

	// the twelite main class
	the_twelite
		<< set
		<< TWENET::rx_when_idle();  // open receive circuit (if not set, it can't listen packts from others)

Use << to apply the setting to the_twelite.

  • set reflects some of the settings (Application ID, radio channel, etc.) read from Interactive settings mode. The items to be reflected are described in <STG_STD>.

  • TWENET::rx_when_idle() Specification to open the receive circuit.

The <<, >> operator is originally a bit shift operator, but it is used differently from its meaning. Within the MWX library, the above settings and serial port input/output are used in the library, following the C++ standard library's use of input/output.

// The following statements are not available in the MWX library
#include <iostream>
std::cout << "hello world" << std::endl;

Next, register the network.

auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << set;
nwk << NWK_SIMPLE::logical_id(LID);

The first line is written in the same way as for board registration, where <> is <NWK_SIMPLE>.

The second and third lines are settings for <NWK_SIMPLE>. The first one reflects the value of the settings in Interactive settings mode. The items to be reflected are LID and the number of retransmissions. In this application, LID is set again in the third line because LID=0 may be set depending on the state of the M1 pin.

Analogue

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

Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0);

The Initialization is performed by Analogue.setup(). The parameter true specifies to wait in place until the ADC circuit is stable; the second parameter specifies to start the ADC synchronously with Timer0.

	Analogue.begin(pack_bits(
						BRD_APPTWELITE::PIN_AI1,
						BRD_APPTWELITE::PIN_AI2,
						BRD_APPTWELITE::PIN_AI3,
						BRD_APPTWELITE::PIN_AI4,
				   	PIN_ANALOGUE::VCC));

To start the ADC, call Analogue.begin(). The parameter is a bitmap corresponding to the pin of the ADC target.

The pack_bits() function is used to specify a 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.

Parameters and BRD_APPTWELITE:: specified as PIN_AI1..4 are defined, corresponding to AI1..AI4 used in App_Twelite. AI1=ADC1, AI2=DIO0, AI3=ADC2, AI4=DIO2 and so on are assigned. PIN_ANALOGUE. PIN_ANALOGUE:: defines the list of pins available for ADC.

Except for the first time, the ADC is started in the 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 chattering (sliding) on mechanical buttons.

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(
						BRD_APPTWELITE::PIN_DI1,
						BRD_APPTWELITE::PIN_DI2,
						BRD_APPTWELITE::PIN_DI3,
						BRD_APPTWELITE::PIN_DI4),
					5, 		// history count
					4);  	// tick delta

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. Since 4 is specified, the HIGH and LOW states are determined when the same value is detected five times in a row every 4ms.

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

Timer0

Timer0.begin(32, true); // 32hz timer

Since App_Twelite uses a timer origin to control the application, the same timer interrupt/event should be run in this ACT. Of course, you can also use the system's TickTimer, which runs every 1 ms.

The first parameter in the above example specifies a timer frequency of 32 Hz; setting the second parameter to `true' enables software interrupts.

After calling Timer0.begin(), the timer starts running.

Start of the operation of the_twelite

the_twelite.begin(); // start twelite!

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

Serial

Serial objects can be used without initialization or initiation procedures.

	Serial 	<< "--- BRD_APPTWELITE ---" << mwx::crlf;
	Serial	<< format("-- app:x%08x/ch:%d/lid:%d"
					, the_twelite.get_appid()
					, the_twelite.get_channel()
					, nwk.get_config().u8Lid
				)
			<< mwx::crlf;
	Serial 	<< format("-- pw:%d/retry:%d/opt:x%08x"
					, the_twelite.get_tx_power()
					, nwk.get_config().u8RetryDefault
					, OPT_BITS
			)
			<< mwx::crlf;

This sample displays some system configuration values as a startup message. The Serial object is given a string of type const char*, a string of type int (no other integer types), a format() that behaves almost identically to printf, and a crlf that outputs newline characters to the << operator.

The namespace mwx:: may be omitted in the sample. In the above, it is described as mwx::crlf, but may be described as crlf. mwx::namespace is designed to be partially optional.

ループ loop()

Loop functions 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 of the 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.

/*** loop procedure (called every event) */
void loop() {
	if (Buttons.available()) {
		uint32_t bp, bc;
		Buttons.read(bp, bc);

		u8DI_BM = uint8_t(collect_bits(bp, 
							BRD_APPTWELITE::PIN_DI4,   // bit3
							BRD_APPTWELITE::PIN_DI3,   // bit2
							BRD_APPTWELITE::PIN_DI2,   // bit1
							BRD_APPTWELITE::PIN_DI1)); // bit0

		transmit();
	}

	if (Analogue.available()) {
		au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC);
		au16AI[1] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI1);
		au16AI[2] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI2);
		au16AI[3] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI3);
		au16AI[4] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI4);
	}

	if (Timer0.available()) {
		static uint8_t u16ct;
		u16ct++;

		if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
			if ((u16ct % 32) == 0) { // every 32ticks of Timer0
				transmit();
			}
		}
	}
}

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 bp, bc;
		Buttons.read(bp, bc);

The first parameter is a bitmap of the HIGH/LOW of the current DIO, ordered from bit0 to DIO0,1,2,... in order from bit0. For example, for DIO12, HIGH / LOW can be determined by evaluating bp & (1UL << 12). The bit that is set to 1 is HIGH.

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

Next, the values are extracted from the bitmap and stored in u8DI_BM. Here, the collect_bits() function provided by the MWX library is used.

u8DI_BM = uint8_t(collect_bits(bp, 
		BRD_APPTWELITE::PIN_DI4,   // bit3
		BRD_APPTWELITE::PIN_DI3,   // bit2
		BRD_APPTWELITE::PIN_DI2,   // bit1
		BRD_APPTWELITE::PIN_DI1)); // bit0

/* collect_bits performs the following operations
u8DI_BM = 0;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI1)) u8DI_BM |= 1;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI2)) u8DI_BM |= 2;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI3)) u8DI_BM |= 4;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI4)) u8DI_BM |= 8;
*/

collect_bits() takes an integer argument of bit positions similar to pack_bits() above. It is a function with a variable number of arguments and arranges the parameters as many as necessary. In the above process, bit0 is stored in u8DI_BM as the value of DI1, bit1 as the value of DI2, bit2 as the value of DI3, and bit3 as the value of DI4.

In App_Twelite, since wireless transmission is performed when there is a change from DI1 to DI4, the transmission process is performed starting from Buttons.available(). The details of the transmit() process are described below.

transmit();

Analogue

It becomes available at loop() immediately after the analog-to-digital conversion of the ADC is completed. Until the next ADC starts, data can be read back as if it was acquired immediately before.

if (Analogue.available()) {
	au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC);
	au16AI[1] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI1);
	au16AI[2] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI2);
	au16AI[3] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI3);
	au16AI[4] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI4);
}

Use the Analogue.read() or Analogue.read_raw() method to read ADC values. read() is the value converted to mV, read_raw() is the ADC value of 0..1023. The parameter is the pin number of ADC, which is defined in PIN_ANALOGUE:: or BRD_APPTWELITE::.

ADC values that are executed cyclically may read more recent values prior to the AVAILABLE notification, depending on the timing.

Since this ACT processes at a relatively slow cycle of 32 Hz, it is not a problem if the processing is done immediately after the AVAILABLE decision, but if the conversion cycle is short, or if you are doing a relatively long process in loop(), be careful.

In Analogue, you can specify a callback function that will be called from within the interrupt handler after the conversion is complete. For example, this callback function will store the value in the FIFO queue, and the application loop will perform asynchronous processing such as sequential reading of the queue value.

Timer0

The Timer0 runs at 32 Hz. It becomes available at loop() immediately after a timer interrupt occurs. In other words, it processes 32 times per second. Here, the transmission is processed when it reaches exactly 1 second.

if (Timer0.available()) {
	static uint8_t u16ct;
	u16ct++;

	if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
		if ((u16ct % 32) == 0) { // every 32ticks of Timer0
			transmit();
		}
	}
}

AppTwelite performs a periodic transmission about every second. When Timer0 becomes available, it increments u16ct. Based on this counter value, transmit() is called to send a radio packet when the count is completed 32 times.

The value judgment of u8DI_BM and au16AI[] is whether or not it is just after initialization. If the values of DI1..DI4 and AI1..AI4 are not yet stored, nothing is done.

transmit()

This function requests TWENET to send a radio 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.

MWX_APIRET transmit() {
	if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
	  auto&& set = the_twelite.settings.use<STG_STD>();
		if (!set.is_screen_opened()) {
			Serial << "..DI=" << format("%04b ", u8DI_BM);
			Serial << format("ADC=%04d/%04d/%04d/%04d ", au16AI[1], au16AI[2], au16AI[3], au16AI[4]);
			Serial << "Vcc=" << format("%04d ", au16AI[0]);
			Serial << " --> transmit" << mwx::crlf;
		}

		// set tx packet behavior
		pkt << tx_addr(u8devid == 0 ? 0xFE : 0x00)  // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
			<< tx_retry(0x1) // set retry (0x1 send two times in total)
			<< tx_packet_delay(0,50,10); // 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(APP_FOURCHAR, 4) // string should be paired with length explicitly.
			, uint8_t(u8DI_BM)
		);

		for (auto&& x : au16AI) {
			pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
		}
		
		// do transmit 
		return pkt.transmit();
	}
	return MWX_APIRET(false, 0);
}

function prototype

MWX_APIRET transmit()

MWX_APIRET is a class that handles return values with data members of type uint32_t, where MSB (bit31) is success/failure and the rest are used as return values.

Obtaining 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 with .prepare_tx_packet().

Here it is declared in a conditional expression in an 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 the TWENET send request queue is free and the send request is accepted, or false if there is no free queue.

Display suppression during Interactive settings mode screen display

auto&& set = the_twelite.settings.use<STG_STD>();
if (!set.is_screen_opened()) {
    //Not during Interactive settings mode screen!
}

Suppresses screen output when Interactive settings mode screen is displayed.

Packet transmission settings

pkt << tx_addr(u8devid == 0 ? 0xFE : 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,50,10); // 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 initialization settings of the_twelite.

  • 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. The 1 in the example sends one retry, i.e., a total of two packets. Sending only one wireless packet will 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, the second is the maximum wait time, in this case approximately from 0 to 50 ms after issuing a request to send. The third is the retransmission interval. The third is the retransmission interval, meaning that a retransmission is performed every 10 ms after the first packet is sent.

Data payload of the 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.

Be aware of the data order of the data payload for correct transmission and reception. 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
04: uint8_t    : 1 : Bitmap of DI1..4
06: uint16_t   : 2 : Voltage value of Vcc
08: uint16_t   : 2 : ADC value of AD1 (0..1023)
10: uint16_t   : 2 : ADC value of AD2 (0..1023)
12: uint16_t   : 2 : ADC value of AD3 (0..1023)
14: uint16_t   : 2 : ADC value of AD4 (0..1023)

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

Every byte of 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, since it is necessary to take into account transmission failures. Also, sending one extra byte consumes energy equivalent to approximately 16 µs x the current used during transmission, which is especially significant for battery-powered applications.

The above example is a compromise to some extent for the sake of explanation. To save money, the 00: identifier should be a simple 1 byte, and the Vcc voltage value may be rounded to 8 bits. Also, each AI1..AI4 value is 10 bits, using 6 bytes for a total of 40 bits = 5 bytes.

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

auto&& payl = pkt.get_payload();
payl.reserve(16); // Resize to 16 bytes
payl[00] = APP_FOURCHAR[0];
payl[01] = APP_FOURCHAR[1];
...
payl[08] = (au16AI[0] & 0xFF00) >> 8; //Vcc
payl[09] = (au16AI[0] & 0xFF);
...
payl[14] = (au16AI[4] & 0xFF00) >> 8; // AI4
payl[15] = (au16AI[4] & 0xFF);

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(APP_FOURCHAR, 4) // string should be paired with length explicitly.
	, uint8_t(u8DI_BM)
);

for (auto&& x : au16AI) {
	pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
}

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

The parameter after that is a variable number of arguments, pack_bytes, specifying the required number of values of the corresponding type. 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 to avoid confusion of string types (specifically, including or excluding 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.

The fourth line is of type uint8_t and writes a bitmap of DI1..DI4.

Lines 7-9 write the values of the au16AI array in sequence. The values are of type uint16_t and are 2 bytes, but are written in big-endian order.

The for statement in line 7 is a range for statement introduced in C++. This syntax can be used for arrays of known size or container classes that can be accessed by begin(), end() iterators. au16AI types can also be determined at compile time, so the type specification is also omitted with auto&& (see Universal Reference).

If rewritten as a normal for statement, it would look like this

for(int i = 0; i < sizeof(au16AI)/sizeof(uint16_t)); i++) {
  pack_bytes(pkt.get_payload(), au16AI[i]);
}

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

return pkt.transmit();

Packets are sent using the pkt.transmit() method of the pkt object. It returns a return value of type MWX_APIRET, which is not used in this ACT.

The return value contains information on the success or failure of the request and the number corresponding to the request. To perform a process that waits until the completion of transmission, use this return value.

on_rx_packet()

When a wireless packet is received, on_rx_packet() is called as a receive event.

The procedure with the_twelite.receiver stores incoming packets in an internal queue (up to 2 packets) before processing them, but on_rx_packet() is called directly from a callback from the TWENET library, so it is more resistant to overflow. However, loop() is called directly from a callback from the TWENET library, and is less likely to cause overflow. However, if the processing is stopped for a long time in a loop() statement, it may cause the same kind of overflow.

Here, the values of DI1...DI4 and AI1...AI4 communicated by the other party are set to its own DO1...DO4 and PWM1...PWM4.

void on_rx_packet(packet_rx& rx, bool_t &handled) {	
	auto&& set = the_twelite.settings.use<STG_STD>();

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

  // expand the packet payload
	char fourchars[5]{};
	auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
		, make_pair((uint8_t*)fourchars, 4)  // 4bytes of msg
    );

	// check header
	if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }

	// read rest of payload
	uint8_t u8DI_BM_remote = 0xff;
	uint16_t au16AI_remote[5];
	expand_bytes(np, rx.get_payload().end()
		, u8DI_BM_remote
		, au16AI_remote[0]
		, au16AI_remote[1]
		, au16AI_remote[2]
		, au16AI_remote[3]
		, au16AI_remote[4]
	);

	Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
	for (auto&& x : au16AI_remote) {
		Serial << format("/%04d", x);
	}
	Serial << mwx::crlf;

	// set local DO
	digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
	digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
	digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
	digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);

	// set local PWM : duty is set 0..1024, so 1023 is set 1024.
	Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
	Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
	Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
	Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);
}

First, the data rx of the received packet is passed as a parameter. From rx the address information and data payload of the wireless packet is accessed. The parameter handled is not normally used.

void on_rx_packet(packet_rx& rx, bool_t &handled)

Received packet data is referenced to the source address (32-bit long address and 8-bit logical address) and other information. Output is suppressed when the Interactive settings mode screen is displayed.

if (!set.is_screen_opened()) {
   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().

char fourchars[5]{};
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
	, make_pair((uint8_t*)fourchars, 4)  // 4bytes of msg
  );

The first line declares an array of type char for data storage. The size of the array is 5 bytes because the null character is included at the end for convenience in character output, etc. The trailing {} specifies initialization. The trailing {} specifies initialization, and although it is sufficient to set the fifth byte to 0, here the entire array is initialized in the default way, that is, to 0.

The 4-byte string is extracted by expand_bytes() in the second line. The reason for not specifying the container type in the parameters is that we need to know the read position to read this continuation. the first parameter specifies the first iterator of the container (uint8_t* pointer), which can be obtained by the .begin(). The second parameter is an iterator that points to the next to the end of the container and can be retrieved with the .end() method. The second parameter is used to avoid reading beyond the end of the container.

The third variable to read is specified, again by make_pair, which is a pair of string array and size.

The ACT of expand_bytes() omits error checking, for example, if the packet length is incorrect. 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) for accesses beyond the end.

If the identifier in the 4-byte string read is different from the identifier specified in this ACT, this packet is not processed.

if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }

In TWENET, if the Application ID and the physical radio CHANNEL match, any application can receive the packet, even if it is of a different type. For the purpose of avoiding unintended reception of packets created by other applications, it is recommended to check such identifiers and the structure of data payloads to prevent coincidental matches.

The requirements for packet structure on the simple network <NWK_SIMPLE> must also be satisfied, so mixed packet reception will not occur unless other applications that do not use the simple network define packets with the same structure (which seems very rare).

The next step is to get the data part: store the values of DI1..DI4 and AI1..AI4 in separate variables.

	// read rest of payload
	uint8_t u8DI_BM_remote = 0xff;
	uint16_t au16AI_remote[5];
	expand_bytes(np, rx.get_payload().end()
		, u8DI_BM_remote
		, au16AI_remote[0]
		, au16AI_remote[1]
		, au16AI_remote[2]
		, au16AI_remote[3]
		, au16AI_remote[4]
	);

The first parameter is the return value np of the previous expand_bytes(). The second parameter is the same.

The third and subsequent parameters are variables of the matching type alongside the data payload, in the same order as the sender's data structure. Once this process is complete, the specified variable will contain the value read from the payload.

Output to serial port for confirmation. Output is suppressed when the Interactive settings mode screen is displayed.

auto&& set = the_twelite.settings.use<STG_STD>();
...
Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
for (auto&& x : au16AI_remote) {
	Serial << format("/%04d", x);
}
Serial << mwx::crlf;

We use format() because we need to format output of numbers. helper class that allows the same syntax as printf() for the >> operator, but limits the number of arguments to four. (Serial.printfmt() has no limit on the number of arguments.)

The first line "DI:%04b" prints a 4-digit bitmap of DI1..DI4, like "DI:0010" The third line "/%04d" prints the values of Vcc/AI1..AI4 as integers, like "/3280/0010/0512/1023/" The fifth line mwx::crlf prints a newline string.

Now that the necessary data has been extracted, all that remains is to change the values of DO1..DO4 and PWM1..PWM4 on the board.

// set local DO
digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);

// set local PWM : duty is set 0..1024, so 1023 is set 1024.
Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);

digitalWrite() changes the value of the digital output, the first parameter is the pin number and the second is HIGH (Vcc level) or LOW (GND level).

Timer?.change_duty() changes the duty ratio of the PWM output. Specify a duty ratio of 0..1024 for the parameter. Note that the maximum value is not 1023 (the maximum value is 1024, a power of 2, due to the high cost of the division performed in the library). Setting the value to 0 will result in an output equivalent to the GND level, and setting it to 1024 will result in an output equivalent to the Vcc level.

最終更新