Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
License
The Mono Wireless Software License Agreement (MW-SLA) applies to anything in this package that is not specifically described in the license.
This document is also handled under MW-SLA as a part of this library package part.
This software is not officially supported by Mono Wireless Ltd. Please note that we may not be able to respond to your inquiries. Please understand beforehand.
In response to reports of defects, Mono-Wireless, Inc. does not promise to fix or improve the problem.
There are also cases where the software may not work depending on the customer's environment, such as the package installed.
/* Copyright (C) 2019-2022 Mono Wireless Inc. All Rights Reserved.
Released under MW-SLA-*J,*E (MONO WIRELESS SOFTWARE LICENSE
AGREEMENT) */
Mono Wireless C++ Library for TWELITE.
Please refer to the Handling of Materials section. If you have any questions or concerns, please contact our support desk.
This page contains information that is still under development. The contents of this page may not yet be reflected in the public source code.
The MWX library is intended to simplify the code notation of the TWELITE radio module. a program created with MWX is called an act. There are two types of acts: descriptions by loops and descriptions by events (called BEHAVIOR).
This page introduces the main features of ACT.
Description by loop (setup()
, loop()
). Suitable for writing small-scale functions.
#include <TWELITE>
const uint8_t PIN_LED = 5;
void setup() {
pinMode(PIN_LED, OUTPUT);
}
void loop() {
if (TickTimer.available()) {
uint32 t_now = millis();
// blink LED every 1024ms
digitalWrite(PIN_LED, (t_now >> 10) & 1 ? HIGH : LOW);
}
}
Event-driven application description. It is possible to define various event and interrupt handlers, as well as a state machine within a class that is suitable for describing complex behavior of the application, and write code with good visibility. This description is called BEHAVIOR.
// myApp.hpp
...
class myApp : MWX_APPDEFS_CRTP(myApp) {
...
void loop() {
// main loop
}
void receive(mwx::packet_rx& rx) {
// on receive
}
};
// myApp.cpp
...
MWX_DIO_EVENT(12, uint32_t arg) {
// on event from DIO12
}
Simplify peripheral procedures. Defines class objects to handle commonly used UART, I2C, SPI, ADC, DIO, timer, and pulse counter.
void loop() {
while(Serial.available() {
auto x = Serial.read(); ... } // serial message
if (Analogue.available() {
auto x = Analogue.read(...); } // adc values
if (Buttons.available() {
Buttons.read(...); } // DIO changes
if (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read(); } // on rx packet
}
A simple relay network is defined. This relay network is implemented in the same way as the TWELITE standard application, and is characterized by the fact that individual addresses are managed by 8-bit logical IDs, and that wireless packets can be sent to the network immediately after power-on because there is no communication to build the network.
#include <TWELITE>
#include <NWK_SIMPLE>
void setup() {
...
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.
}
void loop() {
...
vTransmit();
...
}
void vTransmit() {
if (auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet();
pkt << tx_addr(0x00) // to parent
<< tx_retry(0x3); // set retry
pack_bytes(pkt.get_payload() // prepare payload data
, uint8_t(0x01)
, uint16_t(analogRead(PIN_ANALOGUE::A1))
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)));
pkt.transmit(); // transmit!
}
Board definition for PAL and MONOSTICK. Easy handling of sensors etc. on the board.
#include <TWELITE>
#include <PAL_AMB> // include the board support of PAL_AMB
void setup() {
auto&& brd = the_twelite.board.use<PAL_AMB>(); // use PAL AMB
uint8_t u8dip = brd.get_DIP_SW(); // check DIP s/w status
brd.set_led(LED_TIMER::BLINK, 100); // LED switchs on/off every 100ms
...
// start capture of sensors
brd.sns_SHTC3.begin();
}
void loop() {
if (TickTime.available()) { // check every ms
auto&& brd = the_twelite.board.use<PAL_AMB>();
if (brd.sns_LTR308ALS.available()) {
Serial << brd.sns_SHTC3..get_temp();
} else {
// notify sensor that 1ms passed.
brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
}
}
}
The MWX library is designed to make the programming of TWELITE modules easier and more extensible. Based on the TWENET C library that has been used in the MWSDK, the MWX library has been developed as a library for the application development layer.
The name of the MWX library is Mono Wireless C++ Library for TWELITE, where MW comes from MonoWireless, and C++ -> CXX -> double X -> WX, where MW and WX are combined to form MWX.
The code written using this library is called "ACT".
This section describes the notation used in this explanation.
This is called a universal reference, and is often used in standard libraries. In our library, auto&& is used in most cases
The namespace
, inline namespace
, and using
are used to redefine names and so on. Some of them are abbreviated in the explanation.
The MWX library has not been developed to support all the functions of the underlying libraries and functions (functions in the TWNET C library, microcontroller peripheral functions provided by semiconductor vendors, and IEEE802.15.4 functions).
The MWX library is written in the C++ language, and the act description is also written in C++. However, not all functions are available even in the C++ language. Please pay particular attention to the following points.
You can allocate memory with the new
and new[]
operators, but you cannot destroy the allocated memory, and most C++ libraries that allocate memory dynamically are virtually unusable.
The constructor of the global object is not called.
Note: If necessary, initialization can be done including a call to the constructor by initializing it as in the initialization function ((void*)&obj_global) class_foo();
).
exception
cannot be used.
Unable to use virtual
functions.
Due to the above limitations, only some of the C++ standard libraries such as STL can be used.
※ This is a description of what we know.
We have not conducted a comprehensive verification of the standard library as to whether it can be used or not, nor what might be available. If you find any inconvenience in operation, please try to implement it in another way.
The source code can be found at the followings:
{MWSDK install dir}/TWENET/current/src/mwx
description sample. For details, please refer to for details.
Act (USER APPs)...
+-----------------------+
| MWX C++ LIB |
+---------------+ |
| TWENET C LIB | |
+------------+----------+
| MAC LAYER | AHI APIs |
+-----------------------+
| TWELITE HARDWARE |
+-----------------------+
#include <algorithm>
int v[] = { 1, 3, -1, 5, 10 };
auto&& result = std::minmax_element(std::begin(v), std::end(v));
Serial << "min=" << int(result.first)
<< ",max=" << int(result.second);
Class Objects
Class objects are predefined objects in the MWX library, such as the_twelite
, which handles TWENET, and objects for using peripherals.
Each object must be initialized by calling the .setup()
or .begin()
method. (Only Serial objects that use UART0 do not require initialization.)
twe::stream に改行コードを出力する
Instance of a helper class to output a newline code (CR LF) for the <<
operator of mwx::stream
.
Serial << "hello world!" << mwx::crlf;
Usually not used.
Called at the re-initialization of code execution, with no peripheral API or initialization.
Output data of numeric type in big-endian order to twe::stream
Obtain the system time, [ms].
Obtain the system time, [ms].
uint32_t millis()
The system tick time is updated by TickTimer interrupt.
Act/behavior Programming Interface
The MWX library API may be subject to specification changes in the future for the purpose of improvement.
to unregister the interrupt handler.
Unregisters the interrupt handler.
void detachIntDio(uint8_t u8pin)
It is called only once before the first call to the loop()
function; since TWENET is already initialized, there is no need to consider constraints such as setup()
.
The main usage is to
Displaying startup messages
Writing code for testing
Sleep transition immediately after startup
Processing that is inconvenient for setup()
(radio packet processing, timer operation, etc.)
System functions (time, random numbers)
DESC
Usually not used.
Called in re-initialization when the peripheral API is not initialized after returning from sleep.
Functions
the function called when waking up from sleep.
Called before loop()
when waking up from sleep, and includes procedures for initialization after returning from sleep and for branching processing depending on the state of return.
the main loop function.
This is the main loop of the application. After the loop ends, the CPU transitions to DOZE mode and waits for the next interrupt with low current consumption.
In the Act description, most of the processing is described in this loop.
It is a base class of packet type, but the member structure common
contains address information and other common information.
the setup function to initialize an application.
Called at the beginning of code execution to write initialization code.
TWENET initialization is also executed after the setup()
function exits. Do not do anything other than initialization here, since most of the processing is done after TWENET exits.
Items to be noted are listed below.
Sleep the_twenet.sleep()
cannot be executed. If you want to sleep immediately after initialization, write the first sleep process in the begin()
function.
The delay()
function is replaced by the processing described below. In this case, the parameter ms
does not specify milliseconds. \frz
* Alternative processing for delay()
.
SPI access (using member functions)
After initializing the hardware by the begin()
method, the bus can be read and written by beginTransaction()
. Executing beginTransaction()
selects the SPI select pin. Read/write is done with the transfer()
function; SPI reads and writes at the same time.
Starts the use of the bus; sets the SPI select pin.
If called with the settings
parameter given, the bus is set.
Terminates the use of the bus; releases the SPI select pin.
Reads and writes the bus. transfer()
transfers 8 bits, transfer16()
transfers 16 bits, and transfer32()
transfers 32 bits.
class TwePacket {
public:
static const E_PKT _pkt_id = E_PKT::PKT_ERROR;
struct {
uint32_t tick; // system time at interpretation execution [ms]
uint32_t src_addr; // source address (serial number)
uint8_t src_lid; // source address (logical address)
uint8_t lqi; // LQI
uint16_t volt; // voltage[mV]
} common;
};
static inline void delay(uint32_t ms) {
volatile uint32_t ct = ms * 4096;
while (ct > 0) {
--ct;
}
}
void beginTransaction()
void beginTransaction(SPISettings settings)
void endTransaction()
inline uint8_t transfer(uint8_t data)
inline uint16_t transfer16(uint16_t data)
inline uint32_t transfer32(uint32_t data)
DESC
In order to create a development environment, you need to install the software and agree to the license agreement. In addition, you may need to configure security settings on your PC or workstation.
Although we take great care when distributing the software, we ask that you also check for viruses.
Please check with the administrator of your environment regarding your security approach and operations (e.g., whether or not to install external applications).
In addition, the installation and operation of the development environment may require the intervention and configuration of the operating system (e.g., running an application whose developer is unknown; many of the development environments and tools introduced here do not have a built-in mechanism to prove the origin of the application) Please refer to the general information on how to configure.
To write an application using the MWX library, you will need the following:
MWSDK(Software Development Environment)
An editor for development (Microsoft's VisualStudio Code will be introduced)
Compiler toolchains, etc., are relatively less dependent on the environment, so they can be expected to work in many environments, but we recommend the Windows 10 version that is currently supported. If your system does not work due to differences in the operating environment, please prepare a separate environment by referring to the environment we have confirmed.
The following is the version we are using for development.
Windows11 21H2 (Visual Studio 2019)
FTDI driver must be running (to run MONOSTICK, TWELITE R)
Compiler toolchains, for example, are relatively less dependent on the environment, so they can be expected to work in many environments, but we recommend distributions that are currently supported. If your system does not work due to differences in the operating environment, please prepare a separate environment by referring to the environment that we have confirmed.
The following is the version we are using for development.
Ubuntu 18.04 LTS 64bit
Ubuntu 20.04 LTS 64bit
32bit systems are not supported.
Compiler toolchains, for example, are relatively less dependent on the environment, so they can be expected to work in many environments, but we recommend distributions that are currently supported. If your system does not work due to differences in the operating environment, please prepare a separate environment by referring to the environment that we have confirmed.
The following is the version we are using for development.
macOS 10.14 (Mojave, Intel)
macOS 12.4 (Monterey, Apple Silicon)
About Visual Studio Code and other development environments
For information on how to run and use the development environment, please refer to the information of its developer or community.
The build results for Linux/macOS are different from the results for Windows 10. As far as we know, there is no difference in the normal operation, but the binary size tends to be a few percent larger, especially since the LTO of gcc is disabled.
If you have any doubts about the operation, be sure to run the build on Windows 10 and confirm that it reproduces before contacting us.
The acts starting with act0 are the ones introduced in Starting an act - Opening act, and although they are simple, we recommend you try them out first.
act0
Templates with no processing description
act1
L-tica (flashing LED)
act2
L-tica with timer
act3
L-tica with two timers
act4
LED lighting using a button (switch)
Determines the type of packet using the packet data byte sequence as input. The return value is E_PKT.
E_PKT identify_packet_type(uint8_t* p, uint8_t u8len)
If the packet cannot be interpreted as a specific packet, E_PKT::PKT_ERROR
is returned.
Specified as a template argument of a container class (smplbuf
, smplque
) to allocate or specify an area of memory to be used internally.
This class is not called directly from user code, but is used internally to declare containers.
alloc_attach<T>
specify an already existing buffer
alloc_local<T, int N>
statically allocate a buffer of N bytes internally
alloc_heap<T>
allocate a buffer of the specified size in the heap
In alloc_attach
and alloc_heap
, initialization methods (init_???()
) must be executed according to the memory allocation class.
void attach(T* p, int n) // alloc_attach
void init_local() // alloc_local
void init_heap(int n) // alloc_heap
Initialize with buffer p
and size n
.
uint16_t alloc_size()
Returns the size of the buffer.
This method is used to generate a compile error, like static_assert
, for a method call description that is different from the expected alloc class.
Flush buffered output to twe::stream.
Flush the output buffer of mwx::stream
. Instance to a helper class that calls the flush()
method.
for (int i = 0; i < 127; ++i) {
Serial << "hello world! (" << i << ")" << twe::endl << twe::flush;
}
For serial ports, wait polling until output completes.
For mwx::simpbuf
buffers, output 0x00
at the end (size is not changed)
Call back functions describing an application.
This is a callback function that describes the application. Callback means called by the system (library). The user defines several callback functions to describe the behavior of the system.
The following callback functions are mandatory definitions.
setup()
loop()
If no other functions are defined, an empty function that does nothing is linked instead.
init_coldboot()
↓ (TWENET internal processing: Initialization 1)
setup()
↓(TWENET internal processing: Initialization 2)
begin() --- First time only
↓
loop() <--+
↓ |Event processing, behavior processing
CPU DOZE -+
Please refer to the source code mwx_appcore.cpp
if you want to see the exact behavior.
the_twelite.sleep()
↓ sleeping...
init_warmboot()
↓ (TWENET internal processing: Initialization 3)
wakeup()
↓(TWENET internal processing: Initialization 4)
loop() <--+
↓ |Event processing, behavior processing
CPU DOZE -+
Please refer to the source code mwx_appcore.cpp
if you want to see the exact behavior.
The TwePacketAppIO
class interprets the standard app serial message (0x81) of App_IO The TwePacketAppIO
class is the one that interprets the
class TwePacketAppIO : public TwePacket, public DataAppIO { ... };
Various information in the packet data is stored in DataTwelite
after parse<TwePacketIO>()
execution.
struct DataAppIO {
// Serial # of sender
uint32_t u32addr_src;
// Logical ID of the sender
uint8_t u8addr_src;
// Destination logical ID
uint8_t u8addr_dst;
// Timestamp at the time of transmission
uint16_t u16timestamp;
// Flag for low latency transmission
bool b_lowlatency_tx;
// Number of repeat relays
uint16_t u8rpt_cnt;
// LQI value
uint16_t u8lqi;
// DI status bitmap (DI1,2,3,4,...) in order from LSB
uint8_t DI_mask;
// Active (1 if used) bitmap of DI (DI1,2,3,4,...) in order from LSB
uint8_t DI_active_mask;
// Bitmap of whether DI is interrupt-derived or not (DI1,2,3,4,...) in order from LSB
uint16_t DI_int_mask;
};
on_rx_packet()
Receives incoming packets.
void on_rx_packet(mwx::packet_rx& pkt, bool_t &b_handled)
When a wireless packet is received, this function is called from within the MWX library with the data stored in pkt
as packet_rx. If this function is not defined in the application, a weak function that does nothing is linked.
Setting b_handled
to true in this function tells the MWX library that the incoming packet has been processed in the application. If set to processed', it suppresses unnecessary processing. (Do not process
the_twelite.receiver`)
When using BEHAVIOR, use the callback function in BEHAVIOR.
The the_twelite.receiver
is not recommended.
The receiver was previously processed by the_twelite.receiver
with the intention of describing it in loop()
. However, on_rx_packet()
was added because it is a delayed processing by a queue, which in principle causes overflow, and also because it tends to be complicated to describe.
Creating a new project
To create a new project, copy the folder of an already existing sample act with a different name and edit the file name.
The destination folder does not have to be a folder under the MWSDK. However, the folder name must not contain any whitespace characters or Japanese names.
The file structure of the project is as follows (we'll use PingPong
as an example)
Act_samples
+-PingPong
+-PingPong.cpp : ACT file
+-build : build dir
+-.vscode : setting fils for VSCode
Copy this PingPong
folder to another location (but without Japanese characters or spaces in the folder name).
SomeDir
+-AlphaBravo
+-PingPong.cpp -> AplhaBravo.cpp (Note: Changed file name)
+-build : build dir
+-.vscode : setting files for VSCode
The only thing you need to edit is the file name PingPong.cpp
. Change it to AlphaBravo.cpp
, the same as the folder name.
Run build\build-BLUE.cmd
and if a BIN file is generated, it' is done (Windows 10).
On Linux/macOS, run make TWELITE=BLUE
to see if the build succeeds.
To add files to be built, edit build/Makefile
. The .c
.cpp
files directly under the project will be added automatically, but other files will need to be edited.
See Makefile Description for how to edit the file.。
If you use VSCode, edit the definition under .vscode as necessary.
Most of the examples included in the TWELITE STAGE SDK are as follows
The source code for the TWELITE STAGE SDK library cites ${env:MWSDK_TWENET_LIBSRC}/include/**
${env:MWSDK_TWENET_LIBSRC}/src/**
. This environment variable MWSDK_TWENET_LIBSRC
is automatically set when the project is opened in VSCode from the TWELITE STAGE app.
For the build task, no additional options such as -D
are set by default.
Install and Build
In order to write an application using the MWX library (called ACT in this document) and run it, you need to set up a development environment.
Sets the DIO (general-purpose digital IO) pin.
Sets the DIO (general-purpose digital IO) pin.
void pinMode(uint8_t u8pin, E_PIN_MODE mode)
This function allows you to change the state of DIO0..19 and the pins DO0,1. The setting contents are described in the enumeration value of E_PIN_MODE
, description of DIO and Description of DO.
DO0,1 are special pins, which in principle are used for other purposes, but can also be configured as outputs. However, these pins have hardware restrictions, so care must be taken when using them.
Both pins must be guaranteed to be at a HIGH level when power is applied. If the circuit is configured to take unstable voltages, the module will not start up.
The uint8_t
type smplbuf_strm_u8??
is also available in the stream(stream) interface, so several methods for streams can be used.
// smplbuf_strm_u8<N> : locally allocated
template <int N> using smplbuf_strm_u8
= _smplbuf_stream<uint8_t, mwx::alloc_local<uint8_t, N>>;
// smplbuf_strm_u8_attach : attach to an existing buffer
using smplbuf_strm_u8_attach
= mwx::_smplbuf_stream<uint8_t, mwx::alloc_attach<uint8_t>>;
// smplbuf_strm_u8_heap : allocate at HEAP
using smplbuf_strm_u8_heap
= mwx::_smplbuf_stream<uint8_t, mwx::alloc_heap<uint8_t>>;
// definition of operator <<
template <class L_STRM, class ALOC>
mwx::stream<L_STRM>& operator << (
mwx::stream<L_STRM>& lhs,
mwx::_smplbuf_stream<uint8_t, ALOC>& rhs)
{
lhs << rhs.to_stream();
return lhs;
}
Example
smplbuf_strm_u8<128> sb1;
sb1 << "hello";
sb1 << uint32_t(0x30313233);
sb1 << format("world%d",99);
sb1.printfmt("Z!");
Serial << sb1;
// hello0123world99Z!
Digital input management class (mwx::periph_buttons)
Detects changes in digital inputs. This class detects changes when the same detected value is obtained multiple times. This is useful for reducing the effect of chattering on mechanical buttons.
void setup(uint8_t max_history);
The parameter max_history
is the maximum number of references that can be set with begin()
. Memory is allocated and initialized here.
void begin(uint32_t bmPortMask,
uint8_t u8HistoryCount,
uint16_t tick_delta);
Starts the Buttons
operation, the first parameter bmPortMask
specifies the bitmap of digital inputs to be monitored. bit 0 corresponds to DIO 0, ... , bit N corresponds to DIO N. More than one can be specified: the second u8HistoryCount
is the number of times required to confirm the value; the third tick_delta
specifies the interval in ms between value checks; the fourth u8HistoryCount
is the number of times to confirm the value.
It will take u8HistoryCount*tick_delta
[ms] to confirm the value. For example, if u8HistoryCount
=5 and tick_delta
=4, it will take at least 20ms to confirm the state.
The confirmation is done in the TickTimer
event handler. Since it is not an interrupt handler, it is affected by delays in processing, etc., but it is sufficient to suppress chattering of mechanical buttons, etc.
void end()
Terminates the Buttons
operation.
inline bool available()
Returns true
when a change is detected. It is cleared when read()
is executed.
bool read(uint32_t& u32port, uint32_t& u32changed)
Called when u32port
becomes available. u32portis the bitmap of the current input DIO and
u32changed` is the bitmap of the DIO where the change was detected.
Returns false
if Buttons is not working.
When Buttons starts to operate, the input status of DIO is not yet determined. When the value is determined, it becomes available. At this time, the MSB (bit31) of the bitmap read by read()
is set to 1.
Since this function requires the operation to be determined, it cannot be used to monitor ports whose input values change constantly.
If Buttons is in operation before Sleep, it will resume after it is restored. After resumption, initial determination is performed.
Wait for time by polling (specified in μsec).
It is not included in MWSDK2020_05. Supported packages will be MWSDK_2020_07_UNOFFICIAL or later.
Wait for time by polling (specified in μsec).
void delayMicroseconds(uint32_t microsec)
Wait for a given period of time in microsec
.
The time is measured by the TickTimer count. When waiting for a long time, the CPU clock is reduced and polling is performed.
In the setup(), wakeup()
function, TickTimer is not yet running, so it waits for a while in a while loop. In this case, the error with the specified value will be large. This loop counter is adjusted to 32Mhz. If the CPU clock is changed in these functions, the error will be proportional to the clock.
If you specify a short time, such as less than 10 for a parameter, the error may be large.
Generates an random number.
Generates an random number.
uint32_t random(uint32_t maxval)
uint32_t random(uint32_t minval, uint32_t maxval)
The first line returns the value of 0.. (maxval-1)
value is returned. Note that the value of maxval is not the maximum value.
The second line returns the value of minval..maxval-1
.
format input parser for serial input (mwx::serial_parser)
This built-in class is defined as a built-in object, intended to be used for format input via serial port.
It is defined as a mwx::serial_parser<mwx::alloc_heap<uint8_t>>
type that allocates buffer space for internal use from the heap at initialization (begin()
).
For details, see class serparser for details.
Reads the values of all ports in the input settings at once.
Included in mwx library 0.1.4 or later
Reads the values of all ports in the input settings at once.
uint32_t digitalReadBitmap()
The values are stored in the order of DIO0 ... DIO19 from the LSB side. DIO19 are stored in this order.
The pins on the HIGH side are set to 1 and the pins on the LOW side are set to 0.
Reads the value of the port of the input configuration.
Reads the value of the port of the input configuration.
static inline E_PIN_STATE digitalRead(uint8_t u8pin)
Get the input value of a pin that has been previously set as an input, either LOW
or HIGH
.
The sample in is slightly modified so that the waiting time (approx. 50 ms) during sensor data acquisition is replaced by a sleep wait.
Please see the explanation of the ACT in before the explanation of this ACT.
The begin()
function exits the setup()
function (after which TWENET is initialized) and is called just before the first loop()
.
The first sleep is performed after setup()
ends. Although sensor data acquisition is started during setup()
, this result is not evaluated and is not necessarily necessary, since it means that the sensor is run once in advance.
Procedures after waking up. The following process is performed.
If the sensor data acquisition start has not yet been performed, the sensor data acquisition is performed and a short sleep is entered.
Since the start of sensor data acquisition was performed immediately before, the data is checked and sent wirelessly.
The above branch is controlled by the global variable b_sensor_started
. If !b_sensor_started
, it starts acquiring a sensor (startSensorCapture()
) and goes into a short sleep by napNow()
. The time is 100ms.
After returning from sleep by napNow()
, the clause b_sensor_started==true
is executed. Here, the E_EVENT_START_UP
event is notified to the two sensors. This event means that enough time has passed for the sensors to finish acquiring. Based on this notification, sns_LTR308ALS
and sns_SHTC3
are made available. After this, it will go to loop()
and wireless packets will be sent.
The event that notifies the sensor is used to determine if the required time wait is over. Whether or not the actual time has elapsed depends on whether or not the correct time was set with napNow()
. If the wake-up time is short, it is expected that the time elapsed is not enough to meet the required time elapsed and that subsequent processing will result in errors such as sensor data not being available.
Perform a very short sleep.
If the second parameter of sleep is set to true, the next wake-up time is adjusted based on the previous sleep wake-up time. This is useful if you always want to wake up every 5 seconds.
If the third parameter is set to true, the sleep mode is set without memory retention. After waking up, wakup() will not be called and the process will be the same as power-on.
The fourth specifies the use of the second wake-up timer. Here, the first is used for normal sleep and the second is used for short sleep. There is no strong reason to use the second timer in this ACT, but if, for example, the user wants to wake up every 5 seconds as described above, using the first timer for a short sleep would reset the counter value, which would complicate the elapsed time correction calculation, so the second timer is used.
Installing VSCode
VisualStudio Code (VSCode) is recommended to make Act (source code) writing easier. The attached Act contains a file that has been configured to ensure that the code is interpreted properly in VSCode.
VSCode reads source files and header files and interprets the source code, thus providing function definition information and function and method name completion to help you write source code. The MWX library requires more header files to be loaded than the C library. In comparison to C development, the MWX library requires more header files to be loaded and the editor may be slower in some environments.
The project settings of VSCode requires some information, such as library source code location, to analyse source codes. These inforamtion are passed by TWELITE STAGE app as environmental variable when launching VSCode. Therefore, application instance of VSCode should not be present when launching from TWELITE STAGE app, otherwise VSCode will not refer to these environmental values.
Please note that this support does not cover questions about how to install or use VSCode. Please refer to the information available in the public domain.
Depending on your environment, you may need to configure security settings or other settings in order to install the software. Please check with your system administrator whether installation is possible, and refer to the distributor or general information for instructions.
VSCode allows you to do the following:
Editing the source code
The IntelliSense based on source code interpretation (*not all definitions can be guaranteed to be interpreted correctly)
VSCode can be downloaded from the link below.
The code
command must be enabled to invoke VSCode from TWELITE STAGE.
The following information is from code.visualstudio.com
- PATH must be set so that the code
command can be executed.
To enable Visual Studio Code to interpret C/C++ language descriptions, install a plugin.
C/C++ for Visual Studio Code
The MWX library examples include a .vscode
definition. This definition uses the MWSDK_ROOT environment variable to identify the source code of the library (under {MWSDK_ROOT}/TWENET/current
).
When launching VSCode from TWELITE STAGE, those settings such as MWSDK_ROOT
will be setup automatically. In some cases, such as when you have already started VSCode, the settings may not be reflected.
VSCode's interpretation of source code is not always the same as the compiler's interpretation. Also, depending on how the source code is edited, the interpretation may be more incomplete.
API return value class that wraps 32-bit type. MSB (bit31) is a flag for failure or success. bit0..30 is used to store the return value.
The default constructor is constructed with a combination of false
and 0
.
It can also be explicitly constructed with bool
and uint32_t
types as parameters.
Since the constructor of type bool
is implemented, you can use true
/false
as follows.
Return true
if 1
is set in MSB.
Return true
if MSB is 0
.
Obtain the value part of bit0..30.
packet type definition
Corresponds to the following packet
Operators and methods by via a that references a smplbuf array of type uint8_t
.
Helper object type names are resolved by auto&&
because they are long. The interfaces defined in mwx::stream
, such as <<
operator, can be used for this object.
The generated helper object bs
starts reading/writing from the beginning of the main array b
when it is created. If it is at the end of the array, data is added by append()
. Each time a read/write operation is performed, the position is moved to the next one.
Helper functions can use the >>
operator for reading.
Timers, PWM (mwx::periph_timer)
The timer has two functions: to generate a software interrupt at a specified period, and to output a PWM at a specified period. 5 timers in total are available in the TWELITE radio module, from 0...4.
The name of the built-in object is Timer0..4
, but will be referred to as TimerX
on this page.
Initializes the timer. This call allocates the necessary memory space.
Starts a timer; the first parameter is the period of the timer in Hz; setting the second parameter to true
enables software interrupts; setting the third parameter to true
true` enables PWM output.
Stops the timer operation.
It becomes true
at loop()
immediately after a timer interrupt occurs, and becomes false
when loop()
ends.
Set the duty ratio. the first parameter specifies the duty ratio (a small value makes the average of the waveform closer to the GND level, a large value makes it closer to the Vcc level). the second parameter specifies the maximum duty ratio value of the duty ratio.
It is recommended that duty_max
be one of 1024,4096,16384
.
The internal calculation of the count value involves division, but only for these three values is bit shifting used, while for other values, a computationally expensive division process is performed.
Sets the frequency of the timer; the second parameter is an integer with three decimal places for the frequency. For example, to set the frequency to 10.4 Hz, specify hz=10, mil=400
.
on_tx_comp()
Called when transmission is complete.
This function is called from within the MWX library with data stored in ev
as packet_ev_tx
when the wireless packet transmission is finished. If this function is not defined in the application, a weak function that does nothing is linked.
ev.u8CbId
is the ID at the time of transmission and ev.bStatus
is a flag indicating success (1) or failure (0) of the transmission.
Setting b_handled
to true in this function tells the MWX library that the incoming packet has been processed in the application. If set to processed, it suppresses unnecessary processing. (Do not call event callback functions for the_twelite.app
, .board
, or .settings
)
Waiting for time by polling.
Waiting for time by polling.
The program waits for a given period of time in ms
.
The time is measured by the TickTimer count. When waiting for a long period of time, the CPU clock is decreased and polling is performed.
In the setup(), wakeup()
function, the TickTimer is not yet running, so it waits for a time by a while loop. In this case, the error with the specified value will be large. This loop counter is adjusted to 32Mhz. If the CPU clock is changed in these functions, the error will be proportional to the clock.
If you specify a short time, such as 1 or 2 as a parameter, the error may be large.
system timer (mwx::periph_ticktimer)
TickTimer is used for internal control of TWENET and is implicitly executed. The period of the timer is 1ms. Only the available()
method is defined in loop()
for the purpose of describing processing every 1ms by the TickTimer event.
Note that it is not always available in 1ms increments.
There are cases in which events are skipped due to a large delay caused by factors such as the contents of the user program description or the system's internal interrupt processing.
available()
It is set after the TickTimer interrupt occurs and becomes true
in the loop()
immediately following it. It is cleared after loop()
ends.
class MWX_APIRET {
uint32_t _code;
public:
MWX_APIRET() : _code(0) {}
MWX_APIRET(bool b) {
_code = b ? 0x80000000 : 0;
}
MWX_APIRET(bool b, uint32_t val) {
_code = (b ? 0x80000000 : 0) + (val & 0x7fffffff);
}
inline bool is_success() const { return ((_code & 0x80000000) != 0); }
inline bool is_fail() const { return ((_code & 0x80000000) == 0); }
inline uint32_t get_value() const { return _code & 0x7fffffff; }
inline operator uint32_t() const { return get_value(); }
inline operator bool() const { return is_success(); }
};
MWX_APIRET()
MWX_APIRET(bool b)
MWX_APIRET(bool b, uint32_t val)
MWX_APIRET myfunc() {
if (...) return true;
else false;
}
inline bool is_success()
inline operator bool()
inline bool is_fail()
inline uint32_t get_value()
inline operator uint32_t()
PKT_ERROR
TwePacket does not contain meaningful data such as before packet interpretation or packet type cannot be identified.
PKT_TWELITE
0x81 command of the standard application App_Twelite is interpreted.
PKT_PAL
Interpret serial format of TWELITE PAL
PKT_APPIO
UART message of remote control application App_IO products/ TWE-APPS/App_IO/uart.html) interpreted by the remote control application App_IO.
PKT_APPUART
Extended format of the serial communication application App_UART products/ TWE-APPS/App_Uart/mode_format.html) is interpreted.
PKT_APPTAG
The UART message of the wireless tag application App_Tag is interpreted. The sensor specific part is not interpreted and the byte string is reported as payload.
PKT_ACT_STD
Output format used in Act (Act) sample, etc.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled)
void delay(uint32_t ms)
void loop() {
if (TickTimer.available()) {
if ((millis() & 0x3FF) == 0) { // This may not be processed (could be skipped)
Serial << '*';
}
}
}
inline bool available()
void begin() {
sleepNow(); // the first time is just sleeping.
}
void wakeup() {
if (!b_senser_started) {
// delete/make shorter this message if power requirement is harder.
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
startSensorCapture();
b_senser_started = true;
napNow(); // short period sleep.
} else {
Serial << "..wake up from short nap.." << mwx::crlf;
auto&& brd = the_twelite.board.use<PAL_AMB>();
b_senser_started = false;
// tell sensors waking up.
brd.sns_LTR308ALS.process_ev(E_EVENT_START_UP);
brd.sns_SHTC3.process_ev(E_EVENT_START_UP);
}
}
void napNow() {
uint32_t u32ct = 100;
Serial << "..nap " << int(u32ct) << "ms." << mwx::crlf;
the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
class TwePacketTwelite : public TwePacket, public DataTwelite { ... };
struct DataTwelite {
//Serial # of sender
uint32_t u32addr_src;
// Logical ID of the sender
uint8_t u8addr_src;
// Destination logical ID
uint8_t u8addr_dst;
// Timestamp at time of transmission
uint16_t u16timestamp;
// Flag for low latency transmission
bool b_lowlatency_tx;
// Number of repeat relays
uint16_t u8rpt_cnt;
// LQI
uint16_t u8lqi;
// DI status (true is active Lo,GND)
bool DI1, DI2, DI3, DI4;
// DI status bitmap (DI1,2,3,4 in order from LSB)
uint8_t DI_mask;
// true if DI active (has been active in the past)
bool DI1_active, DI2_active, DI3_active, DI4_active;
// Active bitmap of DI (DI1,2,3,4 in order from LSB)
uint8_t DI_active_mask;
// Supply voltage of the module [mV].
uint16_t u16Volt;
// AD value [mV]
uint16_t u16Adc1, u16Adc2, u16Adc3, u16Adc4;
// Bitmap that is set to 1 if AD is active (AD1,2,3,4 in order from LSB)
uint8_t Adc_active_mask;
};
smplbuf_u8<32> b;
auto&& bs = b.get_stream_helper(); // helper object
// Data String Generation
uint8_t FOURCHARS[]={'A', 'B', 'C', 'D'};
bs << FOURCHARS;
bs << ';';
bs << uint32_t(0x30313233); // "0123"
bs << format(";%d", 99);
Serial << b << crlf; // Output to Serial via smplbuf_u8<32> class
//Result: ABCD;0123;99
//..Continuation of the above example
// ABCD;0123;99 <- stored in b
//Variable for storing read data
uint8_t FOURCHARS_READ[4];
uint32_t u32val_read;
uint8_t c_read[2];
// Read out by operator>>
bs.rewind(); //Rewind the position to the beginning.
bs >> FOURCHARS_READ; //4 chars
bs >> mwx::null_stream(1); //1 char skipping
bs >> u32val_read; //32bit data
bs >> mwx::null_stream(1); //1 char skipping
bs >> c_read; //2 chars
// Result
Serial << crlf << "4chars=" << FOURCHARS_READ;
Serial << crlf << format("32bit val=0x%08x", u32val_read);
Serial << crlf << "2chars=" << c_read;
// 4chars=ABCD
// 32bit val=0x30313233
// 2chars=99
void setup()
void begin(uint16_t u16Hz, bool b_sw_int = true, bool b_pwm_out = false)
void end()
inline bool available()
void change_duty(uint16_t duty, uint16_t duty_max = 1024)
void change_hz(uint16_t hz, uint16_t mil = 0)
PulseCounter (mwx::periph_pulse_counter)
The pulse counter is a circuit that reads and counts pulses even when the microcontroller CPU is not running. There are two systems of pulse counters: PC0 is assigned to PulseCounter0
and PC1 to PulseCounter1
.
PC0 is assigned to PulseCounter0
and PC1 is assigned to PulseCounter1
.
void begin(uint16_t refct = 0,
E_PIN_INT_MODE edge = PIN_INT_MODE::FALLING,
uint8_t debounce = 0)
Initializes the object and starts counting. the first parameter refct
is the count number on which interrupts and available decisions are based. When this number is exceeded, it is reported to the application. You can also set refct
to 0. In this case, it is not a sleep wake-up factor.
The second parameter edge
specifies whether the interrupt is rising (PIN_INT_MODE::RISING
) or falling (PIN_INT_MODE::FALLING
).
The third debounce
takes the values 0, 1, 2, or 3. 1, 2, or 3 settings require the same consecutive value to detect a change in value to reduce the effect of noise.
0
-
100Khz
1
2
3.7Khz
2
4
2.2Khz
3
8
1.2Khz
void end()
Discontinue detection.
inline bool available()
If the specified count (refct
in begin()
) is 0, true
is returned when the count exceeds 1.
If the specified count (refct
in begin()
) is 1 or more, true
is returned when the number of detections exceeds the specified count.
uint16_t read()
Reads the count value. Resets the count value to 0 after reading.
Read/write two-wire serial (I2C) master (mwx::periph_wire)
Reads and writes two-wire serial (I2C) master.
using TwoWire = mwx::periph_twowire<MWX_TWOWIRE_RCVBUFF>;
The mwx::periph_wire<MWX_TWOWIRE_RCVBUFF>
can be referred as TwoWire
.
The following definition types describe the types of arguments and return values.
typedef uint8_t size_type;
typedef uint8_t value_type;
Some APIs make calls where the STOP bit is not strictly handled.
Instance creation and necessary initialization is done in the library. In user code, it is made available by calling Wire.begin()
.
When using the requestFrom()
method, you can specify the size of the FIFO queue for temporary data storage. At compile time, compile the macro MWX_TWOWIRE_BUFF
with the required number of bytes. The default is 32 bytes.
Example:
-DMWX_TWOWIRE_BUFF=16
void begin(
const size_type u8mode = WIRE_100KHZ,
bool b_portalt = false)
Initialize hardware.
Performing any Wire operation without initialization will cause the TWELITE radio module to hang.
When waking from sleep, if the module was operating just before sleep, it will return to the previous state.
u8mode
Specifies the bus frequency. Default is 100Khz (WIRE_CONF::WIRE_100KHZ
)
The frequency is specified by WIRE_CONF::WIRE_? KHZ
and ? KHZ
can be 50
,66
,80
,100
,133
,160
,200
,266
,320
,400
.
b_portalt
Change hardware pin assignments.
void setup() {
...
Wire.begin();
...
}
void wakeup() {
...
Wire.begin();
...
}
There are two types of read/write procedures. Select and use one of them.
Member function (input/output using the following member functions)
requestFrom(), beginTransmission(), endTransmission(), write()
Helper class (stream function available)
reader, writer
bool probe(uint8_t address)
Checks if the device specified by address
is responding. If the device exists, true
is returned.
void setClock(uint32_t speed)
The procedure is originally intended to change the bus frequency, but no action is taken.
Reads and writes the SPI bus (as Controller).
Reads and writes the SPI bus (as Controller).
const uint8_t
SPI_CONF::MSBFIRST
MSB as the first bit
const uint8_t
SPI_CONF::LSBFIRST
make LSB the first bit
const uint8_t
SPI_CONF::SPI_MODE0
set SPI MODE 0
const uint8_t
SPI_CONF::SPI_MODE1
set to SPI MODE 1
const uint8_t
SPI_CONF::SPI_MODE2
set to SPI MODE 2
const uint8_t
SPI_CONF::SPI_MODE3
set to SPI MODE 3
The procedure for using the SPI bus depends on the begin()
method.
Reads and writes the SPI bus (MASTER).
void begin(uint8_t slave_select, SPISettings settings)
SPISettings(uint32_t clock, uint8_t bitOrder, uint8_t dataMode)
Initialize hardware.
This process is also required after sleep recovery.
slave_select
Specify the SPI slave select pin to be used. 0 : DIO19
, 1 : DIO0
(DIO 19 is reserved), 2 : DIO1
(DIO 0,19 is reserved)
settings
Specifies the SPI bus setting.
The clock
[hz] specifies the SPI bus frequency. A divisor closer to the specified frequency will be selected: 16Mhz or 16Mhz divided by an even number.
bitOrder
specifies SPI_CONF::MSBFIRST
or SPI_CONF::LSBFIRST
.
dataMode
specifies SPI_CONF::SPIMODE0..3
.
void setup() {
...
SPI.begin(0, SPISettings(2000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));
...
}
void wakeip() {
...
SPI.begin(0, SPISettings(2000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));
...
}
void end()
Terminate the use of SPI hardware.
There are two types of read/write procedures. Select and use one of them.
Member function version (input/output using the following member functions) beginTransaction(), endTransaction(), transfer(), transfer16(), transfer32()
Helper class version (stream function available) transceiver
UART0 port of TWELITE (mwx::serial_jen)
Implement mwx::stream
and input/output with UART0 of TWELITE.
The Serial
object is initialized at system startup with UART0, 115200 bps, and the initialization process is performed in the library. On the user code, it is available from setup()
.
The Serial1
object is provided in the library, but no initialization process is performed; to enable UART1, perform the necessary initialization procedures Serial1.setup(), Serial1.begin()
.
Output may become unstable during setup(), wakeup()
just after startup or flush
process just before sleep.
void setup(uint16_t buf_tx, uint16_t buf_rx)
Initialize objects.
Allocate memory for FIFO buffers for TX/RX
Allocating memory for TWE_tsFILE structure
Serial
(UART0) will automatically call setup()
in the library. There is no need for a user call.
Also, the buffer size of Serial
(UART0) is determined at compile time. You can change it by the macro MWX_SER_TX_BUFF
(768 if not specified) or MWX_SER_RX_BUFF
(256 if not specified).
buf_tx
FIFO buffer size for TX
buf_rx
FIFO buffer size for RX
void begin(unsigned long speed = 115200, uint8_t config = 0x06)
Initialize hardware.
The Serial
(UART0) has an automatic begin()
call in the library. No user call is required.
speed
Specifies the baud rate of the UART.
config
When the serial_jen::E_CONF::PORT_ALT
bit is specified, UART1 is initialized with DIO14,15. If not specified, initializes UART1 with DIO11(TxD),9(RxD).
(Not implemented) Stop using hardware.
TWE_tsFILE* get_tsFile();
Get a structure in the TWE_tsFILE*
format used in the C library.
pktparser(parser_packet) performs content interpretation on the byte sequence converted by serparser performs content interpretation on the byte sequence converted by serparser.
serparser_heap parser_ser;
void setup() {
// init ser parser (heap alloc)
parser_ser.begin(PARSER::ASCII, 256);
}
void loop() {
int c;
while ((c = Serial.read()) >= 0) {
parser_ser.parse(c);
if (parser_ser.available()) {
// get buffer object
auto&& payl = parser_ser.get_buf();
// identify packet type
auto&& typ = identify_packet_type(payl.begin(), payl.end());
// if packet type is TWELITE standard 0x81 message
if (typ == E_PKT::PKT_TWELITE) {
pktparser pkt; // packet parser object
// analyze packet data
typ = pkt.parse<TwePacketTwelite>(payl.begin(), payl.end());
if (typ != E_PKT::PKT_ERROR) { // success!
// get data object
auto&& atw = pkt.use<TwePacketTwelite>();
// display packet inforamtion
Serial << crlf << format("TWELITE: SRC=%08X LQI=%03d "
, app.u32addr_src, app.u8lqi);
Serial << " DI1..4="
<< atw.DI1 ? 'L' : 'H' << atw.DI2 ? 'L' : 'H'
<< atw.DI3 ? 'L' : 'H' << atw.DI4 ? 'L' : 'H';
}
}
}
}
}
The above example interprets standard application 0x81 message. parser_ser object converts the message input from Serial into a byte string. This byte string is first identified by identify_packet_type()
to determine the type of the message E_PKT
. Once the message type is determined, the message is parsed by .parse<TwePacketTwelite>()
. The parsed result will be of type TwePacketTwelite
, and .use<TwePacketTwelite>()
is the procedure to extract this object. The TwePacketTwelite
type is a class, but it refers to its member variables directly as a structure.
template <class T>
E_PKT parse(const uint8_t* p, const uint8_t* e)
Parses a sequence of bytes.
The T
specifies the packet type to be parsed. For example, TwePacketTwelite
is specified for 0x81 messages in standard applications.
p
and e
specify the next to the beginning and the end of the byte sequence.
The return value is of type E_PKT
. In case of an error, E_PKT::PKT_ERROR
is returned.
template <class T>
T& use()
Returns a reference to an object corresponding to the packet type of the interpreted byte sequence. It can be called if parse<T>
was executed beforehand and there were no errors.
The T
can be the same type as the one executed with parse<T>
, or a TwePacket
from which only basic information can be obtained.
to enable DIO interrupt.
Enables DIO interrupts.
void attachIntDio(uint8_t u8pin, E_PIN_INT_MODE mode)
For a preconfigured pin, the first parameter is the pin number for which you want to enable interrupts, the second is the interrupt direction (rising, falling.
Set up an interrupt to be generated when the DIO5 pin changes from HIGH to LOW.
void setup() {
the_twelite.app.use<myAppClass>();
pinMode(PIN_DIGITAL::DIO5, PIN_MODE::INPUT_PULLUP);
attachIntDio(PIN_DIGITAL::DIO5, PIN_INT_MODE::FALLING);
}
void loop() {
;
}
class myAppClass: public mwx::BrdPal, MWX_APPDEFS_CRTP(myAppClasslMot)
{
};
Basic definition of the application behavior myAppClass
. Details are omitted.
/*****************************************************************/
// MUST DEFINE CLASS NAME HERE
#define __MWX_APP_CLASS_NAME myAppClass
#include "_mwx_cbs_cpphead.hpp"
/*****************************************************************/
MWX_DIO_INT(PIN_DIGITAL::DIO5, uint32_t arg, uint8_t& handled) {
static uint8_t ct;
digitalWrite(PIN_DIGITAL::DIO12, (++ct & 1) ? HIGH : LOW);
handled = false; // if true, no further event.
}
MWX_DIO_EVENT(PIN_DIGITAL::DIO5, uint32_t arg) {
Serial << '*';
}
/*****************************************************************/
// common procedure (DO NOT REMOVE)
#include "_mwx_cbs_cpptail.cpp"
// MUST UNDEF CLASS NAME HERE
#undef __MWX_APP_CLASS_NAME
} // mwx
/*****************************************************************/
Description of the interrupt handler of the application behavior myAppClass
, which inverts the output setting of DIO12 when an interrupt of DIO5 is generated and displays *
on the serial port Serial
for events occurring after the interrupt handler is finished.
Installing the TWELITE SDK
Download the TWELITE STAGE SDK distribution archive (e.g. ZIP) and extract it to an appropriate folder.
The folder names of each level of the destination folder must not contain anything other than one-byte numbers 0..9
, one-byte alphabets a..zA..Z
, and some symbols -_.
. In short, it must not contain spaces, Kanji characters, Hiragana characters, etc.
The following is an example folder after extracting the TWELITE STAGE SDK archive. (Windows, c:\work\MWSTAGE...
)
See Installing the TWELITE STAGE SDK for more information.
This is the end of the normal installation process. Refer to the following "Set Environmental variables" and below as necessary.
If you want to build with other than TWELITE STAGE application, please set the environment variables (e.g. MWSDK_ROOT
).
MWSDK_ROOT
, MWSDK_ROOT_WINNAME
(for Windows10) need to be set.
In this example, the name of the extracted folder is C:\MWSTAGE
. If you have installed the software in a different folder, please change the name.
Run C:\MWSTAGE\Tools\SET_ENV.CMD
to set the following environment variables:
MWSDK_ROOT
MWSDK_ROOT_WINNAME
For example, the following variables are set:
MWSDK_ROOT=C:/MWSTAGE/MWSDK/
MW_ROOT_WINNAME=C:\MWSTAGE\MWSDK\
To uninstall the TWELITE STAGE SDK from the installed PC, please do the following:
Run UNSET_ENV.cmd
. This will unset the environment variables.
Delete the MWSTAGE folder.
Set the development environment and shell to reflect the MWX_ROOT environment variable.
There are several ways to do this, but add the following setting to the .profile
of your home folder (if the file does not exist, please create a new one). You can even build VSCode with this setting.
MWSDK_ROOT=/foo/bar/MWSTAGE/MWSDK/
export MWSDK_ROOT
To add it without using the editor, enter the command as follows. The $ is a prompt and will be displayed differently depending on your environment. The /foo/bar/MSWSDK
part should be rewritten according to the installed folder.
$ cd $HOME
$ echo MWSDK_ROOT=/foo/bar/MWSTAGE/MWSDK>>.profile
$ echo export MWSDK_ROOT>>.profile
Set the development environment and shell to reflect the MWX_ROOT
environment variable.
There are several ways to do this, but add the following settings to .profile
in your home folder (create a new file if you don't have one). You can even build VSCode with this setting.
MWSDK_ROOT=/foo/bar/MWSTAGE/MWSDK/
export MWSDK_ROOT
To add it without using the editor, enter the command as follows. The $ is a prompt and will be displayed differently depending on your environment. The /foo/bar/MSWSDK
part should be rewritten according to the installed folder.
$ cd $HOME
$ echo MWSDK_ROOT=/foo/bar/MWSTAGE/MWSDK>>.profile
$ echo export MWSDK_ROOT>>.profile
pulse counter is an example of an ACT.
The pulse counter counts the number of rising or falling pulses of a signal without intervening microcontroller. It can be used to count irregular pulses and send wireless packets when the count reaches a certain number of times.
Counts the pulses connected to DIO8 on the Child Node side and transmits them wirelessly after a certain period of time or when a certain number of counts are detected.
The Child Node side operates while sleeping.
Parent Node
ACT in action.。
Child Node
1. 2. +
// Pulse Counter setup
PulseCounter.setup();
Initializes the pulse counter.
void begin() {
// start the pulse counter capturing
PulseCounter.begin(
100 // 100 count to wakeup
, PIN_INT_MODE::FALLING // falling edge
);
sleepNow();
}
Starts the pulse counter operation and performs the first sleep. The first parameter of PulseCounter.begin()
is the count number 100
to generate a wake-up interrupt, and the second is the falling detection PIN_INT_MODE::FALLING
.
void wakeup() {
Serial << mwx::crlf
<< "--- Pulse Counter:" << FOURCHARS << " wake up ---"
<< mwx::crlf;
if (!PulseCounter.available()) {
Serial << "..pulse counter does not reach the reference value." << mwx::crlf;
sleepNow();
}
}
Checks PulseCounter.available()
when waking up. available, that is, true
, indicates that the count is greater than or equal to the specified count. If it is false
, it resleeps.
If the count is more than the specified number, loop()
processes the transmission and waits for the completion of the transmission.
uint16_t u16ct = PulseCounter.read();
Reads the pulse count value. The counter is reset after the readout.
After the release of the TWELITE STAGE distribution package, fixes and additions are stored in the GitHub repository. Please replace the location of the distribution package if necessary.
Other updates to the MWSDK may be required. Please refer to the release description at the time of the update; see for information on updating the MWSDK.
The source code of the library is available on GitHub (). To replace the source code of the library, please follow the steps below.
Click on the link for each release to clone the Git file or download the source code in zip format.
Replace the contents of the following folders.
Updated information prior to major releases may be posted on the above link.
changed Wire object that reserves memory in heap area.
changed function name from G_OCTET()
to G_BYTE()
](api-reference/funcs/utility/byte-array-utils.md to avoid name conflict in utils.h.
changed an order of vAHI_DioInterruptEnable()
in the attachIntDio()
for code efficiency.
added secondary network behavior the_twelite.network2 to support universal receiver (receive packets from NWK_LAYERED, NWK_SIMPLE or networkless packets in the same executable code.)
added (At this time, only Parent Node reception is supported.)
introduced MWX_Set_Usder_App_Ver() function to set application version during MWX intitialization, mainly for interactive mode.
added to describe placement new simply.
added support of
added new[]
operators for EASTL
pre-compled most of source codes in MWX to quicker compile.
fixed: DIO events were being passed on to unrelated ports.
Added board support for TWELITE ARIA and sensor definition
Added an internal procedure to allow output using Serial class objects in Interactive settings mode. (Serial._force_Serial_out_during_intaractive_mode()
)
Main revisions
Serial1
port and alternate port were not properly defined.
Enabled to change the baud rate of (Serial
UART0).
Added event callbacks to notify received packets () and completed transmission ().
If you don't define a callback function, you can use the previous procedure.
Wrong definition ID for interactive mode setting<STG_STD>
and some default values.
Added support for changing the default values of channel and logical device IDs in addition to AppID in interactive mode settings<STG_STD>
.
added support for setting the_twelite and <NWK_SIMPLE>
objects in interactive mode <STG_STD>
object for some settings.
added support for setting the default value of the number of retransmissions in <NWK_SIMPLE>
.
Serial
(UART0) input and output from the application is not performed while the interactive mode screen<STG_STD>
is displayed.
added CUE::PIN_SET
, PAL???"":PIN_SET
(Since it is unnatural to use PIN_BTN
for CUEs without buttons)
Move namespace of random() to mwx::.
MONOSTICK watchdog setting is now done in 32ms units.
When sleep was performed using BRD_TWELITE
, the pins were not initialized correctly upon recovery.
Sorry. The following has not been translated.
Added board behavior () for TWELITE CUE.
Added method to receive other packets (without network usage) that are not in NWK_SIMPLE format when using NWK_SIMPLE. Add NWK_SIMPLE::receive_nwkless_pkt()
to initialize NWK_SIMPLE. When using this packet information, use only the TWENET C library layer structure by .get_psRxDataApp()
and the data array obtained by .get_payload()
. Information obtained from other methods of the incoming packet (auto&& rx = the_twelite.receiver.read()
) is undefined.
Refine get_stream_helper()
code and read/write position API.
Add EEPROM class object. ()
Samples ()
Fixed bugs in smplbuf::get_stream_helper()
.
Added pktparser class ()
Added sample ()
sample serparser/pktparser
so that it can be built on other platforms ()
Modified so that div100()
, which calculates the quotient and remainder, can be output to Serial, etc.
Changed implementation of smplbuf<>
array class. The inheritance from mwx::stream
is removed to reduce memory consumption, and a helper class and an inheritance class are defined separately.
Added mwx_printf()
mwx_snprintf()` function.
added the_twelite.stop_watchdog()
and the_twelite.restart_watchdog()
functions.
mwx::stream
maintenance: operator bool()
is obsolete. Disable timeout when .set_timeout(0xff)
is set to 0xff in the read timeout setting. Add definition of <<
operator.
Added NOTICE PAL / PCA9632 support (Description , sample )
Add scale functions between 8bit and 0..1000 with no division.
Added division by 10,100,1000 (quotient and remainder at the same time) div10()
, div100()
, div1000()
. Restricted the value range and composed mainly of multiplication and bit shift.
Added corresponding methods for encrypted packets.
packet_rx::is_secure_pkt()
: determine if received packet is encrypted or not.
STG_STD::u8encmode()
: Obtain encryption setting in interactive mode.
STG_STD::pu8enckeystr()
: Obtain encryption key byte sequence in interactive mode.
Serial1: Default port is DIO11(TxD), DIO9(RxD) because they are usually assigned to I2C, though DIO14,15 overlap with I2C in the semiconductor specification.
The calculation of the baud rate is omitted for the main baud rates.
Serial: The proxy functions for Serial: available()
and read()
are now held only by void*
, and the specification memory is reduced by 8 bytes.
Add typedef boolean
.
Network: added support for encryption.
The first parameter is the encryption key, and the second parameter is true
, so that plaintext packets are also received. packets are also received.
Added sensor support for SHT3x and BME280
Sensor: added a mechanism to exchange configuration parameters and status in legacy code (C library wrapper class).
Sensors: I2C address can be specified for SHT3x and BME280.
Configuration: added hide_items()
. Unnecessary items can be deleted.
Added `H/W UTIL menu' to display DI status, I2C probe, and PAL EEPROM contents.
Configuration: Add encryption related menus.
I2C related fixes (to improve compatibility with code implemented using TwoWire class)
Processing of requestFrom(false)
did not work correctly because there was no code to send the NO_STOP message when processing requestFrom(false)
.
Added TwoWire
class name alias.
Modified not to initialize multiply in begin()
process.
Added setClock()
method (but it is a dummy function and does nothing).
Added WIRE_CONF::WIRE_? KHZ
added. Added the main configuration values for the bus clock.
()
Added
Channel Manager. Implement
()
Addition of delayMilliseconds()
function
Addition of digitalReadBitmap()
function
Improved accuracy of delay()
.
Fixed the problem that Serial1
instance was not defined
Fixed problem with Analogue
interrupt handler not being called
Support for MWSDK202020_05
Duplicate checker duplicate_checker was not removed as expected due to incomplete initialization, etc.
The implementation of format() was made less machine-dependent. If 64-bit arguments are included, the number of arguments is limited.
The fix assumes MWSDK2020_05.
Updates are recommended for this fix.
Support for MWSDK2020_04
Fixed initialization problem with Timer0..4
Changed internal processing of mwx::format()
Added experimental code to support interactive mode
This fix assumes MWSDK2020_04.
We recommend an update to this fix.
Fixed problems with handling of relay flags in packets
We recommend an update to this correction.
First release (included in SDL Dec 2019)
WirelessUART performs serial communication.
Communicate between two UART-connected TWELITEs in ASCII format.
Two of the following devices serially connected to a PC.
connected to UART with products/TWE-Lite-DIP/index.html) etc.
Interactive settings mode is initialized. This sample provides two or more devices that have different logical device IDs (LIDs) from each other.
Initialize .
When data input from the serial is received, one byte is input to the serial parser. When the ASCII format is accepted to the end, SerialParser.parse()
returns true
.
The SerialParser
can access the internal buffer with smplbuf
. In the example above, the first byte of the buffer is taken as the destination address, and the second byte to the end is passed to the transmit()
function.
When a packet is received, a buffer smplbuf_u8<128> buf
containing the payload followed by the source as the first byte is created and output serially from the serial parser serparser_attach pout
for output.
Test data must be entered into the terminal using the paste function. This is because there is a timeout for input.
Note: To paste in TWE Programmer or TeraTerm, use Alt+V.
Send 00112233
to any Child Node.
Send AABBCC00112233
to Child Node #3.
Sent to any Parent Node or Child Node (0xFF
) and to the Parent Node (0x00
).
EEPROM
TWELITE Reads and writes to the built-in EEPROM of the wireless microcontroller.
The built-in EEPROM has 3480 bytes available from address 0x000 to 0xEFF.
The first part is , so it is recommended to use the second half of the address when used together. The amount of space consumed by the settings (Interactive settings mode) depends on its implementation. Even with minimal settings, up to 256 bytes are used from the beginning, so use of the later addresses is recommended.
Read the data corresponding to address
from EEPROM.
No error detection.
Write value
from EEPROM to address
.
No error detection.
This function is used when you want to reduce the number of rewrites in consideration of the rewrite life of EEPROM.
Obtain a helper object to read and write using mwx::stream
described below.
operators and methods. Using mwx::stream
, you can read and write integer types such as uint16_t
and uint32_t
types, read and write fixed-length array types such as uint8_t
, and format them using format()
objects.
Interfaces defined in mwx::stream
, such as the <<
operator, can be used on this object.
.seek()
is used to move the EEPROM address to 1024.
The above writes an 8-byte string (00bc614e
), a 4-byte integer (0x12ab34cd
), a 16-byte byte string (HELLO WORLD!....
), and a 1-byte terminating character.
Move the EEPROM address to 1024 using .seek()
.
Reads the data sequence written out earlier. In order, 8-byte characters, 4-byte integers, and 16-byte strings are read out using the >>
operator.
This class is a wrapper class for TWENET's tsRxDataApp
structure.
This class object is a wrapper class for callback function or by .
In packet_rx
, in particular, the data payload of the packet can be handled by the smplbuf
container, and utility functions such as expand_bytes()
simplify the payload interpretation description.
Get the data payload of the packet.
Obtain the receiving structure of the TWENET C library.
Returns the data length of the payload. The value is the same as .get_payload().size()
.
Obtain the LQI value (Link Quality Indicator).
Get the address of the sender.
get_addr_src_long()
is the serial number of the sender and MSB(bit31) is always 1.
get_addr_src_lid()
is the logical ID of the sender and takes values from 0x00
-0xFE
(the logical ID specified by <NWK_SIMPLE>
).
Gets the destination address.
The destination address is specified by the source, and the range of values varies depending on the type of destination.
Returns true
for encrypted packets and false
for plaintext.
Returns network type of the packet identified by Network BEHAVIOR.
Wire (using member function)
The method using member functions has a relatively low level of abstraction and follows the general API system as provided by the C library. The procedures for operating the two-wire serial bus are more intuitive.
However, it is necessary to be explicitly aware of the start and end of bus usage.
Reads the specified number of bytes at once. Since the result of reading is stored in a queue, call the .read()
method immediately afterward until the queue is empty.
Writing is performed by the write()
method after executing beginTransmission()
. Call endTranmission()
after a series of writing is finished.
Initialize the export transfer. Call endTransmission()
as soon as the writing process is finished.
Writes one byte.
Writes a byte sequence.
Processes the end of the export.
SPI access (using helper class)
The helper class version is a more abstract implementation. Creating a transceiver
object for reading and writing is the start of using the bus, and destroying the object is the end of using the bus.
By creating the object in the decision expression of the if statement, the validity period of the object is limited to the scope of the if clause, and the object is destroyed when it exits the if clause, at which point the bus usage termination procedure is performed.
In addition, read/write objects implement the mwx::stream
interface, allowing the use of the <<
operator, etc.
The start and end of bus usage is aligned with the validity period of the object to improve the visibility of the source code and to prevent omissions in the termination procedure, etc.
Unify read/write procedures with the mwx::stream
interface
Reading process and its termination procedure in scope if() { ... }
to do the reading with a helper class.
In the above, the x
object created by the get_rwer()
method is used to read and write one byte at a time. 1.
create x
object in if(...)
Generate the x
object in the get_rwer()
method. At the same time, set the select pin of the SPI bus. (The type is resolved with the universal reference auto&&
by type inference. 2.)
the generated x
object has a operator bool ()
defined, which is used to evaluate the decision expression, which is always true
for the SPI bus.
x
object defines uint8_t transfer(uint8_t)
method, which is called to perform 8bit read/write transfer to SPI. 4. 4.if() { ... }
Destructor of x
is called at the end of the scope to release the select pin of the SPI bus.
Obtains the worker object used to read/write the SPI bus.
Each transfers 8-bit, 16-bit, and 32-bit data, and returns the read result with the same data width as the written data width.
The int
and uint8_t
types perform 8-bit transfer.
Types uint16_t
and uint32_t
perform 16-bit and 32-bit transfers, respectively.
The transfer result is stored in an internal FIFO queue of up to 16 bytes and read by the >>
operator. Since the buffer is not large, it is assumed to be read out after each transfer.
Specify a variable with the same data width as the previous transfer.
If the result of the read is not needed, use the null_stream() object; it skips reading by the data byte specified by i. If the result of the read is not needed, use the null_stream() object.
This structure is used to store the values of 3-axis accelerometers, but procedures have been added to increase convenience when stored in a container class.
Generates an iterator that accesses an element on the X, Y, or Z axis using the iterator of the container class containing axis_xyzt
as a parameter.
In the example below, buf.begin()
and buf.end()
are used as iterators for the X axis in the algorithm std::minmax_element
.
This function generates a virtual container class from which one of the XYZ axes of the container class containing axis_xyzt
is taken. Only the begin()
and end()
methods are implemented in this generated class. The iterator that can be obtained by these begin()
and end()
methods is the same as the iterator in the previous section .
The TwePacketAppUart
class is the format in which the extended format of the App_UART is received by the parent and repeater application App_Wings.
Various information in the packet data is stored in DataAppUART
after parse<TwePacketUART>()
execution.
The payload
is the data part, but the method of data storage changes depending on the macro definition.
If MWX_PARSER_PKT_APPUART_FIXED_BUF
is compiled with the value 0
, payload
refers directly to the byte sequence for packet analysis. If the value of the original byte sequence is changed, the data in payload
will be destroyed.
If you define the value of MWX_PARSER_PKT_APPUART_FIXED_BUF
to be greater than 0
, the payload
will allocate a buffer of that value (bytes). However, if the data of the serial message exceeds the buffer size, parse<TwePacketAppUART>()
fails and returns E_PKT::PKT_ERROR
.
Enter printf format in mwx::stream
Helper class for writing format format to the << operator of mwx::stream
. In the library, it is alias defined as Using format=mwx::mwx_format;
.
The maximum number of arguments that can be registered in the variable number argument list is 8. 64-bit parameters such as double and uint64_t type are limited in number. If the limit is exceeded, a compile error will result due to static↵_assert.
Store the argument list received in the constructor in a variable internal to the class using the expand function of the parameter pack.
Call fctprintf()
at the point operator <<
is called to write data to the stream
The constructor stores the format pointer and parameters. The subsequent call with the <<
operator interprets the format and processes the output.
The fmt
must remain accessible until this object is destroyed.
size_type requestFrom(
uint8_t u8address,
size_type length,
bool b_send_stop = true)
u8address
I2C address to be read
length
Number of bytes read
b_send_stop=true
When true
, the STOP
bit is set at the end of reading.
return type size_type
Number of bytes read. 0
means read failure.
int len = Wire.requestFrom(0x70, 6);
for (int i = 0; i < 6; i++) {
if (Wire.available()) {
au8data[i] = Wire.read();
Serial.print(buff[i], HEX);
}
}
// skip the rest (just in case)
// while (Wire.available()) Wire.read(); // normally, not necessary.
#define DEV_ADDR (0x70)
const uint8_t msg[2] =
{SHTC3_SOFT_RST_H, SHTC3_SOFT_RST_L};
Wire.beginTransmission(DEV_ADDR);
Wire.write(msg, sizeof(msg));
Wire.endTransmission();
void beginTransmission(uint8_t address)
u8address
I2C address to be written out
size_type write(const value_type value)
Return value value
Bytes to be written.
Return value size_type
Number of bytes written. A value of 0
is an error.
size_type write(
const value_type* value,
size_type quantity)
*value
the byte sequence to be written
Return value size_type
Number of bytes to be written.
Return value size_type
Number of bytes written. 0 is an error.
uint8_t endTransmission(bool sendStop = true)
sendStop = true
Issue the STOP bit.
Return value uint8_t
0: Success 4: Failure
uint8_t c;
if (auto&& trs = SPI.get_rwer()) { // Object creation and device communication determination
// Within this scope (wave brackets) is the validity period of trs.
trs << 0x00; // Writes 0x00 with mwx::stream interface
trs >> c; // Store the read data in c.
}
// wrt is discarded at the point where the if clause is exited, and the use of the bus is terminated
inline uint8_t _spi_single_op(uint8_t cmd, uint8_t arg) {
uint8_t d0, d1;
if (auto&& x = SPI.get_rwer()) {
d0 = x.transfer(cmd); (void)d0;
d1 = x.transfer(arg);
// (x << (cmd)) >> d0;
// (x << (arg)) >> d1;
}
return d1;
}
periph_spi::transceiver get_rwer()
uint8_t transfer(uint8_t val)
uint16_t transfer16(uint16_t val)
uint32_t transfer32(uint32_t val)
operator << (int c)
operator << (uint8_t c)
operator << (uint16_t c)
operator << (uint32_t c)
operator >> (uint8_t& c)
operator >> (uint16_t& c)
operator >> (uint32_t& c)
null_stream(size_t i = 1)
operator >> (null_stream&& p)
class TwePacketAppUART : public TwePacket, public DataAppUART
struct DataAppUART {
/**
* source address (Serial ID)
*/
uint32_t u32addr_src;
/**
* source address (Serial ID)
*/
uint32_t u32addr_dst;
/**
* source address (logical ID)
*/
uint8_t u8addr_src;
/**
* destination address (logical ID)
*/
uint8_t u8addr_dst;
/**
* LQI value
*/
uint8_t u8lqi;
/**
* Response ID
*/
uint8_t u8response_id;
/**
* Payload length
*/
uint16_t u16paylen;
/**
* payload
*/
#if MWX_PARSER_PKT_APPUART_FIXED_BUF == 0
mwx::smplbuf_u8_attach payload;
#else
mwx::smplbuf_u8<MWX_PARSER_PKT_APPUART_FIXED_BUF> payload;
#endif
};
Serial << format("formatted print: %.2f", (double)3123 / 100.) << mwx::crlf;
// formatted print: 31.23[改行]
format(const char *fmt, ...)
fmt
Format format. See TWESDK/TWENET/current/src/printf/README.md
...
Parameters according to the format format. * The maximum number of parameters is 4. 5 or more parameters will result in a compile error. * Since consistency with the format is not checked, it is not safe for inconsistent input.
.../MWSTAGE/ --- TWELITE STAGE distribution dir
.../MWSDK --- MWSDK dir
.../TWENET/current/src/mwx <-- Replace this dir.
mwx
twesettings
TWENET C
1.3.5
mwx
twesettings
TWENET C
1.3.5
mwx
twesettings
TWENET C
1.3.5
mwx
twesettings
TWENET C
1.3.4
mwx
twesettings
TWENET C
1.3.4
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE) // set Logical ID. (0xFE means a child device with no ID)
<< NWK_SIMPLE::secure_pkt((const uint8_t*)"0123456789ABCDEF");
;
mwx
twesettings
TWENET C
1.3.4
mwx
twesettings
TWENET C
1.3.3
void setup() {
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
/*** INTERACTIE MODE */
// settings: configure items
set << SETTINGS::appname("WirelessUART");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set << SETTINGS::lid_default(DEFAULT_LID); // set default lid
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.
/*** SETUP section */
// the twelite main class
the_twelite
<< set // from iteractive mode (APPID/CH/POWER)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
nwk << set; // from interactive mode (LID/REPEAT)
/*** BEGIN section */
SerialParser.begin(PARSER::ASCII, 128); // Initialize the serial parser
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- WirelessUart (id=" << int(nwk.get_config().u8Lid) << ") ---" << mwx::crlf;
}
SerialParser.begin(PARSER::ASCII, 128);
while(Serial.available()) {
if (SerialParser.parse(Serial.read())) {
Serial << ".." << SerialParser;
const uint8_t* b = SerialParser.get_buf().begin();
uint8_t addr = *b; ++b; // the first byte is destination address.
transmit(addr, b, SerialParser.get_buf().end());
}
}
void on_rx_packet(packet_rx& rx, bool_t &handled) {
// check the packet header.
const uint8_t* p = rx.get_payload().begin();
if (rx.get_length() > 4 && !strncmp((const char*)p, (const char*)FOURCHARS, 4)) {
Serial << format("..rx from %08x/%d", rx.get_addr_src_long(), rx.get_addr_src_lid()) << mwx::crlf;
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // src addr (LID)
, make_pair(p+4, rx.get_payload().end()) ); // data body
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << pout;
}
}
:FE00112233X
:FE001122339C
:03AABBCC00112233X
:03AABBCC0011223366
:FF00112233X
:00112233X
uint8_t read(uint16_t address)
void write(uint16_t address, uint8_t value)
void update(uint16_t address, uint8_t value)
auto&& get_stream_helper()
// The return type is abbreviated as auto&& due to its length.
auto&& strm = EEPROM.get_stream_helper();
// Helper object type names are resolved by auto&& due to their length.
strm.seek(1024); // Move to 1024th byte
strm << format("%08x", 12345678); // Record 12345678 as 8 characters in hexadecimal
strm << uint32_t(0x12ab34cd); // Record 4 bytes of 0x12ab34cd
uint8_t msg_hello[16] = "HELLO WORLD!";
strm << msg_hello; // Record "HELLO WORLD!" byte sequence (unterminated)
// result
// 0400: 30 30 62 63 36 31 34 65 12 ab 34 cd 48 45 4c 4c
// 0 0 b c 6 1 4 e 0x12ab34cd H E L L
// 0410: 4f 20 57 4f 52 4c 44 21 00 00 00 00 ff ff ff ff
// O SP W O R L D !
strm.seek(1024);
uint8_t msg1[8];
strm >> msg1;
Serial << crlf << "MSG1=" << msg1;
// MSG1=00bc614e
uint32_t var1;
strm >> var1;
Serial << crlf << "VAR1=" << format("%08x", var1);
// VAR1=12ab34cd
uint8_t msg2[16]; // "HELLO WORLD!"の文字数
strm >> msg2;
Serial << crlf << "MSG2=" << msg2;
// MSG2=HELLO WORLD!
smplbuf_u8_attach& get_payload()
const tsRxDataApp* get_psRxDataApp()
uint8_t get_length()
uint8_t get_lqi()
uint32_t get_addr_src_long()
uint8_t get_addr_src_lid()
uint32_t get_addr_dst()
MSB (bit31) is set.
0x00
-0xFF
Logical ID (8bit) is specified as the destination.
0x00
-0xFF
bool is_secure_pkt()
uint8_t get_network_type()
mwx::NETWORK::LAYERED
packets from <NWK_LAYERED>
mwx::NETWORK::SIMPLE
packets from <NWK_SIMPLE>
mwx::NETWORK::NONE
neworkless packets
others
error or unknow packet type
struct axis_xyzt {
int16_t x;
int16_t y;
int16_t z;
uint16_t t;
};
/*The return type is described as auto&& because it is a long template type name.*/
auto&& get_axis_x_iter(Iter p)
auto&& get_axis_y_iter(Iter p)
auto&& get_axis_z_iter(Iter p)
#include <algorithm>
void myfunc() {
// container class
smplbuf_local<axis_xyzt, 10> buf;
// Submit data for testing
buf[0] = { 1, 2, 3, 4 };
buf[1] = { 2, 3, 4, 5 };
...
// Algorithm to obtain maximum and minimum values
auto&& minmax = std::minmax_element(
get_axis_x_iter(buf.begin()),
get_axis_x_iter(buf.end()));
Serial << "min=" << int(*minmax.first)
<< ",max=" << int(*minmax.second) << mwx::crlf;
}
/*The return type is described as auto&& because it is a long template type name.*/
auto&& get_axis_x(T& c)
auto&& get_axis_y(T& c)
auto&& get_axis_z(T& c)
#include <algorithm>
void myfunc() {
// container class
smplbuf_local<axis_xyzt, 10> buf;
// Submit data for testing
buf[0] = { 1, 2, 3, 4 };
buf[1] = { 2, 3, 4, 5 };
...
// Extract the X axis in the queue
auto&& vx = get_axis_x(que);
// Use of ranged for statement
for (auto&& e : vx) { Serial << int(e) << ','; }
// Algorithm to obtain maximum and minimum values
auto&& minmax = std::minmax_element(
vx.begin(), vx.end());
Serial << "min=" << int(*minmax.first)
<< ",max=" << int(*minmax.second) << mwx::crlf;
}
This class is a wrapper class for the tsTxDataApp
structure of the TWENET C library, and objects of derived classes based on this class are obtained from network behaviors or on_tx_comp()
.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0,50,10);
pack_bytes(pkt.get_payload()
, make_pair("APP1", 4)
, uint8_t(u8DI_BM)
);
pkt.transmit();
}
This is done by the network behavior .prepare_tx_packet()
.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
...
}
In the above example, the_twelite.network.use<NWK_SIMPLE>()
retrieves an object of the network behavior. The object pkt
is created by .prepare_tx_packet()
of this object. It is a derived class of packet_tx
, although the type name is inferred by auto&&.
This pkt
object first returns true
or false
in the if()
. A false
return occurs when the queue for sending is full and no more requests can be added.
Various settings are configured in the radio packet to deliver the packet to the destination, including destination information. To configure the settings, an object containing the settings is given as the right-hand side value of the << operator.
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0,50,10);
The following is a description of the objects used for configuration.
The availability and implications of each setting depend on the network behavior specification.
tx_addr(uint32_t addr)
Specify the destination address addr
. See the Network Behavior specification for the destination address value.
<NWK_SIMPLE>
An address set with an MSB (bit31=0x80000000
) means that it is addressed to the serial number of the radio module. 0x00
. 0xFE means that the address is addressed to the child module (0x01
...0xEF
). 0xFE means broadcast to the child (
0x01...
0xEF), and
0xFF` means broadcast to both the parent and the child.
tx_retry(uint8_t u8count, bool force_retry = false)
Specifies the number of retransmissions. The number of retransmissions is specified by u8count. force_retry is a setting to retransmit a specified number of times regardless of whether the transmission is successful or not.
<NWK_SIMPLE>
ネットワークビヘイビア<NWK_SIMPLE>
では、同じ内容のパケットをu8count+1
回送信します。 force_retry
の設定は無視されます。
tx_packet_delay(uint16_t u16DelayMin,
uint16_t u16DelayMax,
uint16_t u16RetryDur)
Sets the delay between sending packets and the retransmission interval. Specify two values, u16DelayMin
and u16DelayMax
, in milliseconds [ms]. Transmission will be started at some point during this interval after a transmission request is made. Specify the retransmission interval as the value of u16RetryDur
in milliseconds[ms]. The retransmission interval is constant.
The transmission process may not start at the specified timing due to internal processing. In addition, IEEE802.15.4 processing also causes time blurring before packets are created. These timing blurs are an effective means of avoiding packet collisions in many systems.
Strict timing of packet transmission should be considered an exception due to the nature of the IEEE802.15.4 standard.
<NWK_SIMPLE>
This specification is valid.
If the same packet is retransmitted and arrives more than 1 second after the first transmission, it is not deduplicated because a new packet has arrived. The same packet may be received after 1 second due to a longer retransmission interval or packet arrival delay in relay. You can configure the handling of duplicate packets by initializing the <NWK_SIMPLE>
behavior.
tx_process_immediate()
This setting requests that packet transmission be performed "as quickly as possible." Packet transmission processing in TWENET is performed from the TickTimer, which runs every 1 ms. By setting this parameter, the packet transmission request is processed as soon as possible after the request is made. Of course, any setting other than tx_packet_delay(0,0,0)
is a meaningless specification.
If other packet transmission processing is in progress, it will be processed as usual.
<NWK_SIMPLE>
This designation is valid.
tx_ack_required()
In wireless packet communications, there is a transmission method in which a short radio message called ACK (ACK) is obtained from the destination after the transmission is completed to indicate that the transmission was successful. By setting this option, transmission with ACK is performed.
<NWK_SIMPLE>
This specification is VALID for <NWK_SIMPLE>
. It will result in a compile error. <NWK_SIMPLE>
is intended to implement a simple working relay network and does not communicate with ACK.
tx_addr_broadcast()
Specify broadcast.
<NWK_SIMPLE>
This specification is VALID for <NWK_SIMPLE>
. It will result in a compile error.
Specify the destination address tx_addr(0xFF)
(broadcast) or tx_addr(0xFE)
(broadcast to the child) instead.
tx_packet_type_id(uint8_t)
Specify the type ID of the TWENET radio packet that can be 0..7.
<NWK_SIMPLE>
This specification is VALID for <NWK_SIMPLE>
. It will result in a compile error.
`` uses type ID internally. Users cannot use it.
stream_helper is a helper object that grants the mwx::stream
interface. It creates a helper object that references a data class and performs data input/output via the helper object.
The following creates a helper object bs
from an array b
of smplbufs and inputs data using the mwx::stream::operator <<()
operator.
smplbuf_u8<32> b;
auto&& bs = b.get_stream_helper(); // helper object
// Data String Generation
uint8_t FOURCHARS[]={'A', 'B', 'C', 'D'};
bs << FOURCHARS;
bs << ';';
bs << uint32_t(0x30313233); // "0123"
bs << format(";%d", 99);
Serial << b << crlf; // Output to Serial via smplbuf_u8<32> class
//result: ABCD;0123;99
stream_helper behaves as if the data array were a stream.
Internally, it holds read/write positions in the data array. It behaves as follows.
After the last data is read or written, it moves to the next read/write position.
After the last data is read or data is appended to the end, the read/write position becomes the end.
If the read/write position is the end of the line, then
available()
becomes false
.
Read cannot be performed.
Writing is appended if it is within the writable range.
stream_helper is a data class (smplbuf, EEPROM) member functions.
auto&& obj_helper = obj.get_stream_helper()
// obj is an object of data class, obj_helper type is accepted by auto&& because it is long.
void rewind()
Moves the read/write position to the beginning.
int seek(int offset, int whence = MWX_SEEK_SET)
Set the read/write position.
MWX_SEEK_SET
set from the leading position. If offset
is 0
, it means the same as rewind()
.
MWX_SEEK_CUR
Move by offset
with respect to the current position.
MWX_SEEK_END
end position. Setting offset
to 0
sets the end position. If -1
is set, it moves to the last character.
int tell()
Returns the read/write position. Returns -1
for the end position.
int available()
Returns 0
if the read/write position is the end. If it is not the end, it returns any other value.
An ACT (Act) beginning with _Unit_ is used to describe a very single function or to check the operation of a function.
Unit_ADC
This sample runs the ADC, reading and displaying about every second while continuously running the ADC every 100ms. Sleep by [s]
key.
Unit_I2Cprobe
Scan the I2C bus for responding device numbers (some devices may not respond to this procedure).
Unit_delayMicoroseconds
, and compare with the count of 16Mhz TickTimer.
Unit_brd_CUE
Check the operation of the acceleration sensor, magnetic sensor, and LED of . Input [a]
,[s]
,[l]
keys from the terminal.
Unit_brd_PAL_NOTICE
. All lights flash at startup, then operate via serial input.
- r
,g
,b
,w
: Toggle each color lighting mode
- R
,G
,B
,W
: change the brightness of each color (disabled when lights off or all lights on)
- c
: change the cycle (when blinking)
- C
: change the duty when blinking (when blinking)
Unit_div100
Find the divisor and quotient of 10,100,1000 . -9999999 to 999999 and compare the elapsed time with the usual division by /
,%
.
Unit_div_format
results in string output.
Unit_UART1
UART1 () is a sample of using UART0 () outputs input from UART1 to UART1 and input from UART1 to UART0.
Unit_Pkt_Parser
Sample use of the packet information parser <a href="../It can interpret the output of App_Wings. * You may want to connect two TWELITE radio modules serially and use one as App_Wings and the other to interpret its output. If you want to connect the other to a non-TWELITE radio module, you can use "".
Uint_EEPROM
EEPROM read/write test code.
Unit_Cue_MagBuz
This program uses the TWELITE CUE magnet sensor and SET pin (connected to a piezoelectric buzzer) to sound a buzzer when the magnet is released.
Unit_doint-bhv
Example of BEHAVIOR description for handling IO interrupts.
Unit_EASTL
A collection of small codes using the EASTL library.
Sample Acts
To help you understand how the ACT works, we have prepared some samples.
Several samples are provided to help you understand how the act works.
act0..4 is a very simple example, without any radio functions, to give you an idea of the basic structure of Act.
This is an example of a wireless sensor implementation that connects an I2C sensor and sends wireless packets while performing a brief operation by sleeping. Since this is a relatively simple and typical structure, it is recommended that you refer to it after reviewing act0
through act4
.
Typical elements for implementing wireless sensors in TWELITE (simple relay net <NWK_SIMPLE>
, Interactive settings mode <STG_STD>
, I2C sensor handling Wire
, intermittent operation with sleep, etc.).
These are samples of sending or receiving wireless packets, each implemented from a slightly different perspective.
Scratch is a simple code that receives 1 byte commands from the UART, sends them and so on.
Slp_Wk_and_Tx uses a state machine and intermittent operation with sleep, repeating the process of sleep recovery, radio transmission and sleep.
PingPong is a sample of sending packets from one side to the other and receiving packets back by the other side. It contains basic procedures for sending and receiving.
WirelessUART interprets the UART input into ASCII format using serparser and sends it.
Refer to this when implementing your own Receiving Parent Node application.
Parent_MONOSTICK exclusively receives and outputs the reception result to the serial port. The wireless packets in this sample, which are addressed to the parent device (0x00) or to the child device broadcast (0xFE), can be received. It also includes a procedure to add the interactive mode <STG_STD> to Act.
Rcv_Univsl is an example code for universal packets receiver (e.g. TWENET layered tree network, App_Twelite, Act, ...). It alse uses EASTL library for container or some algorithm.
The explanation of the ACT using Interactive settings mode describes the general flow of the program. There are no major differences between the explanations for any of the samples. (Here we quote BRD_I2C_TEMPHUMID)
BRD_I2C_TEMPHUMID executes I2C sensor device read/write commands and wirelessly transmits measurements obtained from I2C sensors. It also uses the Interactive settings mode <STG_STD> to Act.
Setting provides a higher degree of customisation of the interactive mode <STG_STD>.
This sample obtains sensor information from built-in peripherals and external sensor devices.
BRD_I2C_TEMPHUMID executes I2C sensor device read/write commands and wirelessly transmits measurements obtained from I2C sensors. It also uses the Interactive settings mode <STG_STD> to Act.
BRD_APPTWELITE provides bidirectional communication using digital input, analogue input, digital output and analogue output. It also contains the procedure for adding the interactive mode <STG_STD> to Act.
PulseCounter uses a pulse counter function to count the number of pulses detected on the input port, even during sleep, and to transmit this data wirelessly.
PAL_AMB_behavior is a behavioural example: in PAL_AMB the temperature and humidity sensors are called by the code inside the library, but this sample also includes its own procedure for accessing the temperature and humidity sensors via I2C bus.
Although a standard PAL application is written into TWELITE PAL, it is possible to write a description using ACT without using the PAL application. The MWX library provides standard procedures for MOTION SENSE PAL use.
This is a sample for various PAL boards. This sample acquires sensor values, transmits these valuse, and sleeps on the PAL board.
The following is an example of an advanced application, which is a little more complicated to describe than the above ACT.
PAL_AMB_usenap is a sample that aims to save more power by allowing the TWELITE microcontroller to sleep briefly during the sensor's operating time, which can take several tens of milliseconds.
PAL_AMB_behavior is a behavioural example: in PAL_AMB the temperature and humidity sensors are called by the code inside the library, but this sample also includes its own procedure for accessing the temperature and humidity sensors via I2C bus.
PAL_MOT_fifo for continuous acquisition and wireless transmission of samples without interruption, using the accelerometer's FIFO and FIFO interrupts.
Acts with names starting with Unit are intended to introduce features and APIs.
The following items are common settings in the Act sample and are explained below.
const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const char APP_FOURCHAR[] = "BAT1";
The TwePacketPal
class interprets TWELITE PAL packet data. This class handles TWELITE PAL (sensor data and other upstream data) commonly.
class TwePacketPal : public TwePacket, public DataPal { ... };
PAL common data is defined in DataPal
.
Generator functions are provided to retrieve data specific to each PAL sensor board.
Although the packet data structure of PAL differs depending on the connected sensors, etc., DataPal
holds the data structure of the common part.
struct DataPal {
uint8_t u8lqi; // LQI value
uint32_t u32addr_rpt; // address of the relay
uint32_t u32addr_src; // source address
uint8_t u8addr_src; // source logical address
uint16_t u16seq; // sequence number
E_PAL_PCB u8palpcb; // PAL board type
uint8_t u8palpcb_rev; // Revision of PAL board
uint8_t u8sensors; // Number of sensor data in the data (MSB=1 is an error)
uint8_t u8snsdatalen; // sensor data length (in bytes)
union {
const uint8_t *au8snsdata; // reference to sensor data part
uint8_t _pobj[MWX_PARSER_PKT_APPPAL_FIXED_BUF]; // each sensor object
};
};
The PAL packet data structure consists of two blocks: the common part for all PALs and the individual data part. The individual data part does not interpret the packet, but stores it as is. To simplify handling, data exceeding 32 bytes is stored in dynamically allocated uptr_snsdata
.
The individual data parts are stored in a structure whose base class is PalBase
. This structure is generated by the generator function defined in TwePacketPal
.
If the size fits in MWX_PARSER_PKT_APPPAL_FIXED_BUF
when parse<TwePacketPAL>()
is executed, a separate sensor object is generated.
If it does not fit, a reference to the byte sequence used for analysis is stored in au8snsdata
. In this case, if the data in the byte string used for analysis is rewritten, sensor-specific objects cannot be generated.
All data structures for each sensor in PAL inherit from PalBase
. The sensor data storage status u32StoredMask
is included.
struct PalBase {
uint32_t u32StoredMask; // Data acquisition flags used internally
};
PAL events are information that is sent when certain conditions are met by processing sensor information, rather than directly from sensors. For example, when an acceleration sensor detects a certain level of acceleration from a stationary state.
struct PalEvent {
uint8_t b_stored; // true if stored
uint8_t u8event_source; // spare
uint8_t u8event_id; // event ID
uint32_t u32event_param;// event parameter
};
If event data exists, it can be determined by .is_PalEvent()
of TwePacketPal
being true
, and .get_PalEvent()
will yield a PalEvent
data structure.
Generator functions are used to extract various types of data from sensor PAL.
void print_pal(pktparser& pkt) {
auto&& pal = pkt.use<TwePacketPal>();
if (pal.is_PalEvent()) {
PalEvent obj = pal.get_PalEvent();
} else
switch(pal.u8palpcb) {
case E_PAL_PCB::MAG:
{
// generate pal board specific data structure.
PalMag obj = pal.get_PalMag();
} break;
case E_PAL_PCB::AMB:
{
// generate pal board specific data structure.
PalAmb obj = pal.get_PalAmb();
} break;
...
default: ;
}
}
To use the generator function, first determine if pkt
is an event (.is_PalEvent()
). If it is an event, it has get_PalEvent()
. Otherwise, it creates an object according to u8palpcb
.
PalMag get_PalMag()
// MAG
struct PalMag : public PalBase {
uint16_t u16Volt; // module voltage [mV]
uint8_t u8MagStat; // state of magnetic switch [0:no magnet,1,2].
uint8_t bRegularTransmit; // MSB flag of u8MagStat
};
If .u8palpcb==E_PAL_PCB::MAG
, the data PalMag
of the open/close sensor pal is taken.
PalAmb get_PalAmb()
// AMB
struct PalAmb : public PalBase {
uint16_t u16Volt; // module voltage [mV]
int16_t i16Temp; // temperature (100x value)
uint16_t u16Humd; // humidity (100x value)
uint32_t u32Lumi; // illuminance (equivalent to Lux)
};
If .u8palpcb==E_PAL_PCB::AMB
, the data PalAmb
of the environmental sensor pal is taken.
PalMot get_PalMot()
// MOT
struct PalMot : public PalBase {
uint16_t u16Volt; // module voltage [mV]
uint8_t u8samples; // number of samples
uint8_t u8sample_rate_code; // sample rate (0: 25Hz, 4:100Hz)
int16_t i16X[16]; // X axis
int16_t i16Y[16]; // Y axis
int16_t i16Z[16]; // Z axis
};
If .u8palpcb==E_PAL_PCB::MOT
, the data PalMot
of the operating sensor pal is taken.
PalEvent get_PalEvent()
// PAL event
struct PalEvent {
uint8_t b_stored; // if true, event information is available
uint8_t u8event_source; // event source
uint8_t u8event_id; // event ID
uint32_t u32event_param; // 24bit, event parameter
};
Extracts a PalEvent
(PAL event) if .is_PalEvent()
is true
.
ADC (mwx::periph_analogue.hpp)
Analogue performs ADC and acquires values. It can continuously acquire multiple channels at a time, and can do so sequentially according to a timer or other cycle.
uint8_t PIN_ANALOGUE::A1 = 0
ADC1 pin
AI1
uint8_t PIN_ANALOGUE::A2 = 1
ADC2 pin
AI3
uint8_t PIN_ANALOGUE::A3 = 2
uint8_t PIN_ANALOGUE::D0 = 2
ADC3 pin (DIO0) *1
AI2
uint8_t PIN_ANALOGUE::A4 = 3
uint8_t PIN_ANALOGUE::D1 = 3
ADC4 pin (DIO1) *1
AI4
uint8_t PIN_ANALOGUE::VCC = 4
Vcc Supply voltage
In the standard application (App_Twelite), the pin names ADC2/ADC3 in the semiconductor data sheet are AI3/AI2 to match the TWELITE DIP. Please note this.
*1 ADC2/ADC3 pins for both digital and analog are subject to usage procedures and restrictions.
Assume no pull-up on the pin to be used before the ADC starts. If this is not done, the pull-up voltage will always be observed in the ADC.
pinMode(PIN_DIGITAL::DIO0, PIN_MODE::INPUT);
pinMode(PIN_DIGITAL::DIO1, PIN_MODE::INPUT);
In a normal circuit configuration, current leakage occurs during sleep. It cannot be circumvented by software description alone.
To avoid current leakage during sleep, the GND of the analog circuit section should be disconnected with a FET switch or the like and left floating during sleep. Also, before sleep, set the pin to input and pull-up state.
void setup(
bool bWaitInit = false,
uint8_t kick_ev = E_AHI_DEVICE_TICK_TIMER,
void (*fp_on_finish)() = nullptr)
Initializes ADCs. setup() starts the regulator inside the semiconductor, specifies the timer device for periodic execution, and specifies the callback function to be called when all ADCs on the specified channel have completed.
bWaitInit
If true is specified, waits for initialization of the regulator inside the semiconductor.
kick_ev
Specify the timer device to be specified for cyclic execution. The following five types of devices can be specified, and AD is started in the interrupt handler except for the first time.
E_AHI_DEVICE_TICK_TIMER (TickTimer)
E_AHI_DEVICE_TIMER0 .. 4 (Timer0 .. 4)
fp_on_finish
This callback function is called from within the interrupt handler after all ADCs on the specified ports have finished, and is used when ADC measurement values are to be stored separately in a FIFO queue, etc.
void begin(uint8_t bmPorts, uint8_t capt_tick = 1)
The first parameter specifies the port where the ADC is to be made. The port specification is a bitmap with the bits set corresponding to the port numbers mentioned in the pin definition.For example, if you want to get the values of two pins PIN_ANALOGUE::A2
and PIN_ANALOGUE::VCC
, specify (1 <<PIN_ANALOGUE::A1 | 1<<PIN_ANALOGUE::VCC )
. You can also use pack_bits
and write pack_bits(PIN_ANALOGUE::A1,PIN_ANALOGUE::VCC)
.
After calling begin()
, the first ADC processing starts immediately, and the processing of the next pin starts from its end interrupt. When all processing is finished (if specified), the callback function is called. It waits until the next timer interrupt occurs before starting a new ADC process.
The second parameter specifies the number of timer interrupts before AC starts. For example, TickTimer
is called every 1 ms, but if you specify 16 for the parameter, it will be processed every 16 ms.
void begin()
Starts ADC processing with default ADC pins (PIN_ANALOGUE::A1
, PIN_ANALOGUE::A2
). end()
resumes the interrupted ADC processing.
void end()
ADC processing is terminated and the regulator inside the semiconductor is stopped.
inline bool available()
The value of the ADC is set to true
after the acquisition. After confirmation by this function, it is false
until the next ADC completion.
inline int16_t read(uint8_t s)
inline int16_t read_raw(uint8_t s)
Reads ADC values. The parameter specifies the ADC pin to be read. read()
returns the read value converted to mV and read_raw() returns the ADC value (0..1023).
After ADC completion (available), if the value is read after a delay until near the timing when the next ADC process is executed, the next ADC value may be returned, because the ADC process is executed by an interrupt handler and the value is updated even during the loop()
process.
The ADC interrupt handler is set to periph_analogue::ADC_handler()
when setup()
is called.
If a different handler is specified by the semiconductor peripheral library, it will not work properly.
If the ADC is in cyclic execution state by begin()
, ADC processing is resumed after returning from sleep.
Build definition Makefile
The Makefile is stored in build/Makefile and is pre-defined to build the act by running the make command.
After copying the project folder from another environment, make sure to delete the build/objs_???
folder. If any intermediate files from the other environment remain, make will fail.
(MWSDK 2020-04)
You can avoid errors by adding USE_APPDEPS=0 to clean and then running make again.
$ make USE_APPDEPS=0 TWELITE=BLUE clean
...
$ make TWELITE=BLUE
``
Specify the build target as BLUE or RED; for TWELITE BLUE, use make TWELITE=BLUE
.
Run the build. Usually, you can omit this and use make TWELITE=BLUE
.
Remove intermediate files from the build. Do this as make TWELITE=BLUE clean
.
Remove all intermediate files. Do this as make cleanall
, the same as removing all of the objs_???
folder in the build folder.
When set to 1 (the default), the build file is determined based on file dependencies. For example, if there is a change in a header file, the associated source file will be recompiled.
If set to 0, the makefile will not error if there are inconsistent intermediate files left.
Depending on the size of the act, and when defining behaviours, the source files are usually split and built separately.
One of the build files is {project folder name.cpp}.
If you want to define other files, edit the build/Makefile
in your project folder.
The above is an example Makefile with sample PAL_AMB-bhv.
Specify the version number. This will be reflected in the build result file name.
During compilation, it is passed as a definition like -DVERSION_MAIN=0
-DVERSION_SUB=1
-DVERSION_VAR=0
.
(MWSDK 2020-04)
If you do not place files in subfolders, you no longer need to specify additions. All .c .cpp files in the project file will be added.
When you add a source file, you need APPSRC_CXX
and APP_COMMON_SRC_DIR_ADD?
.
Append the name of the source file to `APPSRC_CXX'. This file name must not contain a folder name. Anything in a subfolder should also be specified without a folder (i.e. if the same filename is in a subfolder, the build will fail)
Next, specify the search path if the source files are stored in a location other than the project folder. You can set up to four.
The folder specification is relative to the Makefile.
A number of other options can be passed to the compiler linker.
Container class with FIFO queue structure.
Container class with FIFO queue structure.
smplque
is a container class that provides FIFO queue operations for memory areas specified by element type T
and . Since the specification of alloc
is complicated, an alias definition using using
is used.
As a rule, element types are assumed to be structures that store numbers, numerical values, etc. It is not assumed to store objects that need to be destroyed by the destructor (since there is no process to delete the object when the element is deleted from the queue).
A class Intr
can be registered to set interrupt disabling at declaration time. If this class is not specified, normal operation without interrupt disable control is performed.
Example of object declaration. Immediately after the declaration, a method call is made for initialization. The maximum size of any of these objects is 128 bytes immediately after initialization, and the initial size is 0 and nothing is stored. The maximum size cannot be changed.
Since it is a FIFO queue, it is operated using methods such as push()
,pop()
,front()
.
Access by iterator is also possible.
Declares a container of type T
and size N
. After the declaration, call the initialization methods.
The smplque_local
allocates the area by a fixed array inside. Initialization by the constructor is also possible.
In smplque_attach
, specify the first pointer T* buf
of the buffer to be used, the initial size size
and the maximum size N
of the array. Initialization by the constructor is also possible.
smplque_heapallocates memory in the HEAP area (an area of memory that cannot be released but can be allocated at any time). Once allocated, this area cannot be released, so it is usually defined in the global area. Allocation is done by
init_heap(). Memory allocation by the constructor is not allowed. Please call
init_heap()` to use this function.
When creating a global object, initialization by the constructor is not possible (due to compiler limitation). Please call initialization functions init_local()
,attach()
,init_heap()
at the beginning of execution (setup()
is recommended).
push()` adds an entry to the queue.
pop()` removes an entry from the queue.
front()` refers to the first entry (the first one added).
back()` refers to the last entry (the last one added).
pop_front()` refers to the first entry as a return value and deletes it from the queue.
empty()
returns true
if the array contains no elements. is_full()
returns true
when the array is full.
size()
returns the number of elements stored in the queue.
capacity()
returns the maximum number of elements stored in the queue.
Erase all elements of the queue.
element. 0
is the first element added.
You can get an iterator by begin()
and end()
. The beginning of the iterator is the first registered element of the queue. By using the iterator, range for statements and algorithms can be used.
One application is .
API for DIO (General-purpose digital IO)
The following functions are used for general-purpose digital IO (DIO) operations.
pinMode()
digitalWrite()
digitalRead()
attachIntDio()
detachIntDio()
The following enumeration values are handled with the type name E_PIN_MODE
.
The following enumeration values are handled with the type name E_PIN_MODE
.
The following enumeration values are handled with the type name E_PIN_STATE
.
The following enumeration values are handled with the type name E_PIN_INT_MODE
.
Change the setting of the digital output pins.
Change the setting of the digital output pins.
The first parameter specifies the pin number to be set, and the second parameter specifies either HIGH
or LOW
.
##############################################################################
# Copyright (C) 2019 Mono Wireless Inc. All Rights Reserved.
# Released under MW-SLA-*J,*E (MONO WIRELESS SOFTWARE LICENSE
# AGREEMENT).
##############################################################################
# USER PROJECT BUILD DEFINITION.
##############################################################################
#####################################################################
### set TWELITE model
TWELITE ?= BLUE
#TWELITE = RED
#####################################################################
### set application version (MUST SET THIS.)
VERSION_MAIN = 0
VERSION_SUB = 1
VERSION_VAR = 0
#####################################################################
### set an additional source file
### the default file name is dirname.
### for C++ files compiled with g++ (must have .cpp suffix)
APPSRC_CXX += myAppBhvParent.cpp
APPSRC_CXX += myAppBhvParent-handlers.cpp
APPSRC_CXX += myAppBhvChild.cpp
APPSRC_CXX += myAppBhvChild-handlers.cpp
### for C files compiled with gcc (must have .c suffix)
#APPSRC += my_c_file.c
### Additional Src/Include Path
# if set, find source files from given dirs.
#
APP_COMMON_SRC_DIR_ADD1 = ../Parent
APP_COMMON_SRC_DIR_ADD2 = ../Child
#APP_COMMON_SRC_DIR_ADD3 =
#APP_COMMON_SRC_DIR_ADD4 =
#####################################################################
### set misc option for compiler
### C++ flags passed to g++
# e.g. CXXFLAGS += -DMY_DEFS
#CXXFLAGS +=
### C++/C flags passed to g++/gcc
# e.g. CFLAGS += -DMY_DEFS
#CFLAGS +=
### include opts
# e.g. INCFLAGS += -I../my_common_src/
#INCFLAGS +=
### optimize flag (default is -Os, normally no need to change)
#OPTFLAG=-O2
#####################################################################
### must include mwx.mk (the makefile body part.)
MWSDK_PATH?=$(realpath $(MWSDK_ROOT))
include $(MWSDK_PATH)/MkFiles/mwx.mk
#####################################################################
### set application version (MUST SET THIS.)
VERSION_MAIN = 0
VERSION_SUB = 1
VERSION_VAR = 0
APPSRC_CXX += myAppBhvParent.cpp
APPSRC_CXX += myAppBhvParent-handlers.cpp
APPSRC_CXX += myAppBhvChild.cpp
APPSRC_CXX += myAppBhvChild-handlers.cpp
APP_COMMON_SRC_DIR_ADD1 = ../Parent
APP_COMMON_SRC_DIR_ADD2 = ../Child
CXXFLAGS
Specify compilation options for C++ source files.
CFLAGS
Specify compile options for C/C++ source files.
INCFLAGS
Specify the include file for the header file.
OPTFLAGS
Define this if you have a special reason for wanting to apply a compile option other than -Os
.
LDFLAGS
Specify linker options. (This is not mentioned in the comments of the Makefile above, but can be specified)
const uint8_t PIN_DIGITAL::DIO0 .. 19
DIO pins 0 to 19
const uint8_t PIN_DIGITAL::DO0 .. 1
DO pin 0,1
PIN_MODE::INPUT
None
Input
PIN_MODE::OUTPUT
None
Output
PIN_MODE::INPUT_PULLUP
Yes
Input
PIN_MODE::OUTPUT_INIT_HIGH
None
Output(init HIGH)
PIN_MODE::OUTPUT_INIT_LOW
None
Output(init LOW)
PIN_MODE::WAKE_FALLING
None
Input, raised pin, falling
PIN_MODE::WAKE_RISING
None
Input, rising pin, rising
PIN_MODE::WAKE_FALLING_PULLUP
Yes
Input, raised pin, falling
PIN_MODE::WAKE_RISING_PULLUP
Yes
Input, rising pin, rising
PIN_MODE::DISABLE_OUTPUT
Yes
return to the input state
PIN_MODE::OUTPUT
Contribute
PIN_MODE::OUTPUT_INIT_HIGH
Output (initial state HIGH)
PIN_MODE::OUTPUT_INIT_LOW
Output (initial state LOW)
PIN_MODE::DISABLE_OUTPUT
Stop setting output
PIN_STATE::HIGH
1
HIGH(=Vcc) level
PIN_STATE::LOW
0
LOW(=GND) level
PIN_INT_MODE::FALLING
falling edge
PIN_INT_MODE::RISING
rising edge
static inline void digitalWrite(uint8_t u8pin, E_PIN_STATE ulVal)
template <typename T, int N, class Intr> smplbuf_local
template <typename T, class Intr> smplbuf_attach
template <typename T, class Intr> smplbuf_heap
void some_func() {
// alloc locally
smplque_local<uint8_t, 128> q1;
// attach to existing buffer
uint8_t buf[128];
smplque_attach<uint8_t> q2;
// alloc from the HEAP
smplque_heap<uint8_t> q3;
void setup() {
// Globally defined objects are initialized with setup()
q1.init_local();
q2.attach(buf, 128);
q3.init_heap(128);
}
void some_func() {
// Local definition of smplque_local can omit init_local()
smplque_local<uint8_t, 128> q_local;
..
}
}
void begin() { // begin() runs only once on startup
smplque_local<int, 32> q1;
q1.push(1);
q1.push(4);
q1.push(9);
q1.push(16);
q1.push(25);
while(!q1.empty()) {
Serial << int(q1.front()) << ',';
q1.pop();
}
// output -> 1,4,9,16,25,
}
void begin() { // begin() runs only once on startup
smplque_local<int, 32> q1;
q1.init_local();
q1.push(1);
q1.push(4);
q1.push(9);
q1.push(16);
q1.push(25);
// Use iterators
for(int x : q1) {
Serial << int(x) << ',';
}
// Application of STL Algorithm
auto&& minmax = std::minmax_element(q1.begin(), q1.end());
Serial << "min=" << int(*minmax.first)
<< ",max=" << int(*minmax.second);
// output -> 1,4,9,16,25,min=1,max=25[]
}
smplbuf_local<T,N>
smplbuf_local<T,N>::init_local()
smplbuf_attach<T>
smplbuf_attach<T>::attach(T* buf, uint16_t N)
smplbuf_heap<T>
smplbuf_heap<T>::init_heap(uint16_t N);
// Example
// Fixed array inside
smplque_local<uint8_t, 128> q1;
q1.init_local();
// Use an already existing array
uint8_t buf[128];
smplque_attach<uint8_t> q2;
q2.attach(buf, 128);
// Allocate to heap
smplque_heap<uint8_t> q3;
q3.init_heap(128);
inline void push(T&& c)
inline void push(T& c)
inline void pop()
inline T& front()
inline T& back()
inline T& pop_front()
inline bool empty()
inline bool is_full()
inline uint16_t size()
inline uint16_t capacity()
inline void clear()
inline T& operator[] (int i)
inline smplque::iterator begin()
inline smplque::iterator end()
Template code.
void setup() {
/*** SETUP section */
tx_busy = false;
// 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&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // check every 10ms, a change is reported by 5 consequent values.
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Scratch act ---" << mwx::crlf;
}
Set the_twelite
to set the application ID APP_ID
, the radio channel CHANNEL
and the receive presence.
It also generates nwk
and specifies the child machine address 0xFE
. This address means that this is an unnamed child machine that has not been addressed by a child machine.
It also initializes the Buttons object. This is a chatter-suppressing algorithm by successive references, which determines HI or LOW of the target port (PIN_BTN only) if the value is the same 5 times in 10ms. pack_bits
(N1, N2, ...)
pack_bits(N1, N2, ...)' does 1UL<<N1 | 1UL << N2 | ...
to make a bitmap.
the_twelite.begin(); // start twelite!
This is the procedure to start the_twelite
, it didn't appear in act0..4, but you should call it if you set up the_twelite
or register various behaviors.
void begin() {
Serial << "..begin (run once at boot)" << mwx::crlf;
}
Called only once after setup()
on startup. Only displays a message.
if (Buttons.available()) {
uint32_t bm, cm;
Buttons.read(bm, cm);
if (cm & 0x80000000) {
// the first capture.
}
Serial << int(millis()) << ":BTN" << format("%b") << mwx::crlf;
}
The state is determined by continuous reference to Buttons. When the button state changes, it is output serially.
while(Serial.available()) {
int c = Serial.read();
Serial << '[' << char(c) << ']';
switch(c) {
case 'p': ... // display millis()
case 't': ... // transmit radio packet (vTransmit)
if (!tx_busy) {
tx_busy = Transmit();
if (tx_busy) {
Serial << int(millis()) << ":tx request success! ("
<< int(tx_busy.get_value()) << ')' << mwx::crlf;
} else {
Serial << int(millis()) << ":tx request failed" << mwx::crlf;;
}
}
case 's': ... // sleeping
Serial << int(millis()) << ":sleeping for " << 5000 << "ms" << mwx::crlf << mwx::flush;
the_twelite.sleep(5000);
break;
}
}
If Serial.available()
is true
, the input from the serial port is stored. It reads one character from the serial and processes it according to the input character.
t
for wireless transmissionWhen 't
' is input, sending is done. In this sample, the tx_busy flag is used to prevent continuous input.
s
to sleep.the_twelite.sleep(5000);
The system will sleep for 5000ms=5 seconds. After recovery, wakeup()
is executed.
void wakeup() {
Serial << int(millis()) << ":wake up!" << mwx::crlf;
}
First to be called on sleep wake up. Display of message only.
MWX_APIRET Transmit() {
Serial << int(millis()) << ":Transmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0xFF) // Broadcast communications
<< tx_retry(0x1) // 1 resend
<< tx_packet_delay(100,200,20); // Transmission delay between 100-200 ms, retransmission interval 20 ms
// Specification of the data to be sent (to be determined for each application)
pack_bytes(pkt.get_payload()
, make_pair("SCRT", 4) // 4-character identifier
, uint32_t(millis()) // Timestamp
);
// Make a request to send
return pkt.transmit();
} else {
// Failed at .prepare_tx_packet() (send queue is full)
Serial << "TX QUEUE is FULL" << mwx::crlf;
return MWX_APIRET(false, 0);
}
}
This is the minimum procedure for making a transmission request.
When you leave this function, the request has not yet been executed. You need to wait a while. In this example we have set a delay of 100-200ms for the start of the transmission, so the transmission will not start until 100ms at the earliest.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
Serial << int(millis()) << ":tx completed!"
<< format("(id=%d, stat=%d)", ev.u8CbId, ev.bStatus) << mwx::crlf;
tx_busy = false; // clear tx busy flag.
}
Called on completion of a transmission; ev contains the transmission ID and completion status.
void on_rx_packet(packet_rx& rx, bool_t &handled) {
Serial << format("rx from %08x/%d",
rx.get_addr_src_long(), rx.get_addr_src_lid()) << mwx::crlf;
}
When a packet is received, the sender's address information is displayed.
Rcv_Unvsl (Universal Reciever)
By running NWK_LAYERED
on twe_twelite.network
and NWK_SIMPLE
on twe_twelite.network2
in the MWX library, packets of different types, including packets of layered tree net (TWELITE PAL, ARIA, etc) You can receive and interpret various types of packets.
However, wireless packets must be on the same CHANNEL and have the same Application ID.
setup()
, loop()
, and the callback function on_rx_packet()
for incoming packets.
These objects are declared in pkt_handler.cpp
and initialized by pnew()
in setup()
. It mainly interprets the payload (data) of the packet.
mwx::pnew(g_pkt_pal);
mwx::pnew(g_pkt_apptwelite);
mwx::pnew(g_pkt_actsamples);
mwx::pnew(g_pkt_unknown);
Two network objects are created. Be sure to NWK_LAYERED
to the_twelite.network
.
auto&& nwk_ly = the_twelite.network.use<NWK_LAYERED>();
auto&& nwk_sm = the_twelite.network2.use<NWK_SIMPLE>();
In this sample, the important part of the loop()
process is the .refresh()
process, which is performed about every second. Only g_pkt_apptwelite().refresh()
does the duplicate checker timeout. Other objects do nothing.
if (TickTimer.available()) {
static unsigned t;
if (!(++t & 0x3FF)) {
g_pkt_pal.refresh();
g_pkt_apptwelite.refresh();
g_pkt_actsamples.refresh();
g_pkt_unknown.refresh();
}
}
void on_rx_packet(packet_rx& rx, bool_t &handled) {
auto type = rx.get_network_type();
bool b_handled = false;
// PAL
if (!b_handled
&& type == mwx::NETWORK::LAYERED
&& g_pkt_pal.analyze(rx, b_handled)
) {
g_pkt_pal.display(rx);
}
// Act samples
if (!b_handled
&& type == mwx::NETWORK::SIMPLE
&& g_pkt_actsamples.analyze(rx, b_handled)
) {
g_pkt_actsamples.display(rx);
}
// Standard application (e.g. App_Twelite)
if (!b_handled
&& type == mwx::NETWORK::NONE
&& g_pkt_apptwelite.analyze(rx, b_handled)
) {
g_pkt_apptwelite.display(rx);
}
// unknown
if (!b_handled) {
g_pkt_unknown.analyze(rx, b_handled);
g_pkt_unknown.display(rx);
}
}
This is the most important part of this sample code. auto type = rx.get_network_type();
determines the packet type.
mwx::NETWORK::LAYERED
: NWK_LAYERED
layer tree net ket
mwx::NETWORK::SIMPLE
: NWK_SIMPLE
packets
mwx::NETWORK::NONE
: no network (e.g. App_Twelite)
other : error or unsupported packets
In the case of mwx::NETWORK::NONE
, the MWX library does not handle duplicate checkers for the same packet that may be sent in multiple retransmissions. It is necessary to describe the handling of these. In this sample, dup_checker.hpp
and dup_checker.cpp
are provided.
Interpretation of the packet refers to the packet_rx&
object that wraps tsRxDataApp*
. The packet_rx
class itself has no special functionality, it only defines a means of accessing some information obtained from tsRxDataApp*
using get_psRxDataApp()
.
It is defined for the purpose of unifying the interface of the packet interpretation part.
template <class D>
struct pkt_handler {
D& self() { return static_cast<D&>(*this); }
bool analyze(packet_rx& rx, bool &b_handled) {
return self().pkt.analyze(rx, b_handled);
}
void display(packet_rx& rx) {
Serial
<< crlf
<< format("!PKT_%s(%03d-%08x/S=%d/L=%03d/V=%04d)"
, self().get_label_packet_type()
, self().pkt.data.u8addr_src
, self().pkt.data.u32addr_src
, rx.get_psRxDataApp()->u8Seq
, rx.get_lqi()
, self().pkt.data.u16volt
);
self().disp_detail(rx);
}
void refresh() {
self()._refresh();
}
};
// packet analyzer for App_Twelite
class pkt_handler_apptwelite : public pkt_handler<pkt_handler_apptwelite> {
friend class pkt_handler<pkt_handler_apptwelite>;
pkt_apptwelite pkt;
void disp_detail(packet_rx& rx);
const char* get_label_packet_type() { return "AppTwelite"; }
void _refresh() { pkt.refresh(); }
public:
pkt_handler_apptwelite() : pkt() {}
};
analyze()
: interpret the payload of a packet.
display()
: Display packet information.
refresh()
: describe the process every second.
self()
: cast to the derived class D
.
In addition, the packet interpretation class (example pkt_handler_apptwelite
above) contains a member object pkt
. The actual packet interpretation part is done in pkt_???.cpp
.
A packet interpretation part analyze()
for each packet type and a data structure data
are defined. The member data
is a structure but inherits the common structure of PktDataCommon
. This common part is used to concisely describe the code for serial output of the packet's data.
Corresponds to PAL-related packets; the PAL packet structure has a complex data structure. The implementation here is based on the EASTL container.
_vect_pal_sensors
: pool of _pal_sensor
objects. This object is a dedicated class for use with instusive map.
_map_pal_sensors
: intrusive map structure for efficient retrieval of sensor data.
Allocate an entry in _vect_pal_sensors
for each of the multiple data in a packet as they are added and store the value. Once all the data in a packet has been interpreted, a _map_pal_sensors
is constructed with the sensor type as the key.
Implement a duplicate checker. The behavior of the checker can be customized by template arguments.
MODE
: If MODE_REJECT_SAME_SEQ
is specified, packets with the same sequence number are excluded. This is used when the packet order is reordering. The MODE_REJECT_OLDER_SEQ
adopts a more recent number.
TIMEOUT_ms
: The interval at which to initialize the duplicate database. If 1000
is specified, data after 1 second will be erased. Packets that were excluded immediately before will be adopted again when the duplicate database is initialized.
N_ENTRIES
: The maximum number of elements to be allocated in the data structure.
N_BUCKET_HASH
: Maximum number of hash values. Specifies a prime number. It is determined based on the type of wireless node being received.
_mmap_entries
: It is an intrusive hash multi-map structure. The search key is the serial number of the wireless node.
_vect_pool
: Allocates a fixed number (N_ENTRIES
) of elements used in the map structure.
_ring_vecant_idx
: Keeps track of _vect_pool
elements not used in _mmap_entries
by array index number. It is a ring buffer structure, which takes one value from the ring buffer when adding elements and returns a value to the ring buffer when removing elements.
bool check_dup(uint32_t u32ser, uint16_t u16val, uint32_t u32_timestamp) {
// find entry by key:u32ser.
auto r = _mmap_entries.equal_range(u32ser);
...
}
To retrieve data from the multimap structure, call .equal_range()
. The resulting r
is an iterator that enumerates elements with the same serial number.
Each element (_dup_checker_entry
) has a timestamp and sequence number. Duplicates are checked according to these values.
accessing Wire (using helper class)
The helper class version is a more abstract implementation. Creating objects reader, writer
for reading and writing is the start of using the bus, and destroying the objects is the end of using the bus.
By creating the object in the decision expression of the if statement, the validity period of the object is limited to the scope of the if clause, and the object is destroyed when it exits the if clause, at which point the bus usage termination procedure is performed.
if (auto&& wrt = Wire.get_writer(...)) { // Object creation and device communication determination
// Within this scope (wave brackets) is the validity period of wrt.
wrt << 0x00; // Writes 0x00 with mwx::stream interface
}
// wrt is discarded at the point where the if clause is exited, and the use of the bus is terminated
Also, read/write objects implement the mwx::stream
interface, so you can use the <<
operator, etc.
Aligning the start and end of bus usage with the validity period of objects improves the visibility of source code and prevents omissions of termination procedures.
Unification of read/write procedures by mwx::stream
interface
Loading process and its termination procedure in scope if() { ... }
in scope.
const uint8_t DEV_ADDR = 0x70;
uint8_t au8data[6];
if (auto&& rdr = Wire.get_reader(DEV_ADDR, 6)) {
for (auto&& c: au8data) {
c = rdr();
}
}
// same above
uint16_t u16temp, u16humd;
uint8_t u8temp_csum, u8humd_csum;
if (auto&& rdr = Wire.get_reader(SHTC3_ADDRESS, 6)) {
rdr >> u16temp;
rdr >> u8temp_csum;
rdr >> u16humd;
rdr >> u8humd_csum;
}
The above reads one byte at a time using the rdr
object generated by the get_readr()
method. The parameter of the method is the two-line serial ID to be read. 1.
in if(...) Create a
rdrobject in
. (The type is resolved with the universal reference auto&&
by type inference. 2.) The generated rdr
object defines an operator bool ()
, which is used to evaluate the decision expression. If communication is possible with the specified ID, true
is returned. 3. The rdr
object defines an int operator () (void)
operator, which is called to read one byte of data from the 2-wire serial bus. If the read fails, -1
is returned, and if it succeeds, the read byte value is returned. 4.
if() { ... }
The destructor of rdr
is called at the end of the scope to STOP
the two-wire serial bus.
periphe_wire::reader
get_reader(uint8_t addr, uint8_t read_count = 0)
I2C 読み出しに用いるワーカーオブジェクトを取得します。
addr
I2C address for reading
read_count
Number of bytes to read (specifying this value will issue a STOP bit on the last transfer); specifying 0 will result in no STOP bit (some devices may work)
Reading with a helper class to perform the writing process and its termination procedure in scope if() { ... }
in scope.
const uint8_t DEV_ADDR = 0x70;
if (auto&& wrt = Wire.get_writer(DEV_ADDR)) {
wrt(SHTC3_TRIG_H);
wrt(SHTC3_TRIG_L);
}
// same above
if (auto&& wrt = Wire.get_writer(DEV_ADDR)) {
wrt << SHTC3_TRIG_H; // int type is handled as uint8_t
wrt << SHTC3_TRIG_L;
}
In the above, the wrt
object generated by the get_writer()
method is used to write one byte at a time. The method parameter is the two-line serial ID to be read.
if(...) Generate a
wrtobject in
. (The type name is resolved with auto' because it is long. 2.) The generated
wrtobject defines an
operator bool (), which is used to evaluate the decision expression. If communication is possible with the specified ID,
trueis returned. 3. The
wrtobject defines an
int operator () (void)operator, which is called to write one byte of data to the 2-wire serial bus. If it fails,
-1` is returned, and if it succeeds, the written byte value is returned. 4.
if() { ... }
The destructor of wrt
is called at the end of the scope
to STOP
the two-wire serial bus.
periph_wire::writer
get_writer(uint8_t addr)
Obtains the worker object used for I2C export.
addr
I2C address for writing out
operator << (int c)
operator << (uint8_t c)
operator << (uint16_t c)
operator << (uint32_t c)
The int
and uint8_t
types transfer 8 bits.
operator() (uint8_t val)
operator() (int val)
Write out 1 byte.
operator >> (uint8_t& c)
operator >> (uint16_t& c)
operator >> (uint32_t& c)
operator >> (uint8_t(&c)[N]) // Fixed array of N bytes
Reads only the size of each data type.
int operator() (bool b_stop = false)
//Example
uint8_t dat[6];
if (auto&& rdr = Wire.get_reader(0x70)) {
for(uint8_t& x : dat) {
x = rdr();
}
}
Reads 1 byte. Returns -1 if there is an error, returns the byte value read if normal.
If b_stop
is set to true
, the STOP bit is issued on that read.
The following example shows a measurement example of the temperature/humidity sensor SHTC3 of the environmental sensor PAL.
Wire.begin();
// reset (may not necessary...)
if (auto&& wrt = Wire.get_writer(0x70)) {
wrt << 0x80; // SHTC3_SOFT_RST_H
wrt << 0x05; // SHTC3_SOFT_RST_L
}
delay(5); // wait some
// start read
if (auto&& wrt = Wire.get_writer(0x70)) {
wrt << 0x60; // SHTC3_TRIG_H
wrt << 0x9C; // SHTC3_TRIG_L
}
delay(10); // wait some
// read result
uint16_t u16temp, u16humd;
uint8_t u8temp_csum, u8humd_csum;
if (auto&& rdr = Wire.get_reader(0x70, 6)) {
rdr >> u16temp;
rdr >> u8temp_csum;
rdr >> u16humd;
rdr >> u8humd_csum;
}
// checksum 0x31, init=0xFF
if (CRC8_u8CalcU16(u16temp, 0xff) != u8temp_csum) {
Serial << format("{SHTC3 T CKSUM %x}", u8temp_csum); }
if (CRC8_u8CalcU16(u16humd, 0xff) != u8humd_csum) {
Serial << format("{SHTC3 H CKSUM %x}", u8humd_csum); }
// calc temp/humid (degC x 100, % x 100)
int16_t i16Temp = (int16_t)(-4500 + ((17500 * int32_t(u16temp)) >> 16));
int16_t i16Humd = (int16_t)((int32_t(u16humd) * 10000) >> 16);
Serial << "temp=" << int(i16Temp)
<< ",humid=" << int(i16Humd) << mwx::crlf;
SM_SIMPLE is provided to wait for processing such as state transitions, waiting for timeouts, and completion of transmission in the sample code.
The following is a basic code excerpt from SM_SIMPLE.
#include <SM_SIMPLE>
enum class STATE : uint8_t {
INIT = 0,
SENSOR,
TX,
TX_WAIT_COMP,
GO_SLEEP
};
SM_SIMPLE<STATE> step;
begin() {
...
step.init(); //initialize
}
loop() {
do {
switch(step.state()) {
case STATE::INIT:
...
step.next(STATE::SENSOR);
break;
case STATE::SENSOR:
...
step.next(STATE::TX);
break;
case STATE::TX:
if (/*success on tx request*/) {
step.set_timeout(100); // set timeout as 100ms
step.clear_flag();
step.next(STATE::TX_WAIT_COMP);
}
break;
case STATE::TX_WAIT_COMP:
if (step.is_timeout()) the_twelite.reset_system(); // is timeout?
if (step.is_flag_ready()) sleepNow(); // is set the flag?
break;
...
}
} while(step.b_more_loop());
}
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
void sleepNow() {
step.on_sleep(false); // reset state machine.
the_twelite.sleep(10000); // 10sec
}
To use SM_SIMPLE, you need to define an enum class
as a list of states. In the above, it is defined as STATE
. The class object is generated as SM_SIMPLE<STATE> step;
with this stage as a parameter. The generated class object should be initialized by .setup()
.
enum class STATE : uint8_t {
INIT = 0,
SENSOR,
TX,
TX_WAIT_COMP,
GO_SLEEP
};
SM_SIMPLE<STATE> step;
void setup() {
step.init();
}
The initial state of SM_SIMPLE has the value 0, corresponding to STATE::INIT
in the above example. To get the current state, use .state()
and use it as a judgment expression in the switch clause of the do while statement as in the above example.
loop() {
do {
switch(step.state()) {
case STATE::INIT: // State with value 0
...
Call .next()
for state transitions. If the state changes, b_more_loop()
is set to true
and the loop in the do while clause is executed again. In the example, calling .next(STATE::TX)
from the STATE::SENSOR
state will cause the loop to be executed again and the case STATE::TX:
clause will also be executed. If the state is not changed, the do while loop is escaped and the loop()
is terminated once. Wait until the next call to loop()
.
do {
switch(step.state()) {
...
case STATE::SENSOR:
...
step.next(STATE::TX); // (1)state transition
break;
case STATE::TX: // (3) Called in the second loop
if (/*success on tx request*/) {
...
}
} while (b_more_loop()); // (2) loop continue check
If you want to wait for processing such as completion of transmission, call .clear_flag()
, and then signal the completion of processing by .set_flag(uint32_t)
in another callback function or the like. Parameters of type uint32_t
specified here can be read from .get_flag_value()
.
If you want to process a timeout, you can record the time when you call .set_timeout(uint32_t)
and check if the timeout time has elapsed with .is_timeout()
.
case STATE::TX:
if (/*success on tx request*/) {
step.set_timeout(100); // set timeout
step.clear_flag();
step.next(STATE::TX_WAIT_COMP);
}
break;
case STATE::TX_WAIT_COMP:
if (step.is_timeout()) ...; // timeout
if (step.is_flag_ready()) ...; // is set the flag?
break;
...
// an event of tx completion
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus); // set the flag
}
SM_SIMPLE will be used again when returning from sleep, but it should always be called .on_sleep(bool)
before sleep. If you put false
in the parameter, it will start from the 0
state after recovery, and if you put true
, it will resume from the state just before sleep.
void sleepNow() {
step.on_sleep(false); // reset state machine.
the_twelite.sleep(10000); // 10sec
}
The following is the source code for SM_SIMPLE.
// very simple class to control state used in loop().
template <typename STATE>
class SM_SIMPLE {
uint32_t _u32_flag_value; // optional data when flag is set.
uint32_t _ms_start; // system time when start waiting.
uint32_t _ms_timeout; // timeout duration
STATE _step; // current state
STATE _step_prev; // previous state
bool_t _b_flag; // flag control.
public:
// init
void setup() { memset(this, 0, sizeof(SM_SIMPLE)); }
// call befoer sleeping (save state machine status)
void on_sleep(bool b_save_state = false) {
STATE save = _step;
setup();
if(b_save_state) _step = _step_prev = save;
}
// state control
void next(STATE next) { _step = next; } // set next state
STATE state() { return _step; } // state number
bool b_more_loop() { // if state is changed during the loop, set true
if (_step != _step_prev) { _step_prev = _step; return true; }
else return false;
}
// timeout control
void set_timeout(uint32_t timeout) {
_ms_start = millis();
_ms_timeout = timeout;
}
bool is_timeout() { return (millis() - _ms_start) >= _ms_timeout; }
// flag control
void clear_flag() { _b_flag = false; _u32_flag_value = 0; }
void set_flag(uint32_t u32_flag_value = 0) {
_b_flag = true;
_u32_flag_value = u32_flag_value; }
uint32_t get_flag_value() { return _u32_flag_value; }
bool is_flag_ready() { return _b_flag; }
};
Contents may change depending on the version.
The main body will be stored in SM_SIMPLE.hpp in the mwx library resource folder.
Other platforms
Build definitions are provided so that some features (serparser, pktparser, Serial object for console) can be built on other platforms. Only the necessary files are cut out.
The build definitions are stored in the {mwx library storage}/stdio folder. See README.md (link is on GitHub) for build instructions.
Must be able to compile in C++11.
Ability to use C++11 standard library headers (utility, algorithm, functional, iterator, etc.)
new/delete/virtual are not used.
Memory allocation by new may be used in exceptional cases.
In serparser/pktparser, alloc_heap which uses new operator is processed by delete.
(Reference) However, the mwx library has been designed on the assumption that delete is not taken into account.
is used to acquire sensor values.
This ACT includes
Sending and receiving wireless packets
Configuring settings via Interactive settings mode -
State transition control by state machine -
or board manipulation via board behavior
Uses the OPEN-CLOSE SENSE PAL to interrupt and wake up when the magnetic sensor is detected and transmit wirelessly.
Uses the sleep function to operate on coin cell batteries.
Board BEHAVIOR for open/close sensor pal is included.
First, register the board behavior <PAL_MAG>
. When the board behavior is initialized, the sensors and DIOs are initialized. The reason for doing this first is that it is common to check the status of the board's DIP SW, etc., and then configure the network settings and so on.
Here, 3 bits of the 4-bit DIP SW on the board are read and set as the ID of the Child Node; if 0, the Child Node without ID (0xFE
) is assumed.
Set the LED settings. Here the ON/OFF blinking is set to blink every 10ms (in an application that sleeps and has a short wake-up time, this is almost equivalent to setting the LED to turn on during wake-up).
The begin()
function exits the setup()
function (after which TWENET is initialized) and is called just before the first loop()
.
Call sleepNow()
after setup()
ends to perform the first sleep.
Before going to sleep, set the interrupt for the DIO pin of the magnetic sensor. Use pinMode()
, the second parameter is PIN_MODE::WAKE_FALLING
. This is a setting to wake up when the pin state changes from HIGH to LOW.
In line 7, the_twelite.sleep()
executes sleep. The parameter 60000 is the wake-up setting required to reset the watchdog on the TWELITE PAL board. If not reset, a hard reset will be performed after 60 seconds.
When the program wakes up from sleep, wakeup()
is called. After that, loop()
is called each time. Before wakeup()
, each peripheral such as UART and devices on the board are woken up (e.g. resetting the watchdog timer). For example, it restarts the LED lighting control.
Here, in the case of wake-up from the wake-up timer (the_twelite.is_wokeup_by_wktimer()
), sleep is performed again. This is a wake-up only for the purpose of resetting the watchdog timer described above.
In the case of wakeup upon detection of the magnetic sensor, the loop() process will continue.
Here, the DIO of the detected magnetic sensor is checked, packets are sent, and sleep is performed again after packet transmission is complete.
The b_transmit
variable controls the behavior within loop()
. After a successful transmission request, this value is set to 1, and the program waits for the packet transmission to complete.
Check the detection DIO pins of the magnetic sensor. There are two types of detection pins: N-pole detection and S-pole detection. If you simply want to know that a magnet is approaching, you need to know that one of the pins has been detected.
Use the_twelite.is_wokeup_by_dio()
to check the pin of the wakeup factor. The parameter is the pin number. The reason why the return value is stored in uint8_t is to store it in the payload of the packet.
After setting communication conditions and storing data in the payload, transmission is performed.
Then, if b_transmit
is true
during loop()
, a completion check is performed, and if completed, sleepNow()
puts the program to sleep.
The completion of transmission is confirmed by the_twelite.tx_status.is_complete(u8txid)
. The u8txid
is the ID value returned on transmission.
This is a container class with an array structure inside. The maximum size of the buffer is determined at initialization, and it behaves as a variable-length array up to that maximum size.
smplbuf
is a container class that provides array operations on memory areas specified by element type T
and . Since the specification of alloc
is complicated, alias definitions using using
are used.
Here is an example of object declaration. Immediately after the declaration, a method call is made for initialization. The maximum size of each object is 128 bytes immediately after initialization, and the size is 0. Use while extending the size as needed.
Alias classes are defined for uint8_t
type.
Elements can be accessed like normal arrays, using the [] operator, etc., and iterators can also be used to access elements.
The class also has push_back()
method, which enables some type of algorithm from standard C++ library.
Declares a container of type T
and size N
. After the declaration, call the initialization method.
smplbuf_local
allocates an area by a fixed array inside. It can also be initialized by the constructor.
In smplbuf_attach
, specify the first pointer T* buf
of the buffer to be used, the initial size size
and maximum size N
of the array. Initialization by the constructor is also possible.
The smplbuf_heap
allocates memory in the HEAP area (a memory area that cannot be released but can be allocated at any time). Once allocated, this area cannot be released, so it is usually defined as a global area. Allocation is done by init_heap()
. Memory allocation by the constructor is not allowed. Please call init_heap()
to use this function.
When creating a global object, initialization by the constructor is not possible. Please call initialization functions init_local()
,attach()
,init_heap()
at the beginning of execution (setup()
is recommended).
Initializer list { ... }
can be used to initialize members. Except for use in the constructor in a local declaration of smplbuf_local
, it is valid after calling the initialization method.
right side value of assignment operator (smplbuf_local
, smplbuf_attach
, smplbuf_heap
)
constructor (local declaration of smplbuf_local
, global declaration is not allowed)
Add a member c
at the end. The return value of append()
is bool
, which returns false
if the buffer is full and cannot be added.
The pop_back()
deletes the trailing entry. However, it does not clear the entry.
empty()
returns true
when no elements are stored in the array. is_end()
returns true
when the array is full.
size()
returns the number of array elements.
capacity()
returns the maximum number of elements in the array.
reserve()
extends the size of the array. The area where the array is not stored is initialized by default.
reserve_hear()
allocates an area of the specified size at the top of the array. This area cannot be referenced by the container object. For example, it can be used as a container to access a sub-array of a packet payload whose header part is skipped. To return the container to the allocated area so that all of it can be accessed again, give it the same negative value as when it was allocated.
redim()
resizes the allocated area. Unlike reserve()
, it does not initialize the unused area.
element.
A negative value for i
makes the element from the end of the buffer. For -1
, the element is the last element, and for -2
, it is one before the end.
Array objects of type uint8_t
(smplbuf<uint8_t, *>
) can be output as is to derived objects of mwx::stream
.
Outputs a sequence of bytes for mwx::stream
derived objects such as Serial
.
Used for output to stream purposes. Used to implement the << operator.
mwx::streamdefines functions and operators to output bytes to a stream, such as
<<operator and
printfmt()method. You can use the stream output procedure with a smplbuf array of type
uint8_t` as the output destination.
There are two methods.
Using helper object generated by .
Use the smplbuf class that .
Parent Node
MONOSTICK BLUE or RED Act Parent_MONOSTICK in action.
Child Node
#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_MAG>
void setup() {
/*** SETUP section */
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_MAG>();
// now it can read DIP sw status.
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
// LED setup (use periph_led_timer, which will re-start on wakeup() automatically)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PAL_MAG:" << FOURCHARS << " ---" << mwx::crlf;
}
auto&& brd = the_twelite.board.use<PAL_MAG>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
void begin() {
sleepNow(); // the first time is just sleeping.
}
void sleepNow() {
uint32_t u32ct = 60000;
pinMode(PAL_MAG::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(PAL_MAG::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);
the_twelite.sleep(u32ct);
}
void wakeup() {
if (the_twelite.is_wokeup_by_wktimer()) {
sleepNow();
}
}
void loop() {
if (!b_transmit) {
if (auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
Serial << "..sensor north=" << int(b_north)
<< " south=" << int(b_south) << mwx::crlf;
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet payload
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, b_north
, b_south
);
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
u8txid = ret.get_value() & 0xFF;
b_transmit = true;
}
else {
// fail to request
sleepNow();
}
} else {
sleepNow();
}
} else {
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
}
}
if (!b_transmit) {
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
// do transmit
MWX_APIRET ret = pkt.transmit();
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
template <typename T, int N> smplbuf_local
template <typename T> smplbuf_attach
template <typename T> smplbuf_heap
// Array area is a fixed array of class member variables
smplbuf_local<uint8_t, 128> b1;
// Refer to buffer memory that already exist
uint8_t buf[128];
smplbuf_attach<uint8_t> b2;
// Allocate buffer memory at heap
smplbuf_heap<uint8_t> b3;
// Initialize (must do initlialize at setup() for global objects)
void setup() {
b1.init_local();
b2.attach(buf, 0, 128);
b3.init_heap(128);
}
// In some function
void some_func() {
smplbuf_local<uint8_t, 128> bl;
// bl.init_local(); // It can omit if smplbuf_local is declared locally.
bl.push_back('a');
}
template <int N>
smplbuf_u8
// smplbuf<uint8_t, alloc_local<uint8_t, N>>
smplbuf_u8_attach
// smplbuf<uint8_t, alloc_attach<uint8_t>>
smplbuf_u8_heap
// smplbuf<uint8_t, alloc_heap<uint8_t>>
void begin() { // begin() runs only once at startup
smplbuf_u8<32> b1; // initially b1 behaves empty buffer (32bytes is allocated).
b1.reserve(5); // reserves 5bytes in b1.
b1[0] = 1;
b1[1] = 4;
b1[2] = 9;
b1[3] = 16;
b1[4] = 25;
for(uint8_t x : b1) { // can use ranged for.
Serial << int(x) << ",";
}
}
smplbuf_local<T,N>()
smplbuf_local<T,N>::init_local()
smplbuf_attach<T>(T* buf, uint16_t size, uint16_t N)
smplbuf_attach<T>::attach(T* buf, uint16_t size, uint16_t N)
smplbuf_heap<T>()
smplbuf_heap<T>::init_heap(uint16_t N)
// Example
// Allocated internally as fixed length array.
smplbuf_local<uint8_t, 128> b1;
b1.init_local();
// attach to an existing array.
uint8_t buf[128];
smplbuf_attach<uint8_t> b2;
b2.attach(buf, 0, 128);
// allocate array at heap area.
smplbuf_heap<uint8_t> b3;
b3.init_heap(128);
void in_some_func() {
smplbuf_local<uint8_t, 5> b1;
b1.init_local();
b1 = { 0, 1, 2, 3, 4 };
smplbuf_local<uint8_t, 5> b2{0, 1, 2, 3, 4};
}
inline bool append(T&& c)
inline bool append(const T& c)
inline void push_back(T&& c)
inline void push_back(const T& c)
inline void pop_back()
inline bool empty()
inline bool is_end()
inline uint16_t size()
inline uint16_t capacity()
inline bool reserve(uint16_t len)
inline void reserve_head(uint16_t len)
inline void redim(uint16_t len)
inline T& operator [] (int i)
inline T operator [] (int i) const
template <class L_STRM, class AL>
mwx::stream<L_STRM>& operator << (
mwx::stream<L_STRM>& lhs, mwx::_smplbuf<uint8_t, AL>& rhs)
//例
smplbuf_u8<128> buf;
buf.push_back('a');
buf.push_back('b');
buf.push_back('c');
Serial << buf;
// 出力: abc
inline std::pair<T*, T*> to_stream()
//Example
smplbuf_u8<128> buf;
buf.push_back('a');
buf.push_back('b');
buf.push_back('c');
Serial << buf.to_stream();
// Output: 0123
serial format input-output (mwx::serial_parser)
Used for serial format input/output. It has an internal buffer that holds the interpreted binary series. On input, the series is stored in the internal buffer one byte at a time according to the read format, and becomes complete when the interpretation of the series is complete. Conversely, on output, the buffer is output from the internal buffer according to the specified output format.
Three class names are defined according to the memory buffer handling method (alloc
).
// serparser_attach : Use existing buffers
serparser_attach
// serparser : Allocate N bytes of buffer internally
serparser_local<N>
// serparser_heap : Allocate buffers in the heap area
serparser_heap
The type of format to pass in the initialization parameter of begin()
. There are two types here: ASCII format and binary format.
uint8_t PARSER::ASCII = 1
ASCII format
uint8_t PARSER::BINARY = 2
binary format
Binary format is generally more complicated to handle than ASCII format, including the necessary tools and confirmation methods. Normally, ASCII format should be used.
The ASCII format is a way to represent a sequence of data in binary as a string.
For example, 00A01301FF123456
in ASCII format is expressed as follows. The beginning is :
, B1
is the checksum, and the end is [CR:0x0d][LF:0x0a]
.
:00A01301FF123456B1[CR][LF]
You can omit the terminating checksum. Replace the CRLF series from the checksum with X
. This is useful when you want to send data for experiments, etc., although it is less vulnerable to wrong data series due to garbled characters.
:00A01301FF123456X
header
1
Data part
N
2N
Each byte of the original data is represented by two ASCII characters (A-F in upper case).
For example, 0x1F is represented as 1
(0x31) F
(0x46).
checksum
2
The sum of each byte of the data part is calculated in 8-bit width and taken as 2's complement. In other words, the sum of each byte of the data part plus the checksum byte is calculated to be 0 in 8-bit width.
The checksum byte is represented by two ASCII characters.
For example, in 00A01301FF123456
, 0x00 + 0xA0 + ... + 0x56 = 0x4F, the two's complement of which is 0xB1. (i.e. 0x4F + 0xB1 = 0x00)
footer
2
[CR] (0x0D) [LF] (0x0A)
Normally, ASCII format should be used.
However, to check the transmission and reception in experiments, it is necessary to prepare a special terminal that supports binary communication, and checksum calculation is also required. It is more difficult to use than the ASCII format.
Binary format is a method of sending a sequence of data consisting of binary data with a header and a checksum.
For example, 00A01301FF123456
is expressed in binary format as follows.
0xA5 0x5A 0x80 0x08 0x00 0xA0 0x13 0x01 0xFF 0x12 0x34 0x56 0x3D
header
2
Data Length
2
The data length is two bytes in big-endian format, with MSB (0x8000) set and the length of the data part specified.
For example, if the length of the data part is 8 bytes, specify 0x80 0x08
.
Data part
N
N
Specifies the original data.
Checksum
1
Calculates the XOR of each byte of the data part.
For example, if the data part is 00A01301FF123456
, 0x00 xor 0xA0 xor ... 0x56 = 0x3D.
footer
(1)
The checksum is effectively the end of the line. The output from the radio module will be appended with 0x04
(EOT).
// serparser_attach : use existing buffer
serparser_attach p1;
uint8_t buff[128];
p1.begin(ARSER::ASCII, buff, 0, 128);
// serparser : allocate N bytes buffer inside
serparser p2<128>;
p2.begin(PARSER::ASCII);
// serparser_heap : allocate buffer in heap area
serparser_heap p3;
p3.begin(PARSER::ASCII, 128);
The declaration specifies the memory allocation class. Since this specification is complicated, an alias definition is used as described above.
Class name (alias definition) Memory Allocation
Contents
serparser_attach
specify an already existing buffer with begin()
serparser_local<N>
allocate a buffer of N bytes internally
serparser_heap
allocate a heap of the size specified by the parameters of the begin()
method
Calls the begin()
method according to the memory allocation class.
void begin(uint8_t ty, uint8_t *p, uint16_t siz, uint16_t max_siz)
The buffer specified by p
is used in the [format] (ser_parser.md#nitsuite) specified by ty
. The maximum length of the buffer is specified by max_siz
and the effective data length of the buffer by siz
.
This definition is used especially when you want to format output data columns (see >>
operator).
void begin(uint8_t ty)
Initialize with the format specified by ty
.
void begin(uint8_t ty, uint16_t siz)
Initialize the heap by allocating the size specified by siz
to the heap in the format specified by ty
.
Once allocated, heap space cannot be released.
BUFTYPE& get_buf()
Returns an internal buffer. The buffer will be of type smplbuf<uint8_t, alloc>
.
inline bool parse(uint8_t b)
Processes input characters. Receives a single byte of input string of formatted input and interprets it according to the format. For example, in ASCII format, it receives a series like :00112233X
as input. X, one byte at a time, and when the last
X` is entered, the interpretation of the format is completed and reported as done.
The parameter to parse()
is the input byte, and the return value is true
if the interpretation is complete.
while (Serial.available()) {
int c = Serial.read();
if (SerialParser.parse(c)) {
// Format interpretation complete, data sequence obtained in b (smplbuf<uint8_t>)
auto&& b = SerialParser.get_buf();
// The following is the processing of the resulting data sequence
if (b[0] == 0xcc) {
// ...
}
}
}
operator bool()
If true
, reading is completed by parse()
; if false
, interpretation is in progress.
while (Serial.available()) {
int c = Serial.read();
SerialParser.parse(c);
if(SerialParser) {
// Format interpretation complete, data sequence obtained in b (smplbuf<uint8_t>)
auto&& b = SerialParser.get_buf();
// ...
}
}
Outputs the internal buffer to the stream (Serial) in a formatted format.
uint8_t u8buf[] = { 0x11, 0x22, 0x33, 0xaa, 0xbb, 0xcc };
ser_parser pout;
pout.begin(ARSER::ASCII, u8buf, 6, 6); // Specify 6 bytes of u8buf
Serial << pout;// Format output to Serial -> :112233AABBCC??[CR][LF]
The following is a supplement to the terminology used in this document.
The explanation of terms may not be in accordance with the definitions provided in standards and other documents.
Software Development Environment
This is the radio standard used by the TWELITE radio module; as long as you use the MWX library, you do not need to be aware of the details of the radio standard.
The smallest unit of communication in wireless communications.
The maximum amount varies depending on the communication method and settings during communication, but in the MWX library standard communication <NWK_SIMPLE>, the amount of data a user can send in one packet is 90 bytes.
It refers to the body of data contained in a wireless packet.
It refers to a radio station in a wireless network.
A program created using this library. This refers to its source code or the program that runs.
A program in the form of an event, among other ACTs. The source code or the program that runs it.
BEHAVIORs are described by a single class definition, which describes callback functions from TWENET, events, and interrupt processing, all in one place. There are three types of behaviors available in the MWX library:
Application BEHAVIOR: A class defined by the user that describes the application in an event-driven manner.
Board BEHAVIOR: A class to simplify the use of the functionality of the board that implements the TWELITE radio module.
Network BEHAVIOR: A class for simplifying procedures in wireless networks.
Behavior names are enclosed in < >. For example, the behavior name for a simple relay network is <NWK_SIMPLE>.
In the description of this library, objects that are declared globally from the beginning in the library are referred to as class objects: Serial
, Wire
, etc. These class objects can be used without any procedure or by performing the start procedure.
Class objects that consume relatively large amounts of memory allocate memory along with the initialization parameters during the initialization procedure (.setup() or .begin() method).
The C++ language.
One of the versions of the C++ standard, meaning C++ as of 2011, which was standardized by ISO in 2011. It has been greatly enhanced since its predecessor, C++03. Newer versions such as C++14 and C++17 are available.
A collection of procedures that focus on some data in a single place. A structure contains the procedures for handling that structure. It actually develops into a much deeper topic, but please refer to technical books.
In C++, the keywords struct and class are essentially the same thing, and a class is a class regardless of which keyword it is declared with.
If the above class definition was also done in C, for example, it would look like the following:
It is a class that includes existing C libraries and their internal structures, and adds C++ specific functionality to make it easier to use. In some cases, the description "wrapped ~structure" is used in the explanation.
A function that is defined in a class and is associated with the class.
A class is materialized (allocated memory).
In this explanation, object and instance are treated as having the same meaning.
Initialization procedure at the time of object creation.
This is the procedure when the object is destroyed, paired with the constructor.struct myhello {
In C++, polymorphism is achieved by virtual classes. Specifically, a class that defines a pure virtual function specified by the virtual keyword.
The MWX library does not use virtual functions due to compiler limitations and performance reasons. We use a to achieve polymorphism.
In the C/C++ language, think of it as a scope enclosed in { }. The objects created in this scope are destroyed when they leave the scope. The destructor is called at this time.
The following is an explicitly scoped version of helo2, which is discarded after line 8 and the destructor is called.
MThe MWX library uses the following notation. Here, the validity period of an object declared within the conditional expression of an if statement (older C languages such as C89 do not allow declarations in such a place) is within {} of the if statement.
For example, a two-wire serial bus is a procedure where there is a start and end procedure and the bus is manipulated by an object only during that time. After the object is created, if the bus is properly connected, the true clause of the if statement is executed and the bus is written or read by the created object. When the bus read/write operation is completed, the if statement is exited and the destructor is called to terminate the bus usage.
The namespace are actively used in C++ to avoid duplication of definition names. To access a definition in a namespace, use::.
Think of a template as an extension of a C macro.
This example defines a simple array, where T and N are template parameters, where T is the type name and N is a number, and defines an array class of type T with N elements.
In C++11, NULL pointers are now written as nullptr.
In C++, reference types are available. This is similar to access by pointer, but with the restriction that it must refer to an object.
For functions with pass-by-reference parameters like the one below, the value of i
can be rewritten in incr()
.
In the example of the template explanation, the return type of operator[]
is changed to T&
. By doing so, it becomes possible to perform assignment operations directly on the data inside the array, such as a[0]=1
.
C++11 introduces the auto keyword for type inference. This allows the compiler to infer the type of an object from its initialization description, thus omitting the need to specify the specific type name. This is effective in cases where class names using template are very long.
In most of the explanations, auto&&, which is called universal reference, is used. Universal references can be written here without being aware of passing references.
A class for storing multiple objects of a specific data type such as arrays is called a container. An array class such as myary mentioned in the template example is also called a container.
A pointer in C can be thought of as a way to access a contiguous set of memory elements in a continuous manner from the beginning to the end. The simplest implementation of a FIFO queue is a ring buffer, but there is no memory continuity. Even such a data structure can be described in the same way as a pointer using an iterator.
The methods .begin()
and .end()
are used to get the iterator. The iterator that points to the beginning of the container is obtained with .begin()
. The iterator that points to the next to the end is obtained with .end()
. The reason for using the next to the end instead of the end is for the clarity of the loop description in the for and while statements, and to handle the case where the number of elements stored in the container is zero.
In the above, some_process()
is applied to each element of the que
using the iterator p
. p
is incremented by the ++
operator as an iterator that points to the next element. Even if the container has a data structure that cannot be described by a pointer, it can be processed in the same way as using a pointer.
Since .end()
indicates the next to the end, the end decision of the while statement is as simple as (p ! = e)
, which is concise. If there is no element in the queue, .begin()
returns the same iterator as .end(). If there are no elements in the queue, .begin()
will return the same iterator as .end(), which is the next iterator after the iterator for the unstored elements.
For a contiguous container in memory, its iterator will usually be a normal pointer. It is not expected to be a large overhead during its operation.
The C++ standard library includes the STL (Standard Template Library), which is part of the MWX library.
Due to the of the C/C++ compiler for TWELITE, only a few features are available.
In C, for example, the process of finding the maximum or minimum value is written separately depending on the type. In C++, you can use templates, iterators, etc. to describe such operations independently of their types. This is called an algorithm.
For example, the algorithm to find the maximum value as shown above. This algorithm is type-independent. (It is called generic programming.)
Here we specify an iterator for que and apply the algorithm std::minmax_elenet
to obtain its maximum and minimum. std::minmax_elemet
is defined in the C++ standard library. Its return value is std::pair
, which combines any two values. The algorithm calculates the maximum and minimum values if the elements indicated by the iterator can be compared with each other using operators such as <
,>
,==
. The return type is also derived from the iterator type.
Parent application (for MONOSTICK)
This act uses MONOSTICK as a parent device. It outputs the data payload of packets from the child machine to the serial port. It can display packets in many samples of sample acts.
This act includes
Receiving wireless packets
Data interpretation of received packets
Interactive mode settings -
Conversion of byte strings to ASCII format -
Receives packets from the child of the sample act and outputs them to the serial port.
Please check the following default settings at first.
Application ID: 0x1234abcd
Channel: 13
Include board behavior for . This board support includes LED control and watchdog support.
<NWK_SIMPLE> Loads the definition of a simple relay net
<STG_STD> Loads the interactive mode definition
Declaration of default values, function prototypes, etc.
In setup()
, first load the <MONOSTICK>
board behavior, the <STG_STD>
interactive mode behavior, and the <NWK_SIMPLE>
behavior using use<>
. This procedure is always done in setup()
.
The interactive mode is then set up and the settings are read out. The interactive mode provides standard items, but allows for some customization for each act you create.
appname
→ Act name that appears in the title line of the configuration screen
appid_default
→ Default Application ID
ch_default
→ Default channel
lid_default
→ Default value of device ID (LID)
.hide_items()
→ Item Hide Settings
Always call .reload()
before reading the configuration values. Each set value has its own method for reading, such as .u32opt1()
.
Some settings can be directly reflected using objects. In addition, if you want to rewrite a specific value due to a DIP switch setting, for example, you can rewrite the value separately after it is reflected. In the above example, the application ID, channel, radio output, etc. are set in object, the LID and the retransmission count are set in the object, and then the LID is set to 0 again.
Procedures for controlling LED lighting are available in the <MONOSTICK>
board behaviour.
The first line sets the red LED to switch on for 200 ms after receiving a radio packet. The first parameter LED_TIMER::ON_RX
means when a radio packet is received; the second specifies the lighting time in ms.
The second line specifies the blinking of the LEDs: the first parameter LED_TIMER::BLINK
specifies the blinking, the second parameter is the blinking on/off switching time: every 500ms the LEDs are switched on and off (i.e. Repeat blinking with a 1 s cycle).
Procedure for starting the_twelite
, which did not appear in act0..4, but must be called if you have configured the_twelite
or registered various behaviours.
There is no processing during loop()
in this sample.
Callback function called when a packet is received. In this example, some output is produced for the received packet data.
The analyze_payload()
called at the end of the function contains code to interpret some sample act packets. Refer to the code in correspondence with the packet generation part in the sample act.
This function first reads the four-character identification data into the fourchars[4]
array.
Reading is done using the function.The first and second parameters of this function follow the C++ standard library's practice of giving the first pointer .begin()
and the next .end()
of the payload section of the incoming packet. The following parameters are variable arguments, giving the data variables to be read. The return value is nullptr in case of an error, otherwise the next interpretation pointer. If interpreted to the end, .end()
is returned. The parameter here is uint8_t fourchars[4]
.
Processing is then carried out for the 4-byte header. Here, the packets of the sample act Slp_Wk_and_Tx are interpreted and the contents are displayed.
Set b_handled
to true so that the other interpreters' decisions are skipped.
"TXSP" packets contain the values of a system timer count of type uint32_t
and a dummy counter of type uint16_t
. Each variable is declared and read using the expand_bytes()
function. The difference from the above is that the first parameter is np
as the first pointer to read. The tick_ms
and u16work_ct
are given as parameters and the value stored in the payload is read as a big-endian format byte sequence.
If the readout is successful, the contents are output and the process is complete.
It is structured by ASCII format in a user-defined order.
The first line declares a buffer as a local object to store the data sequence before conversion to ASCII format.
The second line uses pack_bytes()
to store the data sequence into the buf mentioned earlier. See comments in the source code for the data structure. The pack_bytes()
parameter can also be a container of the form smplbuf_u8 (smplbuf<uint8_t, ALLOC>)
.
Lines 13, 14 and 17 are the declaration, configuration and output of the serial parser.
The first output (which is prevented from being executed by if(0)
) displays all data including the control data of <NWK_SIMPLE>
. There are 11 bytes of control data. Normally, the control information is not directly referenced.
The first line declares the serial parser for output as a local object. It does not have an internal buffer, but diverts an external buffer and uses the output function of the parser to output the byte sequence in the buffer in
The second line sets the buffer for the serial parser. It specifies an already existing data array, i.e. the payload part of the incoming packet. serparser_attach pout
declares the serial parser using an already existing buffer. The first parameter of pout.begin()
specifies the corresponding format of the parser as PARSER::ASCII
, i.e. ASCII format; the second specifies the first address of the buffer; the third specifies the The length of valid data in the buffer and the fourth specifies the maximum length of the buffer. The fourth parameter has the same value as the third, as it is for output and is not used for format interpretation.
Output to the serial port in line 6 using the >>
operator.
The Serial << mwx::flush
in line 7 is a specification to wait until the output of data that has not been output here is finished. (Serial.flush()
is the same process.)
struct myhello {
int _i;
void say_hello() { printf("hello %d\n", _i); }
};
typedef struct _c_myhello {
int _i;
void (*pf_say_hello)(struct _c_myhello *);
} c_myhello;
void say_hello(c_myhello*p) { p->pf_say_hello(); }
void init_c_my_hello(c_myhello*p) {
p->pf_say_hello = say_hello;
}
struct myhello {
int _i;
void say_hello() { printf("hello %d\n", _i); } //Method
};
void func() {
myhello obj_hello; // obj_hello is an object of myhello class
obj_hello._i = 10;
obj_hello.say_hello();
}
struct myhello {
int _i;
void say_hello() { printf("hello %d\n", _i); }
myhello(int i = 0) : _i(i) {} // constructor
};
void my_main() {
myhello helo(10); // Here, the constructor is called and set to _i=10
}
int _i;
void say_hello() { printf("hello! %d\n", _i); }
myhello(int i = 0) : _i(i) {} // constructor
~myhello() {
printf("good bye! %d\n", _i);
} // destructor
};
struct Base {
virtual void say_hello() = 0;
};
struct DeriveEng : public Base {
void say_hello() { printf("Hello!"); }
};
struct DeriveJpn : public Base {
void say_hello() { printf("Kontiwa!"); }
};
void my_main() {
myhello helo1(1);
helo1.say_hello();
{
myhello helo2(2);
helo2.say_hello();
}
}
// hello! 1
// hello! 2
// good bye! 2
// good bye! 1
struct myhello {
int _i;
void say_hello() { printf("hello! %d\n", _i); }
operator bool() { return true; } // Operators for judgment in if()
myhello(int i = 0) : _i(i) {} // constructor
~myhello() { printf("good bye! %d\n", _i); } // constructor
};
// Function to create a myhello object (generator)
myhello gen_greeting() { return my_hello(); }
void my_main() {
if (myhello x = gen_greeting()) {
// The object x in myhello is valid during the if statement
x.say_hello();
}
// object x is destroyed when exiting the if minute
}
const uint8_t DEV_ADDR = 0x70;
if (auto&& wrt = Wire.get_writer(DEV_ADDR)) { //バスの初期化、接続判定
wrt(SHTC3_TRIG_H); // 書き出し
wrt(SHTC3_TRIG_L);
} // バスの利用終了手続き
namespace MY_NAME { // 名Namespace declaration
const uint8_t MYVAL1 = 0x00;
}
...
void my_main() {
uint8_t i = MY_NAME::MYVAL1; // reference of MY_NAME
}
template <typename T, int N>
class myary {
T _buf[N];
public:
myary() : _buf{} {}
T operator [] (int i) { return _buf[i % N]; }
};
myary<int, 10> a1; // Array of type int with 10 elements
myary<char, 128> a2; // Array of char type with 128 elements
void incr(int& lhs, int rhs) { lhs += rhs; }
void my_main() {
int i = 10; j = 20;
incr(i, j);
}
template <typename T, int N>
class myary {
T _buf[N];
public:
myary() : _buf{} {}
T& operator [] (int i) { return _buf[i % N]; }
};
myary<int, 10> a1;
void my_main() {
a1[0] = 1;
a1[1] = 2;
}
auto&& p = std::make_pair("HELLO", 5);
// const char* と int のペア std::pair
my_queue que; // my_queue is a class of queue
auto&& p = que.begin();
auto&& e = que.end();
while(p != e) {
some_process(*p);
++p;
}
// Return the iterator with the maximum value with any iterator as a parameter.
template <class Iter>
Iter find_max(Iter b, Iter e) {
Iter m = b; ++b;
while(b != e) {
if (*b > *m) { m = b; }
++b;
}
return m;
}
#include <algorithm>
auto&& minmax = std::minmax_element( // Algorithm for obtaining the maximum minimum
que.begin(), que.end());
auto&& min_val = *minmax.first;
auto&& max_val = *minmax.second;
Parent Device
Child Device
Sample Act Child Setup (e.g. Slp_Wk_and_Tx
, PAL_AMB
, PAL_MAG
, PAL_MOT???, etc...
)
// use twelite mwx c++ template library
#include <TWELITE>
#include <MONOSTICK>
#include <NWK_SIMPLE>
#include <STG_STD>
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
/*** function prototype */
bool analyze_payload(packet_rx& rx);
auto&& brd = the_twelite.board.use<MONOSTICK>();
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
set << SETTINGS::appname("PARENT"); // Title in the settings screen
set << SETTINGS::appid_default(DEFAULT_APP_ID); // Application ID Default
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // channel default
set << SETTINGS::lid_default(0x00); //LID Default
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(); // Read settings from non-volatile memory
OPT_BITS = set.u32opt1(); // Example of reading (option bits)
the_twelite
<< set // Reflects interactive mode settings
<< TWENET::rx_when_idle() // Set RF to receive when idle.
;
// Register Network
nwk << set; // Reflects interactive mode settings
nwk << NWK_SIMPLE::logical_id(0x00) // Only the LID is reconfigured.
;
brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)
the_twelite.begin(); // start twelite!
void loop() {
}
void on_rx_packet(packet_rx& rx, bool_t &handled) {
Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;
...
// packet analyze
analyze_payload(rx);
}
bool b_handled = false;
uint8_t fourchars[4]{};
auto&& np = expand_bytes(
rx.get_payload().begin(), rx.get_payload().end()
, fourchars
);
if (np == nullptr) return;
// display fourchars at first
Serial
<< fourchars
<< format("(ID=%d/LQ=%d)", rx.get_addr_src_lid(), rx.get_lqi())
<< "-> ";
char fourchars[5]{}; // Allocate 5 bytes including the terminating character `\0`.
auto&& np = expand_bytes(
rx.get_payload().begin(), rx.get_payload().end()
, make_pair((char *)fourchars, 4)
);
// Slp_Wk_and_Tx
if (!b_handled && !strncmp(fourchars, "TXSP", 4)) {
b_handled = true;
uint32_t tick_ms;
uint16_t u16work_ct;
np = expand_bytes(np, rx.get_payload().end()
, tick_ms
, u16work_ct
);
if (np != nullptr) {
Serial << format("Tick=%d WkCt=%d", tick_ms, u16work_ct);
} else {
Serial << ".. error ..";
}
}
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid() // Logical ID of the sender
, uint8_t(0xCC) // 0xCC
, uint8_t(rx.get_psRxDataApp()->u8Seq) // Sequence number of the packet
, uint32_t(rx.get_addr_src_long()) // Serial number of the sender.
, uint32_t(rx.get_addr_dst()) // destination address
, uint8_t(rx.get_lqi()) // LQI:Link Quality Indicator
, uint16_t(rx.get_length()) // Number of following bytes
, rx.get_payload() // data payload
);
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << "FMT PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
serparser_attach pout;
pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData,
rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len);
Serial << "RAW PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
// Reference: packet structure of the control unit.
// uint8_t : 0x01 fixws
// uint8_t : LID of sender
// uint32_t : Long address (Serial Number) of sender
// uint32_t : Destination address
// uint8_t : Number of repeating
The core class object of TWENET (mwx::twenet)
The the_twelite
object summarizes the procedures for using TWENET and includes procedures for operating the wireless microcontroller, such as basic wireless settings, sleep, and other procedures.
the_twelite
is set up in the setup()
function, which also calls the_twelite.begin()
to start the process. It is not possible to set up the_twelite
except in setup()
.
void setup() {
...
the_twelite
<< TWENET::appid(APP_ID)
<< TWENET::channel(CHANNEL)
<< TWENET::rx_when_idle();
...
the_twelite.begin();
}
In the above example, the application ID is set, the communication channel is set, and the receiving circuit is set.
Various procedures are included.
// Get a serial number
uint32_t u32hwser = the_twelite.get_hw_serial();
// Set channel to 11
the_twelite.change_channel(11);
// Sleep for 1 second.
the_twelite.sleep(1000);
// Perform a reset
the_twelite.reset_system();
It is also possible to register classes that handle wireless networks, classes that summarize board support, and classes that perform event-driven processing described by the user. By registering these classes, dedicated functions can be easily used. These classes are referred to as "behaviors" in this explanation.
void setup() {
/*** SETUP section */
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_AMB>();
...
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID);
In the above example, two types of behaviors are registered: environmental sensor pal behavior <PAL_AMB>
and simple relay network <NWK_SIMPLE>
. By registering these, hardware such as sensors on the environmental sensor pal can be easily handled. In addition, the complicated handling of wireless packets can be implicitly provided with functions such as relay processing and automatic discarding of duplicate packet arrivals.
The MWX library defines other methods in addition to those introduced here.
Act descriptions include those that are not directly related to the act description, those that are set up but do not function effectively, and those that are used internally.
<<operator
(setting)The <<
operator is used to initialize the object the_twelite
.
The following configuration class objects are used as input, and default values are applied if no configuration is made.
Set the parameter id
to the specified application ID. This is a required specification.
Reading the settings is done with uint32_t the_twelite.get_appid()
.
Set the specified channel number (11
..26
) in parameter ch
.
Reading the settings is done with uint8_t
the_twelite.get_channel()
.
Parameter pw
is set to (0
. 3
) for the radio output. The default is (3: no output attenuation).
Reading the settings is done with uint8_t the_twelite.get_tx_power()
.
If the parameter bEnable
is 1
, the receiver circuit is always activated and can receive radio packets from others. The default is 0
, which is dedicated to transmission only.
Reading the settings is done with uint8_t the_twelite.get_rx_when_idle()
.
Enables the channel manager. If multiple channels are specified, sending/receiving is performed on multiple channels; if 0 is specified for ch2 and ch3, the specification is disabled.
Reflects the setting values in interactive mode.
auto&& set = the_twelite.settings.use<STG_STD>();
...
set.reload(); // Load settings
the_twelite << set; // Reflects interactive mode settings
The items that will be reflected are as follows
app_id
channel
tx_power
Number of retransmissions when MAC ack is used
There are other settings in the MWX library code that are irrelevant to the library's functionality at this time or that may cause conflicts if set.
void begin()
It is executed after preconfiguration (see <<
operator) and registration of the behavior. It is usually placed at the end of the setup()
function.
the_twelite
finished setting up
Behavior initialization
TWENET initialization is also performed after the setup()
function exits. Since many processes are to be executed after setup()
exits, do not do anything other than initialization here.
void setup() {
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_AMB>();
// settings
the_twelite
<< TWENET::appid(APP_ID)
<< TWENET::channel(CHANNEL)
<< TWENET::rx_when_idle();
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID);
// somo others
// begin the TWENET!
the_twelite.begin();
}
inline bool change_channel(uint8_t u8Channel)
Changes channel settings. On failure, the channel is not changed and false
is returned.
uint8_t get_channel_phys()
Obtains the currently set channel number (11..26) from the MAC layer API.
inline uint32_t get_hw_serial()
Get the module serial number.
inline void sleep(
uint32_t u32Periodms,
bool_t bPeriodic = true,
bool_t bRamOff = false,
uint8_t u8Device = TWENET::SLEEP_WAKETIMER_PRIMARY)
Put the module to sleep.
u32Periodms
Sleep duration[ms]
bPeriodic
Recalculate the next wake-up time based on the previous wake-up time. ※It may be from the current timing for reasons such as the next wake-up timing is imminent.
bRamoff
Set to true
to sleep without retaining RAM (must be reinitialized from setup()
instead of wakeup()
after wakeup)
u8Device
Designation of a wake-up timer to be used for sleep. Specify
TWENET::SLEEP_WAKETIMER_PRIMARY
orTWENET::SLEEP_WAKETIMER_SECONDARY
.
bool is_wokeup_by_dio(uint8_t port)
Returns true
if the return factor from sleep is the specified digital pin.
bool is_wokeup_by_wktimer()
Returns true
if the wake-up timer is the wake-up factor for returning from sleep.
inline void reset_system()
Resets the system. After reset, the process starts from setup()
.
inline void stop_watchdog()
Stops the watchdog timer. Stop the timer if you are going to wait for polling for a long time.
inline void restart_watchdog()
Restart the watchdog timer.
Three behaviors can be registered with the_twelite
, and the following class objects are defined to store them.
network
: A behavior that implements a network. Normally, <NWK_SIMPLE>
is registered.
network2
: BEHAVIOR that implements networking. Use this behavior if you want another network BEHAVIOR to process packets which were not accepted by the first network
due to the data structure of the payload or other reasons. (Reference: Using NWK_LAYERED and NWK_SIMPLE together)
board
: Behaviors for board support. Procedures for using each device on the board are added.
app
: Behaviors describing user applications. Behaviors can be written in terms of interrupts, events, and state transitions using state machines. It is also easy to define multiple application descriptions and select an application with completely different behavior at startup.
settings
: Behavior for performing configuration (interactive mode). Registers <STG_STD>
.
// example
auto&& brd = the_twelite.board.use<PAL_AMB>();
Register behavior . Registration is done within setup()
. The return value is a reference to the object corresponding to the registered behavior.
After registration, the object is retrieved in the same way as at the time of registration.
If an incorrect behavior is specified, a panic operation (infinite loop) occurs and program operation stops.
the_twelite
defines the three class objects board
, network
, and app
mentioned above, but also the following
Notification of transmission completion status.
bool is_complete(uint8_t cbid)
Returns true
when the packet with the specified ID has completed transmission.
bool is_success(uint8_t cbid)
Returns true
when the packet with the specified ID has completed transmission and has been successfully sent.
Retrieve incoming packets.
The received packet data obtained by the read()
method is designed to be overwritten when subsequent packets are received and processed. If you read the data immediately after available()
and do some short processing, this will not be a problem, but as a general rule, read the data, copy the data needed for application use, and finish loop()
promptly. For example, a long delay()
during loop()
will cause incoming packets to be dropped.
bool available()
Returns true
if there is an incoming packet that has not yet been read.
packet_rx& read()
Read packets.
MOTION SENSE PAL is used to acquire sensor values.
The ACT of the ACT includes the following.
Sending and receiving wireless packets
Interactive settings mode configuration - <STG_STD>
State transition control by state machine - <SM_SIMPLE>
Uses the motion sensor PAL MOTION SENSE PAL to continuously measure the acceleration of the accelerometer and transmit it wirelessly.
Use the sleep function, to operate on coin cell batteries.
Parent Node
Act to work.
Child Node
+
#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_>
Board BEHAVIOR <PAL_MOT>
is included.
void setup() {
/*** SETUP section */
// board
auto&& brd = the_twelite.board.use<PAL_MOT>();
brd.set_led(LED_TIMER::BLINK, 100);
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID)
<< TWENET::channel(CHANNEL);
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE);
/*** BEGIN section */
the_twelite.begin(); // start twelite!
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
/*** INIT message */
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " ---" << mwx::crlf;
}
First, register the board BEHAVIOR <PAL_MOT>
. When the board BEHAVIOR is initialized, the sensors and DIOs are initialized. The reason for doing this first is that it is common to check the status of the board's DIP SW, etc., and then configure the network settings, etc.
auto&& brd = the_twelite.board.use<PAL_MOT>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
Here, 3 bits of the 4-bit DIP SW on the board are read and set as the Child Node ID. 0 means no ID (0xFE
).
Set the LED settings. Here the ON/OFF blinking is set to blink every 10ms (in an application that sleeps and has a short wake-up time, this is almost equivalent to setting the LED to turn on during wake-up).
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
Starts accelerometer measurement. The accelerometer settings (SnsMC3630::Settings
) specify the measurement frequency and measurement range. Here we use 14HZ measurement (SnsMC3630::MODE_LP_14HZ
) with ±4G range (SnsMC3630::RANGE_PLUS_MINUS_4G
).
Once started, the accelerometer takes 14 measurements per second and the values are stored in a FIFO queue inside the sensor. The sensor is notified when 28 measurements have been taken.
The begin()
function exits the setup()
function (after which TWENET is initialized) and is called just before the first loop()
.
void begin() {
sleepNow(); // the first time is just sleeping.
}
Call sleepNow()
after setup()
ends to perform the first sleep.
void sleepNow() {
pinMode(PAL_MOT::PIN_SNS_INT, WAKE_FALLING);
the_twelite.sleep(60000, false);
}
Before going to sleep, set up an interrupt for the accelerometer's DIO pin, which is generated when the FIFO queue reaches a certain number. The second parameter is PIN_MODE::WAKE_FALLING
. This is a setting to wake up when the pin state changes from HIGH to LOW.
The third line executes sleep with the_twelite.sleep()
. The parameter 60000 is the wake-up setting required to reset the watchdog on the TWELITE PAL board. If not reset, a hard reset will be performed after 60 seconds.
When the accelerometer wakes up from sleep due to a FIFO interrupt from the accelerometer, wakeup()
is called. After that, loop()
is called each time. Before wakeup()
, each peripheral such as UART and devices on the board are woken up (e.g. resetting the watchdog timer). For example, it restarts the LED lighting control.
void wakeup() {
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " wake up ---" << mwx::crlf;
b_transmit = false;
txid[0] = 0xFFFF;
txid[1] = 0xFFFF;
}
Here we are initializing variables to be used in loop()
.
Here, the acceleration information stored in the FIFO queue in the accelerometer is retrieved and packet transmission is performed based on this information. After the packet transmission is completed, sleep is executed again.
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
if (!b_transmit) {
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
// send a packet
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().back().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
for (int ip = 0; ip < 2; ip++) {
if(auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
// store sensor data (36bits into 5byts, alas 4bits are not used...)
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
// perform transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
}
}
// finished tx request
b_transmit = true;
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
}
The b_transmit
variable controls the behavior within loop()
. After a successful transmission request, this value is set to 1, and the program waits for the packet transmission to complete.
if (!b_transmit) {
First check to see if the sensor is available. Since it is after an interrupted wake-up, it is not normal for it not to be AVAILABLE, and it will go straight to sleep.
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
It is not used in the wireless transmission packet, but we will check the information on the acceleration taken out.
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().front().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
The measurement results of the accelerometer are stored in a FIFO queue obtained by brd.sns_MC3630.get_que()
.
The structure axis_xyzt
that stores the measurement results of the accelerometer contains the information of three axes (x, y, z) and the sequence number t. The number of samples stored is the size of the queue.
The number of samples stored can be checked by reading the queue size (brd.sns_MC3630.get_que().size()
). Normally 28 samples, but it may go a little further due to processing delays, etc. The first sample is taken from front()'. The first sample can be obtained by
front(). Its sequential number is
front().t`.
Here we will take the average of the samples before taking them out of the queue. Each element of the queue can be accessed with a for statement (for (auto&& v: brd.sns_MC3630.get_que()) { ... }
), where v.x, v.y, v.z
in the for statement is each element. Here, the sum of each element is calculated, and after the for statement, the average is calculated by dividing by the number of elements.
Next, a packet is generated and a request for transmission is made, but because the data volume is large, the packet is divided into two and transmitted twice. Therefore, the sending process is performed twice in the for statement.
for (int ip = 0; ip < 2; ip++) {
The number of samples to be included in the packet to be sent and the sample first sequence number are stored in the first part of the packet payload.
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
Finally, the acceleration data is stored. While earlier we only referenced each element of the queue to calculate the average value, here we read one sample at a time from the queue and store it in the payload of the packet.
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
Use .front()
to read the head of the data queue from the accelerometer. After reading, .pop()
is used to release the top of the queue.
The data acquired from the accelerometer is in milli-G units, where 1G is 1000. Since the range is set to ±4G, the data is divided by 2 to be stored within a 12-bit range. To save the number of data, the first 4 bytes store the upper 8 bits of the X, Y and Z axes, and the next 1 byte stores the lower 4 bits of the Z axis, generating a total of 5 bytes.
The transmission ID is stored in the txid[]
array to wait for two transmissions.
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
Then, if b_transmit
is true
during loop()
, a completion check is performed, and if completed, sleepNow()
puts the program to sleep.
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
The completion of transmission is confirmed by the_twelite.tx_status.is_complete()
The .txid[]
is the ID value returned on transmission.
is used to acquire sensor values.
[BEHAVIOR] (./) is used to describe the Parent Node Child Node.
The sensor is described directly using instead of using the function to obtain values.
Child Nodes are described by a state machine.
See , , and before the explanation of this ACT. Also see .
Uses the environmental sensor PAL AMBIENT SENSE PAL to acquire sensor values.
Use the sleep function to operate with coin cell batteries.
When using PAL as the Parent Node, coin cell batteries cannot be used. As a rule of thumb, prepare a power supply environment that can provide a stable current of 50 mA or more.
PAL_AMB-behavior.hpp : Only setup()
is defined. read DIP-SW and if D1..D3 is upper position, it works as Parent Node, otherwise it sets ID corresponding to DIP SW as Child Node.
Parent/myAppBhvParent.hpp : behavior class definition for Parent Node
Parent/myAppBhvParent.cpp : implementation
Parent/myAppBhvParent-handlers.cpp : implementation of handlers
Parent/myAppBhvParent.hpp : behavior class definition for Child Nodes
Parent/myAppBhvParent.cpp : implementation
Parent/myAppBhvParent-handlers.cpp : implementation of handlers
The Parent Node's BEHAVIOR name is <MY_APP_PARENT>
and the Child Node is <MY_APP_CHILD>
.
If the DIP SW reading is 0, register the behavior <MY_APP_PARENT>
for the Parent Node, otherwise register the behavior <MY_APP_CHILD>
for the Child Node.
If the Parent Node is MONOSTICK, the DIP SW for PAL reads 0 and behaves as the Parent Node. However, this behavior is not defined in the MONOSTICK specifications.
The Parent Node behaves as a non-sleeping receiver and outputs packet information to the serial port when it receives a packet from a Child Node.
When a packet is received for the Parent Node, if the first four characters of the packet can be matched (FOURCHARS
), the contents of the packet are displayed.
The Parent Node's interrupt handler blinks the LED.
When the button (5) on the PAL is pressed, the E_ORDER_KICK
event is issued to the state machine.
The state machine is described as a reference for state transitions and is not meaningful for the operation of the application. It executes state transitions by the E_ORDER_KICK event sent from the button, timeouts, and so on.
The behavior flow of the Child Node is the same as that of the PAL_AMB-usenap. From the initial sleep, "wake up → start sensor operation → short sleep → wake up → acquire sensor value → wireless transmission → wait for wireless transmission completion → sleep" is repeated.
The _begin()
function, called from on_begin()
, executes the first sleep.
(*It is acceptable to describe this process directly in on_begin()
without describing it in _begin()
function.)
This is a description of the process of waking up from sleep.
The first time Wire.begin()
is executed here, which is redundant for the second and later times when the device wakes from sleep. This process can be moved to on_begin()
.
Processes E_ORDER_KICK
messages to the state machine upon completion of transmission.
Defines the state name.
This is an example of sensor acquisition implementation for SHTC3. For details on sending commands, etc., refer to the SHTC3 datasheet.
This is an example of LTR308ALS sensor acquisition implementation. Please refer to the LTR308ALS datasheet for details on sending commands, etc.
WireWriteAndGet()
sends 1 byte of cmd
to the device of addr
, then receives 1 byte and returns the value.
State 0 has a special meaning. It is the state immediately after startup or after returning from sleep.
Immediately after startup PEV_is_coldboot(ev,evarg)
judgment becomes true
and is called. Since it goes straight to sleep from on_begin()
, it does not contain any code that transitions the state. **At this point, the major initialization has not yet been completed, so complex processing such as sending wireless packets cannot be performed. **In order to perform the first state transition for such processing, send an event from on_begin()
and perform the state transition according to that event.
After returning from sleep, there is a first call to PEV_is_warmboot(ev,evarg)
which will be true
. A call to PEV_SetState()
will transition to the STATE_SENSOR
state.
When the transition is made from STATE_IDLE
after returning from sleep, the state handler for STATE_SENSOR
is called continuously. The event ev
at this time is E_EVENT_NEW_STATE
.
Here, the operation of two sensors, SHTC3 and LTR308ALS, is started. After a certain period of time, the sensors will be ready to acquire data. This time wait is done with the sleep setting of 66
ms. Note that PEV_KeepStateOnWakeup()
is called before sleep. After this call, the state after returning from sleep is not STATE_IDLE
, but the state it was in when it went to sleep, i.e. STATE_SENSOR
.
When returning from a short sleep, a call is first made with the PEV_is_warmboot(ev,evarg)
decision set to true
. At the time of this call, wireless packets can be sent, etc. Transition to STATE_TX
.
Here, at the time of the E_EVENT_NEW_STATE
event, the sensor data reading and wireless packet transmission procedures are started. Please refer to other act sample examples for details of the transmission procedure.
Unlike the ACT description in the loop, the process of waiting for the message by PEV_Process()
from transmit_complete()
is used as a confirmation of completion. Sleep is performed upon receipt of the message. Sleep processing is done by transitioning to STATE_SLEEP
.
Finally, timeout processing is performed. This is in case the completion message of the sent packet has not been returned. PEV_u32Elaspsed_ms()
returns the elapsed time since the transition to that state in milliseconds [ms]. If the time has elapsed, the above will perform a system reset the_twelite.reset_system()
(assuming this timeout is too much).
Perfom sleep. Describe it in E_EVENT_NEW_STATE
immediately after the transition from the previous state. Since other events may be called just before sleep, be sure to execute the_twelite.sleep()
in a decision expression that is executed only once.
BEHAVIOR can be defined by defining the class in the specified way, so that class object. The registered BEHAVIOR will be embedded in TWENET, allowing the user code to describe the application's behavior. It is possible to define callback functions for interrupts and events from TWENET, which is not possible in a loop description. Although it requires more definitions than a loop description, it is suitable for describing more complex applications.
See sample BEHAVIOR .
BEHAVIOR definition requires a class definition as shown below.
The above example defines a BEHAVIOR class with the name MY_APP_CLASS. MY_APP_CLASS must be described in several places.
Define the class name and the base (parent) class. MWX_APPDEFS_CRTP()
is a macro.
Here, the necessary definitions are imported by #include
.
Constructor definition.
This is the main loop and has the same role as loop()
in the global definition.
on_create()
is called at object creation time (use<>()
method). The val
is a parameter for future extension.
on_begin()is called after
setup()ends.
val` is a parameter for future extension.
Called before sleep. val
is a parameter for future extension.
Called in the initial stage when returning from sleep. The val
is a parameter for future expansion.
At this point, the peripherals have not yet been initialized. The sleep wake-up factor can be checked.
Called when waking up from sleep. The val
is a parameter for future extension.
When a packet is received, it is called with the received packet information as rx
.
The transmission information is called as evTx
when packet transmission is completed. The evTx.u8CbId
is the ID at the time of transmission and evTx.bStatus
is the flag indicating success (1
) or failure (0
) of the transmission.
BEHAVIOR handlers (interrupt, event, and state definitions) are defined in a cpp file. The file cannot be split and all handler definitions must be in one file.
Even in the case of BEHAVIORs that do not define handlers, be sure to create the following cpp file.
The required definitions of the MWX library (#include "_mwx_cbs_cpphead.hpp"
) must be included at the beginning and end of the cpp file.
At the beginning of the file, include the .hpp file of the BEHAVIOR definition as shown above. Specify the class name of the behavior in __MWX_APP_CLASS_NAME
. In the above, it is MY_APP_CLASS
.
At the end of the file, include the necessary definitions (#include "_mwx_cbs_cpptail.cpp"
).
The handler definition is written as shown in the following example. Types of definitions are described later. The definition of the handler to be used is described by using the macro for definition. Do not write handlers that are not used.
The MWX_????? _INT()
is the definition of an interrupt handler, and MWX_? _EVENT()
is the definition of an event handler, and MWX_STATE()
is the state definition of a state machine.
Interrupt handlers are executed when a microcontroller interrupt occurs, interrupting the code currently being executed. For this reason, it is desirable to write as short a process as possible, and great care must also be taken with regard to manipulation of variables and the like.
The interrupt handler has a parameter uint8_t& handled
, and setting this value to true
will prevent subsequent event calls from being made.
If the interrupt handler exits with handled
set to false
, the event handler will be called in the application loop (normal code). The event handler has no handled
parameter. Since the event handler is normal code, it can perform relatively large processing. However, the event handler also incurs overhead, so it may not be able to handle the processing that is called at each frequent interrupt. In addition, since events are processed by the system's internal FIFO queue, events may be lost if they cannot be processed within a certain period of time.
The following is an explanation of macros for defining handler functions.
DIO (digital IO) interrupt event. N
specifies the target DIO number. The arg
is a definition for future extension.
To generate an interrupt, use , .
TickTimer interrupt and event. The arg
is a definition for future extension.
The handled
flag of the TickTimer must not be set to true
, otherwise TWENET will not work.
Timer interrupt event. TheN
specifies the number of the target timer. The arg
is a definition for future extension.
In order to generate an interrupt, the is started with software interrupts enabled.
Definition of other interrupts and events that are not defined standardly in the MWX library and require an understanding of the AHI Peripherals Manual.
Other interrupt events can be received by the following handler functions. These will not be available in the future when dedicated handlers are defined.
Peripheral (AHI) interrupt handler u32DeviceId
corresponds to arg
and u32ItemBitmap
corresponds to arg2
.
A state machine (state machine) is a method of describing an application that receives messages and operates by transitioning its state in response to those messages.
The sample describes the flow of the application's operation, including the start of sensor operation, acquisition of sensor values, wireless packet transmission to completion of transmission, and sleep transition. Please refer to it as an actual example.
The events to be received are as follows.
The state is set to s
.
Exiting the state handler causes a transition to the next state, followed by a state handler being called with the E_EVENTS_NEW_STATE
event.
Returns the elapsed time ≪ ms] since the state transition. It is used for purposes such as managing timeouts.
In the above example, a system reset is performed after 100 ms.
Called from outside the state handler. Execute the state handler with the event ev
parameter u32evarg
.
The transmission completion event is communicated to the state machine. In other words, call the state handler.
Do not call the state handler directly. It will cause problems such as E_EVENT_NEW_STATE
not being executed.
Set just before sleep. After returning from sleep, the previous state is maintained. That is, the state handler is called with E_EVENT_START_UP
with sleep started.
Determine if the event is E_EVENT_START_UP
on wake-up.
Judges whether the event is E_EVENT_START_UP
when returning from sleep.
Parent Node
Child Node
// now read DIP sw status can be read.
u8ID = (brd.get_DIPSW_BM() & 0x07);
// Register App Behavior (set differnt Application by DIP SW settings)
if (u8ID == 0) {
// put settings to the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open RX channel
the_twelite.app.use<MY_APP_PARENT>();
} else {
// put settings to the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
the_twelite.app.use<MY_APP_CHILD>();
}
void MY_APP_PARENT::receive(mwx::packet_rx& rx) {
uint8_t msg[4];
uint32_t lumi;
uint16_t u16temp, u16humid;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end(), msg);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, (const char*)FOURCHARS, 4)) {
// get rest of data
expand_bytes(np, rx.get_payload().end(), lumi, u16temp, u16humid);
// print them
Serial << format("Packet(%x:%d/lq=%d/sq=%d): ",
rx.get_addr_src_long(), rx.get_addr_src_lid(),
rx.get_lqi(), rx.get_psRxDataApp()->u8Seq)
<< "temp=" << double(int16_t(u16temp)/100.0)
<< "C humid=" << double(int16_t(u16humid)/100.0)
<< "% lumi=" << int(lumi)
<< mwx::crlf << mwx::flush;
}
}
MWX_TICKTIMER_INT(uint32_t arg, uint8_t& handled) {
// blink LED
digitalWrite(PAL_AMB::PIN_LED,
((millis() >> 9) & 1) ? PIN_STATE::HIGH : PIN_STATE::LOW);
}
MWX_DIO_EVENT(PAL_AMB::PIN_BTN, uint32_t arg) {
Serial << "Button Pressed" << mwx::crlf;
static uint32_t u32tick_last;
uint32_t tick = millis();
if (tick - u32tick_last > 100) {
PEV_Process(E_ORDER_KICK, 0UL);
}
u32tick_last = tick;
}
void _begin() {
// sleep immediately.
Serial << "..go into first sleep (1000ms)" << mwx::flush;
the_twelite.sleep(1000);
}
void wakeup(uint32_t & val) {
Serial << mwx::crlf << "..wakeup" << mwx::crlf;
// init wire device.
Wire.begin();
// turn on LED
digitalWrite(PAL_AMB::PIN_LED, PIN_STATE::LOW);
// KICK it!
PEV_Process(E_ORDER_KICK, 0); // pass the event to state machine
}
void transmit_complete(mwx::packet_ev_tx& txev) {
Serial << "..txcomp=" << int(txev.u8CbId) << mwx::crlf;
PEV_Process(E_ORDER_KICK, txev.u8CbId); // pass the event to state machine
}
static const uint8_t STATE_IDLE = E_MWX::STATE_0;
static const uint8_t STATE_SENSOR = E_MWX::STATE_1;
static const uint8_t STATE_TX = E_MWX::STATE_2;
static const uint8_t STATE_SLEEP = E_MWX::STATE_3;
MWX_APIRET MY_APP_CHILD::shtc3_start()
MWX_APIRET MY_APP_CHILD::shtc3_read()
MWX_APIRET MY_APP_CHILD::ltr308als_read()
MWX_APIRET MY_APP_CHILD::ltr308als_start()
static MWX_APIRET WireWriteAngGet(uint8_t addr, uint8_t cmd)
MWX_STATE(MY_APP_CHILD::STATE_IDLE, uint32_t ev, uint32_t evarg) {
if (PEV_is_coldboot(ev,evarg)) {
Serial << "[STATE_IDLE:START_UP(" << int(evarg) << ")]" << mwx::crlf;
// then perform the first sleep at on_begin().
} else
if (PEV_is_warmboot(ev,evarg)) {
Serial << "[STATE_IDLE:START_UP(" << int(evarg) << ")]" << mwx::crlf;
PEV_SetState(STATE_SENSOR);
}
}
MWX_STATE(MY_APP_CHILD::STATE_SENSOR, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_NEW_STATE) {
Serial << "[STATE_SENSOR:NEW] Start Sensor." << mwx::crlf;
// start sensor capture
shtc3_start();
ltr308als_start();
// take a nap waiting finish of capture.
Serial << "..nap for 66ms" << mwx::crlf;
Serial.flush();
PEV_KeepStateOnWakeup(); // stay this state on waking up.
the_twelite.sleep(66, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
} else
if (PEV_is_warmboot(ev,evarg)) {
// on wakeup, code starts here.
Serial << "[STATE_SENSOR:START_UP] Wakeup." << mwx::crlf;
PEV_SetState(STATE_TX);
}
}
MWX_STATE(MY_APP_CHILD::STATE_TX, uint32_t ev, uint32_t evarg)
static int u8txid;
if (ev == E_EVENT_NEW_STATE) {
Serial << "[STATE_TX:NEW]" << mwx::crlf;
u8txid = -1;
auto&& r1 = shtc3_read();
auto&& r2 = ltr308als_read();
Serial << "..shtc3 t=" << int(i16Temp) << ", h=" << int(i16Humd) << mwx::crlf;
Serial << "..ltr308als l=" << int(u32Lumi) << mwx::crlf;
if (r1 && r2) {
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
void transmit_complete(mwx::packet_ev_tx& txev) {
Serial << "..txcomp=" << int(txev.u8CbId) << mwx::crlf;
PEV_Process(E_ORDER_KICK, txev.u8CbId); // pass the event to state machine
}
// ↓ ↓ ↓ Send a message
} else if (ev == E_ORDER_KICK && evarg == uint32_t(u8txid)) {
Serial << "[STATE_TX] SUCCESS TX(" << int(evarg) << ')' << mwx::crlf;
PEV_SetState(STATE_SLEEP);
}
if (PEV_u32Elaspsed_ms() > 100) {
// does not finish TX!
Serial << "[STATE_TX] FATAL, TX does not finish!" << mwx::crlf << mwx::flush;
the_twelite.reset_system();
}
MWX_STATE(MY_APP_CHILD::STATE_SLEEP, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_NEW_STATE) {
Serial << "..sleep for 5000ms" << mwx::crlf;
pinMode(PAL_AMB::PIN_BTN, PIN_MODE::WAKE_FALLING_PULLUP);
digitalWrite(PAL_AMB::PIN_LED, PIN_STATE::HIGH);
Serial.flush();
the_twelite.sleep(5000); // regular sleep
}
}
class MY_APP_CLASS: MWX_APPDEFS_CRTP(MY_APP_CLASS)
{
public:
static const uint8_t TYPE_ID = 0x01;
// load common definition for handlers
#define __MWX_APP_CLASS_NAME MY_APP_CLASS
#include "_mwx_cbs_hpphead.hpp"
#undef __MWX_APP_CLASS_NAME
public:
// constructor
MY_APP_CLASS() {}
void _setup() {}
void _begin() {}
public:
// TWENET callback handler (mandate)
void loop() {}
void on_sleep(uint32_t & val) {}
void warmboot(uint32_t & val) {}
void wakeup(uint32_t & val) {}
void on_create(uint32_t& val) { _setup(); }
void on_begin(uint32_t& val) { _begin(); }
void on_message(uint32_t& val) { }
public:
void network_event(mwx::packet_ev_nwk& pEvNwk) {}
void receive(mwx::packet_rx& rx) {}
void transmit_complete(mwx::packet_ev_tx& evTx) {}
};
class MY_APP_CLASS: MWX_APPDEFS_CRTP(MY_APP_CLASS)
#define __MWX_APP_CLASS_NAME MY_APP_CLASS
#include "_mwx_cbs_hpphead.hpp"
#undef __MWX_APP_CLASS_NAME
MY_APP_CLASS() {}
void receive(mwx::packet_rx& rx)
void transmit_complete(mwx::packet_ev_tx& evTx)
#include <TWELITE>
#include "myAppClass.hpp" // BEHAVIOR definition file
/*****************************************************************/
// MUST DEFINE CLASS NAME HERE
#define __MWX_APP_CLASS_NAME MY_APP_CLASS
#include "_mwx_cbs_cpphead.hpp" // Definition at the beginning
/*****************************************************************/
/*****************************************************************/
// common procedure (DO NOT REMOVE)
#include "_mwx_cbs_cpptail.cpp"
// MUST UNDEF CLASS NAME HERE
#undef __MWX_APP_CLASS_NAME
/*****************************************************************/
// TickTimer interrupt
MWX_TICKTIMER_INT(uint32_t arg, uint8_t& handled) {
// blink LED
digitalWrite(PAL_AMB::PIN_LED,
((millis() >> 9) & 1) ? PIN_STATE::HIGH : PIN_STATE::LOW);
}
// PAL_AMB::PIN_BIN(12) event
MWX_DIO_EVENT(PAL_AMB::PIN_BTN, uint32_t arg) {
Serial << "Button Pressed" << mwx::crlf;
static uint32_t u32tick_last;
uint32_t tick = millis();
if (tick - u32tick_last > 100) {
PEV_Process(E_ORDER_KICK, 0UL);
}
u32tick_last = tick;
}
// Operation definition of state STATE_0
MWX_STATE(E_MWX::STATE_0, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_START_UP) {
Serial << "[STATE_0:START_UP]" << mwx::crlf;
} else
if (ev == E_ORDER_KICK) {
PEV_SetState(E_MWX::STATE_1);
}
}
// Operation definition for state STATE_1
MWX_STATE(E_MWX::STATE_1, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_NEW_STATE) {
Serial << "[STATE_1]" << mwx::crlf;
} else
if (ev == E_ORDER_KICK) {
PEV_SetState(E_MWX::STATE_2);
} else
if (ev == E_EVENT_TICK_SECOND) {
Serial << "<1>";
}
}
MWX_DIO_INT(N, uint32_t arg, uint8_t& handled)
MWX_DIO_EVENT(N, arg)
MWX_TICKTIMER_INT(uint32_t arg, uint8_t& handled)
MWX_TICKTIMER_EVENT(uint32_t arg)
MWX_TIMER_INT(N, uint32_t arg, uint8_t& handled)
MWX_TIMER_EVENT(N, uint32_t arg)
MWX_MISC_INT(uint32_t arg, uint32_t arg2, handled)
MWX_MISC_EVENT(auint32_t rg, uint32_t arg2)
E_EVENT_START_UP
It is called at system startup. Immediately after power-on, it is called with 0
parameters. Because it is in the initial stage of execution, PEV_Process()
is called once from the begin() method to start the operation when transitioning to the normal processing state.
It is still called after returning from sleep, but with parameters other than 0
. Normal processing can be performed from this state.
E_EVENT_NEW_STATE
It is called in a new state immediately after a state transition. Describes the process that is first executed when a transition is made to a certain state.
E_EVENT_TICK_TIMER
Called by TickTimer every 1ms
E_EVENT_TICK_SECOND
It is called every second.
void PEV_SetState(uint32_t s)
uint32_t PEV_u32Elaspsed_ms()
MWX_STATE(MY_APP_CHILD::STATE_TX, uint32_t ev, uint32_t evarg) {
...
if (PEV_u32Elaspsed_ms() > 100) {
// does not finish TX!
Serial << "[STATE_TX] FATAL, TX does not finish!" << mwx::crlf << mwx::flush;
the_twelite.reset_system();
}
}
void PEV_Process(uint32_t ev, uint32_t u32evarg) {
void transmit_complete(mwx::packet_ev_tx& txev) {
Serial << "..txcomp=" << int(txev.u8CbId) << mwx::crlf;
PEV_Process(E_ORDER_KICK, txev.u8CbId); // pass the event to state machine
}
void PEV_KeepStateOnWakeup()
bool PEV_is_coldboot(uint32_t ev, uint32_t u32evarg)
bool PEV_is_warmboot(uint32_t ev, uint32_t u32evarg)
input-output stream
Upper-level class that handles input/output streams.
Interfaces to several classes (Serial, Wire, SPI, smplbuf
) by polymorphism using CRTP (Curiously Recurring Template Pattern) method.
In CRTP, lower classes are defined as template class Derived : public stream<Derived>;
and methods of lower classes are referenced from upper classes.
This class defines common processing such as print
method, <<
operator, etc., and calls write()
method, etc. implemented in lower classes, which is similar to using virtual functions.
Lower classes implement the functions listed below.
int available()
// example
while(Serial.available()) {
int c = Serial.read();
// ... any
}
Returns 1 if the input exists, 0 if it does not.
Return value int
0: no data 1: data present
The return value of this implementation is not the buffer length.
void flush()
// example
Serial.println("long long word .... ");
Serial.flush();
Flush output (wait for output to complete).
int read()
// example
int c;
while (-1 != (c = read())) {
// any
}
Inputs 1-byte data from the stream. If the data does not exist, return -1
.
size_t write(int c)
// example
Serial.write(0x30);
Outputs 1 byte to the stream.
n
The character you want to output.
Return value size_t
1 if output succeeds, 0 if it fails.
static void vOutput(char out, void* vp)
This is a static function that produces a single byte output. Since this is not a class method, member variables and other information are not available. Instead, a pointer to the class instance is passed to vp, which is passed as a parameter.
This static function is used internally and the function pointer is passed as a one-byte output function of fctprintf()
. This is used to implement the print
method, etc.
out
the character to output
vp
pointer to a class instance Usually, cast to the original class and call the write() method
void mwx::stream::putchar(char c)
// example
Serial.putchar('A');
// result -> A
Output a single byte.
size_t print(T val, int base = DEC) // T: 整数型
size_t print(double val, int place = 2)
size_t print(const char*str)
size_t print(std::initializer_list<int>)
// example
Serial.print("the value is ");
Serial.print(123, DEC);
Serial.println(".");
// result -> the value is 123.
Serial.print(123.456, 1);
// result -> 123.5
Serial.print({ 0x12, 0x34, 0xab, 0xcd });
// will output 4byte of 0x12 0x34 0xab 0xcd in binary.
Performs various types of formatted output.
val
Integer powerups
base
power form BIN binary / OCT 8 math / DEC 10 math / HEX 16 math
place
Number of decimals below the decimal point
back size_t
the number of booklets
size_t printfmt(const char* format, ...);
// example
Serial.printfmt("the value is %d.", 123);
// result -> the value is 123.
Prints output in printf format.
TWESDK/TWENET/current/src/printf/README.md 参照
// examples
Serial << "this value is" // const char*
<< int(123)
<< '.';
<< mwx::crlf;
// result -> this value is 123.
Serial << fromat("this value is %d.", 123) << twe::crlf;
// result -> this value is 123.
Serial << mwx::flush; // flush here
Serial << bigendian(0x1234abcd);
// will output 4byte of 0x12 0x34 0xab 0xcd in binary.
Serial << int(0x30) // output 0x30=48, "48"
<< '/'
<< uint8_t(0x31); // output '1', not "48"
// result -> 48/1
smplbuf<char,16> buf = { 0x12, 0x34, 0xab, 0xcd };
Serail << but.to_stream();
// will output 4byte of 0x12 0x34 0xab 0xcd in binary.
Seiral << make_pair(buf.begin(), buf.end());
// will output 4byte of 0x12 0x34 0xab 0xcd in binary.
Serial << bytelist({ 0x12, 0x34, 0xab, 0xcd });
// will output 4byte of 0x12 0x34 0xab 0xcd in binary.
char
1-byte output (not formatted as a number)
1-byte output (not formatted as a number)
double
numeric output (printf's "%.2f")
uint8_t
output 1 byte (same as char type)
uint16_t
output 2 bytes (big-endian order)
uint32_t
output 4 bytes (big-endian order)
const char*
uint8_t*
const char[S]
Output up to the terminating character. Output does not include the terminating character. >(S
specifies the size of the fixed array)
uint8_t[S]
Output the array size S
bytes as is. (S
is the fixed array size specification)
format()
output in printf format
mwx::crlf
output of newline CRLF
mwx::flush
flush output
bigendian()
output numeric types in big-endian order. (right-hand side value)
std::pair<T*, T*>
A pair containing begin(), end()
pointers of byte type. Can be created by make_pair
. Tis assumed to be of type
uint8_t`. (right-hand side value)
output byte string using bytelist()
std::initializer_list
.
smplbuf<uint8_t,AL>&
Output the contents of an array class of type uint8_t
. ALC
is .
smplbuf<uint8_t, AL>::to_stream()
Outputs data of smplbuf<T>
T
is of type uint8_t
, AL
is a .
uint8_t get_error_status()
void clear_error_status()
void set_timeout(uint8_t centisec)
// example
Serial.set_timeout(100); // 1000msのタイムアウトを設定
uint8_t c;
Serial >> c;
Manages input timeouts and errors using the >>
operator.
The timeout period is specified by set_timeout()
, and input is processed using the >>
operator. If no input is received within the given timeout period, the error value can be read out with get_error_status()
. The error status is cleared by clear_error_status()
.
centisec
Sets the timeout period in units of 1/10 second. If 0xff
is specified, timeout is disabled.
0
No Error
1
Error Status
inline D& operator >> (uint8_t& v)
inline D& operator >> (char_t& v)
template <int S> inline D& operator >> (uint8_t(&v)[S])
inline D& operator >> (uint16_t& v)
inline D& operator >> (uint32_t& v)
inline D& operator >> (mwx::null_stream&& p)
//// Example
uint8_t c;
the_twelite.stop_watchdog(); // stop watchdog
Serial.set_timeout(0xFF); // no timeout
// read out 1 byte
Serial >> c;
Serial << crlf << "char #1: [" << c << ']';
// skipping
Serial >> null_stream(3); // Read away 3 bytes
Serial << crlf << "char #2-4: skipped";
// Read 4 bytes (limited to fixed-length arrays of type uint8_t)
uint8_t buff[4];
Serial >> buff;
Serial << crlf << "char #5-8: [" << buff << "]";
Input processing.
Cannot be executed within setup()
.
Because it waits for polling, depending on the timeout time setting (e.g. no timeout), the watchdog timer may be triggered and reset.
The following is a list of types that can be read and stored.
1-byte input (big-endian order)
2-byte input (big-endian order)
uint32_t
4-byte input (big-endian order)
uint8_t[S]
input for S
bytes (S
specifies fixed array size)
null_stream(int n)
read n
bytes away
Slp_Wk_and_Tx is a template source code for an application which, after a regular wake-up, does something (e.g. acquires sensor data) and sends the result as a wireless packet.
In the form of setup()
and loop()
, conditional branches which are difficult to read in loop()
tend to occur. In this Act, we use in loop()
and simple state transition by switch syntax to improve the visibility of the code.
This act includes
The control structure of a typical intermittent operation (sleep -> wake -> measure -> radio transmission -> sleep)
Generation and transmission procedures for outgoing packets and waiting for completion
After starting up, the system goes through an initialization process and then goes to sleep.
setup()
Initialise
begin()
Run sleep
After waking up from sleep, the state variables are initialized and the actions are performed in the following order
wakeup()
wakes up from sleep, performs each initialization
loop()/INIT->WORK_JOB state
: does some processing (in this Act, the counter is updated every TickCount for 1ms and moves to TX state after a count determined by a random number)
loop()/TX state
: Make a request to send
loop()/WAIT_TX state
: Waiting for transmission completion
loop()/EXIT_NORMAL state
: Run sleep (back to 1.)
loop()/EXIT_FATAL state
:Resetting the module if an error occurs
To send packets, <NWK_SIMPLE>
is included. Also, basic definitions such as application ID are in "Common.h"
.
In order to describe the sequential processing in loop()
, this sample uses the concept of a state machine (state transition). It uses <SM_SIMPLE>
, which summarizes the processing of very simple state transitions.
An enum STATE
is defined in Common.h
for the following states.
Declares an (state transition) using the STATE
state enumeration.
The step
declared here contains functions for managing state, timeouts and waiting for processing.
In this sample we do not process the sensor data, but we prepare dummy data for explanation.
Initializes variables and class objects.
Initialization of the step
state machine
Initialization of the_twelite
class object
Register and initialize the network <NWK_SIMPLE>
(register DEVICE_ID
).
This is followed by the initiation of class objects and hardware.
This is the procedure to start the_twelite
, it didn't appear in act0..4, but you should call it if you set up the_twelite
or register various behaviors.
Called only once, immediately after setup()
. The SleepNow()
function is called to perform the first sleep procedure.
Called immediately after waking up. Here, it initializes the sensor data area and outputs a message on waking.
The above code is a simplified version of the actual code.
This control structure uses the . It is a loop with do..while()
syntax. Inside the loop is a _switch case
_ clause, which splits the process according to the state obtained by .state()
. State transitions call .next()
to rewrite internal variables in the state machine to the new state values.
step.b_more_loop()
is set to true if there is a state transition by .next()
. The purpose of this is to execute the code of the next state (case clause) without escaping loop()
when a state transition occurs.
Below is a description of each state.
Initialises the sensor values of the dummies. One is determined randomly by an add counter and one by a counter stop value.
In the WORK_JOB state, we work on a timer every 1ms; every Tick timer becomes TickTimer.available()
; every Tick timer adds a counter and when it reaches dummy_work_ct_max
, we move to the next state STATE::TX
.
Call the Transmit()
function to request packet transmission. If the request succeeds, the function transits to STATE::WAIT_TXEVENT
and waits for the completion of transmission. Here, we use the timeout and flag functions of the SM_SIMPLE state machine to wait for completion (it is simple to determine the value of a variable by changing its value in the waiting loop).
We don't usually expect a single request to fail, but if it does, it will go to the STATE::EXIT_FATAL
exception handling state.
You should not sleep at this point, as the packet has not yet been sent. In most cases, you should wait for transmission to complete before continuing.
Waiting for completion of transmission is judged by setting the flag of the state machine function with on_tx_comp()
described later. The timeout is judged by the elapsed time since .set_timeout()
was done by calling .is_timeout()
.
Normally you will get a completion notice whether the transmission succeeds or fails, but it will time out and go to STATE::EXIT_FATAL
for exception handling.
Call SleepNow()
to start the sleep process.
As a critical error, a system reset is performed.
Periodic sleep is performed. The sleep time is set to a constant time blur using the random()
function. This is because when the transmission cycles of several devices are synchronized, the failure rate may increase significantly.
Before sleeping, the state of the SM_SIMPLE state machine is set by calling .on_sleep()
.
This function requests to send a wireless packet to the parent device with ID=0x00
. The data to be stored is a four-character identifier (FOURCC
) commonly used in the Act sample, plus the system time [ms] and a dummy sensor value (sensor.dummy_work_ct_now
).
The first step is to get an object that contains the transmitted packets. This object can then be manipulated to set the data and conditions for transmission.
In the mwx library, the if
statement is used to get an object and the bool
decision of the object is true
. Here, a board object is retrieved by the_twelite.network.use<NWK_SIMPLE>()
, and a packet object is retrieved by .prepare_tx_packet()
of the board object. Failure to retrieve the packet object is not normally expected, but when it does occur, it is when the transmit queue is full and the transmit request cannot be accepted. Since this example is for a single transmission only, the error is limited to an unexpected serious problem.
For the resulting pkt
object, set the conditions for transmission (destination, retransmission, etc.) using the <<
operator. specifies the destination of the packet. The tx_addr
specifies the destination of the packet, the specifies the number of retransmissions, and the specifies the transmission delay.
The payload of a packet is an array of derivatives obtained by pkt.get_payload()
. You can set the value of this array directly, but here we use to set the value.
This function can be specified by a variable number of arguments. The first parameter is an array object obtained from .get_payload()
.
make_pair(FOURCC,4)
: make_pair is from the C++ standard library and creates a std::pair object. It means to write out 4 bytes from the beginning for string type.(This is done to explicitly specify the number of bytes to be written, as the topic of including or excluding the end of an array of string types is confusing)
If uint32_t
type data is specified, 4 bytes of data are written in big-endian order.
The same applies to data of type uint16_t
.
Finally, .transmit()
is called to request sending. The return value is of the type MWX_APIRET
. After the request, the actual transmission takes place, which may take several to several tens of milliseconds to complete, depending on the transmission parameters and the size of the transmission. On completion, on_tx_comp()
is called.
This is a system event that is called when the transmission is complete. Here, it is set to complete by .set_flag()
.
#include <TWELITE>
#include <NWK_SIMPLE>
#include <SM_SIMPLE>
#include "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)
};
SM_SIMPLE<STATE> step;
struct {
uint16_t dummy_work_ct_now;
uint16_t dummy_work_ct_max; // counter for dummy work job.
} sensor;
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;
}
the_twelite.begin(); // start twelite!
void begin() {
Serial << "..begin (run once at boot)" << crlf;
SleepNow();
}
void wakeup() {
memset(&sensor, 0, sizeof(sensor));
Serial << crlf << int(millis()) << ":wake up!" << crlf;
}
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());
}
sensor.dummy_work_ct_now = 0;
sensor.dummy_work_ct_max = random(10,1000);
step.next(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);
}
}
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);
}
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);
}
SleepNow();
Serial << crlf << "!FATAL: RESET THE SYSTEM.";
delay(1000); // wait a while.
the_twelite.reset_system();
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);
}
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);
}
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
pkt << tx_addr(0x00) // Destination
<< tx_retry(0x1) // Number of resends
<< tx_packet_delay(0,0,2); // 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.
);
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());
return pkt.transmit();
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
This ACT acquires several samples of acceleration data after sleep recovery and sends that data.
The ACT of the "Act" includes the following
Sending and receiving wireless packets
Configuration through Interactive settings mode - <STG_STD>
State transition control by state machine - <SM_SIMPLE>
Wake up → start acquiring accelerometer data → wait for accelerometer FIFO interrupt → retrieve accelerometer data → wireless transmission → sleep.
#include <TWELITE> // MWX library basic
#include <NWK_SIMPLE> // network
#include <SM_SIMPLE> // State machine (state transition)
#include <STG_STD> // Interactive settings mode
/*** board selection (choose one) */
#define USE_PAL_MOT
//#define USE_CUE
// board dependend definitions.
#if defined(USE_PAL_MOT)
#define BRDN PAL_MOT
#define BRDC <PAL_MOT>
#elif defined(USE_CUE)
#define BRDN CUE
#define BRDC <CUE>
#endif
// include board support
#include BRDC
To support MOT PAL or TWELITE CUE, the include part is a macro. Define either USE_PAL_MOT
or USE_CUE
.
If USE_PAL_MOT
is defined, the board BEHAVIOR <PAL_MOT>
is included.
enum class E_STATE : uint8_t {
INTERACTIVE = 255,
INIT = 0,
START_CAPTURE,
WAIT_CAPTURE,
REQUEST_TX,
WAIT_TX,
EXIT_NORMAL,
EXIT_FATAL
};
SM_SIMPLE<E_STATE> step;
Define states for sequential processing during loop()
and also state machinestep
is declared.
struct {
int32_t x_ave, y_ave, z_ave;
int32_t x_min, y_min, z_min;
int32_t x_max, y_max, z_max;
uint16_t n_seq;
uint8_t n_samples;
} sensor;
Data structure for storing sensor data.
/// load board and settings objects
auto&& brd = the_twelite.board.use BRDC (); // load board support
auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support
Registers board, configuration, and network behavior objects.
// settings: configure items
set << SETTINGS::appname("MOT");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set << SETTINGS::lid_default(0x1); // set default LID
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);
// if SET=LOW is detected, start with intaractive mode.
if (digitalRead(brd.PIN_SET) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
brd.set_led(LED_TIMER::BLINK, 300); // slower blink
step.next(STATE::INTERACTIVE);
return;
}
// load settings
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
Initialize the Interactive settings mode.
First, adjust the configuration items. Here, we set the title name SETTINGS::appname
to be displayed in menu items, the default value of Application ID SETTINGS::appid_default
, the default value of CHANNEL SETTINGS::ch_default
, the default value of logical device ID default SETTINGS::lid_default
, and .hide_items()
for hidden items.
This sample transitions to Interactive settings mode when the SET pin is LO at startup. If digitalRead(brd.PIN_SET)
confirms that the pin is LO, specify SETTINGS::open_at_start()
. This specification causes the Interactive settings mode screen to appear immediately after exiting SETUP()
. When the screen is displayed, begin()
and loop()
are executed. In this sample, the state STATE::INTERACTIVE
is set so that no action such as sleep is performed during loop()
and nothing is done.
Next, the configuration values are read. To read the configuration values, be sure to execute .reload()
. In this sample, the option bit setting .u32opt1()
is read.
the_twelite << set;
the_twelite
is a class object that manages the basic behavior of the system. This object performs various initializations such as Application ID and CHANNEL within setup()
.
Here some of the settings for Interactive settings mode is reflected.
nwk << set;
Settings are also made for the network behavior object. The logical device ID (LID) and retransmission settings in Interactive settings mode are reflected.
brd.set_led(LED_TIMER::BLINK, 100);
LED blink settings and other settings.
void begin() {
auto&& set = the_twelite.settings.use<STG_STD>();
if (!set.is_screen_opened()) {
// sleep immediately, waiting for the first capture.
sleepNow();
}
}
Called after setup()
is finished. Here, the first sleep is performed. However, if the screen in Interactive settings mode is displayed, it does not sleep.
void wakeup() {
Serial << crlf << "--- PAL_MOT(OneShot):"
<< FOURCHARS << " wake up ---" << crlf;
eState = E_STATE::INIT;
}
After waking up, the state variable eState
is set to the initial state INIT. After this, loop()
is executed.
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
do {
switch(step.state()) {
case STATE::INTERACTIVE:
break;
...
} while(step.b_more_loop());
}
The basic structure of loop()
is <SM_STATE>
state machine state
with switch ... . case clause. The initial state is STATE::INIT
or STATE::INTERACTIVE
.
This is the state of the Interactive settings mode screen when it is displayed. Nothing is done. Interactive settings mode is used for Serial input and output in this screen.
INIT in its initial state.
case STATE::INIT:
brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
memset(&sensor, 0, sizeof(sensor)); // clear sensor data
step.next(STATE::START_CAPTURE);
break;
In state INIT, initialization (clearing the queue for storing results) and initialization of the data structure for storing results is performed. transition to STATE::START_CAPTURE. After setting this transition, the while loop is executed again.
case STATE::START_CAPTURE:
brd.sns_MC3630.begin(
// 400Hz, +/-4G range, get four samples and will average them.
SnsMC3630::Settings(
SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, N_SAMPLES));
step.set_timeout(100);
step.next(STATE::WAIT_CAPTURE);
break;
In the state START_CAPTURE, the FIFO acquisition of the MC3630 sensor is started. Here, the FIFO interrupt is set to occur when 4 samples are acquired at 400 Hz.
Set timeout for exception handling and transition to the next state STATE::WAIT_CAPTURE
.
case STATE::WAIT_CAPTURE:
if (brd.sns_MC3630.available()) {
brd.sns_MC3630.end(); // stop now!
In the state WAIT_CAPTURE, it waits for a FIFO interrupt. When an interrupt occurs and data is stored in the result storage queue, sns_MC3630.available()
becomes true
. Call sns_MC3630.end()
to terminate the process.
sensor.n_samples = brd.sns_MC3630.get_que().size();
if (sensor.n_samples) sensor.n_seq = brd.sns_MC3630.get_que()[0].get_t();
...
Get the number of samples and sample sequence numbers.
// get all samples and average them.
for (auto&& v: brd.sns_MC3630.get_que()) {
sensor.x_ave += v.x;
sensor.y_ave += v.y;
sensor.z_ave += v.z;
}
if (sensor.n_samples == N_SAMPLES) {
// if N_SAMPLES == 2^n, division is much faster.
sensor.x_ave /= N_SAMPLES;
sensor.y_ave /= N_SAMPLES;
sensor.z_ave /= N_SAMPLES;
}
...
Reads and averages all sample data.
// can also be:
// int32_t x_max = -999999, x_min = 999999;
// for (auto&& v: brd.sns_MC3630.get_que()) {
// if (v.x >= x_max) x_max = v.x;
// if (v.y <= x_min) x_min = v.x;
// ...
// }
auto&& x_minmax = std::minmax_element(
get_axis_x_iter(brd.sns_MC3630.get_que().begin()),
get_axis_x_iter(brd.sns_MC3630.get_que().end()));
sensor.x_min = *x_minmax.first;
sensor.x_max = *x_minmax.second;
...
Here, the maximum and minimum are obtained for the acquired samples using the iterator corresponding to each axis.
if (brd.sns_MC3630.available()) {
...
brd.sns_MC3630.get_que().clear(); // clean up the queue
step.next(STATE::REQUEST_TX); // next state
} else if (step.is_timeout()) {
Serial << crlf << "!!!FATAL: SENSOR CAPTURE TIMEOUT.";
step.next(STATE::EXIT_FATAL);
}
break;
Call .sns_MC3630.get_que().clear()
to clear the data in the queue. If this is not called, subsequent sample acquisitions will not be possible. After that, it transits to the STATE::REQUEST_TX
state.
. is_timeout()
checks for timeout. If timeout occurs, it transits to STATE::EXIT_FATAL
as an error.
case STATE::REQUEST_TX:
if (TxReq()) {
step.set_timeout(100);
step.clear_flag();
step.next(STATE::WAIT_TX);
} else {
Serial << crlf << "!!!FATAL: TX REQUEST FAILS.";
step.next(STATE::EXIT_FATAL);
}
break;
In state REQUEST_TX
, the locally defined function TxReq()
is called to process the obtained sensor data and generate and send a transmission packet. The transmission request may fail due to the status of the transmission queue or other factors. If the transmission request succeeds, TxReq()returns as true, but no transmission is performed yet. The
on_tx_comp()` callback is called to complete the transmission.
Also, .clear_flag()
clears the flag to indicate that transmission is complete. At the same time, a timeout is set.
case STATE::WAIT_TX:
if (step.is_flag_ready()) {
step.next(STATE::EXIT_NORMAL);
}
if (step.is_timeout()) {
Serial << crlf << "!!!FATAL: TX TIMEOUT.";
step.next(STATE::EXIT_FATAL);
}
break;
In state STATE::WAIT_TX
, it waits for the completion of wireless packet transmission. The flag is set by the on_tx_comp()
callback function, and .is_flag_ready()
becomes true after it is set.
case STATE::EXIT_NORMAL:
sleepNow();
break;
case STATE::EXIT_FATAL:
Serial << flush;
the_twelite.reset_system();
break;
When a series of operations is completed, it transits to the state STATE::EXIT_NORMAL
and calls the locally defined function sleepNow()
to execute sleep. When an error is detected, it transits to state STATE::EXIT_FATAL
and performs a system reset.
MWX_APIRET TxReq() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
MWX_APIRET ret = false;
// prepare tx packet
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 (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
// prepare packet (first)
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint16_t(sensor.n_seq)
, uint8_t(sensor.n_samples)
, uint16_t(sensor.x_ave)
, uint16_t(sensor.y_ave)
, uint16_t(sensor.z_ave)
, uint16_t(sensor.x_min)
, uint16_t(sensor.y_min)
, uint16_t(sensor.z_min)
, uint16_t(sensor.x_max)
, uint16_t(sensor.y_max)
, uint16_t(sensor.z_max)
);
// perform transmit
ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
}
}
return ret;
}
The last step is to generate a packet and request that it be sent. The packet should include the continuation number, the number of samples, the average value of XYZ, the minimum sample value of XYZ, and the maximum sample value of XYZ.
void sleepNow() {
Serial << crlf << "..sleeping now.." << crlf;
Serial.flush();
step.on_sleep(false); // reset state machine.
the_twelite.sleep(3000, false); // set longer sleep (PAL must wakeup less than 60sec.)
}
Sleep procedure.
Serial ports should call Serial.flush()
to output all before sleep.
The <SM_SIMPLE>
state machine must do on_sleep()
.
Building ACT
The application program written in the MWX library is called ACT. The first step is to build and write it.
About the build folder structure
About the build script
About building with Visual Studio Code (Described as VSCode)
Depending on the OS environment, security warnings may appear when running each executable program (e.g. make
, build toolchain like gcc
). It is necessary to configure the settings to suppress the warning. (Please consult with yourself or your system administrator to determine if you wish to run the program with the warning suppressed.)
Open the folder MWSDK_ROOT
(e.g. C:\MWSDK
) where you have installed the MWSDK. It has the following structure
MWSDK_ROOT
|
+-ChipLib : Semiconductor library
+-License : Software License Agreement
+-MkFiles : makefile
+-Tools : A set of tools such as compilers
+-TWENET : TWENET/MWX library
+-Act_samples : Sample codes of ACT
...
Tool act files such as compilers are stored under Act_samples
. (Some of them are omitted below)
Act_samples
|
+-BRD_APPTWELITE : ACT for boards with the same configuration as App_TweLite
+-PAL_AMB : ACT for environmental sense PAL
+-PAL_MAG : ACT for open-close PAL
+-PAL_MOT : ACT for motion PAL
..
+-Parent-MONOSTICK : ACT for parent device (for MONOSTICK)
+-PingPong : ACT for transmit and receive like Ping Pong
+-PulseCounter : ACT using a pulse counter
+-act0 : Scratching ACT
These acts are simple examples to help you write your own MWX library, but many of them have the following features
Obtaining sensor values
After obtaining the sensor value, send a wireless packet to the master
Sleeps for a period of time (or waits for an interrupt) after transmission is complete
The Parent-MONOSTICK act is used to receive and display packets. This act for the parent is output in ASCII format. (:00112233AABBCC.... .FF[CR][LF]
, and the middle part is a hexadecimal byte expressed by two ASCII characters. The trailing ? is also a two-character byte, but it becomes a checksum byte called LRC. Reference: ASCII format)
When trying to get it to work in practice, try the following combinations:
The parent device is started with the M1 pin low (GND level). In normal mode (always running), you can see it works like App_TweLite.
The system works with two children. When one of them sends a ping packet, the other sends a pong packet back.
Other
You can check the transmission of packets of the act for the child machine.
Now let's take a look inside the PingPong folder from within ACT.
Act_samples
+-PingPong
+-PingPong.cpp : ACT code
+-build : build folder
+-.vscode : setting files for VSCode
You must have a .cpp
file with the same name as the folder directly under the folder.
The ACT file PingPong.cpp is located directly under the PingPong folder. If you change the name of the folder, make sure to rename the .cpp file to the same name as the folder.
Next, open the build folder.
Act_samples
+-PingPong
+-build
+-Makefile : makefile
+-build-BLUE.cmd : build script for TWELITE BLUE
+-build-RED.cmd : build script for TWELITE RED
+-build-clean.cmd : clean up obj_* files
It contains the scripts and Makefiles needed for the build.
Build by running make TWELITE={BLUE or RED}
in the folder containing this Makefile
. Building with VSCode is the same, calling make internally.
The TWELITE STAGE app can be used to build, write, and run. This section describes the TWELITE STAGE application from startup to build.
Connect MONOSTICK or TWELITE R to your USB port.
TWELITE is a sensitive electronic component and should be handled with care. Typical precautions are listed below.
Especially when TWELITE R is used, the electronic board is often in direct contact with the outside without a case, which may cause unintended shorts, noise, or other problems that prevent the USB device from operating properly.
In this case, closing the application and unplugging and plugging in the USB device will usually restore it. In the worst case, the USB device may be damaged or the PC may be corrupted.
Also, handle electronic circuit boards with care.
Circuit error.
Check the circuit again before turning on the power.
Be careful not to reverse-insert batteries or over-voltage.
Static electricity
Even a voltage that is not human-sensitive can cause a semiconductor failure. Even simple measures such as touching metal parts before working, wristbands, and special mats can have a reasonable effect.
Short-circuits caused by touching metal objects, etc.
Make sure that there are no metal objects near the electronic board. If clips or other objects are scattered around, this may cause a short circuit, or even a dangerous situation in which the battery heats up due to a large discharge.
Launch the executable TWELITE_Stage.{extension}
located in the {TWELITE SDK installation} folder. (reference: TWELITE STAGE Application Manual - Usage)。
Below is an example of the screen while the TWELITE STAGE application is running. The main screen on the left and the command prompt screen are shown, but the main screen is usually used. The command prompt screen is not normally used, but it displays various information and input data from the TWELITE microcontroller serial port.
The main operations on the main screen are as follows
Left mouse click (selection)
Double right mouse click (return to previous screen)
Quickly press ESC
twice, or ESC
once on some screens (return to previous screen)
Hold down the Alt(Cmd) key (help screen)
Normal keyboard input (follow the screen)
(Reference: TWELITE STAGE App Manual - Key and Mouse Operations)
This is the first screen that appears when you start the TWELITE STAGE application. If TWELITE R or MONOSTICK is connected in advance, it will be listed on this screen. Select the TWELITE you wish to operate on this screen. It is also possible to select the TWELITE without selecting it on this screen.
(Reference: TWELITE STAGE App Manual)
After exiting the serial port selection screen, the main menu appears. Select the "Application Rewrite" menu for build and write.
(Reference: TWELITE STAGE App Manual)
Before selecting the application programming menu, please check the TWELITE connection and serial port selection. The serial port selection status can be checked on the help screen that appears by holding down the Alt(Cmd) key.
There are several categories of projects that can be referenced from the TWELITE STAGE application. HELP
on the right side displays related information in a browser. Foldr
displays the folder where the project is located.
If TWELITE is already connected, the TWELITE model is determined when the menu is selected. (Inside the TWELITE STAGE application, the build is performed according to the TWELITE model that has been determined.)
If an error occurs here, return to the main menu from this screen and reselect the menu. If necessary, deselect the serial port by pressing Alt(Cmd)
+ 0
on the TWELITE STAGE application and check the various connections, including the USB connection. Some USB connection errors may not be resolved until you reboot your computer.
(Reference: TWELITE STAGE App Manual)
Here, select "Act Build & Wrt" from the " Wrt Firmware" menu.
Project names, such as sample acts, are listed. The HELP
on the right side displays the related information in a browser. The foldr
displays the folder where the project is located.
Reference: TWELITE STAGE App Manual-Act Build & Wrt)
Here, select BRD_APPTWELITE
in the project selection screen shown earlier.
Once selected, writing will be performed as shown in the following screen example. If an error is displayed, follow the on-screen instructions or return to the previous screen and try again.
(Reference: TWELITE STAGE App Manual-build screen)
When the programming is successfully completed, it will continue to Interactive settings mode (settings screen). However, the screen will not be displayed unless the firmware supports Interactive settings mode.
In Interactive settings mode, you can configure various settings, including TWELITE's wireless CHANNEL.
(Reference: TWELITE STAGE App Manual-Interactive settings mode)
Return to the root menu and select Viewer
> Terminal
.
This is a very simple terminal where you can check messages from TWELITE and input data into TWELITE.
The screen displays a message when a wireless transmission is made approximately every second. You can also transition to the Interactive settings mode screen by typing + + +
.
(Reference: TWELITE STAGE App Manual-Terminal)
VSCode is a powerful editor for source editing, but it is also possible to build firmware for TWELITE microcontrollers on VSCode.
VSCode is started from the TWELITE STAGE application from the project listing in Build&Write menu. (Note: The settings is required at TWELITE STAGE App [Setting Menu] > Wrt Firmware > Open a folder with VSCode
. For simplicity of configuration, the executable TWELITE_Stage_VSCode.{extension}
is available for Windows, Linux, and macOS.)
Set "Open folder with code (VSCode)" to 1
in STAGE settings.
Press [VSCode]
on the right end of the list of builds.
If VSCode is already launched, the necessary settings by setting the system environment variables may not be reflected. In this case, exit VSCode and start VSCode again from the TWELITE STAGE application.
Because of the use of system environment variables to reflect information, in principle, simultaneous operation of multiple TWELITE STAGES that reference different library folders may cause problems. If you open Terminal on VSCode and the environment variable MWSDK_ROOT
is properly set, you can expect the build to be successful.
Firstly, open a workspace from TWELITE STAGE app that you want build. The attached workspace in TWELITE STAGE SDK has build task definitions for TWELITE microcontroller.
In the example below, a workspace is opened with an example screen of the English interface.
Open a workspace and select [Terminal>Run Task...]
.
Select the type of TWELITE radio module (BLUE/RED) and the act name to build. In the example below we have selected Build for TWELITE BLUE PingPong (for TWELITE BLUE/PingPong act). The build will start immediately after selection.
The progress of the build is shown in the TERMINAL section at the bottom of the screen.
If the build is successful, you will see a message about the creation of the .elf
file, along with its size information (text data bss dec hex filename
), as shown in the highlighted part of the screenshot above.
Also, a BIN file (in the above example, PingPong_BLUE_???.bin
) file should be created under the build
folder. Please check it.
If the build does not work, check the error messages first; it is often easy to identify the cause of an error from a message on a line containing the string error.。
To be sure, clean (remove intermediate files in the objs_???
folder) and rerun the build to be sure. (All operations, including make clean
, will fail if there are intermediate files left over from builds in other environments).
Additional information about building in the command line environment.
A working knowledge of the command line (e.g. bash) is required.
Depending on the OS environment, security warnings may appear when running each executable program. It is necessary to configure the settings to suppress the warning. (Please consult with yourself or your system administrator to determine if you wish to run the program with the warning suppressed.)
To build by command line, run make
in a window where bash
(or some other shell) is running. Make sure that the environment variable MWSDK_ROOT
is set correctly beforehand. For example, if you install to /work/MWSTAGE/MWSDK
, set ~/.profile
as follows.
MWSDK_ROOT=/work/MWSTAGE/MWSDK
export MWSDK_ROOT
Run make
from the command line (bash). If make
is not available, you need to install a package. (The following is an example for Ubuntu Linux)
$ make
Command 'make' not found, but can be installed with:
sudo apt install make
sudo apt install make-guile
$ sudo apt install make
...
On Windows, run {MWSTAGE SDK install}/MWSDK/WIN_BASH.cmd
. Environment variables and make utility are already set.
A build should look like this:
$ cd $MWSDK_ROOT
$ cd Act_samples/PingPong/build
$ pwd
/mnt/c/MWSDK/Act_samples/PingPong/build
$ ls
... View file list
$ rm -rfv objs_*
... Delete intermediate files just in case
$ make TWELITE=BLUE
... Normal build for BLUE
$ make -j8 TWELITE=BLUE
... Parallel build for BLUE (8 processes simultaneously)
See the Makefile description for more details.
make TWELITE=BLUE
build for TWELITE BLUE
make TWELITE=RED
build for TWELITE RED
make cleanall
Delete intermediate files
When the build is done, the objs_???
folder is created and an intermediate file is created in it. This file is dependent on the environment in which it was compiled, so if any files from other environments are left, make will fail.
Environmental Sensor Pal AMBIENT SENSE PAL is used to acquire sensor values.
This ACT includes
Sending and receiving wireless packets
Configuring settings via Interactive settings mode - <STG_STD>
State transition control by state machine - <SM_SIMPLE>
<PAL_AMB>Board operation with board behavior
Uses the environmental sensor PAL AMPIENT SENSE PAL to acquire sensor values.
Use the sleep function to operate with coin cell batteries.
Parent Node
ACT in action.
Child Node
+
#include <TWELITE>
#include <NWK_SIMPLE>// network support
#include <PAL_AMB> // PAL_AMB
#include <STG_STD> // Interactive settings mode
#include <SM_SIMPLE> // Simple State Machine
Environment sensor pal <PAL_AMB>
) include board behavior.
void setup() {
/*** SETUP section */
step.setup(); // State machine initialization
// Load PAL_AMB board behavior
auto&& brd = the_twelite.board.use<PAL_AMB>();
// Load Interactive settings mode
auto&& set = the_twelite.settings.use<STG_STD>();
set << SETTINGS::appname(FOURCHARS);
set << SETTINGS::appid_default(APP_ID); // set default appID
set.hide_items(E_STGSTD_SETID::POWER_N_RETRY, 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);
// Activate Interactive settings mode when SET pin is detected
if (digitalRead(brd.PIN_BTN) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// Read data from Interactive settings mode
set.reload();
APP_ID = set.u32appid();
CHANNEL = set.u8ch();
OPT_BITS = set.u32opt1();
// Determine LID from DIP switches and Interactive settings mode
LID = (brd.get_DIPSW_BM() & 0x07); // 1st priority is DIP SW
if (LID == 0) LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
// LED initialization
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
Wire.begin(); // start two wire serial bus.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
startSensorCapture(); // start sensor capture!
/*** INIT message */
Serial << "--- PAL_AMB:" << FOURCHARS << " ---" << mwx::crlf;
}
The first step is to initialize variables, etc. Here we are initializing the state machine step.
First, the board support <PAL_AMB>
is registered. The sensors and DIOs are initialized when the board support is initialized. The reason for doing this first is that it is common to check the status of the board's DIP SW, etc., and then configure network settings, etc.
auto&& brd = the_twelite.board.use<PAL_AMB>();
The next step is to initialize and read out the Interactive settings mode.
// Load Interactive settings mode
auto&& set = the_twelite.settings.use<STG_STD>();
set << SETTINGS::appname(FOURCHARS);
set << SETTINGS::appid_default(APP_ID); // set default appID
set.hide_items(E_STGSTD_SETID::POWER_N_RETRY, 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);
// If SET pin is detected, activate Interactive settings mode
if (digitalRead(brd.PIN_BTN) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// Read data from Interactive settings mode
set.reload();
APP_ID = set.u32appid();
CHANNEL = set.u8ch();
OPT_BITS = set.u32opt1();
// Determine LID from DIP switches and Interactive settings mode
LID = (brd.get_DIPSW_BM() & 0x07); // 1st priority is DIP SW
if (LID == 0) LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
Here you can get the set object, reflect the application name, reflect the default Application ID, and delete unnecessary items in the settings menu.
Next, the state of the SET pin is read. Since this sample operates intermittently in sleep mode, Interactive settings mode transition by +++ input is not possible. Instead, the sample transitions to Interactive settings mode with the SET pin = LO state at startup. In this case, SETTINGS::open_at_start()
is specified, which means that the interactive mode screen will be displayed as soon as setup()
is finished.
Finally, .reload()
is executed to read the set values from the EEPROM. The configuration values are copied to each variable.
Next, configure the LED settings. (In an application that sleeps and wakes for a short period of time, this is almost the same as setting the LED to turn on during wake-up.)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
Since this ACT exclusively transmits wireless packets, the TWENET configuration does not include a specification (TWENET::rx_when_idle()
) to open the receive circuit during operation.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
The sensors on the board use the I2C bus, so start using the bus.
Wire.begin(); // start two wire serial bus.
Starts the acquisition of a sensor on the board. See the description of startSensorCapture()
.
startSensorCapture();
void loop() {
auto&& brd = the_twelite.board.use<PAL_AMB>();
do {
switch (step.state()) {
// Behavior of each state
case STATE::INIT:
...
break;
...
}
while(step.b_more_loop());
}
The loop()
is controlled by the SMSMSIMPLE_SIMPLE state machinestep
for control. This is to concisely represent the sequence of events from sleep recovery, sensor value acquisition, wireless packet transmission, waiting for transmission to complete, and sleep. In the combat of the loop, a brd
object is acquired.
It is not convenient to have the main loop running during Interactive settings mode, so it is fixed in this state.
brd.sns_SHTC3.begin();
brd.sns_LTR308ALS.begin();
step.next(STATE::SENSOR);
Start sensor data acquisition.
if (!brd.sns_LTR308ALS.available()) {
brd.sns_LTR308ALS.process_ev(E_EVENT_TICK_TIMER);
}
if (!brd.sns_SHTC3.available()) {
brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
}
The sensors on the board can be accessed with the name .sns_LTR308ALS
or .sns_SHTC3
, and operations are performed on this object. Wait for the sensor to complete. If the sensor has not yet been acquired (.available()
is false
), a time elapsed event (.process_ev(E_EVENT_TICK_TIMER)
) is sent to the sensor.
When the above two sensors become available, the sensor value is retrieved and a transition is made to STATE TOK_TX.
// now sensor data is ready.
if (brd.sns_LTR308ALS.available() && brd.sns_SHTC3.available()) {
sensor.u32luminance = brd.sns_LTR308ALS.get_luminance();
sensor.i16temp = brd.sns_SHTC3.get_temp_cent();
sensor.i16humid = brd.sns_SHTC3.get_humid_per_dmil();
Serial << "..finish sensor capture." << mwx::crlf
<< " LTR308ALS: lumi=" << int(sensor.u32luminance) << mwx::crlf
<< " SHTC3 : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
<< " humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
;
Serial.flush();
step.next(STATE::TX);
}
The illuminance sensor can be obtained with .get_luminance() : uint32_t
.
The temperature and humidity sensor can be obtained as follows.
.get_temp_cent()
: int16_t
: temperature with 1℃ as 100 (2560 for 25.6℃)
.get_temp()
: float
: float value (25.6 for 25.6 ℃)
.get_humid_dmil()
: int16_t
: humidity at 1% as 100 (5680 for 56.8%)
.get_temp()
: float
: float value (56.8 for 56.8%)
The transmission procedure is the same as in the other ACT samples. Here, the settings are set to minimize one retransmission and retransmission delay.
pkt << tx_addr(0x00) // Parent Node 0x00
<< tx_retry(0x1) // 1 retry
<< tx_packet_delay(0, 0, 2); // minimul delay
The identifier FOURCHARS
and the sensor data are stored in the payload part of the packet. Of the values obtained, the temperature value is int16_t
, but is cast to uint16_t
because the data structure of the outgoing packet is to be stored unsigned.
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, uint32_t(sensor.u32luminance)
, uint16_t(sensor.i16temp)
, uint16_t(sensor.i16humid)
);
Requests transmission. If the send request succeeds, prepare the send completion city. Specify .clear_flag()
to wait for the completion event and set_timeout(100)
for timeout in case of emergency. The unit of 100 in the parameter is milliseconds [ms].
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
step.clear_flag(); // waiting for flag is set.
step.set_timeout(100); // set timeout
step.next(STATE::TX_WAIT_COMP);
}
This section determines timeouts and transmission completion events.
if (step.is_timeout()) { // maybe fatal error.
the_twelite.reset_system();
}
if (step.is_flag_ready()) { // when tx is performed
Serial << "..transmit complete." << mwx::crlf;
Serial.flush();
step.next(STATE::GO_SLEEP);
}
Processes sleepNow()
.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
This is a system event called when transmission is complete. Here, .set_flag()
is used to indicate completion.
This section includes a collection of procedures for going to sleep.
void sleepNow() {
step.on_sleep(false); // reset state machine.
// randomize sleep duration.
uint32_t u32ct = 1750 + random(0,500);
// output message
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
Serial.flush(); // wait until all message printed.
// do sleep.
the_twelite.sleep(u32ct);
}
Initialize the state of the state machine by .on_sleep(false)
before sleep. The parameter false' starts from
STATE::INIT(=0)` after returning from sleep.
Here, the time to wake up is set between 1750ms and 2250ms by a random number. This avoids continuous collisions with packets from other devices transmitting at a similar period.
Lines 8 and 9, this example goes to sleep waiting for output from the serial port. Usually, we want to minimize energy consumption, so we minimize (or eliminate) output from the serial port before sleep.
Line 12, to enter sleep, call the_twelite.sleep()
. In this call, the pre-sleep procedures of the hardware on the board are performed. For example, LEDs are turned off.
The sleep time is specified in ms as a parameter.
TWELITE PAL must always wake up once within 60 seconds to reset the watchdog timer. The sleep time must be specified not to exceed 60000
.
When the program wakes up from sleep, wakeup()
is called. After that, loop()
is called each time. Before wakeup()
, each peripheral such as UART and devices on the board are woken up. For example, it restarts the LED lighting control.
void wakeup() {
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
ACT PAL_AMB-UseNap can operate with lower energy consumption by sleeping while waiting for sensor data acquisition.
Send a PING wireless packet from one of the two serially connected TWELITEs and receive a PONG wireless packet back from the other.
Two of any of the following.
TWELITE DIP connected to UART with TWELITE R products/TWE-Lite-DIP/index.html), etc.
// 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.
// 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
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.
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.
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.
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
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.
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 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);
}
}
}
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.
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.
// 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.
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();
}
}
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.
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.
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.
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());
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.
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()
)
This is an Act sample using an I2C sensor device to measure temperature and humidity and send them periodically.
In this sample, using I2C sensors on our product (AMBIENT SENSE PAL or TWELITE ARIA BLUE / RED). However you can also use generic I2C devices by editing receiving/sending procedures. Connect a generic I2C device according to the following diagram.
This Act includes following features:
Sending / Receiving wireless packets
Configuration with "interactive settings mode" - <STG_STD>
Building a state machine with "SM_SIMPLE" - <SM_SIMPLE>
Send / Receive I2C commands
Sleep periodically for running with a button cell.
Parent
Run the Act on the
Children
- + -
#include <TWELITE>
#include <NWK_SIMPLE>// Network support
#include <STG_STD> // Interactive settings mode
#include <SM_SIMPLE> // Simple state machines
Header files are <NWK_SIMPLE>
(Simple network support), <STG_STD>
(Interactive settings mode) and <SM_SIMPLE>
(Simple state machines).
For your reference, this Act is using SHTC3 (TWELITE AMB PAL) or SHT40 (TWELITE ARIA). You can switch them by editing #ifdef
and they have same function interfaces for code portability. Both codes are similar because they were designed by the same manufacturer. (Please #define
either USE_SHTC3
or USE_SHT40
.)
/*** sensor select, define either of USE_SHTC3 or USE_SHT40 */
// use SHTC3 (TWELITE PAL)
#define USE_SHTC3
// use SHT40 (TWELITE ARIA)
#undef USE_SHT40
Below using the SHTC3.
#if defined(USE_SHTC3)
// for SHTC3
struct SHTC3 {
uint8_t I2C_ADDR;
uint8_t CONV_TIME;
bool setup() { ... }
bool begin() { ... }
int get_convtime() { return CONV_TIME; }
bool read(int16_t &i16Temp, int16_t &i16Humd) { ... }
} sensor_device;
The struct (class) SHTC3
has I2C-sensor-realted procedures. The member variable I2C_ADDR
is the I2C address and the CONV_TIME
is the waiting time for getting values. The instance created from the SHTC3
is sensor_device
.
Methods
setup()
Initialize the sensor instead of the constructor (uncalled due to the limitation of the MWX Library).
begin()
Begin getting values from the sensor. After starting, it is necessary to wait a certain amount of time until the appropriate sensor values are obtained.
get_convtime()
Get the waiting time to acquire the sensor value.
read(int&, int&)
Read the sensor value.
Each procedures are shown below.
bool setup() {
// here, initialize some member vars instead of constructor.
I2C_ADDR = 0x70;
CONV_TIME = 10; // wait time [ms]
return true;
}
Set the I2C address and the sensor value acquisition wait time (10 ms for the above) in the member variables.
In principle, these values are fixed values, so there is no need to set them. Valid examples of how to treat them as variables: Managing the conversion time required when operating a sensor with higher precision or selecting an I2C sub-address by setting them.
bool begin() {
// send start trigger command
if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
wrt << 0x60; // SHTC3_TRIG_H
wrt << 0x9C; // SHTC3_TRIG_L
} else {
return false;
}
return true;
}
Write commands to operate the sensor.
In the MWX Libary, you can choose two different ways to control the I2C line from the Wire class object. This act is using Helper functions.
Wire.get_writer(I2C_ADDR)
opens the I2C device and returns a object for reading / writing data. If the device is not available, the object wrt
is evaluated as false
.
The line wrt << 0x60;
is writing a data to the device with the stream operator <<
. The operator is often used for writing data in uint8_t
.
int get_convtime() {
return CONV_TIME;
}
Return the value of CONV_TIME
.
bool read(int16_t &i16Temp, int16_t &i16Humd) {
// read result
uint16_t u16temp, u16humd;
uint8_t u8temp_csum, u8humd_csum;
if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
rdr >> u16temp; // read two bytes (MSB first)
rdr >> u8temp_csum; // check sum (crc8)
rdr >> u16humd; // read two bytes (MSB first)
rdr >> u8humd_csum; // check sum (crc8)
} else {
return false;
}
// check CRC and save the values
if ( (CRC8_u8CalcU16(u16temp, 0xff) == u8temp_csum)
&& (CRC8_u8CalcU16(u16humd, 0xff) == u8humd_csum))
{
i16Temp = (int16_t)(-4500 + ((17500 * int32_t(u16temp)) >> 16));
i16Humd = (int16_t)((int32_t(u16humd) * 10000) >> 16);
} else {
return false;
}
return true;
}
Get values from the sensor device.
SHTC3 reads the sensor value after waiting a few ms after starting the sensor readout by begin()
. The sequence of sensor values is as follows.
0
Temperature Value (MSB side)
1
Temperature Value (LSB side)
2
CRC8 value of the byte 0 and 1
3
Humidity Value (MSB side)
4
Humidity Value (LSB side)
5
CRC8 value of the byte 3 and 4
In SHTC3, the order of data also changes depending on the parameters given at the start of sensor acquisition, but if you start with the 0x609C
command written by the above begin()
, temperature data comes first.
In begin()
, the data was written out, but here the data is read in. To read the data, similarly, Wire.get_reader()
creates a helper object rdr
. If there are no errors, rdr
returns true in the if clause. The second parameter 6
given to get_reader(I2C_ADDR, 6)
is the number of bytes to read. When this number of bytes have been read, the procedure terminates reading the I2C bus. (Some devices may work even if these procedures are omitted, but usually you should give an appropriate value.)
Read is done with the stream operator >>
. There are several other read methods. For details, see Helper functions for details. When using the stream operator, enter values into pre-declared variables of type uint8_t
, uint16_t
, or uint32_t
. rdr >> u16temp
reads from the 2-byte I2C bus for variables of type uint16_t
and stores them in big-endian format (first byte is upper byte).
Finally, 100 times the temperature [°C] and 100 times the humidity [%] are calculated and stored in i16Temp
and i16Humd
.Refer to the I2C device datasheet for the calculation formula.
Called once when the TWELITE is started. This function performs various initializations.
void setup() {
/*** SETUP section */
...
}
// application state defs
enum class STATE : uint8_t {
INTERACTIVE = 255,
INIT = 0,
SENSOR,
TX,
TX_WAIT_COMP,
GO_SLEEP
};
// simple state machine.
SM_SIMPLE<STATE> step;
void setup() {
...
/// init vars or objects
step.setup(); // initialize state machine
...
}
The state machine (state transition machine) is used to simplify the description in the loop()
statement that is called each time. Of course, you do not have to use SM_SMPLE
in this example to describe your application.
SM_SIMPLE is implemented in a very short code that allows simple management of transitions to states, timeouts, and flags. The states are defined in advance as enumerations. In the above example, it is enum class STATE
. The entity of the state machine is declared as SM_SMPLE<STATE> step
with the predefined enum STATE
as a parameter.
void setup() {
...
/// load board and settings objects
auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive settings mode) support
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support
...
}
BEHAVIOR is a collection of functions used in a program. It describes how to behave when various events occur.
In this example, we use two kinds of BEHAVIOR: interactive settings mode screen <STG_STD>
and simple relay network <NWK_SMPLE>
.
...
/// configure settings
// configure settings
set << SETTINGS::appname(FOURCHARS);
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);
In order to make the interactive settings mode configuration items suitable for the application you are describing, initialize STG_STG. /settings/stg_std.md) to set the initial settings.
SETTINGS::appname
: application name (string). It will be displayed on the first line on the interactive settings mode screen. Use a minimum string since there is not enough room for the number of characters on the screen.
SETTINGS::appid_default
: Default application ID. Execute this if you want your own application to have its own defined application ID.
SETTINGS::ch_default
: channel default. Execute if you want your own application to have its own default channel.
Then set.hide_items()
removes unnecessary settings items on the screen in the default interactive settings mode. If you do not mind displaying them all, you do not need to make this call.
// if SET(DIO12)=LOW is detected, start with intaractive mode.
if (digitalRead(PIN_DIGITAL::DIO12) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
This description starts in interactive settings mode when the DIO12 pin is LOW (GND level) and the device is powered up or reset. Read the pin status with digitalRead()
and reflect SETTINGS::open_at_start()
.
Set the state of the state machine to STATE::INTERACTIVE
, since it would be inconvenient if normal application processing is performed during interactive settings mode. In this state, no input or other processing is performed and the application stays in the same state.
// load values
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
// LID is configured DIP or settings.
LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
Finally, load the data in interactive settings mode. By calling set.reload()
, data written to the EEPROM is read. If no setting is made and there is no information in the EEPROM, it can be read as the specified value.
Here, option bits set.u32opt1()
and 8-bit logical ID set.u8devid()
are read. child machine).
/// configure system basics
the_twelite << set; // apply settings (from interactive settings mode)
nwk << set; // apply settings (from interactive settings mode)
nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)
...
Finally, the_twelite
and nwk
reflect (some of) the configuration information. This reflects information essential for wireless communication, such as application ID and channel. There is no explicit read code for these settings in the above, but set.reload()
reads the settings to the specified values if they are not set, or to the set values if they are.
/*** BEGIN section */
Wire.begin(); // start two wire serial bus.
Initialization for the I2C device.
// let the TWELITE begin!
the_twelite.begin();
/*** INIT message */
Serial << "--- TEMP&HUMID:" << FOURCHARS << " ---" << 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_twelite.begin()
is a procedure that declares the completion of the initialization of the MWX library. Without this procedure, the MWX library will not work properly.
Messages at startup, etc. are also displayed here.
void loop() {
do {
switch (step.state()) {
// states
case STATE::INIT:
...
break;
...
}
while(step.b_more_loop());
}
The loop()
is controlled by the SM_SIMPLE state machinestep
for control. The purpose is to concisely express the sequence of steps from sleep recovery, sensor value acquisition, wireless packet transmission, waiting for transmission to complete, and sleep.
The control structure of the above do while statement is described below. The state is determined by step.state()
. The conditional expression of while is step.b_more_loop()
. This is because there are cases where you want to process continuously without leaving loop()
when a state transitions from one state to another. In other words, if you transition to another state and leave the switch clause, the case clause of the next state will be called. Be careful with this behavior.
It is not convenient for the main loop to operate during interactive settings mode, so it is fixed in this state.
// start sensor capture
sensor_device.begin();
step.set_timeout(sensor_device.get_convtime()); // set timeout
step.next(STATE::SENSOR);
Starts acquiring sensor data. The set_timeout()
statement waits for the sensor to acquire data.
For a sensor that waits for a very long time, it is possible to extend the battery life by writing a process to sleep once here, but this example is omitted because of the complexity of the structure. If necessary, you can use Example of waiting for sleep.
if (step.is_timeout()) {
// the sensor data should be ready (wait some)
sensor_device.read(sensor.i16temp, sensor.i16humid);
Serial << "..finish sensor capture." << mwx::crlf
<< " : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
<< " humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
;
Serial.flush();
step.next(STATE::TX);
}
Get the sensor value by sensor_device.read()
and store the value in the sensor
structure.
First, a timeout check is performed by step.is_timeout()
. The starting point of timeout is step.set_timeout()
. If the timeout does not occur, the if clause is not executed and loop()
is left as is. The next hardware event (often a system timer, TickTimer, which generates an interrupt every 1 ms. /api-reference/predefined_objs/ticktimer.md), which generates an interrupt every 1ms), the TWELITE microcontroller will be in DOZE mode, where the CPU is in low-power standby.
As a wireless sensor, there is no need to output the result to the serial port of TWELITE on the sensor side, but the acquired value is displayed on the serial port to facilitate operation check. Here, Serial.flush()
is executed to wait for output, which is a description assuming that the serial port output does not finish before TWELITE goes to sleep. This process also causes battery drain, so either do not do Serial.flush()
or do not output to the serial port.
The div100() is a low-cost function to divide by 100.
step.next(STATE::GO_SLEEP); // set default next state (for error handling.)
// get new packet instance.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
...
}
Describes a communication procedure. In this state, there is no wait processing, etc., and the program transitions to the next state immediately after processing is executed. The reason why step.next(STATE::GO_SLEEP)
is written in advance is to avoid writing the same description in all places, since errors are detected in multiple places.
In if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
, the process is to create an object for the packet to be sent and execute the if clause if the object is successfully created.
// 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 (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
First, configure the transmission settings. The destination tx_addr(0x00)
is set to the parent machine 0x00
, the number of retransmissions tx_retry(0x1)
is set to 1, and the packet delay setting tx_packet_delay(0, 0, 2)
is set to 0 delay for the first transmission and 2 ms for the retransmission interval. The packet delay is set to 0 for the first transmission and 2ms for the retransmission interval.
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, uint16_t(sensor.i16temp)
, uint16_t(sensor.i16humid)
);
The identifier FOURCHARS
and the sensor data are stored in the payload part of the packet. Of the values obtained, the temperature value is int16_t
, but is cast to uint16_t
because the data structure of the outgoing packet is to be stored unsigned.
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
step.clear_flag(); // waiting for flag is set.
step.set_timeout(100); // set timeout
step.next(STATE::TX_WAIT_COMP);
}
Call pkt.transmit()
to request a transmission. The MWX library will process the request at the appropriate time.
If the send request is successful, ret
will be true. Initialize the flags to determine completion step.clear_flag()
, set a timeout step.set_timeout(100)
to handle unexpected errors such as failed transmissions, and set the next state to STATE::TX_WAIT_COMP
(see STATE::GO_SLEEP
is overridden).
Here, it waits for the completion of transmission. Judgment of timeout (in case of error) or transmission completion event is made.
if (step.is_timeout()) { // maybe fatal error.
the_twelite.reset_system();
}
if (step.is_flag_ready()) { // when tx is performed
Serial << "..transmit complete." << mwx::crlf;
Serial.flush();
step.next(STATE::GO_SLEEP);
}
Processes the sleepNow()
function. By calling this function, the TWELITE wireless microcontroller goes to sleep.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
This is a system event called when transmission is completed. Here .set_flag()
is called to set the flag of step
.
Procedures for going to sleep.
void sleepNow() {
step.on_sleep(false); // reset state machine.
// randomize sleep duration.
uint32_t u32ct = 1750 + random(0,500);
// output message
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
Serial.flush(); // wait until all message printed.
// do sleep.
the_twelite.sleep(u32ct);
}
Initialize the state of the state machine by .on_sleep(false)
before sleep. The parameter false' starts from
STATE::INIT(=0)` after returning from sleep.
Here, the time to wake up is set between 1750ms and 2250ms by a random number. This avoids continuous collisions with packets from other devices transmitting at a similar period.
Lines 8 and 9, this example goes to sleep waiting for output from the serial port. Usually, we want to minimize energy consumption, so we minimize (or eliminate) output from the serial port before sleep.
Line 12, to enter sleep, call the_twelite.sleep()
. In this call, the pre-sleep procedures of the hardware on the board are performed.
The sleep time is specified in ms as a parameter.
TWELITE PAL must always wake up once within 60 seconds and reset the watchdog timer. The sleep time must be specified not to exceed 60000
.
When the program wakes up from sleep, wakeup()
is called. After that, loop()
is called each time. Before wakeup()
, each peripheral such as UART and devices on the board are woken up. For example, it restarts the LED lighting control.
void wakeup() {
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
...
}
This section describes the specifications, limitations, notes in this document, and design memos for the C++ language used in the MWX library.
The application loop description is intended to be similar to the commonly used API system, but the implementation should be tailored to the characteristics of TWELITE.
TWENET is an event-driven code description, and it should be classed so that it can be handled. The above classifications will encapsulate the behavior of the application.
Event-driven and loop descriptions should be able to coexist.
Simplify procedures by classifying typical peripherals. Make them accessible by loop descriptions whenever possible.
Simplify the procedures for using the boards we sell, such as MONOSTICK/PAL, by creating classes. (For example, to automate the use of an external watchdog timer.
Application classes and board classes should be made available through a unified procedure, introducing the idea of polymorphism. (For example, to load application classes with several behaviors at startup, and to avoid defining the connection code of the TWENET C library each time).
There are no restrictions on the use of C++ functionality. For example, it provides a means to simplify typical procedures such as packet construction and decomposition, which are complicated in handling wireless packets.
The operator ->
should be avoided as much as possible, and the API should be based on reference types in principle.
gcc version 4.7.4
C++11 (For compiler support status, please refer to the general information.)
※ This is a description of what we know.
You can allocate memory with the new and new[] operators, but you cannot destroy the allocated memory; most C++ libraries that allocate memory dynamically are virtually unusable. It is used for objects that are created only once and not destroyed after that.
The constructor of the global object is not called.
Note: If necessary, you can initialize the constructor call by using the initialization function (setup()
) as shown in (new ((void*)&obj_global) class_foo();
).
Exception cannot be used.
Unable to use virtual function.
This section contains information that will help you understand the code when referring to the MWX library code.
Due to the limited time available for implementation, some of the details may not be sufficiently developed. For example, const is not fully taken into account in many classes.
We have the following policy for namespaces.
In principle, definitions are placed in a common namespace mwx.
We want to be able to use namespaces without identifiers, but we want to require identifiers for some definitions.
Class names should be relatively long, and those used by users should be defined as aliases.
Classes, Functions, and constants are defined within the namespace of mwx names (more precisely, mwx::L1 enclosed in inline namespace L1), with a few exceptions. inline namespace is specified so that definitions that require the specification of mwx:: can coexist with those that do not. The reason why inline namespace is specified is to allow definitions that require the specification of mwx:: to coexist with those that do not.
Most of the definitions do not require namespace names to be specified by using namespace. These specifications are made in using_mwx_def.hpp
in the library.
// at some header file.
namespace mwx {
inline namespace L1 {
class foobar {
// class definition...
};
}
}
// at using_mwx_def.hpp
using namespace mwx::L1; // Definitions in mwx::L1 can be accessed without mwx::.
// But mwx::L2 needs mwx::.
Exceptionally, relatively short names can be specified as mwx::crlf
, mwx::flush
. These are placed in the inline namespace mwx::L2; using namespace mwx::L2; will allow them to be used without specifying the namespace name.
Also, some class names have a using specification.
The std::make_pair
used in the MWX library is specified using.
Since virtual functions (virtual) and run-time type information (RTTI) are not available, and even if they were available, they would be difficult to perform, CRTP (Curiously recurring template pattern) is used as an alternative design method. CRTP is a template pattern for calling methods of a child class from the parent class from which it is inherited.
The following example shows how to implement an interface called interface()
in a Derived
class that inherits from Base
, and calls the Derived::print()
method from Base
.
template <class T>
class Base {
public:
void intrface() {
T* derived = static_cast<T*>(this);
derived->prt();
}
};
class Derived : public class Base<Derived> {
void prt() {
// print message here!
my_print("foo");
}
}
The following are the main classes used in the MWX library.
Basic parts of event processingmwx::appdefs_crtp
state machine public mwx::processev_crtp
stream mwx::stream
In the CRTP class, the class from which it inherits is different for each instance. For this reason, it is not possible to cast it to a parent class and treat it as a member of the same family, nor is it possible to use advanced polymorphism such as virtual functions or RTTI (runtime type information).
The following is an example of implementing the above CRTP example with a virtual function: CRTP cannot manage instances together in the same array as in Base* b[2].
class Base {
virtual void prt() = 0;
public:
void intrface() { prt(); }
};
class Derived1 : public Base {
void prt() { my_print("foo"); }
};
class Derived2 : public Base {
void prt() { my_print("bar"); }
};
Derived1 d1;
Derived2 d2;
Base* b[2] = { &d1, &d2 };
void tst() {
for (auto&& x : b) { x->intrface(); }
}
The MWX library solves this problem by defining a dedicated class for storing class instances of CRTP and defining a similar interface to this class. An example code is given below.
class VBase {
public:
void* p_inst;
void (*pf_intrface)(void* p);
public:
void intrface() {
if (p_inst != nullptr) {
pf_intrface(p_inst);
}
}
};
template <class T>
class Base {
friend class VBase;
static void s_intrface(void* p) {
T* derived = static_cast<T*>(p);
derived->intrface();
}
public:
void intrface() {
T* derived = static_cast<T*>(this);
derived->prt();
}
};
class Derived1 : public Base<Derived1> {
friend class Base<Derived1>;
void prt() { my_print("foo"); }
};
class Derived2 : public Base<Derived2> {
friend class Base<Derived2>;
void prt() { my_print("bar"); }
};
Derived1 d1;
Derived2 d2;
VBase b[2];
void tst() {
b[0] = d1;
b[1] = d2;
for (auto&& x : b) {
x.intrface();
}
}
The VBase class member variable p_inst stores a pointer to an object of type Base , and pf_intrface is a member function pointer to Base::s_intrface. Base::s_intrface invokes the T::intrface method by being passed an object instance of itself as an argument and static_casting it to the T type.
Storage in VBase is implemented here by overloading the = operator (see below for source examples).
In the above example, when making a call to b[0].intrface(), Base::s_intrface() will be called with reference to the VBase::pf_intrface function pointer. In addition, a call to Derived1::intrface() will be made. This part is expected to be expanded inline by the compiler.
It is also possible to perform a conversion from the VBase type to the original Derived1 or Derived2 through a forced cast, but there is no way to directly know the type of the pointer stored in void*. Although there is no completely safe way to do this, a unique ID (TYPE_ID) is provided for each class as shown below, and the ID is checked when the cast is executed (get() method). If the get() method is called with a different type, an error message will be displayed.
B
If a pointer is stored as a Base type, it may not be correctly converted to a T type (e.g., when T has multiple inheritance), so a static_assert is used to determine at compile time that the pointer is derived from a Base type by using is_base_of in <type_trails>.
#include <type_trails>
class Derived1 : public Base<Derived1> {
public:
static const uint8_t TYPE_ID = 1;
}
class Derived1 : public Base<Derived1> {
public:
static const uint8_t TYPE_ID = 2;
}
class VBase {
uint8_t type_id;
public:
template <class T>
void operator = (T& t) {
static_assert(std::is_base_of<Base<T>, T>::value == true,
"is not base of Base<T>.");
type_id = T::TYPE_ID;
p_inst = &t;
pf_intrface = T::s_intrface;
}
template <class T>
T& get() {
static_assert(std::is_base_of<Base<T>, T>::value == true,
"is not base of Base<T>.");
if(T::TYPE_ID == type_id) {
return *reinterpret_cast<T*>(p_inst);
} else {
// panic code here!
}
}
}
Derived1 d1;
Derived2 d2;
VBase b[2];
void tst() {
b[0] = d1;
b[1] = d2;
Derived1 e1 = b[0].get<Derived1>(); // OK
Derived2 e2 = b[1].get<Derived2>(); // OK
Derived2 e3 = b[1].get<Derived1>(); // PANIC!
}
The microcontroller in the TWELITE module does not have enough memory nor does it have advanced memory management. However, the area from the end of the microcontroller's memory map to the stack area is available as a heap area, which can be allocated as needed. An overview of the memory map is shown in the figure below, where APP is the RAM area allocated by the application code, HEAP is the heap area, and STACK is the stack area.
|====APP====:==HEAP==.. :==STACK==|
0 32KB
Even if it is not possible to delete, the new operator may be useful in some situations. For this reason, the new and new[] operators are defined as follows: pvHear_Alloc() is a function for allocating memory provided by the semiconductor library, and the same is true for u32HeapStart and u32HeapEnd. 0xdeadbeef is a dummy address. Please do not point out that it is strange that beef is dead.
void* operator new(size_t size) noexcept {
if (u32HeapStart + size > u32HeapEnd) {
return (void*)0xdeadbeef;
} else {
void *blk = pvHeap_Alloc(NULL, size, 0);
return blk;
}
}
void* operator new[](size_t size) noexcept {
return operator new(size); }
void operator delete(void* ptr) noexcept {}
void operator delete[](void* ptr) noexcept {}
Since exceptions cannot be used, there is no way to deal with failures. Also, if you continue to allocate without being aware of the memory capacity, there is a possibility of interference with the stack area.
The MWX library does not use the container classes provided by the standard library, considering the small resources of the microcontroller and the lack of dynamic memory allocation, but defines two simple container classes. The container classes have defined iterators and begin() and end() methods, so you can use some of the range for statements and STL algorithms.
smplbuf<int16_t, alloc_local<int16_t, 16>> buf;
buf.push_back(-1); // push_back() は末尾に追加
buf.push_back(2);
...
buf.push_back(10);
//範囲for文
for(auto&& x : buf) { Serial << int(x) << ',' }
//アルゴリズム std::minmax
auto&& minmax = std::minmax_element(buf.begin(), buf.end());
Serial << "Min=" << int(*minmax.first)
<< ",Max=" << int(*minmax.second);
smplbuf
It is an array class that manages the maximum area (capacity) and the usable area (size) whose size can be specified within the maximum area. This class also implements the stream interface, so data can be written using the << operator.
smplque
The FIFO queue is implemented. The size of the queue is determined by template parameters. There is also a template argument to manipulate the queue using interrupt inhibition.
In the container class, the memory allocation method is specified as a parameter of the template argument.
alloc_attach
Specify the buffer memory that has already been allocated. This is used when you want to manage the memory area allocated for the C library, or when you want to process the same buffer area as a fragmented area.
alloc_static
Allocate as a static array in the class. The size is determined in advance or used as an area for temporary use.
alloc_heap
Allocate to the heap area. Once allocated to the system heap, it cannot be discarded, but it is suitable for use in initialization to allocate an area according to application settings.
In the MWX library, variable number arguments are used for operations on byte sequences, bit sequences, and printf equivalent operations. The example below shows the process of setting 1 to the specified bit position.
// packing bits with given arguments, which specifies bit position.
// pack_bits(5, 0, 1) -> (b100011) bit0,1,5 are set.
// The first function of recursive extraction
template <typename Head>
constexpr uint32_t pack_bits(Head head) { return 1UL << head; }
// Extract head and transfer the rest of the parameters to pack_bits
//by recursive call
template <typename Head, typename... Tail>
constexpr uint32_t pack_bits(Head head, Tail&&... tail) {
return (1UL << head) | pack_bits(std::forward<Tail>(tail)...);
}
// コンパAfter ILL, the following two will have the same result.
constexpr uint32_t b1 = pack_bits(1, 4, 0, 8);
// b1 and b2 are the same!
const uint32_t b2 = (1UL << 1)|(1UL << 4)|(1UL << 0)|(1UL << 8);
In this process, the parameter pack of template (typename... part of template) to perform recursive processing to expand the arguments. In the above example, since constexpr is specified, the calculation is done at compile time and the result is equivalent to macro definition or const value specification such as b2. It can also behave as a function that dynamically calculates variables as arguments.
In the following example, the expand_bytes function is used to store a value in a local variable from the received packet data string. In the case of using a parameter pack, since the type of each argument can be known, it is possible to store values of different sizes and types from the byte string of the received packet, as shown below.
auto&& rx = the_twelite.receiver.read(); // received packet
// Variable that stores the contents of the packet after expansion
// The payload of a packet is a sequence of bytes, arranged as follows.
// [B0][B1][B2][B3][B4][B5][B6][B7][B8][B9][Ba][Bb]
// <message ><adc* ><vcc* ><timestamp* >
// * Numerical types are big-endian.
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
An iterator is an abstraction of a pointer, which has the effect of making it possible to access data structures as if they were pointers, even if the data structures are not memory-contiguous, for example.
The following example shows the use of an iterator for a FIFO queue that cannot be accessed continuously with a normal pointer, and also an iterator that extracts only a specific member of the FIFO queue structure (the X axis in the example).
//A queue with 5 elements, whose elements are the 4-axis structures of XYZT
smplque<axis_xyzt, alloc_local<axis_xyzt, 5> > que;
// Input data for testing.
que.push(axis_xyzt(1, 2, 3, 4));
que.push(axis_xyzt(5, 2, 3, 4));
...
// Access using iterators as structures
for (auto&& e : v) { Serial << int(e.x) << ','; }
// Extract the X axis in the queue.
auto&& vx = get_axis_x(que);
// Access with X-axis iterator
for (auto&& e : vx) { Serial << int(e) << ','; }
// Since it is an iterator of int16_t elements,
//the STL algorithm (max-min) can be used.
auto&& minmax = std::minmax_element(vx.begin(), vx.end());
The following is an excerpt of the implementation of an iterator for the smplque class. In this iterator, the queue object is managed by its entity and its index. The part of the queue that is discontiguous in memory (ring buffer structure where the next to the tail must point to the beginning) is solved by smplque::operator []. If the addresses of the objects match and the indices match, the iterators point to the same thing.
This implementation part also includes the typedefs required by , allowing more STL algorithms to be applied.
class iter_smplque {
typedef smplque<T, alloc, INTCTL> BODY;
private:
uint16_t _pos; // index
BODY* _body; // point to original object
public: // for <iterator>
typedef iter_smplque self_type;
typedef T value_type;
typedef T& reference;
typedef T* pointer;
typedef std::forward_iterator_tag iterator_category;
typedef int difference_type;
public: // pick some methods
inline reference operator *() {
return (*_body)[_pos];
}
inline self_type& operator ++() {
_pos++;
return *this;
}
};
構造体を格納したコンテナ中の、特定構造体メンバーだけアクセスするイテレータは少々煩雑です。構造体のメンバーにアクセスするメンバー関数を予め定義しておきます。このメンバー関数をパラメータ(R& (T::*get)()
)としたテンプレートを定義します。Iter
はコンテナクラスのイテレータ型です。
Iterators that access only specific structure members in the container that contains the structure are a bit complicated. Define a member function to access the structure members in advance. Next, define a template with this member function as a parameter (R& (T::*get)()
). "Iter
" is the iterator type of the container class.
struct axis_xyzt {
int16_t x, y, z;
uint16_t t;
int16_t& get_x() { return x; }
int16_t& get_y() { return y; }
int16_t& get_z() { return z; }
};
template <class Iter, typename T, typename R, R& (T::*get)()>
class _iter_axis_xyzt {
Iter _p;
public:
inline self_type& operator ++() {
_p++;
return *this; }
inline reference operator *() {
return (*_p.*get)(); }
};
template <class Ixyz, class Cnt>
class _axis_xyzt_iter_gen {
Cnt& _c;
public:
_axis_xyzt_iter_gen(Cnt& c) : _c(c) {}
Ixyz begin() { return Ixyz(_c.begin()); }
Ixyz end() { return Ixyz(_c.end()); }
};
// It's long, so shorten it with using
template <typename T, int16_t& (axis_xyzt::*get)()>
using _axis_xyzt_axis_ret = _axis_xyzt_iter_gen<
_iter_axis_xyzt<typename T::iterator, axis_xyzt, int16_t, get>, T>;
// Generator to extract X axis
template <typename T>
_axis_xyzt_axis_ret<T, &axis_xyzt::get_x>
get_axis_x(T& c) {
return _axis_xyzt_axis_ret<T, &axis_xyzt::get_x>(c);
}
The operator *
that accesses the value calls the member function described above. The *_p
is the axis_xyzt
structure, and (*_p.*get)()
calls _p->get_x()
if &axis_xyzt::get_x
is specified in T::*get
.
The _axis_xyzt_iter_gen
class implements only begin()
, end()
and generates the above iterators. Now you can use range for statements and algorithms.
This class name is very long and difficult to write in the source code. We will prepare a generator function to generate this class. In the example below, it is get_axis_x()
in the last line. By using this generator function, the description becomes as simple as auto&& vx = get_axis_x(que);
as shown in the beginning.
This iterator for extracting only the axes can also be used with the smplbuf
class of array type as well.
In order to describe the application behavior by user-defined classes, typical handlers need to be defined as mandatory methods, but it is complicated to define all the other numerous interrupt handlers, event handlers, and state machine state handlers. Ideally, only those defined by the user should be defined, and only that code should be executed.
class my_app_def {
public: // Define required methods
void network_event(twe::packet_ev_nwk& pEvNwk) {}
void receive(twe::packet_rx& rx) {}
void transmit_complete(twe::packet_ev_tx& pEvTx) {}
void loop() {}
void on_sleep(uint32_t& val) {}
void warmboot(uint32_t& val) {}
void wakeup(uint32_t& val) {}
public: // It is cumbersome to make these descriptions mandatory.
// DIO interrupt handler: There are 20 types.
// DIO event handler: There are 20 types.
// Timer interrupt handler: There are five types
// Timer event handlers: there are 5 types
// ...
}
In the MWX library, a large number of DIO interrupt handlers (on TWELITE hardware, a single interrupt, but for ease of use, a handler is assigned to each DIO) are defined as empty handlers using templates, and user-defined member functions are defined by specializing the templates.
// hpp file
class my_app_def : class app_defs<my_app_def>, ... {
// Empty handler
template<int N> void int_dio_handler(uint32_t arg, uint8_t& handled) { ; }
...
// Implement only number 12.
public:
// Callback function called from TWENET
uint8 cbTweNet_u8HwInt(uint32 u32DeviceId, uint32 u32ItemBitmap);
};
// cpp file
template <>
void my_app_def::int_dio_handler<12>(uint32_t arg, uint8_t& handled) {
digitalWrite(5, LOW);
handled = true;
return;
}
void cbTweNet_u8HwInt(uint32 u32DeviceId, uint32 u32ItemBitmap) {
uint8_t b_handled = FALSE;
switch(u32DeviceId) {
case E_AHI_DEVICE_SYSCTRL:
if (u32ItemBitmap & (1UL << 0)){int_dio_handler<0>(0, b_handled);}
if (u32ItemBitmap & (1UL << 1)){int_dio_handler<1>(1, b_handled);}
...
if (u32ItemBitmap & (1UL << 12)){int_dio_handler<12>(12, b_handled);}
...
if (u32ItemBitmap & (1UL << 19)){int_dio_handler<19>(19, b_handled);}
break;
}
}
The actual user-described code has been simplified by macroizing and including header files, but the above includes the code necessary for the explanation.
The my_app_def::cbTweNet_u8HwInt()
is called from the interrupt handler from TWENET. in the cpp file, only int_dio_handler<12>
is instantiated with the specialization described in it. file is instantiated from a template in the hpp file. The rest are instantiated from templates in the hpp file.
case E_AHI_DEVICE_SYSCTRL:
if (u32ItemBitmap & (1UL << 0)){;}
if (u32ItemBitmap & (1UL << 1)){;}
...
if (u32ItemBitmap & (1UL << 12)){
int_dio_handler<12>(12, b_handled);}
...
if (u32ItemBitmap & (1UL << 19)){;}
break;
// ↓ ↓ ↓
// 結局、このように最適化されることが期待できる。
case E_AHI_DEVICE_SYSCTRL:
if (u32ItemBitmap & (1UL << 12)){
// int_dio_handler<12> もinline展開
digitalWrite(5, LOW);
handled = true;
}
break;
Eventually, we can expect that the compiler optimization will determine that codes other than number 12 are meaningless and disappear from the code (however, we do not guarantee that they will be optimized as described above).
In other words, in user code, if you want to define the behavior at interrupt 12, just write int_dio_handler<12>
(Note: to enable DIO interrupt, you need to call attachInterrupt()
). Handlers that are not registered are expected to be low-cost calls due to compile-time optimization.
The stream class is mainly used for input/output of UART (serial port), and MWX library mainly defines procedures for output. But some of them are also defined for input.
This section describes the implementation required by the derived class.
template <class D>
class stream {
protected:
void* pvOutputContext; // TWE_tsFILE*
public:
inline D* get_Derived() { return static_cast<D*>(this); }
inline D& operator << (char c) {
get_Derived()->write(c);
return *get_Derived();
}
};
class serial_jen : public mwx::stream<serial_jen> {
public:
inline size_t write(int n) {
return (int)SERIAL_bTxChar(_serdef._u8Port, n);
}
};
The above is an implementation of the write()
method that writes a single character. The stream<serial_jen>
of the parent class accesses the serial_jen::write()
method using the get_Drived()
method to perform casting.
Define methods such as write()
, read()
, flush()
, and available()
as needed.
For formatting output, we use Marco Paland's printf library, which needs to be implemented for use with the MWX library. In the following example, the derived class serial_jen
needs to define the vOutput()
method for 1-byte output, and save the auxiliary information for output in the parent class pvOutputContext
since vOutput()
is a static method. The other is to save the auxiliary information in the pvOutputContext
of the parent class since vOutput()
is a static method.
template <class D>
class stream {
protected:
void* pvOutputContext; // TWE_tsFILE*
public:
inline tfcOutput get_pfcOutout() { return get_Derived()->vOutput; }
inline D& operator << (int i) {
(size_t)fctprintf(get_pfcOutout(), pvOutputContext, "%d", i);
return *get_Derived();
}
};
class serial_jen : public mwx::stream<serial_jen> {
using SUPER = mwx::stream<serial_jen>;
TWE_tsFILE* _psSer; // Low-level structure for serial output
public:
void begin() {
SUPER::pvOutputContext = (void*)_psSer;
}
static void vOutput(char out, void* vp) {
TWE_tsFILE* fp = (TWE_tsFILE*)vp;
fp->fp_putc(out, fp);
}
};
By get_pfcOutput()
, the vOutput()
function defined in the derived class is specified, and pvOutputContext
is passed as its parameter. In the above example, when the <<
operator is called with int type, serial_jen::vOutput()
and TWE_tsFILE*
which is already set for UART are passed to the fctprintf()
function.
In the Wire class, it is necessary to manage the communication from start to end when sending and receiving with a 2-wire device. This section describes the contents of the description of using the worker object.
if (auto&& wrt = Wire.get_writer(SHTC3_ADDRESS)) {
Serial << "{I2C SHTC3 connected.";
wrt << SHTC3_TRIG_H;
wrt << SHTC3_TRIG_L;
Serial << " end}";
}
This is an excerpt of the periph_twowire::writer
class, which inherits from mwx::stream<writer>
to implement the stream interface, and implements the write()
and vOutput()
methods to use the steam interface. To use the steam interface, the write()
and `vOutput() methods are implemented.
The constructor calls the method to start communication for 2-wire serial and the destructor calls the method to end communication. Also, the operator bool()
operator returns true if the communication of the 2-wire serial device is successfully started.
class periph_twowire {
public:
class writer : public mwx::stream<writer> {
friend class mwx::stream<writer>;
periph_twowire& _wire;
public:
writer(periph_twowire& ref, uint8_t devid) : _wire(ref) {
_wire.beginTransmission(devid); // Start communication with constructor
}
~writer() {
_wire.endTransmission(); // Communication terminated by destructor
}
operator bool() {
return (_wire._mode == periph_twowire::MODE_TX);
}
private: // stream interface
inline size_t write(int n) {
return _wire.write(val);
}
// for upper class use
static void vOutput(char out, void* vp) {
periph_twowire* p_wire = (periph_twowire*)vp;
if (p_wire != nullptr) {
p_wire->write(uint8_t(out));
}
}
};
public:
writer get_writer(uint8_t address) {
return writer(*this, address);
}
};
class periphe_twowire Wire; // global instance
// ユーザコード
if (auto&& wrt = Wire.get_writer(SHTC3_ADDRESS)) {
Serial << "{I2C SHTC3 connected.";
wrt << SHTC3_TRIG_H;
wrt << SHTC3_TRIG_L;
Serial << " end}";
}
The get_writer()
method creates an object wrt
. Due to the Return Value Optimization (RVO) of the C++ compiler, the writer
is created directly in the wrt
, so no copy is made and the bus running in the constructor is not initialized multiple times. However, RVO is not guaranteed by the C++ specification, and just in case, the MWX library defines copy, delete assignment operators, and move constructors (although it is unlikely that move constructors will be evaluated).
The wrt in the if clause is first initialized by the constructor and starts communication at the same time. If there is no error at the start of communication, the bool operator at the time of conditional judgment returns true, and the processing in the scope of the if clause takes place. If there is no error at the start of communication, the bool operator at the conditional judgment returns true, and the processing in the if clause scope is performed. When the scope is exited, the destructor terminates the 2-wire serial bus. If there is no communication partner, false will be returned and the wrt object will be destroyed.
It overrides the definition of operator << (int)
, which is specific to Wire and SPI. The default behavior of the stream is to convert numeric values to strings and output them, but Wire and SPI rarely write numeric strings to the bus, and on the contrary, we often want to input literals of numeric type such as configuration values. We will change this behavior.
writer& operator << (int v) {
_wire.write(uint8_t(v & 0xFF));
return *this;
}
In this example, the int type values are truncated to 8 bits and the values are output.
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 sample cannot communicate with App_TweLite.
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.
Parent Node
At a minimum, wire M1=GND, DI1:button, DO1:LED.
Child Node
At a minimum, wire M1=open, DI1:button, DO1:LED.
// 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.
/*** 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
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.
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()
.
// 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
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
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.
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.
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.
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.
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.
the_twelite.begin(); // start twelite!
Execute the_twelite.begin()
at the end of the setup()
function.
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.
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();
}
}
}
}
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.
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();
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.
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.
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);
}
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.
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.
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.
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.
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)
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.
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.
When a wireless packet is received, on_rx_packet()
is called as a receive event.
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());
}
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.
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; }
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.