This is the multi-page printable view of this section. Click here to print...

Return to the regular view of this page

As of 2025-07-24

MWX Library

Latest version

1 - General

About the MWX Library
The MWX Library is designed to make programming for TWELITE modules easier and more extensible. Based on the TWENET C Library previously used in MWSDK, the MWX Library serves as the application development layer.
+-----------------------+
|   act (USER APPs)...  |
+-----------------------+
| MWX C++ LIB           |
+---------------+       |
| TWENET C LIB  |       |
+------------+----------+
| MAC LAYER  | AHI APIs |
+-----------------------+
| TWELITE HARDWARE      |
+-----------------------+

The name of the MWX library is Mono Wireless C++ Library for TWELITE. “MW” comes from MonoWireless, and “C++” → “CXX” → double X → “WX”. By overlapping this MW and WX, it became MWX. Code written using this library is called “act”.

Notations

This section describes the notations used in this explanation.

auto&&

Called a universal reference, it is often used in the standard library. In this library as well, auto&& is used in most cases.

About namespaces

namespace, inline namespace, and using are used to redefine names. Some parts are omitted even in the explanation.

Limitations (TWENET)

The MWX library is not developed with the purpose of supporting all the underlying libraries and functions (functions in the TWENET C library, microcontroller peripheral functions provided by semiconductor vendors, IEEE802.15.4 functions).

Limitations (Use of C++)

The MWX library is written in C++ and the act is also written in C++. However, not all features of C++ can be used. Please note the following points especially.

  • Memory allocation with new and new[] operators is possible, but the allocated memory cannot be freed. Most dynamic memory allocations in C++ libraries are practically unusable.
  • Constructors of global objects are not called.
  • Note: If necessary, you can initialize including constructor calls by initializing in the setup function (setup()) like new ((void*)&obj_global) class_foo();.
  • Exceptions (exception) cannot be used.
  • Virtual functions (virtual) cannot be used.
  • Due to the above restrictions, only part of the C++ standard library such as STL can be used.

※ This is based on what we are aware of.

About the Library Source Code

The source code can be referenced from the following.

1.1 - License

Warranty and License

Warranty and License

Unless otherwise noted, the contents of this package are subject to either the MONO WIRELESS Software License Agreement (MW-SLA) or the MONO WIRELESS Open Source Software License Agreement (MW-OSSLA).

This document is also considered part of the library package and is covered under the MW-SLA.

This software is not officially supported by MONO WIRELESS Co., Ltd. We may not be able to respond to inquiries. Thank you for your understanding.

MONO WIRELESS Co., Ltd. does not guarantee fixes or improvements in response to bug reports.

Please note that functionality may also be affected by your environment, such as the installation package or other system dependencies.

1.2 - Terminology

Terminology Guide
This document explains terminology used throughout this guide.

General Terminology

SDK (TWELITE SDK, MWSDK)

Software Development Kit (SDK)

IEEE802.15.4

A wireless standard used by TWELITE wireless modules. As long as you are using the MWX library, you generally do not need to be concerned with the details of the protocol.

Packet

The smallest unit of transmission in wireless communication. The maximum size varies depending on the communication method and settings, but in MWX’s standard <NWK_SIMPLE> communication, up to 90 bytes can be transmitted in one packet.

Payload

While the literal meaning is “cargo”, it refers to the actual data content included in a wireless packet.

Node

Literally means “point” or “junction”, but in this context it refers to a wireless device within a wireless network.

MWX Library-Specific Terminology

Act

A program created using this library. Refers to either the source code or the running program.

Behavior

A program written in an event-driven style, defined within an Act. Refers to either the source code or the running program.

A behavior is written as a single class definition that encapsulates callbacks from TWENET, events, and interrupt handling. MWX defines three types of behaviors:

  • Application Behavior: A user-defined class that describes application logic in an event-driven format.
  • Board Behavior: A class that simplifies usage of functions provided by the TWELITE module board.
  • Network Behavior: A class that simplifies wireless network procedures.

Behavior names are enclosed in angle brackets < >. For example, the behavior name for the simple repeater network is <NWK_SIMPLE>.

Class Object

In this documentation, class objects refer to globally declared objects provided by the library, such as Serial or Wire. These class objects can be used immediately or after an initialization procedure.

Class objects that consume a relatively large amount of memory allocate memory during the initialization process according to the provided parameters (via .setup() or .begin() methods).

C++

The C++ programming language.

C++11

A version of the C++ standard. It refers to the C++ standard established in 2011 by ISO. It added significant features compared to the previous C++03 standard. There are newer versions such as C++14 and C++17.

Class

A construct that groups data and procedures related to that data. It is like a structure that also contains procedures to operate on that structure. This is a simplified explanation; please refer to specialized books for deeper understanding.

In C++, the keywords struct and class are essentially the same; whichever keyword is used, it defines a class.

struct myhello {
  int _i;
  void say_hello() { printf("hello %d\n", _i); }
};

If the above class definition were done in C language, it might look like this:

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

Wrapper Class

A class that encapsulates existing C language libraries or internal structures, adding C++-specific features to improve usability. In this documentation, you might see descriptions such as “wrapped the ~ structure”.

Method / Member Function

A function defined inside a class and bound to that class.

struct myhello {
  int _i;
  void say_hello() { printf("hello %d\n", _i); } // Method
};

Object / Instance

An instantiated class (allocated in memory).

void func() {
    myhello obj_hello; // obj_hello is an object of class myhello
    obj_hello._i = 10;
    obj_hello.say_hello();
}

In this documentation, “object” and “instance” are used interchangeably.

Constructor

An initialization procedure called when an object is created.

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); // Constructor is called here and _i is set to 10
}

Destructor

A procedure paired with the constructor, called when an object is destroyed.

struct myhello {
  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
};

Abstract Class

In C++, polymorphism is achieved through virtual classes. Specifically, a class with pure virtual functions defined by the virtual keyword.

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!"); }
};

Scope

In C/C++ languages, scope is defined by { }. Objects created inside this scope are destroyed when exiting the scope, and their destructors are called.

The following example explicitly sets scope. The object helo2 is destroyed and its destructor called when execution reaches line 8.

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

The MWX library uses the following syntax. Here, an object declared inside the condition expression of an if statement (which is not allowed in older C89-style C) is valid only within the {} block of the if statement.

struct myhello {
  int _i;
  void say_hello() { printf("hello! %d\n", _i); }
  operator bool() { return true; } // Operator for if() condition

  myhello(int i = 0) : _i(i) {} // Constructor
  ~myhello() { printf("good bye! %d\n", _i); } // Destructor
};

// Generator function that creates a myhello object
myhello gen_greeting() { return my_hello(); }

void my_main() {
  if (myhello x = gen_greeting()) {
    // The myhello object x is valid inside the if block
    x.say_hello();
  }
  // Object x is destroyed when exiting the if block
}

For example, in a dual serial bus, there are procedures for start and end, and the bus is operated by the object only during that time. After the object is created, if the bus connection is appropriate, the true branch of the if statement is executed, and the created object performs bus write or read operations. When the bus read/write operations are finished, the if statement is exited, and the destructor is called, performing the bus release procedure.

const uint8_t DEV_ADDR = 0x70;
if (auto&& wrt = Wire.get_writer(DEV_ADDR)) { // Initialize bus and check connection
	wrt(SHTC3_TRIG_H); // Write
	wrt(SHTC3_TRIG_L);
} // Bus release procedure

Namespace

Namespaces are actively used in C++ to avoid name collisions. To access definitions inside a namespace, use the :: operator.

namespace MY_NAME { // Namespace declaration
  const uint8_t MYVAL1 = 0x00;
}

...
void my_main() {
  uint8_t i = MY_NAME::MYVAL1; // Reference MY_NAME
}

Template

Templates can be considered an extension of C language macros.

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 10 integers
myary<char, 128> a2; // Array of 128 chars

This example defines a simple array class. T and N are template parameters where T is a type and N is a number, defining an array class of N elements of type T.

nullptr

In C++11, the null pointer is written as nullptr. NULL is a macro representing 0, but nullptr often has a distinct entity different from 0.

Reference Type

C++ supports reference types. Similar to pointers, but with the constraint that they must always refer to an object.

Functions with reference parameters like below can modify the value of i inside incr().

void incr(int& lhs, int rhs) { lhs += rhs; }

void my_main() {
  int i = 10; j = 20;
  incr(i, j);
}

In this template example, the return type of operator[] is changed to T&. This allows direct assignment to internal array data like a[0] = 1.

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

Type Inference

C++11 introduced the auto keyword for type inference. The compiler deduces the object type from the initializer, allowing omission of explicit type names. This is effective when template class names become very long.

In this documentation, the universal reference auto&& is often used. Consider it as a way to write code without worrying about whether the parameter is passed by reference or not.

auto&& p = std::make_pair("HELLO", 5);
       // std::pair<const char*, int>

Container

A class that holds multiple objects of a specific data type, such as arrays. The array class myary shown in the template example is also a container.

Iterator, .begin(), .end()

An extension of pointers in C language (pointers can also be used in C++). Pointers in C language are a means to access consecutive memory elements from start to end. Consider a FIFO queue. The simplest implementation is a ring buffer, which does not have contiguous memory. Even so, using iterators allows you to write code similarly to pointer use.

.begin() and .end() methods are used to obtain iterators. .begin() returns an iterator pointing to the container’s first element. .end() returns an iterator pointing to one past the last element. The reason it points past the last element, not the last itself, is for clarity in loop constructs like for or while, and for handling containers with zero elements.

my_queue que; // my_queue is a queue class

auto&& p = que.begin();
auto&& e = que.end();

while(p != e) {
  some_process(*p);
  ++p;
}

Here, for each element in que, the iterator p is used to apply some_process(). The iterator p is incremented with ++ to point to the next element. Even for containers without contiguous memory, iterators allow pointer-like processing.

Because .end() points past the last element, the loop termination condition is simply (p != e). If the queue is empty, .begin() equals .end() (both point to the position where the first element would be inserted).

For containers with contiguous memory, iterators are usually normal pointers, so the overhead is minimal.

C++ Standard Library

The C++ standard library includes the STL (Standard Template Library). The MWX library uses parts of it.

Algorithm

For example, finding the maximum or minimum value was written separately for each type in C language. Such code is often the same except for the type part. C++ uses template and iterators to write these operations independent of type. This is called algorithms.

// Returns iterator to the maximum element in the range
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;
}

The above is an algorithm to find the maximum value. It is type-independent (generic programming).

#include <algorithm>

auto&& minmax = std::minmax_element( // Get min and max algorithm
  que.begin(), que.end());

auto&& min_val = *minmax.first;
auto&& max_val = *minmax.second;

Here, the iterator of que is passed to std::minmax_element, an algorithm defined in the C++ standard library. The return value is a std::pair of two values. The algorithm requires the elements to support <, >, and == operators for comparison. The return type is deduced from the iterator type.

1.3 - Design Information

Design information
This section describes the C++ language usage within the MWX library, including specifications, known limitations, notes, and design memos.

Design Policy

  • In application loop code, the goal is to enable code resembling commonly used API structures while adapting implementations to the characteristics of TWELITE.
  • TWENET employs event-driven code, which are encapsulated into classes to make this approach manageable. This class-based structure allows application behavior to be encapsulated.
  • Event-driven and loop-based code are designed to coexist.
  • Representative peripherals are encapsulated into classes to simplify procedures, allowing access through loop-based code where possible.
  • Procedures for using boards sold by our company, such as MONOSTICK/PAL, are also encapsulated to streamline usage. (e.g., automating the use of an external watchdog timer)
  • Application and board classes incorporate polymorphism to enable uniform procedures. (e.g., to allow loading application classes with different behaviors at startup without rewriting connection code to the TWENET C library)
  • All C++ features may be utilized without restriction. For example, the library provides simplified procedures for constructing and parsing complex wireless packets.
  • The use of the -> operator is minimized, and APIs are principally designed to use references.

About the C++ Compiler

Version

gcc version 4.7.4

C++ Language Standard

C++11 (For compiler support status, please refer to publicly available information.)

C++ Limitations

※ These are the known issues as recognized by our team.

  • Memory allocation with the new and new[] operators is possible, but deallocation of the allocated memory is not. Most C++ library features that rely on dynamic memory allocation are effectively unusable. These operators are used only for objects that are created once and never destroyed.
  • Constructors for global objects are not called.
  • Reference: If necessary, you can initialize global objects (including constructor invocation) by calling an initialization function (e.g., setup()) and explicitly initializing like new ((void*)&obj_global) class_foo();.
  • Exceptions (exception) cannot be used.
  • Virtual functions (virtual) cannot be used.

Design Notes

This section provides information that will aid your understanding when referencing the MWX library’s code.

Current Implementation

Due to limited development time, certain implementation details may not be fully refined. For example, proper consideration of const correctness is not adequately implemented across many classes.

Namespace

The following policy is adopted for namespace usage:

  • Definitions are, in principle, placed under the common namespace mwx.
  • While the goal is to allow usage without explicitly specifying the namespace, some definitions require explicit identifiers.
  • Class names are generally long, and user-facing names are provided via alias definitions.

Classes, functions, and constants are defined within the mwx namespace (more precisely, within inline namespace L1 inside mwx::L1), with inline namespace used to allow coexistence of definitions that require the mwx:: prefix and those that do not.

Most definitions are made accessible without needing to specify the namespace, via using namespace directives in using_mwx_def.hpp.

// 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 are accessible without mwx::
                         // But mwx::L2 still requires mwx:: prefix.

Shorter names such as mwx::crlf, mwx::flush are explicitly defined within mwx::L2, and can be accessed without a namespace prefix by including using namespace mwx::L2;.

Additionally, some class names are made available via using declarations.

The std::make_pair used within the MWX library is also made available via using.

CRTP (Curiously Recurring Template Pattern)

Because virtual functions (virtual) and runtime type information (RTTI) are not available—or even if they were, would incur significant performance penalties—MWX uses the CRTP (Curiously Recurring Template Pattern) as an alternative design technique. CRTP is a template pattern that enables calling methods of a derived class from its base class.

The following example demonstrates implementing an interface() in a Derived class that inherits from Base. The Base class calls the Derived::prt() method.

template <class T>
class Base {
public:
  void intrface() {
    T* derived = static_cast<T*>(this);
    derived->prt();
  }
};

class Derived : public Base<Derived> {
  void prt() {
     // print message here!
     my_print("foo");
  }
};

The main classes in the MWX library that utilize CRTP are:

  • Core event processing: mwx::appdefs_crtp
  • State machine: public mwx::processev_crtp
  • Stream: mwx::stream

Virtualization with CRTP

With CRTP, each derived class has a different base class instantiation. This means you cannot treat them as the same type by casting to the parent class, nor can you use advanced polymorphism techniques such as virtual functions (virtual) or RTTI.

Below is an example of how you would implement the same pattern using virtual functions. With CRTP, you cannot directly manage instances in the same array like 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(); }
}

In the MWX library, this limitation is addressed by defining a dedicated container class for CRTP-based instances, which provides a common interface. The following example illustrates this approach:

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 contains a pointer p_inst to a Base<T> object and a function pointer pf_intrface to the corresponding static interface function (Base<T>::s_intrface). Base<T>::s_intrface receives the object instance as a parameter, casts it to the appropriate type, and calls T::intrface().

Assignment to VBase is implemented via an overloaded = operator (see the next code example).

When calling b[0].intrface(), the function pointer VBase::pf_intrface is invoked, which calls Base<Derived1>::s_intrface(), which in turn calls Derived1::intrface(). This call chain is expected to be inlined by the compiler.

It is also possible to cast from VBase back to the original Derived1 or Derived2 type, but since the pointer is stored as void*, the actual type cannot be determined safely at runtime. To mitigate this, a unique type ID (TYPE_ID) is assigned to each class, and the get() method checks the ID before casting. If a mismatched type is requested, an error is reported.

Additionally, when storing a pointer as Base<T>, there is a possibility that it cannot be safely cast back to T (for example, with multiple inheritance). Therefore, a compile-time check using static_assert and <type_traits>::is_base_of ensures that T is indeed derived from Base<T>.

// Note: <type_trails> should be <type_traits>
#include <type_traits>

class Derived1 : public Base<Derived1> {
public:
   static const uint8_t TYPE_ID = 1;
};

class Derived2 : public Base<Derived2> {
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!
}

new and new[] Operators

TWELITE modules do not have abundant memory or advanced memory management. However, the area from the end of the application RAM to the start of the stack is available as a heap, from which memory can be allocated as needed. The following diagram shows the memory map: APP is the RAM area allocated for application code, HEAP is the heap, and STACK is the stack.

|====APP====:==HEAP==..   :==STACK==|
0                                  32KB

Even though delete is not supported, there are cases where the new operator is still useful. Therefore, the new and new[] operators are defined as follows. pvHeap_Alloc() is a memory allocation function provided by the semiconductor library, and u32HeapStart, u32HeapEnd mark the heap boundaries. 0xdeadbeef is used as a dummy address.

Please refrain from comments about “beef” being “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 {}

Because exceptions are not supported, there is no handling for allocation failure. Also, if you continue allocating memory without regard for capacity, you may collide with the stack area.

Container Classes

In the MWX library, considering the limited resources of microcontrollers and the inability to perform dynamic memory allocation, the standard library’s container classes are not used. Instead, two simple container classes are defined. These container classes provide iterators and begin(), end() methods, allowing the use of range-based for loops and some STL algorithms.

smplbuf<int16_t, alloc_local<int16_t, 16>> buf;
buf.push_back(-1); // push_back() adds to the end
buf.push_back(2);
...
buf.push_back(10);

// Range-based for loop
for(auto&& x : buf) { Serial << int(x) << ','; }
// STL algorithm: std::minmax
auto&& minmax = std::minmax_element(buf.begin(), buf.end());
Serial << "Min=" << int(*minmax.first)
       << ",Max=" << int(*minmax.second);
Class NameDescription
smplbufAn array class that manages the maximum capacity and usable size (within the maximum capacity) dynamically. This class also implements a stream interface, so you can write data using the << operator.
smplqueImplements a FIFO queue. The queue size is determined by a template parameter. There is also a template argument for operating the queue with interrupt disabling.

Memory Management for Container Classes

For container classes, the memory allocation method is specified as a template parameter.

Class NameDescription
alloc_attachSpecifies an already allocated buffer memory. Use this when you want to manage a memory region allocated for a C library, or when you want to treat a subdivided region of the same buffer.
alloc_staticAllocates as a static array within the class. Use when the size is known in advance or for temporary use.
alloc_heapAllocates in the heap area. Once allocated in the system heap, it cannot be released, but this is suitable for allocating memory according to application settings at initialization.

Variadic Templates

In the MWX library, variadic templates are used for operations involving byte and bit sequences, or for procedures equivalent to printf. The following example shows a function that sets bits at specified positions.

// packing bits with given arguments, which specifies bit position.
//   pack_bits(5, 0, 1) -> (b100011) bit0,1,5 are set.

// Base case for recursion
template <typename Head>
constexpr uint32_t pack_bits(Head head) { return  1UL << head; }

// Recursive unpacking: takes the head and passes the tail recursively
template <typename Head, typename... Tail>
constexpr uint32_t pack_bits(Head head, Tail&&... tail) {
  return (1UL << head) | pack_bits(std::forward<Tail>(tail)...);
}

// After compilation, the following two will result in the same value.
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);

This procedure uses a parameter pack (typename...) in a template to recursively expand the arguments. Because constexpr is specified in the above example, the computation is performed at compile time and yields the same result as a macro or a const value such as b2. It can also act as a function that dynamically calculates values at runtime.

In the next example, the expand_bytes function stores values from the received packet’s payload into local variables. Since parameter packs allow you to deduce each argument’s type, it becomes possible to safely extract values of appropriate sizes and types from the byte stream.

auto&& rx = the_twelite.receiver.read(); // received packet

// Variables to hold the expanded packet contents
// The packet payload contains bytes arranged as follows:
//   [B0][B1][B2][B3][B4][B5][B6][B7][B8][B9][Ba][Bb]
//   <message       ><adc*  ><vcc*  ><timestamp*    >
//   * Numeric types are in big endian order
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       // 4 bytes of message
    , adcval    // 2 bytes, A1 value [0..1023]
    , volt      // 2 bytes, module VCC [mV]
    , timestamp // 4 bytes of timestamp
);

Iterators

Iterators serve as an abstraction over pointers, allowing access to data structures as if you were using pointers—even for structures where memory is not contiguous.

The following example demonstrates using iterators for a FIFO queue, where contiguous access with a normal pointer is not possible. It also shows how to use an iterator that extracts only a specific member (the X axis in this example) from a structure stored in the queue.

// A queue of 5 elements, each of which is a 4-axis XYZT structure
smplque<axis_xyzt, alloc_local<axis_xyzt, 5> > que;

// Insert test data
que.push(axis_xyzt(1, 2, 3, 4));
que.push(axis_xyzt(5, 2, 3, 4));
...

// Access using an iterator over the structure
for (auto&& e : v) { Serial << int(e.x) << ','; }

// Extract the X axis from the queue
auto&& vx = get_axis_x(que);
// Access using an iterator over the X axis
for (auto&& e : vx) { Serial << int(e) << ','; }

// Since the iterator yields int16_t elements, you can use STL algorithms (min/max)
auto&& minmax = std::minmax_element(vx.begin(), vx.end());

Below is an excerpt from the implementation of the iterator for the smplque class. This iterator manages the underlying queue object and the index. The fact that the queue’s memory is not contiguous (due to the ring buffer structure, where the end wraps to the beginning) is handled by smplque::operator[]. Two iterators are considered equal if both the object addresses and the indices are equal.

This implementation also includes the typedefs required by <iterator>, enabling more STL algorithms to be used.

class iter_smplque {
    typedef smplque<T, alloc, INTCTL> BODY;

private:
    uint16_t _pos; // index
    BODY* _body;   // pointer to the 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: // selected methods
    inline reference operator *() {
        return (*_body)[_pos];
    }

    inline self_type& operator ++() {
        _pos++;
        return *this;
    }
};

Iterators that access only a specific member of a structure stored in a container are somewhat more complex. First, define member functions to access each member. Then, define a template that takes such a member function as a parameter (R& (T::*get)()). Here, 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()); }
};

// Use a type alias to shorten the (long) type
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 for extracting the 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* for value access invokes the above member function. (*_p is an axis_xyzt structure, so (*_p.*get)() calls, for example, _p->get_x() if T::*get is &axis_xyzt::get_x.)

The _axis_xyzt_iter_gen class implements only begin() and end(), and generates the above iterator. This enables the use of range-based for and STL algorithms.

Because the type names are long and unwieldy in source code, a generator function is provided for convenience. In the example above, this is the get_axis_x() function at the end. Using this generator function allows concise code such as auto&& vx = get_axis_x(que); as shown at the start of this section.

This axis-extracting iterator can also be used with the array-type smplbuf class in the same way.

Implementing Interrupt, Event, and State Handlers

To describe application behavior via user-defined classes, it is necessary to define certain representative handlers as required methods. However, defining all the possible interrupt handlers, event handlers, and state handlers for the state machine can be tedious, as there are many of them. Ideally, only the handlers defined by the user should be instantiated, and only those should have code executed.

class my_app_def {
public: // Definition of 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: // Making all of these mandatory would be cumbersome
    // 20 DIO interrupt handlers
    // 20 DIO event handlers
    // 5 timer interrupt handlers
    // 5 timer event handlers
    // ...
};

In the MWX library, for handlers with large numbers (such as DIO interrupt handlers—on TWELITE hardware, there is only a single interrupt, but for usability, a handler is assigned to each DIO pin), we define empty handler templates. The user can then specialize only the handlers they need by specializing these templates with their own member functions.

// hpp file
class my_app_def : class app_defs<my_app_def>, ... {
  // Empty handler template
  template<int N> void int_dio_handler(uint32_t arg, uint8_t& handled) { ; }

  ...
  // Only implement for 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;
  }
}

In actual user code, macros and header file includes can simplify the code, but the above shows the necessary code for explanation.

The interrupt handler from TWENET will call my_app_def::cbTweNet_u8HwInt(). In the .cpp file, only int_dio_handler<12> is specialized and instantiated with the user-defined content; for all other numbers, the template from the .hpp file is instantiated. As a result, the code expands as follows:

    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;

    // ↓ ↓ ↓

    // After optimization, the code is expected to look like this:
    case E_AHI_DEVICE_SYSCTRL:
      if (u32ItemBitmap & (1UL << 12)){
        // int_dio_handler<12> is also inlined
        digitalWrite(5, LOW);
        handled = true;
      }
      break;

Ultimately, it is expected that the compiler’s optimization will recognize the code for all but number 12 as unnecessary and eliminate it from the binary (though this optimization cannot be guaranteed).

In other words, if you want to define the behavior for interrupt number 12 in your user code, it is sufficient to implement int_dio_handler<12>. (Note: To enable DIO interrupts, you must call attachInterrupt().) Handlers that are not registered are expected to result in minimal overhead due to compile-time optimization.

Stream Class

The stream class is primarily used for UART (serial port) input and output. In the MWX library, output procedures are mainly defined, with some input definitions also available.

This section explains the implementation required by derived classes.

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 shows the implementation of the write() method for writing a single character. The base class stream<serial_jen> uses the get_Derived() method to cast itself to serial_jen and access the serial_jen::write() method.

As needed, you can define methods such as write(), read(), flush(), and available().

For formatted output, the library uses Marco Paland’s printf library. To use this from the MWX library, you need to implement the appropriate interface. In the following example, the derived class serial_jen must define a vOutput() method for single-byte output, and since vOutput() is a static method, the base class stores auxiliary information for output in pvOutputContext.

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 using get_pfcOutout(), the function pointer to the derived class’s vOutput() is specified, with pvOutputContext passed as its parameter. In the above example, when the << operator is called with an int, serial_jen::vOutput() and the UART-configured TWE_tsFILE* are passed to the fctprintf() function.

Worker Objects for Wire and SPI

For the Wire class, which handles I2C communication, it is necessary to manage communication from start to finish when transmitting or receiving to/from two-wire devices. The following describes usage with worker objects.

if (auto&& wrt = Wire.get_writer(SHTC3_ADDRESS)) {
	Serial << "{I2C SHTC3 connected.";
	wrt << SHTC3_TRIG_H;
	wrt << SHTC3_TRIG_L;
	Serial << " end}";
}

Below is an excerpt from the periph_twowire::writer class. To implement the stream interface, it inherits from mwx::stream<writer>. The write() and vOutput() methods are implemented to support the stream interface.

The constructor initiates I2C communication, and the destructor finalizes it. The operator bool() returns true if communication with the I2C device was 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 in constructor
		}

		~writer() {
			_wire.endTransmission(); // End communication in 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

// User code
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 the wrt object. Normally, object copying does not occur here. Due to C++’s Return Value Optimization (RVO), the writer is constructed directly in wrt without copying, so bus initialization performed in the constructor is not repeated. However, since RVO is not strictly guaranteed by the C++ standard, the MWX library explicitly deletes copy/move assignment operators and defines a move constructor (even though the move constructor is not expected to be invoked).

Within the if block, wrt is initialized by the constructor, which also starts communication. If communication is successfully started, the operator bool() returns true, and the block is executed. Upon leaving the scope, the destructor finalizes the use of the I2C bus. If the target device is not present, operator bool() returns false and the wrt object is destroyed.

For Wire and SPI in particular, the default behavior of the operator << (int) is overridden. The default stream behavior converts numbers to strings before output, but with Wire and SPI, it is rare to write numeric strings to the bus. More often, you want to send literal numeric values directly (such as configuration values), but numeric literals are typically evaluated as int. Therefore, this behavior is changed.

			writer& operator << (int v) {
				_wire.write(uint8_t(v & 0xFF));
				return *this;
			}

Here, values of type int are truncated to 8 bits and output directly.

2 - Definitions

Definitions commonly included throughout the library

This section cites the definition contents that are commonly included throughout the library.

mwx_common.h

##include <cstdint> // for type name
typedef char char_t;
typedef uint8_t byte;
typedef uint8_t boolean;

##ifndef NULL
##define NULL nullptr
##endif

3 - Class Objects

Objects defined for operating networks and peripherals

Class objects are predefined in the MWX library, including the_twelite for handling TWENET and objects for using peripherals.

Each object must be initialized by calling its .setup() and .begin() methods.

(Only the Serial object that uses UART0 does not require initialization.)

3.1 - the_twelite

Core class for TWENET usage (mwx::twenet)
The the_twelite object consolidates procedures for using TWENET, including basic wireless settings, sleep procedures, and other operations to control the wireless microcontroller.

Overview

the_twelite performs settings and start the_twelite.begin() within the setup() function. Settings cannot be done outside of 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 setting, communication channel setting, and receiver circuit setting are performed.

Various procedures are included.

// Get the 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 reset
the_twelite.reset_system();

Also, classes that handle wireless networks, board support, and user-written event-driven processing can be registered. By registering these classes, specialized functions can be easily utilized. These classes are referred to as “Behaviors” in this documentation.

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 are registered: the environmental sensor PAL <PAL_AMB> and the simple relay network <NWK_SIMPLE>. By registering these, hardware such as sensors on the environmental sensor PAL can be easily handled. Also, complicated wireless packet handling such as relay processing and automatic discarding of duplicate packets can be implicitly enabled.

Methods

<< operator (Settings)

The << operator is used to perform initial settings of the the_twelite object.

The following setting class objects are input, and if not set, default values are applied.

TWENET::appid(uint32_t id)

Sets the application ID specified by parameter id. This is mandatory.

Reading the setting is done by uint32_t the_twelite.get_appid().

TWENET::channel(uint8_t ch)

Sets the channel number (11..26) specified by parameter ch.

Reading the setting is done by uint8_t the_twelite.get_channel().

TWENET::tx_power(uint8_t pw)

Sets the output power setting (0..3) specified by parameter pw. The default is (3: no output attenuation).

Reading the setting is done by uint8_t the_twelite.get_tx_power().

TWENET::rx_when_idle(uint8_t bEnable)

If parameter bEnable is 1, the receiver circuit is always active to receive wireless packets from others. The default is 0, meaning mainly transmission only.

Reading the setting is done by uint8_t the_twelite.get_rx_when_idle().

TWENET::chmgr(uint8_t ch1 = 18, uint8_t ch2 = 0, uint8_t ch3 = 0)

Enables the channel manager. If multiple channels are specified, transmission and reception are performed on multiple channels. If 0 is specified for ch2 or ch3, that specification is disabled.

STG_STD

Applies the Interactive Mode settings.

auto&& set = the_twelite.settings.use<STG_STD>();
...
set.reload();       // Load settings
the_twelite << set; // Apply Interactive Mode settings

The applied items are as follows:

  • app_id
  • channel
  • tx_power
  • Retransmission count when using MAC ack

begin()

void begin()

Execute after completing prior settings (<< operator reference) and behavior registration. Usually placed at the very end of the setup() function.

  • the_twelite setup completed
  • Behavior initialization

Example

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

	// some others

	// begin the TWENET!
	the_twelite.begin();
}

change_channel()

inline bool change_channel(uint8_t u8Channel)

Changes the channel setting. If it fails, the channel is not changed and returns false.

get_channel_phys()

uint8_t get_channel_phys()

Gets the currently set channel number (11..26). Obtained from the MAC layer API.

get_hw_serial()

inline uint32_t get_hw_serial()

Gets the module’s serial number.

sleep()

inline void sleep(
        uint32_t u32Periodms,
        bool_t bPeriodic = true,
        bool_t bRamOff = false,
        uint8_t u8Device = TWENET::SLEEP_WAKETIMER_PRIMARY)

Puts the module to sleep.

ParameterDescription
u32PeriodmsSleep duration [ms]
bPeriodicRecalculates next wake-up time based on the last wake-up time.
※ Sometimes the next wake-up timing may be from the current time due to proximity.
bRamoffIf set to true, sleep without retaining RAM (after waking up, reinitialization should start from setup() instead of wakeup())
u8DeviceSpecifies the wake-up timer used for sleep. Specify either TWENET::SLEEP_WAKETIMER_PRIMARY or TWENET::SLEEP_WAKETIMER_SECONDARY.

is_wokeup_by_dio()

bool is_wokeup_by_dio(uint8_t port)

Returns true if the wake-up cause from sleep is the specified digital pin.

is_wokeup_by_wktimer()

bool is_wokeup_by_wktimer()

Returns true if the wake-up cause from sleep is the wake-up timer.

reset_system()

inline void reset_system()

Resets the system. After reset, processing starts from setup().

stop_watchdog()

inline void stop_watchdog()

Stops the watchdog timer. Stop the timer when performing long polling waits.

restart_watchdog()

inline void restart_watchdog()

Restarts the watchdog timer.

Behaviors

twe_twelite can register three behaviors, and the following class objects are defined to hold them.

  • network : Behavior implementing the network. Usually register <NWK_SIMPLE>.
  • network2 : Behavior implementing the network. Used when you want to process packets rejected by network (due to payload data structure or other criteria) with another network behavior. (Reference: Using NWK_LAYERED and NWK_SIMPLE together)
  • board: Behavior for board support. Adds procedures for using devices on the board.
  • app: Behavior describing user applications. Allows writing interrupt or event descriptions and state transitions using state machines. Also allows defining multiple application descriptions and easily selecting completely different applications at startup.
  • settings: Behavior for executing settings (Interactive Mode). Register <SET_STD>.

use<B>()

// Example
auto&& brd = the_twelite.board.use<PAL_AMB>();

Registers behavior <B>. Registration is done inside setup(). The return value is a reference to the object corresponding to the registered behavior.

After registration, the object can be retrieved with the same syntax as during registration.

Class Objects

the_twelite defines the three class objects board, network, and app mentioned above, and also defines the following.

tx_status

Notifies the transmission completion status.

is_complete()

bool is_complete(uint8_t cbid)

Returns true when the packet with the specified ID has completed transmission.

is_success()

bool is_success(uint8_t cbid)

Returns true when the packet with the specified ID has completed transmission and the transmission was successful.

receiver

Obtains received packets.

available()

bool available()

Returns true if there is a received packet not yet read.

read()

packet_rx& read()

Reads a packet.

3.2 - Analogue

ADC (mwx::periph_analogue.hpp)
Analogue performs ADC execution and value acquisition. Multiple channels can be continuously acquired at once, and this can be sequentially executed according to the cycle of a timer or other periodic event.

Constants

Pin Definitions

ConstantTypeStandard App Pin Name
uint8_t PIN_ANALOGUE::A1 = 0ADC1 PinAI1
uint8_t PIN_ANALOGUE::A2 = 1ADC2 PinAI3
uint8_t PIN_ANALOGUE::A3 = 2``uint8_t PIN_ANALOGUE::D0 = 2ADC3 Pin (DIO0) *1AI2
uint8_t PIN_ANALOGUE::A4 = 3``uint8_t PIN_ANALOGUE::D1 = 3ADC4 Pin (DIO1) *1AI4
uint8_t PIN_ANALOGUE::VCC = 4Vcc Power Supply Voltage

Methods

setup()

void setup(
        bool bWaitInit = false,
        uint8_t kick_ev = E_AHI_DEVICE_TICK_TIMER,
        void (*fp_on_finish)() = nullptr)

Initializes the ADC. In setup(), it starts the internal semiconductor regulator, specifies the timer device for periodic execution, and specifies a callback function called when all specified ADC channels have finished.

ParameterDescription
bWaitInitIf true, waits for the internal semiconductor regulator initialization.
kick_evSpecifies the timer device for periodic execution. The following five devices can be specified. Except for the first time, ADC starts in the interrupt handler. E_AHI_DEVICE_TICK_TIMER (TickTimer)``E_AHI_DEVICE_TIMER0 .. 4 (Timer0 .. 4)
fp_on_finishCallback function called from the interrupt handler after all specified ports’ ADCs finish. Useful for separately storing ADC measurement values in FIFO queues, etc.

begin()

void begin(uint8_t bmPorts, uint8_t capt_tick = 1)

The first parameter specifies the ports for ADC. The port specification is a bitmap with bits set corresponding to the port numbers defined in the pin definitions. For example, to get values of PIN_ANALOGUE::A2 and PIN_ANALOGUE::VCC, specify (1 <<PIN_ANALOGUE::A1 | 1<<PIN_ANALOGUE::VCC ). You can also write pack_bits(PIN_ANALOGUE::A1,PIN_ANALOGUE::VCC) using pack_bits.

After calling begin(), the first ADC process starts promptly, and after its completion interrupt, the next pin process starts. When all processes finish, the callback function (if specified) is called. The next ADC process starts after waiting for the next timer interrupt.

The second parameter specifies the number of timer interrupts before starting ADC. For example, TickTimer is called every 1ms, and specifying 16 means processing every 16ms.

void begin()

Starts ADC processing with default ADC pins (PIN_ANALOGUE::A1,PIN_ANALOGUE::A2). end() resumes interrupted ADC processing.

end()

void end()

Stops ADC processing and stops the internal semiconductor regulator.

available()

inline bool available()

Returns true after ADC values are obtained. After checking with this function, it returns false until the next ADC completion.

read(), read_raw()

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 read. read() returns the value converted to mV, and read_raw() returns the ADC value (0..1023).

ADC Interrupt Handler

The ADC interrupt handler is set to periph_analogue::ADC_handler() when setup() is called.

Specifying a handler separately in the semiconductor peripheral library will cause malfunction.

Behavior During Sleep

If ADC is in periodic execution state by begin(), ADC processing resumes after wake-up from sleep.

3.3 - Buttons

Digital Input Management Class (mwx::periph_buttons)
Detects changes in digital input. This class detects changes when the same detected value is obtained multiple times. It is effective in reducing the effects of mechanical button chatter.

Methods

setup()

void setup(uint8_t max_history);

The parameter max_history is the maximum number of reference counts that can be set in begin(). Memory allocation and initialization are performed here.

begin()

void begin(uint32_t bmPortMask,
				   uint8_t u8HistoryCount,
				   uint16_t tick_delta);

Starts the operation of Buttons. 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. Multiple bits can be specified. The second parameter u8HistoryCount is the number of times required to confirm a value. The third parameter tick_delta specifies the interval for checking the value in milliseconds.

The confirmation of the value takes u8HistoryCount * tick_delta [ms]. For example, if u8HistoryCount=5 and tick_delta=4, it takes at least about 20 ms to confirm the state.

The check is performed in the event handler of TickTimer. Since it is not an interrupt handler, it is affected by processing delays, but it is sufficient for suppressing chatter of mechanical buttons and the like.

end()

void end()

Stops the operation of Buttons.

available()

inline bool available()

Returns true when a change is detected. It is cleared when read() is executed.

read()

bool read(uint32_t& u32port, uint32_t& u32changed)

Called when available is true. u32port is the bitmap of the current input DIO, and u32changed is the bitmap of DIOs where changes were detected.

Returns false if Buttons is not operating.

Operation Details

Initial Value Confirmation

At the time when Buttons starts operating, the input state of DIO is not confirmed. It becomes available when the value is confirmed. At this time, the MSB (bit 31) of the bitmap read by read() is set to 1.

Since operation confirmation is required, it cannot be used for monitoring ports where the input value changes constantly.

Sleep

If Buttons is running before sleep, it will resume after waking up. After resuming, the initial confirmation is performed.

3.4 - EEPROM

Perform read and write operations on the built-in EEPROM

Performs read and write operations on the built-in EEPROM of the TWELITE wireless microcontroller.

The built-in EEPROM has 3480 bytes available from address 0x000 to 0xEFF.

The beginning part is used for Settings (Interactive Mode), so when used together, it is recommended to use the latter half of the addresses. The amount of space consumed by the settings (Interactive Mode) depends on its implementation. Even with minimal settings, the first 256 bytes are used, so it is recommended to use addresses after that.

Methods

read()

uint8_t read(uint16_t address)

Reads data corresponding to address from the EEPROM.

write()

void write(uint16_t address, uint8_t value)

Writes value to address in the EEPROM.

update()

void update(uint16_t address, uint8_t value)

Performs writing similar to write(), but first reads the data at address and writes only if it is different from value. This is used to reduce the number of write cycles considering the EEPROM’s rewrite lifespan.

get_stream_helper()

auto&& get_stream_helper()
// The return type is long, so it is abbreviated as auto&&.

Obtains a helper object to perform read and write operations using the later described mwx::stream.

Input/output using the mwx::stream interface

Through the stream_helper helper object, use operators and methods of mwx::stream. Using mwx::stream allows reading and writing of integer types such as uint16_t and uint32_t, fixed-length arrays of uint8_t, and formatted output via format() objects.

auto&& strm = EEPROM.get_stream_helper();
// The helper object's type name is long, so auto&& is used to resolve it.

You can use the << operator and other interfaces defined in mwx::stream with this object.

strm.seek(1024); // Move to byte 1024

strm << format("%08x", 12345678); // Record 12345678 as an 8-character hexadecimal
strm << uint32_t(0x12ab34cd);     // Record 4 bytes of 0x12ab34cd
uint8_t msg_hello[16] = "HELLO WORLD!";
strm << msg_hello;                // Record byte sequence "HELLO WORLD!" (no terminator)

// 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  !

Using .seek(), the EEPROM address is moved to 1024.

The above writes an 8-byte string (00bc614e), a 4-byte integer (0x12ab34cd), a 16-byte byte array (HELLO WORLD!...), and a 1-byte terminator.

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]; // Number of characters in "HELLO WORLD!"
strm >> msg2;
Serial << crlf << "MSG2=" << msg2;
// MSG2=HELLO WORLD!

Using .seek(), the EEPROM address is moved to 1024.

Reads the previously written data sequence. Reads an 8-byte string, a 4-byte integer, and a 16-byte string in order using the >> operator.

3.5 - PulseCounter

Pulse Counter (mwx::periph_pulse_counter)
The pulse counter is a circuit that counts pulses even when the microcontroller’s CPU is not operating.

There are two pulse counter systems. PC0 is assigned to PulseCounter0, and PC1 is assigned to PulseCounter1. The alias PulseCounter refers to PulseCounter1.

Methods

begin()

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 reference count used to determine whether an interrupt or availability condition is met. When the count exceeds this number, it is reported to the application. A value of 0 may also be specified, in which case it will not trigger a wake-up from sleep.

The second parameter, edge, specifies whether the interrupt is on the rising (PIN_INT_MODE::RISING) or falling (PIN_INT_MODE::FALLING) edge.

The third parameter, debounce, accepts values 0, 1, 2, or 3. Settings 1 through 3 reduce the effect of noise by requiring consecutive identical values for detection.

SettingConsecutive SamplesMax Detection Frequency
0-100kHz
123.7kHz
242.2kHz
381.2kHz

end()

void end()

Stops pulse detection.

available()

inline bool available()

If the specified count (refct in begin()) is 0, returns true when the count is 1 or more.

If the specified count (refct in begin()) is 1 or more, returns true when the count exceeds the specified threshold.

read()

uint16_t read()

Reads the count value. The value is reset to 0 after reading.

3.6 - Serial

UART0 port (mwx::serial_jen)
Implements mwx::stream and performs input/output on TWELITE’s UART0.
  • The Serial object is initialized at system startup with UART0 at 115200 bps, and the initialization process is handled within the library. It can be used from setup() in user code.
  • The Serial1 object is provided within the library but is not initialized. To enable UART1, perform the necessary initialization procedures Serial1.setup(), Serial1.begin().

setup()

void setup(uint16_t buf_tx, uint16_t buf_rx)

Initializes the object.

  • Allocates memory for TX/RX FIFO buffers
  • Allocates memory for the TWE_tsFILE structure
ParameterDescription
buf_txFIFO buffer size for TX
buf_rxFIFO buffer size for RX

begin()

void begin(unsigned long speed = 115200, uint8_t config = 0x06)

Initializes the hardware.

ParameterDescription
speedSpecifies the UART baud rate
configWhen the serial_jen::E_CONF::PORT_ALT bit is specified, UART1 is initialized on ~DIO14,15 instead of ~DIO11(TxD),9(RxD). If not specified, it initializes on ~DIO11(TxD),9(RxD) instead of ~DIO14(TxD),15(RxD).

end()

(Not implemented) Stops using the hardware.

get_tsFile()

TWE_tsFILE* get_tsFile();

Returns the structure in TWE_tsFILE* format used by the C library.

3.7 - SerialParser

Formatted input for serial port (mwx::serial_parser)
This built-in class is defined as an embedded object intended for use in formatted input via the serial port.

It is defined as the type mwx::serial_parser<mwx::alloc_heap<uint8_t>>, which allocates internal buffer space from the heap during initialization (begin()).

For details, refer to the class serparser.

3.8 - SPI

Performs reading and writing on the SPI bus (controller side).
Performs reading and writing on the SPI bus (controller side).

Notes

Constants

ConstantMeaning
const uint8_t
SPI_CONF::MSBFIRST
Set MSB as the first bit
const uint8_t
SPI_CONF::LSBFIRST
Set LSB as the first bit
const uint8_t
SPI_CONF::SPI_MODE0
Set to 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

Initialization and Termination

The procedure to use the SPI bus is via the begin() method.

begin()

void begin(uint8_t slave_select, SPISettings settings)
SPISettings(uint32_t clock, uint8_t bitOrder, uint8_t dataMode)

Initializes the hardware.

ParameterDescription
slave_selectSpecifies the select pin of the target peripheral.
0 : DIO19``1 : DIO0 (DIO19 is reserved)``2 : DIO1 (DIO0,19 are reserved)
settingsSpecifies the SPI bus settings.

clock [Hz] specifies the SPI bus frequency. A divisor close to the specified frequency is selected. The value will be 16MHz or an even division of 16MHz.
bitOrder is either SPI_CONF::MSBFIRST or SPI_CONF::LSBFIRST.
dataMode is one of SPI_CONF::SPIMODE0..3.

Example

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

end()

void end()

Terminates the use of SPI hardware.

Reading and Writing

There are two types of procedures for reading and writing. Use either of them as needed.

Example

The following sample code reads the temperature every second from the Analog Devices temperature sensor ADT7310 and outputs it to the serial port.

#include <TWELITE>
#include <SM_SIMPLE>

enum class STATE : uint8_t {
    INTERACTIVE = 255,
    INIT = 0,
    INIT_WAIT,
    SENSOR,
    LOOP_WAIT
};
SM_SIMPLE<STATE> step;

struct SensorData {
    uint8_t highByte;
    uint8_t lowByte;
    uint16_t rawValue;
    int32_t tempValue16th;
    div_result_i32 temperature;
} sensorData;

void setup() {
    step.setup(); // Initialize the state machine
}

void loop() {
    do {
        switch (step.state()) {
        case STATE::INIT: // Initial state
            SPI.begin(0 /* Use DIO19 as chip select */
                      , { 400000UL /* Clock frequency */
                    , SPI_CONF::MSBFIRST
                    , SPI_CONF::SPI_MODE3
            }
                      );

            // Software reset
            SPI.beginTransaction();
            for (int i = 0; i < 4; i++) {
                SPI.transfer(0xFF);
            }
            SPI.endTransaction();

            // Start Continuous Read mode
            SPI.beginTransaction();
            SPI.transfer(0x54);
            SPI.endTransaction();

            step.set_timeout(300); // Set wait time
            step.next(STATE::INIT_WAIT);
            break;

        case STATE::INIT_WAIT: // Wait
            if (step.is_timeout()) {
                step.next(STATE::SENSOR);
            }
            break;

        case STATE::SENSOR: // Read sensor data
            SPI.beginTransaction();
            sensorData.highByte = SPI.transfer(0x00);  // Send dummy data to generate clock signal
            sensorData.lowByte = SPI.transfer(0x00);   // Send dummy data to generate clock signal
            SPI.endTransaction();

            sensorData.rawValue = (((uint16_t)sensorData.highByte << 8) | sensorData.lowByte) >> 3;
            if (sensorData.rawValue & 0x1000) {
                sensorData.tempValue16th = int32_t(sensorData.rawValue) - 0x2000;
            } else {
                sensorData.tempValue16th = sensorData.rawValue;
            }

            // Calculate temperature using div100()
            sensorData.temperature = div100((sensorData.tempValue16th * 100) / 16);

            // Output result to serial
            Serial << crlf << sensorData.temperature.format() << "°C";

            step.set_timeout(1000); // Wait until next capture
            step.next(STATE::LOOP_WAIT);
            break;

        case STATE::LOOP_WAIT: // Wait
            if (step.is_timeout()) {
                step.next(STATE::SENSOR);
            }
            break;

        default:
            break;
        }
    } while (step.b_more_loop());
}

Here, the member function interface is used.

3.8.1 - SPI (Member Function Version)

SPI (Method-based usage)
After initializing the hardware with the begin() method, use beginTransaction() to enable bus communication. When beginTransaction() is executed, the SPI select pin is asserted. Use the transfer() function for reading and writing. SPI performs read and write operations simultaneously.

beginTransaction()

void beginTransaction()
void beginTransaction(SPISettings settings)

Starts using the SPI bus. Asserts the SPI select pin.

When called with the settings parameter, it also configures the bus settings.

endTransaction()

void endTransaction()

Ends the use of the SPI bus. Deasserts the SPI select pin.

transfer(), transfer16(), transfer32()

inline uint8_t transfer(uint8_t data)
inline uint16_t transfer16(uint16_t data)
inline uint32_t transfer32(uint32_t data)

Performs read/write operations on the SPI bus. transfer() transfers 8 bits, transfer16() transfers 16 bits, and transfer32() transfers 32 bits.

3.8.2 - SPI (Helper Class Version)

SPI (How to use with helper class)
The helper class version is a more abstract implementation. By creating a read/write object transceiver, you start using the bus, and by destroying the object, you end the use of the bus.

By creating the object inside the conditional expression of an if statement, the lifetime of the object is limited to the scope within the if block, and when exiting the if block, the object is destroyed and the bus release procedure is performed at that point.

uint8_t c;
if (auto&& trs = SPI.get_rwer()) { // Create object and check device communication
   // The scope (curly braces) here is the lifetime of trs.
   trs << 0x00; // Write 0x00 using mwx::stream interface
   trs >> c;    // Store the read data into c.
}
// When exiting the if block, trs is destroyed and the bus usage ends

Also, since the read/write object implements the mwx::stream interface, operators like << can be used.

  • Matching the bus usage start and end with the object’s lifetime improves code readability and prevents omission of release procedures.
  • Unifies read/write procedures using the mwx::stream interface.

Read/Write

A reading method using a helper class that performs read operations and their termination procedures within the scope if() { ... }.

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

Above, the x object created by the get_rwer() method is used to perform byte-wise read/write.

  1. The x object is created inside the if(...). At the same time, the SPI bus select pin is set. (The type is resolved by universal reference auto&& with type inference.)
  2. The created x object defines operator bool () which is used for evaluation in the conditional expression. For SPI bus, it always evaluates to true.
  3. The x object defines the method uint8_t transfer(uint8_t), which performs an 8-bit read/write transfer to SPI when called.
  4. At the end of the if() { ... } scope, the destructor of x is called, releasing the SPI bus select pin.

get_rwer()

periph_spi::transceiver get_rwer()

Obtains a worker object used for SPI bus read/write.

Worker Object

transfer(), transfer16(), transfer32()

uint8_t transfer(uint8_t val)
uint16_t transfer16(uint16_t val)
uint32_t transfer32(uint32_t val)

Perform transfers of 8-bit, 16-bit, and 32-bit respectively, and return the read result with the same data width as the written data.

<< operator

operator << (int c)
operator << (uint8_t c)
operator << (uint16_t c)
operator << (uint32_t c)

int and uint8_t types perform 8-bit transfers.

uint16_t and uint32_t types perform 16-bit and 32-bit transfers respectively.

Transfer results are stored in an internal FIFO queue of up to 16 bytes and read out by the >> operator. Since the buffer is not large, it is assumed to be read out each time after transfer.

>> operator

operator >> (uint8_t& c)
operator >> (uint16_t& c)
operator >> (uint32_t& c)

null_stream(size_t i = 1)
operator >> (null_stream&& p)

Specify a variable with the same data width as the previous transfer.

If the read result is not needed, use the null_stream() object. It skips the number of bytes specified by i.

3.9 - TickTimer

System Timer (mwx::periph_ticktimer)
TickTimer is used internally by TWENET for control purposes and operates implicitly. The timer interval is 1 ms. It defines only the available() method for writing processing that occurs every 1 ms in the loop() function based on TickTimer events.

Methods

available()

inline bool available()

Set after the TickTimer interrupt occurs, and becomes true in the immediate loop() execution. It is cleared after the loop() ends.

3.10 - Timer0 .. 4

Timer, PWM (mwx::periph_timer)
Timers have two functions: generating software interrupts at specified intervals and producing PWM output at specified intervals. The TWELITE wireless module provides five timers, from 0 to 4.

Although the embedded object names are Timer0..4, this page refers to them as TimerX.

Methods

setup()

void setup()

Initializes the timer. This call allocates the required memory area.

begin()

void begin(uint16_t u16Hz, bool b_sw_int = true, bool b_pwm_out = false)

Starts the timer. The first parameter specifies the timer frequency in Hz. If the second parameter is set to true, software interrupts are enabled. If the third parameter is set to true, PWM output is enabled.

end()

void end()

Stops the timer operation.

available()

inline bool available()

Becomes true in the loop() immediately after a timer interrupt occurs, and turns false after the loop() finishes.

change_duty()

void change_duty(uint16_t duty, uint16_t duty_max = 1024)

Sets the duty cycle. The first parameter specifies the duty value (a smaller value brings the average waveform level closer to GND, while a larger value brings it closer to Vcc). The second parameter specifies the maximum duty value.

change_hz()

void change_hz(uint16_t hz, uint16_t mil = 0)

Sets the timer frequency. The second parameter specifies the fractional part (three decimal places) of the frequency as an integer. For example, to set 10.4 Hz, specify hz=10, mil=400.

3.11 - Wire

Read/write as 2-wire serial (I2C) master (controller) (mwx::periph_wire)
Performs read/write operations as a 2-wire serial (I2C) master (controller).

Definition

Alias Definition

using TwoWire = mwx::periph_twowire<MWX_TWOWIRE_RCVBUFF>;

mwx::periph_wire<MWX_TWOWIRE_RCVBUFF> can be referenced as TwoWire.

Type Definitions

The following typedefs are used for argument and return types:

typedef uint8_t size_type;
typedef uint8_t value_type;

Notes

Initialization and Termination

Creating a Wire instance

The instance and necessary initialization are handled within the library. In user code, call Wire.begin() to start using it.

When using the requestFrom() method, you can specify the size of a FIFO queue for temporary data storage. At compile time, set the number of bytes via the macro MWX_TWOWIRE_BUFF. The default is 32 bytes.

Example: -DMWX_TWOWIRE_BUFF=16

begin()

void begin(
    const size_type u8mode = WIRE_100KHZ,
    bool b_portalt = false)

Initializes the hardware.

ParameterDescription
u8modeSpecifies the bus frequency. Default is 100KHz (WIRE_CONF::WIRE_100KHZ).
You can specify WIRE_CONF::WIRE_??KHZ where ?? can be 50, 66, 80, 100, 133, 160, 200, 266, 320, 400.
b_portaltChanges the hardware pin assignment.

Example

void setup() {
    ...
    Wire.begin();
    ...
}

void wakeup() {
    ...
    Wire.begin();
    ...
}

Reading and Writing

There are two procedures for reading and writing. Use either as needed.

Others

Probe (Device Presence Check)

bool probe(uint8_t address)

Checks if the device specified by address responds. Returns true if the device exists.

setClock()

void setClock(uint32_t speed)

This is intended to change the bus frequency, but it performs no action.

3.11.1 - Wire (Member Function Version)

Wire (using member functions)

This method using member functions has relatively low abstraction and follows a general API structure similar to that provided by C language libraries. It offers a more intuitive procedure for operating a two-wire serial bus.

However, you need to explicitly manage the start and end of bus usage.

Reading

requestFrom()

size_type requestFrom(
    uint8_t u8address,
    size_type length,
    bool b_send_stop = true)

Reads a specified number of bytes in one operation. The result is stored in a queue, so you should call the .read() method repeatedly until the queue is empty immediately afterward.

ParameterDescription
u8addressI2C address to read from
lengthNumber of bytes to read
b_send_stop=trueIf true, a STOP bit is issued at the end of reading
Return type size_typeNumber of bytes read. 0 indicates failure

Code Example

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.

Writing

The writing process starts with beginTransmission(), then proceeds with the write() method. After the entire write operation is complete, call endTransmission().

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

beginTransmission()

void beginTransmission(uint8_t address)

Initializes a write transfer. After the write process, call endTransmission() promptly.

ParameterDescription
u8addressI2C address to write to

write(value)

size_type write(const value_type value)

Writes a single byte.

ParameterDescription
valueByte to write
Return size_typeNumber of bytes written. 0 indicates an error.

write(*value, quantity)

size_type write(
  const value_type* value,
  size_type quantity)

Writes a sequence of bytes.

ParameterDescription
*valueByte sequence to write
size_typeNumber of bytes
Return size_typeNumber of bytes written. 0 indicates an error.

endTransmission()

uint8_t endTransmission(bool sendStop = true)

Finalizes the write operation.

ParameterDescription
sendStop = trueIssues a STOP bit
Return uint8_t0: success, 4: failure

3.11.2 - Wire (Helper Class Version)

Wire (How to use with helper classes)
The helper class version is a more abstract implementation. Generating objects reader, writer that correspond to reading and writing marks the start of bus usage, and destroying the object performs the bus usage termination procedure.

By creating objects inside the condition expression of an if statement, the object’s lifetime is limited to the scope of the if block. When exiting the if block, the object is destroyed, and at that time, the bus usage termination procedure is performed.

if (auto&& wrt = Wire.get_writer(...)) { // Object creation and device communication check
   // The scope (curly braces) is the lifetime of wrt.
   wrt << 0x00; // Write 0x00 using the mwx::stream interface
}
// wrt is destroyed when exiting the if block, and bus termination is performed

Also, since the read/write objects implement the mwx::stream interface, operators like << can be used.

  • Matching the start and end of bus usage with the object’s lifetime improves code readability and prevents omission of termination procedures.
  • Unified reading and writing procedures via the mwx::stream interface.

Reading

This is a reading method using helper classes to perform reading and its termination procedure within a scope if() { ... }.

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

Above, the rdr object generated by the get_reader() method is used to read one byte at a time. The method parameter specifies the two-wire serial ID to read.

  1. The rdr object is created inside if(...). (The type is resolved as a universal reference auto&& by type inference.)
  2. The created rdr object defines operator bool () and is used for evaluation in the condition. If communication is possible with the specified ID, it returns true.
  3. The rdr object defines int operator () (void), which reads one byte from the two-wire serial bus when called. Returns -1 on failure, or the read byte value on success.
  4. The destructor of rdr is called at the end of the if() { ... } scope, issuing a STOP on the two-wire serial bus.

get_reader(addr, read_count=0)

periphe_wire::reader
get_reader(uint8_t addr, uint8_t read_count = 0)

Obtains a worker object used for I2C reading.

ParameterDescription
addrI2C address for reading
read_countNumber of bytes to read (if specified, a STOP bit is issued at the end of the transfer). If 0 is specified, no STOP bit is issued (some devices may work without it).

Writing (writer)

This is a writing method using helper classes to perform writing and its termination procedure within a scope if() { ... }.

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

Above, the wrt object generated by the get_writer() method is used to write one byte at a time. The method parameter specifies the two-wire serial ID to write.

  1. The wrt object is created inside if(...). (The type is resolved as auto to avoid long type names.)
  2. The created wrt object defines operator bool () and is used for evaluation in the condition. If communication is possible with the specified ID, it returns true.
  3. The wrt object defines int operator () (void), which writes one byte to the two-wire serial bus when called. Returns -1 on failure, or the written byte value on success.
  4. The destructor of wrt is called at the end of the if() { ... } scope, issuing a STOP on the two-wire serial bus.

get_writer()

periph_wire::writer
get_writer(uint8_t addr)

Obtains a worker object used for I2C writing.

ParameterDescription
addrI2C address for writing

Worker Object (writer)

<< operator

operator << (int c)
operator << (uint8_t c)
operator << (uint16_t c)
operator << (uint32_t c)

int and uint8_t types transfer 8 bits. Data order is big-endian (higher byte transferred first).

() operator

operator() (uint8_t val)
operator() (int val)

Writes one byte.

Worker Object (reader)

>> operator

operator >> (uint8_t& c)
operator >> (uint16_t& c)
operator >> (uint32_t& c)
operator >> (uint8_t(&c)[N]) // fixed array of N bytes

Reads the size of each data type. Data order is big-endian (first transferred byte is stored in the higher byte).

() operator

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 one byte. Returns -1 on error, or the read byte on success.

If b_stop is set to true, a STOP bit is issued on that read.

Example

The following example is a measurement example for the environmental sensor pal’s temperature and humidity sensor SHTC3.

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;

4 - Classes

Various Classes

4.1 - MWX_APIRET

Return Value Class
API return value class wrapping a 32-bit type. The MSB (bit 31) indicates success or failure. Bits 0 to 30 are used to store the return value.
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(); }
};

Constructors

MWX_APIRET()
MWX_APIRET(bool b)
MWX_APIRET(bool b, uint32_t val)

The default constructor initializes with the combination false, 0.

You can also explicitly construct with bool and uint32_t parameters.

Since a constructor with a bool parameter is implemented, you can use true/false as return values, like the following example:

MWX_APIRET myfunc() {
  if (...) return true;
  else false;
}

Methods

is_success(), operator bool()

inline bool is_success()
inline operator bool()

Returns true if the MSB is set to 1.

inline bool is_fail()

Returns true if the MSB is 0.

inline uint32_t get_value()
inline operator uint32_t()

Retrieves the value part from bits 0 to 30.

4.2 - alloc

Memory Allocation
Used as a template argument for container classes (smplbuf, smplque) to specify or allocate internal memory regions.
Class NameDescription
alloc_attach<T>Attach an existing buffer
alloc_local<T, int N>Statically allocate a buffer of N bytes internally
alloc_heap<T>Allocate the specified size on the heap

For alloc_attach and alloc_heap, you must call the appropriate initialization method (init_???()) depending on the allocator type.

Initialization

void attach(T* p, int n) // alloc_attach
void init_local()        // alloc_local
void init_heap(int n)    // alloc_heap

Initializes with buffer p and size n.

Methods

alloc_size()

uint16_t alloc_size()

Returns the size of the buffer.

_is_attach(), _is_local(), _is_heap()

These methods trigger a compile-time error using static_assert if an incompatible allocator method is used.

4.3 - axis_xyzt

Container class for accelerometer data
A struct for storing three-axis accelerometer sensor values, with additional utilities to enhance data handling convenience.
struct axis_xyzt {
    int16_t x;
    int16_t y;
    int16_t z;
    uint16_t t;
};

get_axis_{x,y,z}_iter()

/* Return type is a long template name, so we use auto&& for brevity */
auto&& get_axis_x_iter(Iter p)
auto&& get_axis_y_iter(Iter p)
auto&& get_axis_z_iter(Iter p)

Generates an iterator that accesses one of the X, Y, or Z axis elements, given an iterator to a container class holding axis_xyzt elements.

In the following example, buf.begin() and buf.end() are passed to get_axis_x_iter() and used with the std::minmax_element algorithm to analyze the X-axis.

##include <algorithm>

void myfunc() {
  // Container class
  smplbuf_local<axis_xyzt, 10> buf;

  // Insert test data
  buf[0] = { 1, 2, 3, 4 };
  buf[1] = { 2, 3, 4, 5 };
  ...

  // Algorithm to get min and max 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;
}

get_axis_{x,y,z}()

/* Return type is a long template name, so we use auto&& for brevity */
auto&& get_axis_x(T& c)
auto&& get_axis_y(T& c)
auto&& get_axis_z(T& c)

These functions generate a virtual container class that extracts one of the X, Y, or Z axes from a container class holding axis_xyzt elements. The generated container only implements begin() and end() methods, which return iterators equivalent to those from get_axis_{x,y,z}_iter().

##include <algorithm>

void myfunc() {
  // Container class
  smplbuf_local<axis_xyzt, 10> buf;

  // Insert test data
  buf[0] = { 1, 2, 3, 4 };
  buf[1] = { 2, 3, 4, 5 };
  ...

  // Extract the X-axis values from the queue
  auto&& vx = get_axis_x(que);

  // Use range-based for loop
  for (auto&& e : vx) { Serial << int(e) << ','; }

  // Algorithm to get min and max values
  auto&& minmax = std::minmax_element(
      vx.begin(), vx.end());

  Serial << "min=" << int(*minmax.first)
        << ",max=" << int(*minmax.second) << mwx::crlf;
}

4.4 - packet_rx

Received Packet
This class is a wrapper for the tsRxDataApp structure from the TWENET library.

This class object can be obtained via a behavior callback function or the on_rx_packets() method.

In packet_rx, the packet data payload is handled via the smplbuf container, and utility functions like expand_bytes() simplify the interpretation of the payload.

Methods

get_payload()

smplbuf_u8_attach& get_payload()

Retrieves the data payload of the packet.

get_psRxDataApp()

const tsRxDataApp* get_psRxDataApp()

Retrieves the receive structure from the TWENET C library.

get_length()

uint8_t get_length()

Returns the length of the payload. Equivalent to .get_payload().size().

get_lqi()

uint8_t get_lqi()

Retrieves the LQI (Link Quality Indicator).

get_addr_src_long(), get_addr_src_lid()

uint32_t get_addr_src_long()
uint8_t get_addr_src_lid()

Retrieves the source address.

get_addr_src_long() returns the source serial number. The MSB (bit 31) is always set to 1.

get_addr_src_lid() returns the source logical ID, which ranges from 0x00 to 0xFE (used in <NWK_SIMPLE>).

get_addr_dst()

uint32_t get_addr_dst()

Retrieves the destination address.

The destination address is set by the sender, and its value range depends on the address type.

ValueDescription
The serial number is used as the destination.Specifies the serial number as the destination.
0x00-0xFFAn 8-bit logical ID is used as the destination.

is_secure_pkt()

bool is_secure_pkt()

Returns true if the packet is encrypted; otherwise returns false.

get_network_type()

uint8_t get_network_type()

Returns the network type of the packet as identified by the network behavior.

ValueDescription
mwx::NETWORK::LAYEREDPacket from <NWK_LAYERED>
mwx::NETWORK::SIMPLEPacket from <NWK_SIMPLE>
mwx::NETWORK::NONEPacket not transmitted via a network (e.g., App_Twelite)
OtherError or unidentified packet

4.5 - packet_tx

Transmission Packet
This class is a wrapper for the tsTxDataApp structure from the TWENET C library. Objects derived from this class can be obtained via 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();
}

Creating the Object

It is created using the .prepare_tx_packet() method of the network behavior.

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

In the example above, the object pkt is obtained using the_twelite.network.use<NWK_SIMPLE>(). Although the type is inferred using auto&&, it will be a derived class of packet_tx.

The pkt object returns true or false depending on whether the transmission queue is available. It returns false if the transmission queue is full and no more requests can be added.

Transmission Settings

Various settings, such as the destination address, must be configured for a wireless packet to be delivered. These settings are applied by passing configuration objects as the right-hand value of the << operator.

pkt << tx_addr(0x00)
	  << tx_retry(0x1)
  	<< tx_packet_delay(0,50,10);

The following describes the configuration objects used for setting up transmission.

tx_addr

tx_addr(uint32_t addr)

Specifies the destination address addr. Refer to the network behavior specification for valid address values.

  • <NWK_SIMPLE> If MSB (bit 31 = 0x80000000) is set, it indicates the destination is a serial number of a wireless module. 0x00 to 0xEF indicates an 8-bit logical ID. 0xFE is broadcast to child nodes (0x010xEF), and 0xFF is broadcast to all nodes.

tx_retry

tx_retry(uint8_t u8count, bool force_retry = false)

Specifies the number of retransmissions using u8count. If force_retry is set to true, retransmission will occur regardless of whether the transmission succeeds.

  • <NWK_SIMPLE> Sends the same packet u8count+1 times. The force_retry setting is ignored.

tx_packet_delay

tx_packet_delay(uint16_t u16DelayMin,
                uint16_t u16DelayMax,
                uint16_t u16RetryDur)

Configures delay before transmission and retry interval. Specify u16DelayMin and u16DelayMax in milliseconds. Transmission will start at a random point within this interval. Retry interval is specified by u16RetryDur, also in milliseconds.

  • <NWK_SIMPLE> This setting is valid. If the same packet arrives more than one second after the first, it will not be excluded as a duplicate. This can occur due to a long retry interval or delay in relay. Duplicate handling can be configured in <NWK_SIMPLE> initialization.

tx_process_immediate

tx_process_immediate()

Requests that the packet be transmitted as quickly as possible. Transmission is normally triggered by a TickTimer operating every 1ms. This setting forces immediate processing. Has no effect unless used with tx_packet_delay(0,0,0).

Other packet transmissions in progress will be processed normally.

  • <NWK_SIMPLE> This setting is valid.

tx_ack_required

tx_ack_required()

In wireless packet communication, ACK (acknowledgment) is a short packet returned by the recipient to confirm successful reception. This setting enables ACK-based transmission.

  • <NWK_SIMPLE> This setting is invalid in <NWK_SIMPLE> and causes a compile error. <NWK_SIMPLE> does not support ACK-based communication.

tx_addr_broadcast

tx_addr_broadcast()

Specifies broadcast as the destination.

  • <NWK_SIMPLE> This setting is invalid in <NWK_SIMPLE> and results in a compile error. Instead, use tx_addr(0xFF) for general broadcast or tx_addr(0xFE) for broadcast to child nodes.

tx_packet_type_id

tx_packet_type_id(uint8_t)

Specifies the TWENET packet type ID (0 to 7).

  • <NWK_SIMPLE> This setting is invalid in <NWK_SIMPLE> and causes a compile error. The type ID is reserved internally and not user-configurable.

4.6 - serparser

Serial Format Input/Output (mwx::serial_parser)
Used for serial format input and output. It has an internal buffer that holds a parsed binary sequence. During input, it reads a sequence one byte at a time and stores it in the internal buffer according to the specified format, completing the process when the entire sequence has been interpreted. For output, it outputs the buffer according to the specified output format.

Three class names are defined depending on how the memory buffer is managed (alloc).

// serparser_attach : uses an existing buffer
serparser_attach

// serparser : allocates an internal buffer of N bytes
serparser_local<N>

// serparser_heap : allocates a buffer in the heap
serparser_heap

Constants (Format Type)

These are format types passed as parameters to the begin() initializer. Two types are supported here: ASCII and binary formats.

ConstantFormat Type
uint8_t PARSER::ASCII = 1ASCII format
uint8_t PARSER::BINARY = 2Binary format

About Formats

ASCII Format

The ASCII format is a method to represent a binary data sequence as a string.

For example, the byte sequence 00A01301FF123456 is represented in ASCII format as follows. It starts with :, the checksum is B1, and the terminator is [CR:0x0d][LF:0x0a].

:00A01301FF123456B1[CR][LF]

The terminating checksum can be omitted by replacing the checksum and CRLF sequence with X. Although this makes the format more vulnerable to corrupted data, it is useful for quick tests or sending data manually.

:00A01301FF123456X

Definition

SectionByte Count (Original)Byte Count (Format)Description
Header1: (0x3A) colon character
DataN2NEach byte is represented as two ASCII characters (A–F in uppercase). For example, 0x1F is represented as 1 (0x31) and F (0x46).
Checksum2The 8-bit sum of all data bytes is calculated, and the two’s complement of the result is taken. The checksum byte is represented as two ASCII characters. For example, for 00A01301FF123456, the sum is 0x4F, and its two’s complement is 0xB1.
Footer2[CR] (0x0D) and [LF] (0x0A) characters

Binary Format

The binary format appends a header and checksum to a binary data sequence for transmission.

For example, the byte sequence 00A01301FF123456 is represented in binary format as:

0xA5 0x5A 0x80 0x08 0x00 0xA0 0x13 0x01 0xFF 0x12 0x34 0x56 0x3D

Definition

SectionByte Count (Original)Byte Count (Format)Description
Header2Use 0xA5 0x5A
Data Length2Two bytes in big-endian format with MSB (0x8000) set. For example, if the data length is 8 bytes, use 0x80 0x08.
DataNNSpecifies the original data
Checksum1XOR of all data bytes. For example, XOR of 00A01301FF123456 results in 0x3D.
Footer(1)Checksum effectively marks the end. When output from a wireless module, 0x04 (EOT) is appended.

Methods

Declaration: begin()

// serparser_attach : uses an existing buffer
serparser_attach p1;

uint8_t buff[128];
p1.begin(PARSER::ASCII, buff, 0, 128);


// serparser : allocates an internal buffer of N bytes
serparser p2<128>;
p2.begin(PARSER::ASCII);


// serparser_heap : allocates a buffer on the heap
serparser_heap p3;
p3.begin(PARSER::ASCII, 128);

The declaration specifies the memory allocation class. Since this can be cumbersome, aliases are provided as shown above.

Class Name (Alias)
Memory Allocation
Description
serparser_attachUse an existing buffer specified via begin()
serparser_local<N>Allocate an internal buffer of N bytes
serparser_heapAllocate the specified size on the heap using the begin() method

Call the begin() method corresponding to the memory allocation class.

serparser_attach

void begin(uint8_t ty, uint8_t *p, uint16_t siz, uint16_t max_siz)

Uses the format specified by ty (see formats) and the buffer pointed to by p. The total buffer size is max_siz, and the valid data length is specified by siz.

This definition is especially useful when outputting a data sequence in formatted style (see >> operator).

serparser_local<N> - Allocate internal buffer

void begin(uint8_t ty)

Initializes using the format specified by ty (see formats).

serparser_heap - Allocate buffer on heap

void begin(uint8_t ty, uint16_t siz)

Initializes using the format specified by ty and allocates siz bytes on the heap.

get_buf()

BUFTYPE& get_buf()

Returns the internal buffer. The buffer is of type smplbuf<uint8_t, alloc>.

parse()

inline bool parse(uint8_t b)

Processes input characters. Accepts one byte of input at a time and interprets it according to the specified format. For example, in ASCII format, the sequence :00112233X is processed byte-by-byte (:, 0, 0, … X), and the format interpretation is completed upon receiving X.

The parse() parameter is the input byte. It returns true if interpretation has completed.

Example

while (Serial.available()) {
    int c = Serial.read();

    if (SerialParser.parse(c)) {
        // Format parsing complete, b holds the resulting data sequence (smplbuf<uint8_t>)
        auto&& b = SerialParser.get_buf();

        // Below is the processing for the obtained data sequence
        if (b[0] == 0xcc) {
          // ...
        }
    }
}

operator bool()

operator bool()

Returns true if parsing has completed via parse(), otherwise returns false while parsing is in progress.

Example (the parse() example can be rewritten as follows)

while (Serial.available()) {
    int c = Serial.read();

    SerialParser.parse(c);

    if(SerialParser) {
        // Format parsing complete, b holds the resulting data sequence (smplbuf<uint8_t>)
        auto&& b = SerialParser.get_buf();
        // ...
    }
}

<< Operator

Outputs the internal buffer to a stream (e.g., Serial) in formatted style.

Example

uint8_t u8buf[] = { 0x11, 0x22, 0x33, 0xaa, 0xbb, 0xcc };

ser_parser pout;
pout.begin(PARSER::ASCII, u8buf, 6, 6); // Specify 6 bytes from u8buf

Serial << pout;// Output to Serial in formatted style -> :112233AABBCC??[CR][LF]

4.7 - pktparser

Packet Parser
pktparser (parser_packet) interprets byte sequences converted by the 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 example above shows how to interpret a 0x81 message from the standard application. The parser_ser object receives input from Serial and converts it to a byte sequence. This sequence is then passed to identify_packet_type() to determine the packet type (E_PKT). If the packet type is identified, .parse<TwePacketTwelite>() is called to analyze the content. The result is of type TwePacketTwelite, and the .use<TwePacketTwelite>() function is used to retrieve the parsed object. TwePacketTwelite is a class, but its member variables can be accessed like a struct.

parse<T>

template <class T>
E_PKT parse(const uint8_t* p, const uint8_t* e)

Parses the byte sequence.

Specify the packet type T to be parsed. For example, use TwePacketTwelite for a 0x81 message from the standard application.

p and e specify the beginning and one-past-the-end of the byte sequence.

Returns a value of type E_PKT. If an error occurs, it returns E_PKT::PKT_ERROR.

use<T>

template 
T& use()

Returns a reference to the object corresponding to the interpreted byte sequence. Call this only after successfully executing parse<T>.

T must be the same type used in parse<T>, or you can specify TwePacket to retrieve only basic information.

4.7.1 - E_PKT

Packet Type Definitions

The following packet types are supported:

NameDescription
PKT_ERRORUsed before parsing or when the packet type cannot be determined. The TwePacket does not contain valid data.
PKT_TWELITEParsed result of the 0x81 command from the standard application App_Twelite.
PKT_PALParsed format of TWELITE PAL serial output.
PKT_APPIOParsed UART messages from the remote control application App_IO.
PKT_APPUARTParsed extended format from the serial communication application App_UART.
PKT_APPTAGParsed UART messages from the wireless tag application App_Tag. Sensor-specific parts are not interpreted but returned as raw payload bytes.
PKT_ACT_STDOutput format used by samples of Act.

4.7.2 - identify_packet_type()

Determine Packet Type

idenify_packet_type()

Determines the packet type from a byte sequence. Returns a value of E_PKT.

E_PKT identify_packet_type(uint8_t* p, uint8_t u8len)

If the byte sequence cannot be interpreted as a known packet type, E_PKT::PKT_ERROR is returned.

4.7.3 - TwePacket

Packet Type
This is the base class for packet types. The common member struct contains shared information such as address data.
class TwePacket {
	public:
		static const E_PKT _pkt_id = E_PKT::PKT_ERROR;

		struct {
			uint32_t tick;     // System time at the time of interpretation [ms]
			uint32_t src_addr; // Source address (serial number)
			uint8_t src_lid;   // Source address (logical ID)
			uint8_t lqi;       // LQI
			uint16_t volt;     // Voltage [mV]
		} common;
};

4.7.3.1 - TwePacketTwelite

Packet from App_Twelite
The TwePacketTwelite class interprets the 0x81 command from the standard application App_Twelite.
class TwePacketTwelite : public TwePacket, public DataTwelite { ... };

After executing parse<TwePacketTwelite>(), packet information is stored in DataTwelite.

DataTwelite Struct

struct DataTwelite {
		// Source serial number
		uint32_t u32addr_src;

		// Source logical ID
		uint8_t u8addr_src;

		// Destination logical ID
		uint8_t u8addr_dst;

		// Timestamp at transmission
		uint16_t u16timestamp;

		// Flag for low-latency transmission
		bool b_lowlatency_tx;

		// Number of repeat transmissions
		uint16_t u8rpt_cnt;

		// LQI value
		uint16_t u8lqi;

		// DI state (true = active, Lo/GND)
		bool DI1, DI2, DI3, DI4;
		// DI state bitmap (from LSB: DI1, DI2, DI3, DI4)
		uint8_t DI_mask;

		// True if DI has ever been active
		bool DI1_active, DI2_active, DI3_active, DI4_active;
		// DI active bitmap (from LSB: DI1, DI2, DI3, DI4)
		uint8_t DI_active_mask;

		// Module power voltage [mV]
		uint16_t u16Volt;

		// AD values [mV]
		uint16_t u16Adc1, u16Adc2, u16Adc3, u16Adc4;
		// Bitmap indicating which ADs are active (from LSB: AD1, AD2, AD3, AD4)
		uint8_t Adc_active_mask;
};

4.7.3.2 - TwePacketIO

Packet from App_IO
The TwePacketAppIO class interprets the standard application’s App_IO serial message (0x81).
class TwePacketAppIO : public TwePacket, public DataAppIO { ... };

Packet data details are stored in DataTwelite after executing parse<TwePacketIO>().

DataAppIO Struct

struct DataAppIO {
		// Source serial number
		uint32_t u32addr_src;

		// Source logical ID
		uint8_t u8addr_src;

		// Destination logical ID
		uint8_t u8addr_dst;

		// Timestamp at 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 state bitmap (from LSB: DI1, DI2, DI3, DI4, ...)
		uint8_t DI_mask;

		// DI active (1 if in use) bitmap (from LSB: DI1, DI2, DI3, DI4, ...)
		uint8_t DI_active_mask;

		// Bitmap indicating whether DI is triggered by interrupt (from LSB: DI1, DI2, DI3, DI4, ...)
		uint16_t DI_int_mask;
};

4.7.3.3 - TwePacketUART

Packet from App_UART
The TwePacketAppUart class represents the extended format of App_UART received by the parent or repeater application App_Wings.
class TwePacketAppUART : public TwePacket, public DataAppUART

After executing parse<TwePacketUART>(), the packet information is stored in DataAppUART.

DataAppUART Struct

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

payload represents the data portion, but the storage method varies depending on the macro definition.

If MWX_PARSER_PKT_APPUART_FIXED_BUF is set to 0 during compilation, payload will reference the byte sequence directly. If the original byte sequence is modified, the data in payload may become corrupted.

If MWX_PARSER_PKT_APPUART_FIXED_BUF is defined with a value greater than 0, a buffer of that size (in bytes) is allocated for payload. However, if the size of the serial data exceeds the buffer, parse<TwePacketAppUART>() will fail and return E_PKT::PKT_ERROR.

4.7.3.4 - TwePacketPAL

Packet from App_PAL
The TwePacketPal class interprets packet data from TWELITE PAL. This class provides a common interface for handling TWELITE PAL packets (e.g., sensor data sent upstream).
class TwePacketPal : public TwePacket, public DataPal { ... };

Common PAL data is defined in DataPal.

Generator functions are provided to extract sensor-board-specific data for each type of PAL.

DataPal Struct

The data structure of PAL packets varies depending on the connected sensor, but DataPal holds the common part of the data structure.

struct DataPal {
	uint8_t u8lqi;        // LQI value

	uint32_t u32addr_rpt; // Address of repeater

	uint32_t u32addr_src; // Source address
	uint8_t u8addr_src;   // Source logical address

	uint16_t u16seq;      // Sequence number

	E_PAL_PCB u8palpcb;		// Type of PAL board
	uint8_t u8palpcb_rev;	// Revision of PAL board
	uint8_t u8sensors;		// Number of sensor data entries included (MSB=1 indicates error)
	uint8_t u8snsdatalen; // Length of sensor data (in bytes)

	union {
		const uint8_t *au8snsdata; // Pointer to sensor data
		uint8_t _pobj[MWX_PARSER_PKT_APPPAL_FIXED_BUF]; // Sensor-specific objects
	};
};

A PAL packet consists of two main blocks: the common PAL part and a sensor-specific data part. The sensor-specific part is stored as-is without parsing. To simplify handling, data exceeding 32 bytes is stored in uptr_snsdata, which is dynamically allocated.

The sensor-specific part is stored in a structure that inherits from PalBase. This structure is generated by generator functions defined in TwePacketPal.

When parse<TwePacketPAL>() is executed and the data fits within MWX_PARSER_PKT_APPPAL_FIXED_BUF, sensor-specific objects are created.

If it exceeds that size, a reference to the raw byte sequence is stored in au8snsdata.

In such cases, if the original byte sequence is modified, sensor-specific objects can no longer be generated.

PalBase

All sensor-specific structures for PAL inherit from PalBase, which contains the sensor data storage status u32StoredMask.

	struct PalBase {
		uint32_t u32StoredMask; // Internal flag indicating which data is stored
	};

PalEvent

A PAL event does not directly transmit raw sensor data, but rather processed information triggered under specific conditions. For example, when acceleration exceeds a threshold from a stationary state.

	struct PalEvent {
		uint8_t b_stored;       // True if stored
		uint8_t u8event_source; // Reserved
		uint8_t u8event_id;     // Event ID
		uint32_t u32event_param;// Event parameter
	};

If event data is present, .is_PalEvent() in TwePacketPal returns true, and .get_PalEvent() provides the PalEvent structure.

Generator Functions

Generator functions are provided to extract sensor-board-specific data for each type of 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 generator functions, first check whether pkt is an event using .is_PalEvent(). If so, retrieve the data using get_PalEvent(). Otherwise, generate the appropriate object based on u8palpcb.

get_PalMag()

PalMag get_PalMag()

// MAG
struct PalMag : public PalBase {
    uint16_t u16Volt;         // Module voltage [mV]
    uint8_t u8MagStat;        // Magnetic switch state [0: no magnet, 1, 2]
    uint8_t bRegularTransmit; // MSB flag of u8MagStat
};

If .u8palpcb == E_PAL_PCB::MAG, retrieve PalMag data for the open-close sensor PAL.

get_PalAmb()

PalAmb get_PalAmb()

// AMB
struct PalAmb : public PalBase {
    uint16_t u16Volt;       // Module voltage [mV]
    int16_t i16Temp;        // Temperature (value ×100)
    uint16_t u16Humd;       // Humidity (value ×100)
    uint32_t u32Lumi;       // Illuminance (approx. lux)
};

If .u8palpcb == E_PAL_PCB::AMB, retrieve PalAmb data for the environmental sensor PAL.

get_PalMot()

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, retrieve PalMot data for the motion sensor PAL.

get_PalEvent()

PalEvent get_PalEvent()

// PAL event
struct PalEvent {
    uint8_t b_stored;        // True if event information is available
    uint8_t u8event_source;  // Event source
    uint8_t u8event_id;      // Event ID
    uint32_t u32event_param; // 24-bit event parameter
};

If .is_PalEvent() is true, retrieve PalEvent data.

4.8 - smplbuf

Container class with array structure
This is a container class based on an internal array structure. The maximum buffer size is defined at initialization, and within this range it behaves as a variable-length array.
template <typename T, int N> smplbuf_local
template <typename T> smplbuf_attach
template <typename T> smplbuf_heap

smplbuf is a container class that provides array operations for a memory region specified by the element type T and the memory allocation method alloc. Since specifying alloc directly is cumbersome, aliases are defined using using.

Below is an example of object declaration. After declaration, call an initialization method. Each object starts with a maximum size of 128 bytes and a current size of 0. Expand the size as needed during use.

// Array area is a fixed array as a class member variable
smplbuf_local<uint8_t, 128> b1;

// Array area references an existing buffer
uint8_t buf[128];
smplbuf_attach<uint8_t> b2(;

// Array area is allocated on the heap
smplbuf_heap<uint8_t> b3;

// Initialization (if defined globally, do this in setup())
void setup() {
    b1.init_local();
    b2.attach(buf, 0, 128);
    b3.init_heap(128);
}

// Inside a processing function
void some_func() {
    smplbuf_local<uint8_t, 128> bl;
    // Can be omitted if smplbuf_local is defined locally

    bl.push_back('a');
}

Aliases are available for the uint8_t type only.

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

You can access elements using the [] operator like a normal array, and also use iterators.

void begin() { // begin() runs only once at startup
  smplbuf_u8<32> b1;
  b1.reserve(5); // Initializes 5 bytes of usable area (accessible via b1[0..5])

  b1[0] = 1;
  b1[1] = 4;
  b1[2] = 9;
  b1[3] = 16;
  b1[4] = 25;

  for(uint8_t x : b1) { // Loop using .begin() and .end() implicitly
    Serial << int(x) << ",";
  }
}

The push_back() method is defined, allowing algorithms that append data to the end.

Declaration and Initialization

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
// Fixed-size internal array
smplbuf_local<uint8_t, 128> b1;
b1.init_local();

// Using an existing array
uint8_t buf[128];
smplbuf_attach<uint8_t> b2;
b2.attach(buf, 0, 128);

// Allocated on the heap
smplbuf_heap<uint8_t> b3;
b3.init_heap(128);

Declares a container of type T and size N. Call an initialization method after declaration.

smplbuf_local allocates memory using an internal fixed array. Initialization via constructor is also supported.

smplbuf_attach requires the pointer to the buffer T* buf, the initial size size, and the maximum size N. Initialization via constructor is also supported.

smplbuf_heap allocates memory in the heap (a memory region that cannot be released but can be allocated at runtime). Since the memory cannot be freed once allocated, it is typically used as a global object. Memory allocation is done via init_heap(). Allocation via constructor is not supported; always use init_heap().

Initializer List

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

Members can be initialized using an initializer list { ... }. Except for local declarations of smplbuf_local, you must call an initialization method first.

  • As the right-hand value of an assignment (smplbuf_local, smplbuf_attach, smplbuf_heap)
  • Constructor (for local declarations of smplbuf_local, not allowed in global scope)

Methods

append(), push_back(), pop_back()

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

Adds the member c to the end. append() returns a bool, which is false if the buffer is full and the element cannot be added.

pop_back() removes the last entry. Note that it does not clear the content.

empty(), size(), capacity()

inline bool empty()
inline bool is_end()
inline uint16_t size()
inline uint16_t capacity()

empty() returns true if the array has no elements. Conversely, is_end() returns true when the array is full.

size() returns the number of elements in the array.

capacity() returns the maximum number of elements the array can hold.

reserve(), reserve_head(), redim()

inline bool reserve(uint16_t len)
inline void reserve_head(uint16_t len)
inline void redim(uint16_t len)

reserve() expands the array size. The newly allocated region is initialized by default.

reserve_head() reserves space at the head of the array that is not accessible via the container. This can be used, for example, when accessing a subarray after skipping the header of a packet payload. To restore access to the reserved region, provide the same negative value used during reservation.

redim() changes the size of the active region. Unlike reserve(), it does not initialize unused areas.

operator []

inline T& operator [] (int i)
inline T operator [] (int i) const

Accesses an element by index.

If a negative value is given to i, the element is accessed from the end. -1 refers to the last element, -2 to the second last, and so on.

Output to mwx::stream

Array objects of type uint8_t (smplbuf<uint8_t, *>) can be directly output to derived objects of mwx::stream.

<< Operator

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;
// Output: abc

Outputs the byte array to a derived object of mwx::stream, such as Serial.

to_stream()

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

Used for stream output purposes. This method is used in the implementation of the << operator.

Generating Data with mwx::stream

mwx::stream provides functions and operators such as the << operator and printfmt() method for outputting byte arrays to a stream. You can use a smplbuf array of uint8_t as a stream output target.

There are two methods.

4.8.1 - get_stream_helper()

Helper object for using mwx::stream
Use operators and methods provided by mwx::stream via the stream_helper that refers to a uint8_t smplbuf array.
smplbuf_u8<32> b;
auto&& bs = b.get_stream_helper(); // helper object

// Generate data sequence
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

Since the type name of the helper object can be long, it is resolved using auto&&. You can use interfaces defined by mwx::stream, such as the << operator, with this object.

The created helper object bs starts reading and writing from the head of the base array b. If at the end of the array, data is appended using append(). The position advances with each read/write operation.

The helper function supports the >> operator for reading.

// ...continued from the above example
// ABCD;0123;99 <- stored in b

// Variables to store read data
uint8_t FOURCHARS_READ[4];
uint32_t u32val_read;
uint8_t c_read[2];

// Read using >> operator
bs.rewind();                // rewind position to the start
bs >> FOURCHARS_READ;      // 4 characters
bs >> mwx::null_stream(1); // skip 1 character
bs >> u32val_read;         // 32-bit data
bs >> mwx::null_stream(1); // skip 1 character
bs >> c_read;              // 2 characters

// Display results
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

4.8.2 - smplbuf_strm_u8

Type that allows direct use of stream methods
The smplbuf_strm_u8??? type for uint8_t also implements the stream interface, allowing use of several stream-related methods.
// smplbuf_strm_u8<N> : local allocation
template <int N> using smplbuf_strm_u8
  = _smplbuf_stream<uint8_t, mwx::alloc_local<uint8_t, N>>;

// smplbuf_strm_u8_attach : attaches to existing buffer
using smplbuf_strm_u8_attach
  = mwx::_smplbuf_stream<uint8_t, mwx::alloc_attach<uint8_t>>;

// smplbuf_strm_u8_heap : heap allocation
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!

4.9 - smplque

Container class with FIFO queue structure
This is a container class with FIFO queue structure.
template <typename T, int N, class Intr> smplbuf_local
template <typename T, class Intr> smplbuf_attach
template <typename T, class Intr> smplbuf_heap

smplque is a container class that provides FIFO queue operations on a memory region specified by the element type T and the memory allocation method alloc. Since specifying alloc can be complicated, alias definitions using using are provided.

You can register a class Intr that sets interrupt disabling settings at declaration. If not specified, the default behavior will not perform any interrupt control.

Here are examples of object declarations. Initialization methods are called right after declaration. In all cases, the maximum size immediately after initialization is 128 bytes, and the initial size is 0, meaning nothing is stored. The maximum size cannot be changed.

void some_func() {
  // Uses internal fixed-size array
  smplque_local<uint8_t, 128> q1;

  // Uses an existing array
  uint8_t buf[128];
  smplque_attach<uint8_t> q2;

  // Allocates on the heap
  smplque_heap<uint8_t> q3;
}

void setup() {
  // Initialize global objects in setup()
  q1.init_local();
  q2.attach(buf, 128);
  q3.init_heap(128);
}

void some_func() {
  // Local smplque_local can omit init_local()
  smplque_local<uint8_t, 128> q_local;
  ..
}

As a FIFO queue, it is operated using methods such as push(), pop(), and front().

void begin() { // begin() runs only once at 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,
}

Access using iterators is also possible.

void begin() { // begin() runs only once at startup
	smplque_local<int, 32> q1;
	q1.init_local();

	q1.push(1);
	q1.push(4);
	q1.push(9);
	q1.push(16);
	q1.push(25);

	// Using iterator
	for(int x : q1) {
		Serial << int(x) << ',';
	}

	// Using STL algorithms
	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[]
}

Declaration and Initialization

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
// Uses internal fixed-size array
smplque_local<uint8_t, 128> q1;
q1.init_local();

// Uses an existing array
uint8_t buf[128];
smplque_attach<uint8_t> q2;
q2.attach(buf, 128);

// Allocates on the heap
smplque_heap<uint8_t> q3;
q3.init_heap(128);

Declare a container of type T and size N. After declaration, call the initialization method.

smplque_local allocates space with an internal fixed-size array. It can also be initialized using a constructor.

smplque_attach requires the pointer to the buffer to use (T* buf), the initial size of the array, and the maximum size N. It can also be initialized using a constructor.

smplque_heap allocates memory in the HEAP area (memory that cannot be released but can be allocated at any time). Since it cannot be released once allocated, it is usually defined in the global scope. The memory allocation is performed by calling init_heap(). Memory allocation via constructor is not possible. Always call init_heap() to use it.

Methods

push(), pop(), front(), back()

inline void push(T&& c)
inline void push(T& c)
inline void pop()
inline T& front()
inline T& back()

inline T& pop_front()

push() adds an entry to the queue.

pop() removes an entry from the queue.

front() accesses the first entry (the one added first).

back() accesses the last entry (the one added last).

pop_front() returns the first entry and simultaneously removes it from the queue.

empty(), size(), is_full()

inline bool empty()
inline bool is_full()
inline uint16_t size()
inline uint16_t capacity()

empty() returns true if the queue has no elements. is_full() returns true when the queue is full.

size() returns the number of elements currently in the queue.

capacity() returns the maximum number of elements the queue can hold.

clear()

inline void clear()

Removes all elements from the queue.

operator []

inline T& operator[] (int i)

Accesses the element at index i. 0 is the first added element.

Iterator

inline smplque::iterator begin()
inline smplque::iterator end()

Returns iterators via begin() and end(). The beginning iterator points to the first element added to the queue. By using iterators, you can use range-based for loops and algorithms.

As an advanced usage, see iterator access focusing on specific members of the axis_xyzt structure.

4.10 - Input/Output Stream

Base class for handling input/output streams
This is a base class for handling input/output streams.
  • Provides interfaces to several classes (Serial, Wire, SPI, smplbuf) using polymorphism with the CRTP (Curiously Recurring Template Pattern) method.
    • In CRTP, derived classes are defined as template class Derived : public stream<Derived>;, allowing the base class to also refer to methods of the derived class.
  • This class defines common processing such as the print method and the << operator, and calls methods like write() implemented in derived classes, achieving an implementation similar to using virtual functions.

Interface (Implemented in Derived Classes)

Derived classes implement the following functions.

available()

int available()

// example
while(Serial.available()) {
  int c = Serial.read();
  // ... any
}

Returns 1 if input exists, 0 if not.

ParameterDescription
Return int0: No data 1: Data available

flush()

void flush()

// example
Serial.println("long long word .... ");
Serial.flush();

Flushes the output (waits until output is complete).

read()

int read()

// example
int c;
while (-1 != (c = read())) {
    // any
}

Reads one byte of data from the stream. Returns -1 if no data is available.

write()

size_t write(int c)

// example
Serial.write(0x30);

Outputs one byte to the stream.

ParameterDescription
nThe character to output.
Return size_t1 if output succeeded, 0 if failed.

vOutput()

static void vOutput(char out, void* vp)

A static function that outputs one byte. Since it is not a class method, member variables cannot be used. Instead, a pointer to the class instance is passed as the parameter vp.

This static function is used internally and passed as a function pointer for one-byte output to fctprintf(). It is used to implement methods like print.

ParameterDescription
outThe character to output
vpPointer to the class instance
Usually cast back to the original class to call the write() method

Interface

putchar()

void mwx::stream::putchar(char c)

// example
Serial.putchar('A');
// result -> A

Outputs one byte.

size_t print(T val, int base = DEC) // T: integer type
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 formatted outputs.

ParameterDescription
valThe numeric type to format and output
baseOutput format BIN binary / OCT octal / DEC decimal / HEX hexadecimal
placeNumber of decimal places
Return size_tNumber of bytes written

printfmt()

size_t printfmt(const char* format, ...);

// example
Serial.printfmt("the value is %d.", 123);
// result -> the value is 123.

Outputs formatted output in printf style.

See TWESDK/TWENET/current/src/printf/README.md

operator <<

// 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.
Argument TypeDescription
charOutputs one byte (does not format as a number)
intOutputs integer (printf “%d”)
doubleOutputs number (printf “%.2f”)
uint8_tOutputs one byte (same as char type)
uint16_tOutputs 2 bytes (big endian order)
uint32_tOutputs 4 bytes (big endian order)
const char*``uint8_t*``const char[S]Outputs up to the terminating character. The terminator is not included in output. (S is fixed array size)
uint8_t[S]Outputs S bytes of the array as is. (S is fixed array size)
format()Outputs in printf format
mwx::crlfOutputs newline CRLF
mwx::flushFlushes output
bigendian()Outputs numeric types in big endian order (rvalue)
std::pair<T*, T*>Pair containing begin(), end() pointers of byte type. Can be generated by make_pair. T is assumed to be uint8_t. (rvalue)
bytelist()Outputs byte sequence using std::initializer_list
smplbuf<uint8_t,AL>&Outputs contents of an array class of type uint8_t. ALC is memory allocation method.
smplbuf<uint8_t, AL>::to_stream()Outputs data of smplbuf<T>
T is uint8_t type, AL is memory allocation method.

set_timeout(), get_error_status(), clear_error_status()

uint8_t get_error_status()
void clear_error_status()
void set_timeout(uint8_t centisec)

// example
Serial.set_timeout(100); // Set timeout to 1000ms
uint8_t c;
Serial >> c;

Manages input timeout and errors using the >> operator.

Specify the timeout duration with set_timeout(), and perform input with the >> operator. If input is not obtained within the specified time, error value can be read with get_error_status(). Clear error status with clear_error_status().

Argument TypeDescription
centisecSets timeout duration in 1/10 second units. Specifying 0xff disables timeout.

Error Values

ValueMeaning
0No error
1Error status

operator >>

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 the watchdog
Serial.set_timeout(0xFF); // No timeout

// Read one byte
Serial >> c;
Serial << crlf << "char #1: [" << c << ']';

// Discard bytes
Serial >> null_stream(3); // Discard 3 bytes
Serial << crlf << "char #2-4: skipped";

// Read 4 bytes (fixed-length array of uint8_t only)
uint8_t buff[4];
Serial >> buff;
Serial << crlf << "char #5-8: [" << buff << "]";

Performs input.

Below are the types that can be read and stored.

Argument TypeDescription
uint8_t, char_tReads one byte
uint16_tReads 2 bytes (big endian order)
uint32_tReads 4 bytes (big endian order)
uint8_t[S]Reads S bytes (S is fixed array size)
null_stream(int n)Discards n bytes

4.10.1 - mwx::mwx_format

printf format input
This is a helper class that writes format specifiers for the << operator of mwx::stream.

Within the library, it is aliased as Using format=mwx::mwx_format;.

Serial << format("formatted print: %.2f", (double)3123 / 100.) << mwx::crlf;

// formatted print: 31.23[newline]
  • Store the argument list received by the constructor into internal class variables using parameter pack expansion
  • When operator << is called, call fctprintf() and write data to the stream

Constructor

format(const char *fmt, ...)

The constructor saves the format pointer and parameters. The following << operator call interprets the format and performs output processing.

ParameterDescription
fmtFormat string. See TWESDK/TWENET/current/src/printf/README.md
...Parameters corresponding to the format string.
※ The maximum number is 4; using 5 or more parameters will cause a compile error. ※ Consistency with the format is not checked, so unsafe for inconsistent input.

4.10.2 - mwx::bigendian

Data output in big-endian order
This is a helper class that outputs numeric types as big-endian byte sequences for the << operator of mwx::stream.
Serial << mwx::bigendian(0x1234abcdUL);

// output binary -> 0x12 0x34 0xab 0xcd

Constructor

template <typename T>
bigendian::bigendian(T v)
ParameterDescription
vA value of type uint16_t or uint32_t

4.10.3 - mwx::crlf

Output of newline code
An instance of a helper class to output newline code (CR LF) for the << operator of mwx::stream.
Serial << "hello world!" << mwx::crlf;

4.10.4 - mwx::flush

Force output of the output buffer
Flushes the output buffer of mwx::stream.

An instance of a helper class that calls the flush() method.

for (int i = 0; i < 127; ++i) {
    Serial << "hello world! (" << i << ")" << twe::endl << twe::flush;
}
  • In the case of a serial port, polling wait is performed until output is complete
  • In the case of an mwx::simpbuf buffer, 0x00 is output at the end (size does not change)

4.10.5 - stream_helper

Helper object
stream_helper is a helper object that provides the mwx::stream interface. It generates a helper object that references a data class, and performs data input/output through the helper object.

Below, a helper object bs is generated from the array b of smplbuf, and data input is performed using the mwx::stream::operator <<() operator.

smplbuf_u8<32> b;
auto&& bs = b.get_stream_helper(); // Helper object

// Generate data sequence
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 is via smplbuf_u8<32> class

// Result: ABCD;0123;99

Overview

stream_helper behaves as if the data array is a stream.

Internally, it keeps track of the read/write position within the data array. It behaves as follows:

  • When reading or writing, the read/write position moves to the next position.
  • After reading the last data or appending data to the end, the read/write position becomes the end position.
  • When the read/write position is at the end,
    • available() returns false.
    • Reading is not possible.
    • Writing appends data if within writable range.

Creating stream_helper

stream_helper is created from member functions of data classes (smplbuf, EEPROM).

auto&& obj_helper = obj.get_stream_helper()
// obj is an object of a data class, and obj_helper's type is long, so it is received with auto&&.

Methods

rewind()

void rewind()

Moves the read/write position to the beginning.

seek()

int seek(int offset, int whence = MWX_SEEK_SET)

Sets the read/write position.

whencePosition set
MWX_SEEK_SETSets from the beginning. offset of 0 means the same as rewind().
MWX_SEEK_CURMoves by offset from the current position.
MWX_SEEK_ENDSets to the end position. offset of 0 sets to the end. -1 moves to the last character.

tell()

int tell()

Returns the read/write position. Returns -1 if at the end position.

available()

int available()

Returns 0 if the read/write position is at the end. Otherwise, returns a non-zero value.

4.11 - SM_SIMPLE State Machine

State management
SM_SIMPLE is used in sample code for handling state transitions, waiting for timeouts, transmission completion, and similar processing.

Here is a basic example of 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(); // Initialization
}

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 (/* Transmission request successful */) {
        step.set_timeout(100); // Set timeout
        step.clear_flag(); // Waiting for completion

        step.next(STATE::TX_WAIT_COMP);
      }
    break;

    case STATE::TX_WAIT_COMP:
      if (step.is_timeout()) the_twelite.reset_system(); // Timeout
      if (step.is_flag_ready()) sleepNow(); // Flag was set
    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); // 10 sec
}

Explanation

To use SM_SIMPLE, define an enum class that lists the states. In the example above, this is defined as STATE. You create a class object like SM_SIMPLE<STATE> step; using the enum as the template parameter. Then call .setup() on the object to initialize it.

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 a value of 0, which corresponds to STATE::INIT in the above example. To get the current state, use .state() and use it in a switch statement inside a do while loop as shown.

loop() {
  do {
    switch(step.state()) {
    case STATE::INIT: // State with value 0
    ...

To transition states, call .next(). When the state changes, b_more_loop() returns true and the do while loop executes again. In the example, calling .next(STATE::TX) from STATE::SENSOR causes another loop iteration, executing the case STATE::TX: block. If the state does not change, the loop exits, and loop() ends until the next call.

  do {
    switch(step.state()) {
    ...
    case STATE::SENSOR:
      ...
      step.next(STATE::TX); // (1) State transition
    break;

    case STATE::TX: // (3) Called on second loop
      if (/* Transmission request successful */) {
      ...
    }
  } while (b_more_loop()); // (2) Loop continuation check

To wait for completion of processing (e.g., transmission complete), call .clear_flag() and later call .set_flag(uint32_t) from a callback or similar to signal completion. You can retrieve the passed uint32_t value using .get_flag_value().

To handle timeouts, call .set_timeout(uint32_t) to store the current time, then use .is_timeout() to check if the timeout duration has elapsed.

    case STATE::TX:
      if (/* Transmission request successful */) {
        step.set_timeout(100); // Set timeout
        step.clear_flag(); // Wait for completion

        step.next(STATE::TX_WAIT_COMP);
      }
    break;

    case STATE::TX_WAIT_COMP:
      if (step.is_timeout()) ...; // Timeout
      if (step.is_flag_ready()) ...; // Flag was set
    break;
...

// Transmission complete event
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
	step.set_flag(ev.bStatus); // Set the flag
}

To continue using SM_SIMPLE after waking from sleep, be sure to call .on_sleep(bool) before going to sleep. If you pass false, the state machine resets to state 0 upon waking; if true, it resumes from the pre-sleep state.

void sleepNow() {
	step.on_sleep(false); // Reset state machine
  the_twelite.sleep(10000); // 10 sec
}

Source Code

Below 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 vary depending on the version.
  • The source is located in the MWX library source folder under SM_SIMPLE.hpp.

5 - Callback Functions

Various callback functions
These are callback functions where the application description is written. Callback means that it is called from the system (library). By defining several callback functions, the user describes the behavior of the system.

The following callback functions are mandatory definitions.

  • setup()
  • loop()

Functions other than these will be linked as empty functions that do nothing if not defined.

Normal callback invocation order

init_coldboot()
  ↓ (TWENET internal processing: Initialization 1)
setup()
  ↓ (TWENET internal processing: Initialization 2)
begin() --- only once
  ↓
loop() <--+
  ↓       | Event processing, behavior processing
CPU DOZE -+

Callback invocation order when waking up from sleep

the_twelite.sleep()
  ↓ sleeping...


init_warmboot()
  ↓ (TWENET internal processing: Initialization 3)
wakeup()
  ↓ (TWENET internal processing: Initialization 4)
loop() <--+
  ↓       | Event processing, behavior processing
CPU DOZE -+

5.1 - setup()

Initialization process
Called at the beginning of code execution; write initialization code here.

5.2 - begin()

Initialization process (after TWENET initialization)
Called only once just before the first call of the loop() function. Since TWENET initialization is complete, there is no need to consider constraints like those in setup().

Mainly used in the following situations:

  • Displaying the startup message
  • Writing test code
  • Transitioning to sleep immediately after startup
  • Processing that is problematic in setup() (wireless packet processing, timer operation, etc.)

5.3 - loop()

Loop processing
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 power consumption.

In Act, most processing is written inside this loop.

5.4 - wakeup()

Processing after waking from sleep
Called before transitioning to loop() after waking from sleep, this function includes procedures for initialization after waking and branching processes according to the wake-up state.

5.5 - init_coldboot()

Initialization process (before peripheral initialization)
Called at the re-initialization of code execution, before peripheral API and initialization have been performed.

5.6 - init_warmboot()

Process after waking from sleep (before peripheral initialization)
Called again after waking from sleep, before the peripheral API is initialized.

5.7 - on_rx_packet()

Processing upon packet reception
Describes the process when a received packet is received.
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 will be linked.

If true is set to b_handled within this function, it notifies the MWX library that the received packet has been processed within the application. When marked as processed, unnecessary processing is suppressed (the processing of the_twelite.receiver is not performed).

5.8 - on_tx_comp()

Processing upon packet transmission completion
Describes the processing performed when the transmission is completed.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled)

This function is called within the MWX library when the wireless packet transmission is finished, with data stored in ev as packet_ev_tx. If this function is not defined in the application, a do-nothing weak function is linked.

ev.u8CbId is the ID at transmission, and ev.bStatus is a flag indicating transmission success (1) or failure (0).

If true is set to b_handled within this function, it informs the MWX library that the received packet has been processed within the application. When marked as processed, unnecessary processing is suppressed (event callback functions are not called for the_twelite.app, .board, or .settings).

6 - Behavior

An advanced framework for implementing complex applications
In Behavior, defining a class in a specified manner allows it to be registered with the the_twelite class object. Once registered, the behavior is integrated into TWENET and operates accordingly. User code can describe the application’s behavior via this mechanism. Unlike loop-based implementations, it enables defining interrupt handlers and callback functions from TWENET. Although it requires more code, this approach is suitable for constructing more complex applications.

Class Definition (.hpp)

A behavior is defined using a class structure like the one below.

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

In the example above, a behavior class named MY_APP_CLASS is defined. Several places require the name MY_APP_CLASS.

class MY_APP_CLASS: MWX_APPDEFS_CRTP(MY_APP_CLASS)

Defines the class name and the base (parent) class. MWX_APPDEFS_CRTP() is a macro.

    #define __MWX_APP_CLASS_NAME MY_APP_CLASS
    #include "_mwx_cbs_hpphead.hpp"
    #undef __MWX_APP_CLASS_NAME

Includes the necessary definitions via #include.

MY_APP_CLASS() {}

Defines the constructor.

Methods

loop()

The main loop function, serving the same role as the globally defined loop().

on_create()

on_create() is called when the object is created (via the use<>() method). The val parameter is reserved for future extensions.

on_begin()

on_begin() is called after setup() completes. The val parameter is reserved for future extensions.

on_sleep()

Called before entering sleep. The val parameter is reserved for future extensions.

warmboot()

Called at the initial stage of wakeup from sleep. The val parameter is reserved for future extensions.

At this point, peripherals are not yet initialized. You can check the cause of the sleep wakeup.

wakeup()

Called upon waking from sleep. The val parameter is reserved for future extensions.

receive()

void receive(mwx::packet_rx& rx)

Called when a packet is received, with the received packet information passed as rx.

transmit_complete()

void transmit_complete(mwx::packet_ev_tx& evTx)

Called when packet transmission is complete, with the transmission information passed as evTx. evTx.u8CbId is the ID used during transmission, and evTx.bStatus is a flag indicating transmission success (1) or failure (0).

6.1 - PAL_AMB-behavior

A sample behavior using the Ambient Sensor PAL

This sample retrieves sensor values using the Ambient Sensor PAL.

  • Demonstrates parent/child configuration using Behavior.
  • Instead of using the Board Behavior functionality, sensor values are read directly using Wire.
  • The child device is implemented as a state machine.

Act Functionality

  • Retrieves sensor values using the Ambient Sensor PAL.
  • Uses sleep functionality to enable operation with coin cell batteries.

How to Use the Act

Preparing TWELITE

RoleExample
ParentMONOSTICK BLUE or RED
ChildBLUE PAL or RED PAL + Ambient Sensor PAL

File Structure

  • PAL_AMB-behavior.hpp: Defines only setup(). Reads DIP switches and acts as a parent if D1..D3 are ON; otherwise, acts as a child using the DIP switch as ID.
  • Parent/myAppBhvParent.hpp: Behavior class definition for the parent.
  • Parent/myAppBhvParent.cpp: Implementation.
  • Parent/myAppBhvParent-handlers.cpp: Handler implementations.
  • Child/myAppBhvChild.hpp: Behavior class definition for the child.
  • Child/myAppBhvChild.cpp: Implementation.
  • Child/myAppBhvChild-handlers.cpp: Handler implementations.

The behavior name for the parent is <MY_APP_PARENT>, and for the child it is <MY_APP_CHILD>.

Initialization setup()

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

If the DIP switch value is 0, the parent behavior <MY_APP_PARENT> is registered; otherwise, the child behavior <MY_APP_CHILD> is used.

Parent Behavior

The parent device acts as a receiver that does not sleep and outputs packet information to the serial port upon receiving packets from child devices.

MY_APP_PARENT::receive()

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

When the parent receives a packet, if the first four characters of the packet match (FOURCHARS), the packet content is displayed.

MY_APP_PARENT::MWX_TICKTIMER_INT()

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

The interrupt handler for the parent blinks the LED.

MY_APP_PARENT::MWX_DIO_EVENT(PAL_AMB::PIN_BTN)

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

When the button (5) on the PAL is pressed, an E_ORDER_KICK event is issued to the state machine.

MY_APP_PARENT::MWX_STATE(E_MWX::STATE_0 .. 3)

The state machine described here serves as a reference for state transitions and does not have functional significance in the application. It handles state transitions triggered by the E_ORDER_KICK event from the button and timeouts.

Child Behavior

The operation flow of the child device is similar to PAL_AMB-usenap: it repeatedly performs the cycle of wake up → start sensor operation → short sleep → wake up → read sensor values → send via radio → wait for transmission completion → sleep.

MY_APP_CHILD::on_begin()

void _begin() {
    // sleep immediately.
    Serial << "..go into first sleep (1000ms)" << mwx::flush;
    the_twelite.sleep(1000);
}

The _begin() function called from on_begin() performs the initial sleep.

(Note: It is also acceptable to write this process directly in on_begin() instead of _begin().)

MY_APP_CHILD::wakeup()

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
}

This describes the wake-up process from sleep.

Here, the initial Wire.begin() is executed. For subsequent wake-ups from sleep, this is redundant. This process may also be moved to on_begin().

MY_APP_CHILD::transmit_complete()

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
}

Upon transmission completion, an E_ORDER_KICK message is sent to the state machine.

MY_APP_CHILD::transmit_complete()

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;

State names are defined here.

MY_APP_CHILD::shtc3_???()

MWX_APIRET MY_APP_CHILD::shtc3_start()
MWX_APIRET MY_APP_CHILD::shtc3_read()

These are sensor acquisition implementations for SHTC3. For details of commands sent, please refer to the SHTC3 datasheet.

MY_APP_CHILD::ltr308als_???()

MWX_APIRET MY_APP_CHILD::ltr308als_read()
MWX_APIRET MY_APP_CHILD::ltr308als_start()
static MWX_APIRET WireWriteAngGet(uint8_t addr, uint8_t cmd)

These are sensor acquisition implementations for LTR308ALS. For details of commands sent, please refer to the LTR308ALS datasheet.

WireWriteAndGet() sends one byte cmd to the device at addr, then receives one byte and returns the value.

MY_APP_CHILD::STATE_IDLE (0)

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

State 0 has a special meaning: it is the state immediately after startup or wake-up from sleep.

On cold boot, PEV_is_coldboot(ev,evarg) returns true and this is called. Since on_begin() immediately puts the device to sleep, no state transition code is included here. At this point, major initialization has not yet completed, so complex operations such as wireless packet transmission cannot be performed. To perform such operations, the first state transition is triggered by sending an event from on_begin() and performing state transition accordingly.

On wake-up from sleep, PEV_is_warmboot(ev,evarg) returns true on the first call. It calls PEV_SetState() to transition to STATE_SENSOR.

MY_APP_CHILD::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);
	}
}

When waking up from sleep and transitioning from STATE_IDLE, the handler for STATE_SENSOR is called with event E_EVENT_NEW_STATE.

Here, sensor capture is started for SHTC3 and LTR308ALS. After a certain time, the sensors will be ready to provide data. This wait time is implemented as a sleep of 66 ms. Note that PEV_KeepStateOnWakeup() is called before sleeping. This call ensures that upon wake-up, the state remains STATE_SENSOR instead of returning to STATE_IDLE.

After waking from the short sleep, PEV_is_warmboot(ev,evarg) returns true on the first call, allowing wireless packet transmission and other operations. The state transitions to STATE_TX.

MY_APP_CHILD::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()) {

Here, upon receiving the E_EVENT_NEW_STATE event, sensor data is read and the wireless packet transmission procedure begins. For details on the transmission procedure, refer to other Act sample examples.

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 message

} else if (ev == E_ORDER_KICK && evarg == uint32_t(u8txid)) {
		Serial << "[STATE_TX] SUCCESS TX(" << int(evarg) << ')' << mwx::crlf;
		PEV_SetState(STATE_SLEEP);
}

The waiting for transmission completion is handled by waiting for the message sent from transmit_complete() via PEV_Process(). Upon receiving the message, the device goes to sleep by transitioning to 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();
	}

Finally, a timeout process is implemented. This covers the case where the transmission completion message never arrives. PEV_u32Elaspsed_ms() returns the elapsed time in milliseconds since entering this state. If the timeout occurs, the system resets with the_twelite.reset_system().

MY_APP_CHILD::STATE_SLEEP

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
	}
}

The device goes to sleep. The sleep call is placed inside the conditional block that executes only once immediately after transitioning from the previous state (E_EVENT_NEW_STATE). Since other events may be called before sleep, ensure that the_twelite.sleep() is called exactly once here.

7 - Functions

Global Functions

7.1 - System Functions

Functions for handling time and random numbers

7.1.1 - millis()

Get system time
Gets the system time in milliseconds.
uint32_t millis()

The system time is updated by TickTimer interrupts.

7.1.2 - delay()

Delay processing
Performs a delay using polling.
void delay(uint32_t ms)

Performs a delay for the period specified by ms.

Time measurement is done using the TickTimer count. When a long delay is specified, the CPU clock is reduced during the polling process.

7.1.3 - delayMicroseconds()

Delay processing (microseconds)
Performs a delay using polling (specified in microseconds).
void delayMicroseconds(uint32_t microsec)

Performs a delay for the duration specified by microsec.

Time measurement is done using the TickTimer count. When a long delay is specified, the CPU clock is reduced during polling.

7.1.4 - random()

Random number generation
Generates a random number.
uint32_t random(uint32_t maxval)
uint32_t random(uint32_t minval, uint32_t maxval)

The first function returns a value in the range 0..(maxval-1). Note that the value of maxval is not the maximum value itself.

The second function returns a value in the range minval..maxval-1.

7.2 - General Purpose Digital IO

Functions to handle DIO ports

The following functions are used to operate General Purpose Digital IO (DIO).

  • pinMode()
  • digitalWrite()
  • digitalRead()
  • attachIntDio()
  • detachIntDio()

Constants

Pin Names and Numbers

DefinitionName
const uint8_t PIN_DIGITAL::DIO0 .. 19DIO pins 0 to 19
const uint8_t PIN_DIGITAL::DO0 .. 1DO pins 0,1

Pin Modes (DIO0..19)

The following enumerated values are handled by the type E_PIN_MODE.

DefinitionPull-upName
PIN_MODE::INPUTNoInput
PIN_MODE::OUTPUTNoOutput
PIN_MODE::INPUT_PULLUPYesInput with Pull-up
PIN_MODE::OUTPUT_INIT_HIGHNoOutput (initial state HIGH)
PIN_MODE::OUTPUT_INIT_LOWNoOutput (initial state LOW)
PIN_MODE::WAKE_FALLINGNoInput, wake pin, falling edge
PIN_MODE::WAKE_RISINGNoInput, wake pin, rising edge
PIN_MODE::WAKE_FALLING_PULLUPYesInput, wake pin, falling edge with pull-up
PIN_MODE::WAKE_RISING_PULLUPYesInput, wake pin, rising edge with pull-up
PIN_MODE::DISABLE_OUTPUTYesReturn to input state

Pin Modes (DO0,1)

The following enumerated values are handled by the type E_PIN_MODE.

DefinitionName
PIN_MODE::OUTPUTOutput
PIN_MODE::OUTPUT_INIT_HIGHOutput (initial state HIGH)
PIN_MODE::OUTPUT_INIT_LOWOutput (initial state LOW)
PIN_MODE::DISABLE_OUTPUTStop output setting

Pin States

The following enumerated values are handled by the type E_PIN_STATE.

DefinitionValueName
PIN_STATE::HIGH1HIGH level (=Vcc level)
PIN_STATE::LOW0LOW level (=GND level)

Pin Rising and Falling Edges

The following enumerated values are handled by the type E_PIN_INT_MODE.

DefinitionName
PIN_INT_MODE::FALLINGFalling edge
PIN_INT_MODE::RISINGRising edge

7.2.1 - pinMode()

Initialization function
Configures the settings for DIO (General Purpose Digital IO) pins.
void pinMode(uint8_t u8pin, E_PIN_MODE mode)

This function allows configuration of the states of DIO0 to DIO19 and DO0,1 pins. For details on the configuration values, refer to the enumerated values of E_PIN_MODE in the DIO explanation and the DO explanation.

7.2.2 - digitalWrite()

Digital output function
Changes the setting of a digital output pin.
static inline void digitalWrite(uint8_t u8pin, E_PIN_STATE ulVal)

Beforehand, set the target pin as output using pinMode(). The first parameter specifies the pin number to be set. The second parameter specifies either HIGH or LOW.

7.2.3 - digitalRead()

Digital input function
Reads the value of a port configured as input.
static inline E_PIN_STATE digitalRead(uint8_t u8pin)

You can get the input value of a pin previously configured as input as LOW or HIGH.

7.2.4 - attachIntDio()

Function to register a DIO interrupt handler
Registers a DIO interrupt handler.
void attachIntDio(uint8_t u8pin, E_PIN_INT_MODE mode)

For a pin configured as input beforehand, the first parameter is the pin number for which you want to enable the interrupt, and the second parameter specifies the interrupt direction (rising edge, falling edge).

Example

Sets up an interrupt that triggers 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() {
  ;
}

myAppClass.hpp

class myAppClass: public mwx::BrdPal, MWX_APPDEFS_CRTP(myAppClasslMot)
{

};

Basic definition of the behavior myAppClass. Details are omitted.

myAppClass.cpp

/*****************************************************************/
// 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
/*****************************************************************/

Interrupt handler description for the behavior myAppClass. When an interrupt occurs on DIO5, it toggles the output setting of DIO12, and after the interrupt handler finishes, an event occurs that prints * to the serial port Serial.

7.2.5 - detachIntDio()

Function to unregister a DIO interrupt handler
Unregisters a DIO interrupt handler.
void detachIntDio(uint8_t u8pin)

7.2.6 - digitalReadBitmap()

Digital input function (batch)
Reads the values of the input-configured ports in batch.
uint32_t digitalReadBitmap()

Values are stored in order from the LSB side: DIO0 … DIO19.

Pins on the HIGH side are set to 1, and pins on the LOW side are set to 0.

7.3 - Utility Functions

Other utility functions

7.3.1 - Printf Implementation

Functionality similar to C standard printf
The MWX library provides an implementation similar to the commonly used printf() function in C.
int mwx_printf(const char* format, ...)
int mwx_snprintf(char* buffer, size_t count, const char* format, ...)

mwx_printf() outputs formatted text to the Serial object. It performs the same processing as Serial.printfmt().

mwx_snprintf() performs snprintf formatting to a buffer.

7.3.2 - pack_bits()

Set bits to 1 at specified positions
Sets bits to 1 at the specified positions.
constexpr uint32_t pack_bits(...)

Parameters are specified as variadic arguments, each indicating a bit position (integer from 0 to 31). For example, pack_bits(1,3,6) returns ((1UL<<1)|(1UL<<3)|(1UL<<6)).

Background

This function simplifies the notation in situations where values are referenced or set in various bitmaps such as IO port (DI, DO) states.

7.3.3 - collect_bits()

Create a bitmap from specified bit positions
Extracts values from specified bit positions in an integer and constructs a bitmap in the specified order.
constexpr uint32_t collect_bits(uint32_t bm, ...)

From the value specified in the parameter bm, this function extracts the values corresponding to the 0..31 bit positions specified by the subsequent variadic parameters. The extracted values are arranged in the order of the parameters and returned as a bitmap.

The bit ordering of the resulting bitmap places the first parameter in the highest bit and the last parameter at bit 0.

uint32_t b1 = 0x12; // (b00010010)
uint32_t b2 = collect_bits(b1, 4, 2, 1, 0);
  // bit4->1, bit2->0, bit1->1, bit0->0
  // b2=0x10 (b1010)

In this example, bits 4, 2, 1, and 0 of b1 are extracted, resulting in (1,0,1,0). This is interpreted as b1010, resulting in a calculated value of 0x10.

Background

This function simplifies code where values are referenced or set in various bitmaps, such as IO port (DI, DO) statuses.

7.3.4 - Byte array utils

Conversion between byte arrays and 16/32-bit integers
Generates 16/32-bit integers from byte arrays, or generates byte arrays from 16/32-bit integers.

Reading

Retrieve uint16_t or uint32_t values from a byte array interpreted as uint8_t in big-endian order.

	inline uint8_t G_BYTE(const uint8_t*& p) {
		return *(p)++;
	}
	inline uint16_t G_WORD(const uint8_t*& p) {
		uint32_t r = *p++;
		r = (r << 8) + *p++;
		return r;
	}
	inline uint32_t G_DWORD(const uint8_t*& p) {
		uint32_t r = *p++;
		r = (r << 8) + *p++;
		r = (r << 8) + *p++;
		r = (r << 8) + *p++;
		return r;
	}

p is incremented by the number of bytes read.

Writing

Writes uint8_t, uint16_t, or uint32_t values in big-endian order to the byte array pointed to by q.

	inline uint8_t& S_OCTET(uint8_t*& q, uint8_t c) {
		*q++ = c;
		return *q;
	}
	inline uint8_t& S_WORD(uint8_t*& q, uint16_t c) {
		*(q) = ((c) >> 8) & 0xff; (q)++;
		*(q) = ((c) & 0xff); (q)++;
		return *q;
	}
	inline uint8_t& S_DWORD(uint8_t*& q, uint32_t c) {
		*(q) = ((c) >> 24) & 0xff; (q)++;
		*(q) = ((c) >> 16) & 0xff; (q)++;
		*(q) = ((c) >>  8) & 0xff; (q)++;
		*(q) = ((c) & 0xff); (q)++;
		return *q;
	}

q is incremented by the number of bytes written.

Background

These utilities simplify operations during the construction and decomposition of data payloads in wireless packets.

You may also use the simplified pack_bytes() and expand_bytes() functions.

7.3.5 - pack_bytes()

Generate a byte sequence by concatenating element data
Generates a byte sequence by concatenating element data.
uint8_t* pack_bytes(uint8_t* b, uint8_t* e, ...)

pack_bytes takes container class begin(), end() iterators as parameters and writes the data specified by the following parameters into the container as a byte sequence.

The data types given as variadic arguments are as follows.

Data TypeBytesDescription
uint8_t1
uint16_t2Stored in big-endian order
uint32_t4Stored in big-endian order
uint8_t[N]NFixed-length array of uint8_t
std::pair<char*,N>NPair of array and length for char* or uint8_t* arrays. Can be created with make_pair().
smplbuf_u8& pack_bytes(smplbuf_u8& c, ...)

pack_bytes takes a container object as a parameter and writes the data specified by the following parameters into the container as a byte sequence. It appends to the end using the container’s .push_back() method.

The data types given as variadic arguments are as follows.

Data TypeBytesDescription
uint8_t1
uint16_t2Stored in big-endian order
uint32_t4Stored in big-endian order
uint8_t[N]NFixed-length array of uint8_t
std::pair<char*,N>NPair of array and length for char* or uint8_t* arrays. Can be created with make_pair().
smplbuf_u8?.size()smplbuf<> container of uint8_t type. Stores data of container length (.size()).

Example

auto&& rx = the_twelite.receiver.read();

smplbuf<uint8_t, 128> buf;
mwx::pack_bytes(buf
	, uint8_t(rx.get_addr_src_lid())	// src addr (LID)
	, uint8_t(0xCC)							      // cmd id (0xCC, fixed)
	, uint8_t(rx.get_psRxDataApp()->u8Seq)	// sequence number
	, uint32_t(rx.get_addr_src_long())		// src addr (long)
	, uint32_t(rx.get_addr_dst())			// dst addr
	, uint8_t(rx.get_lqi())					  // LQI
	, uint16_t(rx.get_length())				// payload length
	, rx.get_payload() 						    // payload
);

In this example, attributes and payload of the received packet are re-stored into another buffer buf.

Background

To simplify the description of byte arrays of type uint8_t used for generating data payloads of wireless packets and extracting data.

auto&& rx = the_twelite.receiver.read();

uint8_t data[128];
data[0] = rx.get_addr_src_lid();
data[1] = 0xCC;
data[2] = rx.get_psRxDataApp()->u8Seq;
data[4] = rx.get_addr_src_long() & 0x000000FF;
data[5] = (rx.get_addr_src_long() & 0x0000FF00) >> 8;
data[6] = (rx.get_addr_src_long() & 0x00FF0000) >> 16;
data[7] = (rx.get_addr_src_long() & 0xFF000000) >> 24;
...

The above is the simplest description, but a byte array can be generated using Byte array utils as follows.

auto&& rx = the_twelite.receiver.read();

uint8_t data[128], *q = data;
S_OCTET(q, rx.get_addr_src_lid());
S_OCTET(q, 0xCC);
S_OCTET(q, rx.get_psRxDataApp()->u8Seq);
S_DWORD(q, rx.get_addr_src_long());
S_DWORD(q, rx.get_addr_dst());
S_OCTET(q, rx.get_lqi());
S_WORD(q, rx.get_length());
for (auto x : rx.get_payload()) {
  S_OCTET(q, x);
}

7.3.6 - expand-bytes()

“Decompose a byte sequence and store it into variables”
Decompose a byte sequence and store it into variables.
const uint8_t* expand_bytes(
        const uint8_t* b, const uint8_t* e, ...)

expand_bytes() takes a combination of iterators of type uint8_t* as parameters. These specify the beginning of the target to be parsed and the iterator just past the end. If parsing reaches the position of e, it results in an error and returns nullptr.

If there is no error in expansion, it returns the iterator for the next reading.

The variable parameters should be specified as follows:

Number of bytesData lengthExplanation
uint8_t1
uint16_t2Expanded as big-endian order
uint32_t4Expanded as big-endian order
uint8_t[N]NFixed-length array of uint8_t
std::pair<char*,N>NA pair of array and array length N of type char* or uint8_t* created by make_pair()

Example

auto&& rx = the_twelite.receiver.read();

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

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

In this example, first a 4-byte string is read. Here, make_pair() is used to explicitly read 4 bytes of data.

Using the returned iterator np as a base, the next data is read. The next data consists of a uint8_t type followed by five uint16_t types.

Background

To simplify the description of byte arrays of type uint8_t used for generating and extracting data payloads of wireless packets.

auto&& rx = the_twelite.receiver.read();
char fourchars[5]{};
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];

uint8_t *p = rx.get_payload().begin();
fourchars[0] = p[0];
fourchars[1] = p[1];
fourchars[2] = p[2];
fourchars[3] = p[3];
fourchars[4] = 0;
p += 4;

u8DI_BM_remote = (p[0] << 8) + p[1]; p+=2;
au16AI_remote[0] = (p[0] << 8) + p[1]; p+=2;
...

The above is the simplest description, but you can read from byte arrays using Byte array utils as follows:

auto&& rx = the_twelite.receiver.read();
char fourchars[5]{};
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];

uint8_t *p = rx.get_payload().begin();
fourchars[0] = G_BYTE(p);
fourchars[1] = G_BYTE(p);
fourchars[2] = G_BYTE(p);
fourchars[3] = G_BYTE(p);
fourchars[4] = 0;

u8DI_BM_remote = G_WORD(p);
au16AI_remote[0] = G_WORD(p);
...

7.3.7 - CRC8, XOR, LRC

Checksum calculation
Calculates values commonly used in checksum computations.
uint8_t CRC8_u8Calc(uint8_t *pu8Data, uint8_t size, uint8_t init=0)
uint8_t CRC8_u8CalcU32(uint32_t u32c, uint8_t init=0)
uint8_t CRC8_u8CalcU16(uint16_t u16c, uint8_t init=0)
uint8_t XOR_u8Calc(uint8_t *pu8Data, uint8_t size)
uint8_t LRC_u8Calc(uint8_t* pu8Data, uint8_t size)

Performs calculations for CRC8, XOR, and LRC (used in ASCII format).

CRC8_u8CalcU16() and CRC8_u8CalcU32() compute the CRC8 for u16c and u32c assuming big-endian order.

Background

These functions were added as library procedures because they are used for validating wireless packet data sequences, ASCII format checksums (LRC), and various sensor data checks.

7.3.8 - div10(), div100(), div1000()

Calculate quotient and remainder when divided by 10, 100, or 1000
Calculates the quotient and remainder when divided by 10, 100, or 1000.
struct div_result_i32 {
		int32_t quo; // quotient
		int16_t rem; // remainder
		uint8_t b_neg;  // true if negative
		uint8_t digits_rem; // digits of remainder
};

div_result_i32 div10(int32_t val);
div_result_i32 div100(int32_t val);
div_result_i32 div1000(int32_t val);

In some cases, sensor values multiplied by 100 are passed as uint16_t type, but on microcontrollers without division circuits, calculation processing takes considerable time. Therefore, calculations are performed using approximate calculations and corrections with addition, subtraction, multiplication, and bit shifts.

Pass the value to be calculated in val, the variable to store the remainder in rem, and the variable to store the sign in neg.

The return value is the quotient (always positive), rem contains the remainder (always positive), and neg stores true if negative.

Due to algorithm constraints (digit overflow), the calculable value range is limited for div100() and div1000(). div100() supports values from -99999 to 99999, and div1000() supports values from -999999 to 999999.

Example usage

auto d1 = div100(sns_val.u16temp_object);
auto d2 = div100(sns_val.u16temp_object);

Serial
	<< crlf << format("..Object  = %c%2d.%02d"
									, d1.b_neg ? '-' : '+', d1.quo, d1.rem)
	        << format("  Ambient = %c%2d.%02d"
									, d2.b_neg ? '-' : '+', d2.quo, d2.rem);

Calculation speed

About one-tenth of the time.

Output of results

// Conversion options
struct DIVFMT {
  static const int STD = 0; // displays with minimal digits (no padding, no positive sign)
  static const int PAD_ZERO = 1; // set padding character as '0' instead of ' '.
  static const int SIGN_PLUS = 2; // put '+' sign if value is positive or 0.
  static const int PAD_ZERO_SIGN_PLUS = 3; // PAD_ZERO & SIGN_PLUS
  static const int SIGN_SPACE = 4; // put ' ' sign if value is positive or 0.
  static const int PAD_ZERO_SIGN_SPACE = 5; // PAD_ZERO & SIGN_SPACE
};

// Class to store string conversion results
class _div_chars {
  ...
  const char* begin() const {...}
  const char* end() const {...}
  const char* c_str() const { return begin(); }
  operator const char*() const { return begin(); }
};

// format() method
_div_chars div_result_i32::format(
    int dig_quo = 0, uint32_t opt = DIVFMT::STD) const;

// Implementation of interface to Serial
template <class D> class stream {
...
		inline D& operator << (const mwx::_div_chars&& divc);
		inline D& operator << (const mwx::div_result_i32&&);
		inline D& operator << (const mwx::div_result_i32&);
};

The div_result_i32 class that stores the division result has a format() method to obtain a _div_chars class object. The _div_chars class object contains a string buffer and provides methods to access the string buffer as const char*. Also, the << operator for the Serial object is implemented.

The first parameter dig_quo of the format() method specifies the number of output digits (excluding the sign). If the output digits are insufficient (below), it is filled with spaces or 0. The second parameter opt specifies the format.

opt parameterDescription
DIVFMT::STDStandard output, fills insufficient digits with spaces, and adds - only for negative values.
DIVFMT::PAD_ZEROFills insufficient digits with 0.
DIVFMT::SIGN_PLUSAdds + sign for positive values as well.
DIVFMT::PAD_ZERO_SIGN_PLUSFills insufficient digits with 0 and adds + sign for positive values.
DIVFMT::SIGN_SPACEAdds a space sign instead of + for positive values.
DIVFMT::PAD_ZERO_SIGN_SPACEFills insufficient digits with 0 and adds a space sign instead of + for positive values.

Example

//// Direct output from div_result_i32 object
Serial << div100(-1234) << crlf;
// Result: -12.34

//// Output with 3 digits
Serial << div100(3456).format(3, DIVFMT::PAD_ZERO_SIGN_PLUE) << crlf;
// Result: +034.56

//// Use c_str() to get const char*
char str1[128];
auto x = div100(-5678);
mwx_snprintf(str1, 16, "VAL=%s", x.format.c_str()); // const char*
Serial << str1;
// Result: VAL=-56.78

Background

In TWELITE BLUE/RED, division is a costly operation, so a division algorithm with limited purposes was added.

Within the library, some sensor values such as temperature and humidity are represented using values multiplied by 100 (e.g., 25.12℃ as 2512), so a simple procedure to obtain the quotient and remainder when divided by 100 was defined.

dev_result_i32::format() is provided to avoid complexity when formatting output.

7.3.9 - Scale utils

Optimized value scaling processing
Scale (expand/contract) between 8bit values (such as 0..255) and user-friendly 0..1000 (per mille, ‰) values. To achieve low computational cost, division (x*1000/255) is replaced with multiplication and bit shifts for approximate calculation.
static inline uint8_t scale_1000_to_127u8(uint16_t x)
static inline uint16_t scale_127u8_to_1000(uint8_t x)
static inline uint8_t scale_1000_to_255u8(uint16_t x)
static inline uint16_t scale_255u8_to_1000(uint8_t x)
static inline uint8_t scale_1000_to_256u8(uint16_t x)
static inline uint16_t scale_256u16_to_1000(uint16_t x)

scale_1000_to_127u8()

Scales 0..1000 to 0..127. Uses (16646*x+65000)>>17 for approximate calculation.

scale_127u8_to_1000()

Scales 0..127 to 0..1000. Uses (2064000UL*x+131072)>>18 for approximate calculation.

scale_1000_to_255u8()

Scales 0..1000 to 0..255. Uses (33423*x+65000)>>17 for approximate calculation.

scale_255u8_to_1000()

Scales 0..255 to 0..1000. Uses (1028000UL*uint32_t(x)+131072)>>18 for approximate calculation.

scale_1000_to_256u8()

Scales 0..1000 to 0..256. Uses (33554*x+66000) >> 17 for approximate calculation.

Note: For x=999,1000 the calculated value becomes 256, but returns 255 as the range of uint8_t.

scale_256u16_to_1000()

Scales 0..256 to 0..1000. Uses (1028000UL*uint32_t(x)+131072)>>18 for approximate calculation.

Background

Values to be set in hardware are often based on binary such as 0..255, while numbers handled in user applications are easier to manage when based on decimal such as 0..1000. These scale conversions define formulas that do not use division.

7.3.10 - pnew()

Simplifies the syntax for placement new
Simplifies the syntax of placement new.
template <class T, class... Args>
T* pnew(T& obj, Args&&... args) {
    return (T*)new ((void*)&obj) T(std::forward<Args&&>(args)...);
}

For example, it can be used as follows. You can also pass constructor arguments.

class my_class {
    int _a;
public:
    my_class(int a = -1) : _a(a) {}
};

my_class obj_1; // This constructor is not called!
my_class obj_2; // This constructor is not called!

void setup() {
    mwx::pnew(obj_1);    // Equivalent to my_class obj_1;
	mwx::pnew(obj_2, 2); // Equivalent to my_class obj_2(2);
    ...
}

Background

Due to compiler constraints, constructors of global objects are not called. One method to initialize them is using placement new. However, the placement new syntax can appear verbose.

Another method is to use std::unique_ptr (or eastl::unique_ptr).

std::unique_ptr<my_class> obj_3;

void setup() {
    obj_3.reset(new my_class(3));
    		// On TWELITE microcontrollers, `new` can only allocate once and `delete` cannot be used,
            // so in practice it is equivalent to a global object.
}

8 - External Libraries

External Libraries
This section introduces external libraries that can be used with MWX.
  • EASTL - EASTL is a C++ standard library by Electronic Arts. It provides containers that operate with fixed-length memory, making it well-suited for use in embedded microcontrollers. The TWELITE STAGE SDK can be built with it without any special configuration.

8.1 - EASTL

Lightweight template library
EASTL is a standard template library (containers and algorithms) maintained by Electronic Arts, implemented in the style of C++ STL (Standard Template Library). However, it was developed for the highly constrained environment of game console development, so it provides containers and algorithms designed for environments with strict memory handling constraints.

This library makes EASTL available within TWENET.

Below is an example of using a fixed-length array and applying a sorting algorithm.

The features of EASTL are as follows.

  • Containers with fixed memory allocation (fixed_): You can declare containers with a fixed number of elements without dynamic allocation. If declared globally, the memory area is allocated at compile time; if declared locally, it is allocated on the stack and available within that scope.
  • Intrusive containers: While regular containers can store arbitrary data structures, intrusive containers require your data structure to inherit from a special base class, which maintains link information for the container. Each element is dedicated to that container, but for lists and map structures, this is highly memory efficient. (Reference: Intrusive and non-intrusive containers)

The development motivation and more are described in a 2007 article: EASTL (open-std.org). (Related articles: A glimpse into the state of game software development through EASTL Part 1, Part 2)

Usage in TWENET

Please note the following.

We have not comprehensively tested the operation of this library. Please conduct your own operation verification. We are also unable to respond to inquiries about how to use EASTL. Please refer to resources and source code provided by the distributor.

  • Uses EASTL 3.07 (2018/1/31), the last version that can be compiled with C++11.
  • The following libraries are not included:
    • test/packages/EAAssert, source/assert.cpp
    • test/packages/EATest
    • test/packages/EAThread, source/thread_support.cpp
  • Test code in test/source has not been ported.
  • For sprintf-related functions, only EA::StdC::Vsnprintf(char8_t*, ...) is resolved by calling vsnprintf_() in the printf.h library.

How to Embed and Compile

You can use EASTL when writing Act scripts.

The required include paths and library additions for the TWELITE development environment are already set. Please include the library headers in your code as needed.

#include <TWELITE>
#include <EASTL/fixed_string.h>

using namespace eastl;
using tstr128 = fixed_string<char, 127 + 1, false>;

void setup() {
    tstr128 s1;
    s1 = "Hello World";
    Serial << s1.c_str();
}
void loop() {
    ;
}

Embedding Details

Compiling the library and setting the include paths have already been done under the MWSDK/TWENET directories, but the internal settings are described below.

  • Compile the code in EASTL/source into a library archive (libEASTL.a). This library must be referenced at link time.
  • Add the following include paths at compile time.

If you set $(PATH_EASTL) as the EASTL directory, the include paths are as follows:

-I$(PATH_EASTL)/include
-I$(PATH_EASTL)/test/packages/EAAssert/include
-I$(PATH_EASTL)/test/packages/EABase/include/Common
-I$(PATH_EASTL)/test/packages/EAMain/include
-I$(PATH_EASTL)/test/packages/EAStdC/include
-I$(PATH_EASTL)/test/packages/EATest/include
-I$(PATH_EASTL)/test/packages/EAThread/include

Coding Notes

About std:: and eastl::

The MWX library itself uses the standard library in the std:: namespace.

The standard library (std::) and EASTL (eastl::) both define items with the same names and functions. Sometimes they can coexist, but sometimes using both will cause errors. In general, use the definitions within EASTL for EASTL objects (for example, storing an eastl::fixed_string in a std::unique_ptr will result in a compiler error).

Also, be careful about name collisions when using statements like using namespace std;.

Global Object Initialization 1 (Placement new)

In TWENET development, due to compiler restrictions, constructors for globally declared objects are not executed. The memory area for a globally declared object is only zero-initialized. If you run the code as is, it will usually hang due to null pointer access.

To initialize such objects, use placement new.

#include <TWELITE>
#include <EASTL/fixed_string.h>

using namespace eastl;
using tstr128 = fixed_string<char, 127 + 1, false>;

    tstr128 g_str1; // constructor is NOT called! needs to be initialized before use.

void setup() {
    (void) new ((void*)&g_str1) tstr128("Hello World");
    Serial << g_str1.c_str();
}

The placement new code can look a bit messy, so a helper function mwx::pnew() is provided. The previous example can be rewritten as follows:

(void) new ((void*)&g_str1) tstr128("Hello World");
// ↓
mwx::pnew(g_str1, "Hello World");

Note: The second and subsequent arguments are variable and are passed directly to the constructor.

Global Object Initialization 2 (unique_ptr)

Another way to initialize global objects is to use unique_ptr (std::unique_ptr reference). unique_ptr exists in both std:: and eastl::, but for EASTL classes, use the eastl:: version.

Call .reset() at the timing of initialization as shown below.

#include <TWELITE>
#include <EASTL/unique_ptr.h>
#include <EASTL/fixed_string.h>

using namespace eastl;
using tstr128 = fixed_string<char, 127 + 1, false>;

eastl::unique_ptr<tstr128> uq_str1;

void setup() {
	uq_str1.reset(new tstr128("Hello World"));
    if (uq_str1) { // true: object is stored.
        Serial << uq_str1->c_str();
    }
}

About intrusive Containers

The following is an example of defining an element for intrusive_list. The only member is int mX.

struct IntNode : public eastl::intrusive_list_node {
    int mX;
    IntNode(int x = 0) : mX(x) { }
        // no need to call super class's constructor eastl::intrusive_list_node()
};

inline bool operator<(const IntNode& a, const IntNode& b) { return a.mX < b.mX; }
inline bool operator>(const IntNode& a, const IntNode& b) { return a.mX > b.mX; }

Elements for intrusive_list must always inherit from intrusive_list_node as a base class. The base class includes link pointers to maintain the list. Here, comparison operators are also defined for use with sort and similar algorithms.

using tiList = intrusive_list<IntNode>;

void setup() {
    IntNode nodeA(5);
    IntNode nodeB(1);
    IntNode nodeC(9);
    IntNode nodeD(2);
    IntNode nodeE(4);

    tiList l; // intrusive_list body

    l.push_front(nodeA); // forming list structure
                         //   by updating link info in intrusive_list_node.
    l.push_front(nodeB);
    l.push_front(nodeC);
    l.push_front(nodeD);
    l.push_front(nodeE);

    l.sort(); // sort, using < operator
    l.sort(eastl::greater<tilist::value_type>()); // sort, using > operator
}

References

About This Sample

The EASTL license is as follows.

Modified BSD License (3-Clause BSD license) see the file LICENSE in the project root.

/*
Copyright (C) 2015 Electronic Arts Inc.  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1.  Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
2.  Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
3.  Neither the name of Electronic Arts, Inc. ("EA") nor the names of
    its contributors may be used to endorse or promote products derived
    from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY ELECTRONIC ARTS AND ITS CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS OR ITS CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

Sample code applies MWSLA-1J/E.

Code Examples

fixed_vector

An array with a fixed maximum length (i.e., it does not expand). (*Note: mwx::smplbuf is also a fixed-length array, but it is specialized for some internal processing in the MWX library.)

#include <TWELITE>
#include <EASTL/fixed_vector.h>
#include <EASTL/sort.h>

using tvct = eastl::fixed_vector<uint16_t, 64, false>;
tvct v;

void setup() {
    mwx::pnew(v); // initialize
    v = { 3, 1, 2 ,4 }; // set initial list.

    // push and pop
    v.pop_back();   // 3, 1, 2
    v.push_back(5); // 3, 1, 2, 5

    // sort
    eastl::sort(v.begin(), v.end(), eastl::less<tvct::value_type>());
                    // 1, 2, 3, 5

    // display all
    for (auto x : v) { Serial << format(" %d", x); }
    Serial << crlf;
    // using [] operator
    for (int i = 0; i < v.size(); i++) { Serial << format(" %d", v[i]); }
}

The template arguments for fixed_vector are: first is the type, second is the maximum number of elements, and third should be false. Array operations are similar to std::vector, such as .push_back(), .pop_back(), and the [] operator.

You can also apply sorting algorithms. In the example above, eastl::sort is used in ascending order with eastl::less.

fixed_list

A list structure with a fixed maximum number of elements (see also intrusive_list).

#include <TWELITE>
#include <EASTL/fixed_list.h>
#include <EASTL/sort.h>

using tdata = eastl::pair<uint8_t, void (*)(uint8_t)>; // element data type
using tlst = eastl::fixed_list<tdata, 3, false>; // fixed_list with 3 elements.
tlst l; // list object

void setup() {
    mwx::pnew(l); // initialize (call constructor)
    // add
    if (!l.full()) l.insert(l.begin(), eastl::make_pair('A', [](uint8_t v){ Serial << format("(1:%c)", v); } ));
    if (!l.full()) l.insert(l.begin(), eastl::make_pair('B', [](uint8_t v){ Serial << format("(2:%c)", v); } ));
    if (!l.full()) l.insert(l.begin(), eastl::make_pair('C', [](uint8_t v){ Serial << format("(3:%c)", v); } ));
    if (!l.full()) l.insert(l.begin(), eastl::make_pair('D', [](uint8_t v){ Serial << format("(4:%c)", v); } )); // fails
    Serial << crlf << "init: "; for(auto &x: l) x.second(x.first);
    // find & erase
    auto p = eastl::find_if(l.begin(), l.end(), [](tdata& x) { return (x.first == 'B'); } );
    if (p != l.end()) l.erase(p);
    Serial << crlf << "find&erase: "; for(auto &x: l) x.second(x.first);
    // append
    if (!l.full()) l.insert(l.end(), eastl::make_pair('D', [](uint8_t v){ Serial << format("(4:%c)", v); } ));
    Serial << crlf << "append: "; for(auto &x: l) x.second(x.first);
    // sort
    eastl::sort(l.begin(), l.end(), eastl::less<tlst::value_type>());
    Serial << crlf << "sort:"; for(auto &x: l) x.second(x.first);
    // sort reverse
    eastl::sort(l.begin(), l.end(), eastl::greater<tlst::value_type>());
    Serial << crlf << "sort(rev):"; for(auto &x: l) x.second(x.first);
}

The template arguments for fixed_list are: first is the type, second is the maximum number of elements, and third should be false. List operations are similar to std::list, such as .insert(), .erase(), etc.

In the above code, the list stores elements of eastl::pair, with the first being a uint8_t integer and the second being a function pointer void (*)(uint8_t). Lambdas are used directly in the code. The line x.second(x.first); means calling the function stored in second with the value of first.

You can search for elements using eastl::find_if and sort using bubble_sort.

intrusive_list

While regular lists can hold arbitrary data structures as elements, intrusive_list attaches specific data to elements and uses that data to build the structure.

In the following example, the element type for the intrusive_list must inherit from eastl::intrusive_list_node. eastl::intrusive_list_node is an extension that allows storing pointers to previous and next elements.

#include <TWELITE>
#include <EASTL/fixed_vector.h>
#include <EASTL/intrusive_list.h>
#include <EASTL/unique_ptr.h>

// list element of intrusive_list.
struct IntNode : public eastl::intrusive_list_node {
    int mX;
    IntNode(int x = 0) : mX(x) { }
};
inline bool operator>(const IntNode& a, const IntNode& b) { return a.mX > b.mX; } // for sort

using tpool = eastl::fixed_vector<IntNode, 16, false>;
using tlst = eastl::intrusive_list<IntNode>;

tpool pool; // instance pool.
tlst l; // list object

void setup() {
    mwx::pnew(pool); // prepare instances
    mwx::pnew(l); // initialize (call constructor)

    pool.resize(5); // create 5 instances in pool

    // insert an IntNode element into List.
    int i = 0;
    pool[i].mX = 5; l.push_front(pool[i]); i++;
    pool[i].mX = 1; l.push_front(pool[i]); i++;
    pool[i].mX = 2; l.push_front(pool[i]); i++;
    pool[i].mX = 4; l.push_front(pool[i]); i++;
    pool[i].mX = 3; l.push_front(pool[i]); i++;
    Serial << crlf << "init: ";
    for(auto& x : l) { Serial << format(" %d", x.mX); }

    l.remove(pool[2]);
    Serial << crlf << "remove: ";
    for(auto& x : l) { Serial << format(" %d", x.mX); }

    l.sort(eastl::greater<tlst::value_type>());
    Serial << crlf << "sort: ";
    for(auto& x : l) { Serial << format(" %d", x.mX); }
}

In this example, eastl::fixed_vector<> is used to allocate the necessary number of IntNode elements; fixed_vector itself is not required for the list. Five elements are given test values to build the intrusive_list. The example calls l.push_front() to add elements one by one to the list. In reality, instead of storing elements, the pointers in each IntNode are rewired.

Sorting is done by calling the member function l.sort().

ring_buffer

The ring buffer ring_buffer is constructed in combination with another container (in this example, fixed_vector).

#include <TWELITE>
#include <EASTL/fixed_vector.h>
#include <EASTL/bonus/ring_buffer.h>

const size_t N_RING_ELE = 4; // element max for RING BUFFER.
using tvec = eastl::fixed_vector<uint8_t, N_RING_ELE + 1, false>; // One extra element is required.
using tring = eastl::ring_buffer<uint8_t, tvec>;
tring rb;

void setup() {
    mwx::pnew(rb, N_RING_ELE);

    rb.push_front(5);
    rb.push_front(1);
    rb.push_front(4);
    rb.push_front(2);
    Serial << crlf; for (auto x : rb) Serial << format(" %d", x);
    rb.push_front(3);
    Serial << crlf; for (auto x : rb) Serial << format(" %d", x);
    rb.push_front(8);
    Serial << crlf; for (auto x : rb) Serial << format(" %d", x);
    rb.push_front(9);
    Serial << crlf; for (auto x : rb) Serial << format(" %d", x);

    Serial << crlf << format("back=%d", rb.back()) << crlf;
    rb.pop_back();
    for (auto x : rb) Serial << format(" %d", x);
}

The definition of ring_buffer is a combination of the element type and its container type. The element type should have one extra element.

In the example above, .push_front() inserts an element at the front. If it overflows, the oldest element at the back is dropped. Use .back() to get the oldest element, and pop_back() to remove it.

intrusive_hash_map

A map structure is a data structure that holds key-value pairs and is designed to efficiently extract elements by key. intrusive_hash_map is implemented using the intrusive structure and hash values. The definition is a bit complicated, but memory usage is minimized.

As with intrusive_list, you must define your own element type IntNode inheriting from eastl::intrusive_hash_node_key<ElementType>. You also need to define the maximum number of hash buckets (N_BUCKET_CT). This value should be a suitable prime number according to the expected number of elements.

#include <TWELITE>
#include <EASTL/internal/intrusive_hashtable.h>
#include <EASTL/intrusive_hash_map.h>

static const unsigned N_BUCKET_CT = 7;

// intrusive element type
struct IntNode : public eastl::intrusive_hash_node_key<uint8_t> {
    using SUP = intrusive_hash_node_key;
    void (*m_func)(); // member variable is func pointer.
    IntNode(uint8_t key = 0) { SUP::mKey = key; } // key will be passed by the constructor.
};

// intrusive map type
using tmap = eastl::intrusive_hash_map<uint8_t, IntNode, N_BUCKET_CT>;

tmap mp;
IntNode nd_a, nd_b, nd_c, nd_d;

void setup() {
    mwx::pnew(mp); // initialize (call constructor)

    mwx::pnew(nd_a, 'A')->m_func = []() { Serial << "FuncA"; };
    mwx::pnew(nd_b, 'B')->m_func = []() { Serial << "FuncB"; };
    mwx::pnew(nd_c, 'C')->m_func = []() { Serial << "FuncC"; };
    mwx::pnew(nd_d, 'D')->m_func = []() { Serial << "FuncD"; };

    mp.insert(nd_a);
    mp.insert(nd_b);
    mp.insert(nd_c);
    mp.insert(nd_d);
}

void loop() {
    int c = Serial.read();
    if(c != -1) {
        Serial << crlf << '[' << uint8_t(c) << ']';
        auto&& it = mp.find(uint8_t(c));
        if (it != mp.end()) it->m_func();
    }
}

In the example above, the map key is a uint8_t character, and the map value is a function pointer. In loop(), a function is executed according to the key input.

First, the table and elements are defined as global objects, so in setup() you call mwx::pnew() to initialize the data elements (nd_a, nd_b, nd_c, nd_d) and the hash map (mp). The return value of mwx::pnew() is a pointer to the constructed object, so after initialization, you can directly assign a value (lambda) to a member variable.

Once the elements (nd_a, nd_b, nd_c, nd_d) are initialized and their values set, insert them into the map with mp.insert(nd_a) and so on.

In loop(), every time a character is input from serial, a search is performed on the hash map. Search is done with the mp.find() method, which returns an iterator; if the search fails, it returns mp.end(). If the search succeeds, you can access the found element with (*it).

9 - Board Behavior (BRD)

Hardware Abstraction Layer
Board Behaviors are collections of procedures for handling hardware connected to the TWELITE module.
  • Definition of constants (e.g., pin numbers)
  • Hardware initialization
  • Handling sensors and similar peripherals

9.1 - <BRD_APPTWELITE>

Board behavior assuming the same wiring as the “Extremely Simple! Standard App”

This is a board behavior assuming the same wiring as the “Extremely Simple! Standard App (App_Twelite).” It defines constants and provides functions for reading M1–M3 and BPS pins.

Constants

The following constants are defined:

static const uint8_t PIN_DI1 = mwx::PIN_DIGITAL::DIO12;
static const uint8_t PIN_DI2 = mwx::PIN_DIGITAL::DIO13;
static const uint8_t PIN_DI3 = mwx::PIN_DIGITAL::DIO11;
static const uint8_t PIN_DI4 = mwx::PIN_DIGITAL::DIO16;

static const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18;
static const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19;
static const uint8_t PIN_DO3 = mwx::PIN_DIGITAL::DIO4;
static const uint8_t PIN_DO4 = mwx::PIN_DIGITAL::DIO9;

static const uint8_t PIN_M1 = mwx::PIN_DIGITAL::DIO10;
static const uint8_t PIN_M2 = mwx::PIN_DIGITAL::DIO2;
static const uint8_t PIN_M3 = mwx::PIN_DIGITAL::DIO3;
static const uint8_t PIN_BPS = mwx::PIN_DIGITAL::DIO17;

static const uint8_t PIN_AI1 = mwx::PIN_ANALOGUE::A1;
static const uint8_t PIN_AI2 = mwx::PIN_ANALOGUE::A3;
static const uint8_t PIN_AI3 = mwx::PIN_ANALOGUE::A2;
static const uint8_t PIN_AI4 = mwx::PIN_ANALOGUE::A4;

You can access them like BRD_APPTWELITE::PIN_DI1.

Methods

Methods are provided to read the values of DIP SW (M1, M2, M3, BPS) pins.

inline uint8_t get_M1()
inline uint8_t get_M2()
inline uint8_t get_M3()
inline uint8_t get_BPS()
inline uint8_t get_DIPSW_BM()

The return value is not HIGH or LOW: 0 means the switch is not set (HIGH side), 1 means it is set (LOW side).

get_DIPSW_BM() returns the values of M1, M2, M3, and BPS pins in order from bit 0.

9.2 - PAL Common Definitions

Common definitions for the TWELITE PAL series
TWELITE PAL hardware shares common components, and board behaviors define a shared interface for those components.

Constants

The following constants are defined:

static const uint8_t PIN_BTN = 12; // button (as SET)
static const uint8_t PIN_LED = 5;  // LED
static const uint8_t PIN_WDT = 13; // WDT (shall tick every 60sec)

static const uint8_t PIN_D1 = 1; // DIP SW1
static const uint8_t PIN_D2 = 2; // DIP SW2
static const uint8_t PIN_D3 = 3; // DIP SW3
static const uint8_t PIN_D4 = 4; // DIP SW4

static const uint8_t PIN_SNS_EN = 16;
static const uint8_t PIN_SNS_INT = 17;

These can be accessed like PAL_AMB::PIN_BTN.

Hardware Initialization

pinMode(PIN_BTN, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT_INIT_HIGH);
pinMode(PIN_WDT, OUTPUT_INIT_HIGH);

pinMode(PIN_D1, INPUT_PULLUP);
pinMode(PIN_D2, INPUT_PULLUP);
pinMode(PIN_D3, INPUT_PULLUP);
pinMode(PIN_D4, INPUT_PULLUP);

Pins are initialized as shown in the code above.

Watchdog Timer

The external watchdog timer is reset at startup, after waking from sleep, and after a certain amount of time has passed since startup.

Methods

set_led()

void set_led(uint8_t mode, uint16_t tick)

Controls LED (D1).

mode takes the following parameters. tick specifies the ON duration [ms], but see the explanation of mode for details.

ModeDescription
LED_TIMER::BLINKBlinks the LED. Toggles ON/OFF every tick ms. After waking from sleep, it resets and starts ON.
LED_TIMER::ON_RXTurns on the LED for tick ms when a packet is received.
LED_TIMER::ON_TX_COMPTurns on the LED for tick ms when transmission completes.

led_one_shot()

void led_one_shot(uint16_t tick)

Turns on the LED for a specified duration. Cannot be used simultaneously with set_led().

get_D1() .. D4(), get_DIPSW_BM()

inline uint8_t get_D1()
inline uint8_t get_D2()
inline uint8_t get_D3()
inline uint8_t get_D4()
inline uint8_t get_DIPSW_BM()

get_D1() through get_D4() return 0 when the DIP switch is in the HIGH (up) position, and 1 when in the LOW (down) position.

get_DIPSW_BM() returns the DIP switch setting as a value from 0 to 15, calculated as: SW1==LOW = 1, SW2==LOW = 2, SW3==LOW = 4, and SW4==LOW = 8.

9.2.1 - <PAL_NOTICE>

For NOTICE PAL

This board behavior is for the NOTICE PAL.

In addition to the common definitions, this behavior enables access to onboard sensors.

  • LED driver PCA9632
  • Accelerometer MC3630
void setup() {
  auto&& brd = the_twelite.board.use<PAL_NOTICE>();
}

Member Objects

sns_PCA9632

Object for the PCA9632 device. The board definition handles Wire initialization and device setup. Use the control methods described later to interact with the device.

sns_MC3630

Object for the MC3630 sensor. Handles SPI setup, device initialization, and interrupt processing. Use the methods provided in sns_MC3630.

PCA9632 Definitions

static const uint8_t LED_OFF = SnsPCA9632::LED_OFF;
static const uint8_t LED_ON = SnsPCA9632::LED_PWM;
static const uint8_t LED_BLINK = SnsPCA9632::LED_BLINK;
static const uint8_t LED_NOP = SnsPCA9632::LED_NOP;

static const uint8_t LED_R = SnsPCA9632::LED1;
static const uint8_t LED_G = SnsPCA9632::LED2;
static const uint8_t LED_B = SnsPCA9632::LED3;
static const uint8_t LED_W = SnsPCA9632::LED4;

static const uint8_t LED_REG_MAX_PWM = 127;
static const uint8_t LED_REG_BOOST_PWM = 255;

LED States

DefinitionDescription
PAL_NOTICE::LED_OFFOff
PAL_NOTICE::LED_ONOn (PWM brightness control)
PAL_NOTICE::LED_BLINKBlinking
PAL_NOTICE::LED_NOPNo change

LED Identifiers

DefinitionDescription
PAL_NOTICE::LED_RRed LED
PAL_NOTICE::LED_GGreen LED
PAL_NOTICE::LED_BBlue LED
PAL_NOTICE::LED_WWhite LED

Register Brightness Settings

DefinitionDescription
PAL_NOTICE::LED_REG_MAX_PWMStandard brightness PWM register value (approx. 50%)
PAL_NOTICE::LED_REG_BOOST_PWMBoost brightness PWM register value

PCA9632 Control Methods

Master Switch

void set_led_master_sw_on() { digitalWrite(PIN_SNS_EN, LOW); }
void set_led_master_sw_off() { digitalWrite(PIN_SNS_EN, HIGH); }

NOTICE PAL includes a FET switch after the PCA9632 output. LEDs will not light unless this switch is turned ON.

LED State Control

void set_led_r_blink()
void set_led_r_on()
void set_led_r_off()
void set_led_g_on()
void set_led_g_blink()
void set_led_g_off()
void set_led_b_on()
void set_led_b_blink()
void set_led_b_off()
void set_led_w_on()
void set_led_w_blink()
void set_led_w_off()

These functions set individual LEDs to ON, OFF, or BLINK state.

void set_leds(uint8_t r, uint8_t g, uint8_t b, uint8_t w)
void set_leds_off()

set_leds() controls the state of all LEDs. Each parameter must be one of: PAL_NOTICE::LED_OFF, PAL_NOTICE::LED_ON, PAL_NOTICE::LED_BLINK, or PAL_NOTICE::LED_NOP.

LED Brightness Control

void set_led_brightness_r_reg(uint8_t duty)
void set_led_brightness_g_reg(uint8_t duty)
void set_led_brightness_b_reg(uint8_t duty)
void set_led_brightness_w_reg(uint8_t duty)
void set_leds_brightness_reg(uint8_t r, uint8_t g, uint8_t b, uint8_t w)

void set_led_brightness_r1000(uint16_t duty, bool boost = false)
void set_led_brightness_g1000(uint16_t duty, bool boost = false)
void set_led_brightness_b1000(uint16_t duty, bool boost = false)
void set_led_brightness_w1000(uint16_t duty, bool boost = false)
void set_leds_brightness1000(
    uint16_t r, uint16_t g, uint16_t b, uint16_t w, bool boost = false)

Controls PWM duty cycle (brightness) of LEDs.

set_led_brightness_?_reg() and set_leds_brightness_reg() directly specify 0–255 register values, where brightness is duty/256.

set_led_brightness_?1000() and set_leds_brightness1000() accept values from 0 to 1000. 0 means off, higher values increase brightness. When boost=false, a value of 1000 maps to register value 127; when boost=true, it maps to 255.

void set_blink_cycle_ms(uint16_t x)
void set_blink_duty1000(uint16_t x)

LEDs set to PAL_NOTICE::LED_BLINK blink based on the specified cycle and duty.

  • Per-LED blinking patterns are not supported.
  • Brightness during blink is set by the current PWM duty configuration.

set_blink_cycle_ms() sets blink cycle in milliseconds.

set_blink_duty1000() sets ON duration as cycle * x / 1000.

LED Test

void test_led()

Briefly lights up all four LEDs. After this, the master switch is left ON (set_led_master_sw_on()).

9.2.2 - <PAL_AMB>

For Ambient Sensor PAL
This board behavior is for the Ambient Sensor PAL.

In addition to the common definitions, this behavior enables access to onboard sensors.

  • Temperature and Humidity Sensor SHTC3
  • Ambient Light Sensor LTR308ALS
void setup() {
  auto&& brd = the_twelite.board.use<PAL_AMB>();
}

Member Objects

sns_SHTC3

Object for the SHTC3 sensor.

sns_LTR308ALS

Object for the LTR-308ALS sensor.

9.2.3 - <PAL_MAG>

For Open-Close Sensor PAL

This board behavior is for the Open-Close Sensor PAL.

void setup() {
  auto&& brd = the_twelite.board.use<PAL_MAG>();
}

The sensor on the Open-Close Sensor PAL is a magnetic sensor with two interrupt input lines only.

const uint8_t PAL_MAG::PIN_SNS_NORTH = 16;
const uint8_t PAL_MAG::PIN_SNS_OUT1 = 16;
const uint8_t PAL_MAG::PIN_SNS_SOUTH = 17;
const uint8_t PAL_MAG::PIN_SNS_OUT2 = 17;

PAL_MAG::PIN_SNS_NORTH triggers an interrupt when the sensor detects the north pole; PAL_MAG::PIN_SNS_SOUTH also triggers an interrupt upon detecting the north pole.

Configure the following before entering sleep:

pinMode(PAL_MAG::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(PAL_MAG::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);

On wake-up, check the GPIO that triggered the wake-up:

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

9.2.4 - <PAL_MOT>

For Motion Sensor PAL

This board behavior is for the Motion Sensor PAL.

In addition to the common definitions, this behavior enables access to onboard sensors.

  • Accelerometer MC3630
void setup() {
  auto&& brd = the_twelite.board.use<PAL_MOT>();
}

Member Objects

sns_MC3630

Object for the MC3630 sensor.

9.3 - <ARIA>

For TWELITE ARIA
This board behavior supports peripherals on the TWELITE ARIA.

Enables access to the onboard accelerometer, magnetic sensor, and LED.

  • Temperature/Humidity Sensor
  • Magnetic Sensor
  • LED
void setup() {
  auto&& brd = the_twelite.board.use<ARIA>();
}

Temperature/Humidity Sensor

A member object for the SHT4x sensor (sns_SHT4x) is defined.

Magnetic Sensor

The sensor used in the Door Sensor PAL is a magnetic sensor that only has two interrupt input lines.

const uint8_t CUE::PIN_SNS_NORTH = 16;
const uint8_t CUE::PIN_SNS_OUT1 = 16;
const uint8_t CUE::PIN_SNS_SOUTH = 8;
const uint8_t CUE::PIN_SNS_OUT2 = 8;

ARIA::PIN_SNS_NORTH triggers an interrupt when the sensor detects the north pole. ARIA::PIN_SNS_SOUTH also triggers an interrupt when the sensor detects the north pole.

Before entering sleep, configure the pins as follows:

pinMode(CUE::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(CUE::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);

On wake-up, check the cause using:

uint8_t b_north =
  the_twelite.is_wokeup_by_dio(CUE::PIN_SNS_NORTH);
uint8_t b_south =
  the_twelite.is_wokeup_by_dio(CUE::PIN_SNS_SOUTH);

LED

set_led()

void set_led(uint8_t mode, uint16_t tick)

Controls the LED (D1).

mode accepts the following parameters. tick specifies the lighting duration in milliseconds [ms]. Refer to the mode descriptions for details.

ValueDescription
LED_TIMER::BLINKBlinks the LED. Toggles ON/OFF at intervals specified by tick [ms]. After waking from sleep, the timer resets and starts in the ON state.
LED_TIMER::ON_RXTurns on the LED for the specified tick duration [ms] when a packet is received.
LED_TIMER::ON_TX_COMPTurns on the LED for the specified tick duration [ms] when transmission completes.

led_one_shot()

void led_one_shot(uint16_t tick)

Turns on the LED for a specified duration. Cannot be used simultaneously with set_led().

Watchdog Timer

Resets the external watchdog timer at startup, upon waking from sleep, and after a certain time has elapsed post-startup.

9.4 - <CUE>

For TWELITE CUE
This board behavior supports peripherals on the TWELITE CUE.

Provides access to the onboard accelerometer, magnetic sensor, and LED.

  • Accelerometer
  • Magnetic Sensor
  • LED
void setup() {
  auto&& brd = the_twelite.board.use<CUE>();
}

Accelerometer

The member object (sns_MC3630) of the MC3630 sensor is defined.

Magnetic Sensor

The sensor used in the Door Sensor PAL is a magnetic sensor, with two interrupt input lines only.

const uint8_t CUE::PIN_SNS_NORTH = 16;
const uint8_t CUE::PIN_SNS_OUT1 = 16;
const uint8_t CUE::PIN_SNS_SOUTH = 8;
const uint8_t CUE::PIN_SNS_OUT2 = 8;

CUE::PIN_SNS_NORTH triggers an interrupt when the north pole is detected; CUE::PIN_SNS_SOUTH also triggers on north pole detection.

Configure the following before entering sleep:

pinMode(CUE::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(CUE::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);

On wake-up, check which GPIO triggered the event:

uint8_t b_north =
  the_twelite.is_wokeup_by_dio(CUE::PIN_SNS_NORTH);
uint8_t b_south =
  the_twelite.is_wokeup_by_dio(CUE::PIN_SNS_SOUTH);

LED

set_led()

void set_led(uint8_t mode, uint16_t tick)

Controls LED (D1).

mode takes the following parameters. tick specifies the lighting duration [ms]; see the explanation of mode for details.

ValueDescription
LED_TIMER::BLINKBlinks the LED. Toggles ON/OFF every tick milliseconds. Resets counter after sleep and starts ON.
LED_TIMER::ON_RXTurns on LED for tick milliseconds upon packet reception.
LED_TIMER::ON_TX_COMPTurns on LED for tick milliseconds when transmission completes.

led_one_shot()

void led_one_shot(uint16_t tick)

Turns on LED for a specified duration. Cannot be used simultaneously with set_led().

Watchdog Timer

Resets the external watchdog timer at startup, after waking from sleep, and after a certain time post-start.

9.5 - <MONOSTICK>

For MONOSTICK
This board behavior is for MONOSTICK. It includes procedures for controlling the built-in watchdog timer and LED.

Constants

The following constants are defined:

const uint8_t PIN_LED = mwx::PIN_DIGITAL::DIO16;  // LED

const uint8_t PIN_WDT = mwx::PIN_DIGITAL::DIO9;     // WDT (shall tick < 1sec)
const uint8_t PIN_WDT_EN = mwx::PIN_DIGITAL::DIO11; // WDT (LO as WDT enabled)

const uint8_t PIN_LED_YELLOW = mwx::PIN_DIGITAL::DO1; // YELLOW LED

Accessible via MONOSTICK::PIN_LED.

Hardware Initialization

pinMode(PIN_LED, OUTPUT_INIT_HIGH);
pinMode(PIN_WDT, OUTPUT_INIT_LOW);
pinMode(PIN_WDT_EN, OUTPUT_INIT_LOW);
pinMode(PIN_LED_YELLOW, OUTPUT);

Pins are initialized as shown in the code above.

Watchdog Timer

The external watchdog timer is reset at startup, after waking from sleep, and after a set duration.

Methods

set_led()

void set_led_red(uint8_t mode, uint16_t tick)
void set_led_yellow(uint8_t mode, uint16_t tick)

Controls the red and yellow LEDs.

mode accepts the following parameters. tick specifies the lighting duration in milliseconds [ms]; see the descriptions for each mode below.

ParameterMeaning
LED_TIMER::BLINKThe LED blinks. ON/OFF toggles every tick milliseconds. After waking from sleep, the count resets and starts in the ON state.
LED_TIMER::ON_RXLights the LED for tick milliseconds upon packet reception.
LED_TIMER::ON_TX_COMPLights the LED for tick milliseconds upon transmission completion.

10 - Sensor Devices (SNS)

Standardized procedures for sensors and various devices
Provides classes that standardize procedures for sensors and various devices.

Procedures for Handling Sensors

For sensors like temperature sensors, procedures such as sensor activation → waiting time → value reading are often common.

Make sure to call Wire.begin() before using I2C sensors. After waking from sleep, reinitialization of Wire is done automatically, so no special code is required (Note: if Wire.end() is explicitly called in user code, reinitialization must be described in wakeup()).

void setup() {
  auto&& brd = the_twelite.board.use<PAL_AMB>();
  ..
  Wire.begin();
  brd.sns_SHTC3.begin();
  brd.sns_LTR308ALS.begin();
}

Procedures after starting the reading differ by sensor type. For example, both sensors in <PAL_AMB> manage elapsed time. To notify the sensor object of the elapsed time, use the process_ev() method.

void loop() {
  auto&& brd = the_twelite.board.use<PAL_AMB>();

  // mostly process every ms.
  if (TickTimer.available()) {
    // wait until sensor capture finish
    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);
    }
..

In the above example, TickTimer triggers every 1 ms to notify the elapsed time. E_EVENT_TICK_TIMER conveys the 1 ms passage to the sensor object.

When sufficient time has passed due to wake-up from sleep, use E_EVENT_START_UP instead. The sensor object will then quickly become ready for reading.

Common Sensor Methods

setup()

void setup(uint32_t arg1 = 0, uint32_t arg2 = 0)

Initializes the sensor.

begin(), end()

void begin(uint32_t arg1 = 0, uint32_t arg2 = 0)
void end()

Starts or stops sensor acquisition.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors that require a wait period, pass E_EVENT_TICK_TIMER or E_EVENT_START_UP to arg1 to notify time progress. If sufficient time has passed after this call, available will become true, and sensor values can be read.

available()

bool available()

Returns true when the sensor is ready for reading.

probe()

bool probe()

(Only for supported sensors) Returns true when the sensor is connected.

10.1 - SHTC3

Temperature and Humidity Sensor
This is a temperature and humidity sensor that uses the I2C bus.

Processing Flow

  1. Wire.begin(): Initialize the bus
  2. .begin(): Start sensor operation
  3. Wait a few milliseconds
  4. .available() becomes true
  5. .get_temp(), .get_humid(): Read the values

Required Procedures for Operation

Wire Bus

Ensure the Wire is active using Wire.begin() before calling the begin() method.

Procedure After Wake-up from Sleep

Ensure the Wire bus is active just before entering sleep (it will be automatically recovered after waking up).

Methods

get_temp(), get_temp_cent()

double get_temp()
int16_t get_temp_cent()

Reads the temperature. get_temp() returns the value in °C, and get_temp_cent() returns the temperature multiplied by 100 as an integer.

On error, a value between -32760 and -32768 is returned.

get_humid(), get_humid_per_dmil()

double get_humid()
int16_t get_humid_per_dmil()

Reads the humidity. get_humid() returns the value in %, and get_humid_per_dmil() returns the humidity multiplied by 100 as an integer.

On error, a value between -32760 and -32768 is returned.

Common Methods

setup()

void setup()

Allocates memory and performs initialization for the sensor.

begin(), end()

void begin()
void end()

Starts sensor data acquisition. It requires approximately 5ms of wait time before values can be read.

end() is not supported.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors requiring wait-time processing, pass E_EVENT_TICK_TIMER or E_EVENT_START_UP to arg1 to notify time progression. After calling this method and enough time has passed, the sensor becomes available and its value can be read.

available()

bool available()

Returns true when the sensor meets the read condition.

probe()

bool probe()

Returns true when the sensor is connected.

10.2 - SHT3x

Temperature and Humidity Sensor
This is a temperature and humidity sensor using the I2C bus.

Processing Flow

  1. Wire.begin(): Initialize the bus
  2. .setup(): Initialize the sensor
  3. .begin(): Start the sensor
  4. Wait for a few milliseconds
  5. .available() becomes true
  6. .get_temp(), .get_humid(): Read values

Required Procedures for Operation

Wire Bus

Before calling the setup() method, initialize the Wire with Wire.begin().

Procedure After Waking from Sleep

Ensure the Wire bus is active just before entering sleep (Wire will be automatically recovered after waking).

Code Example

##include <TWELITE>
##include <SNS_SHT3X>

SNS_SHT3X sns_sht3x; // Declare the object

Include #include <SNS_SHT3X> and declare an object of the SNS_SHT3X class.

Initialization

void setup() {
    Wire.begin();
    sns_sht3x.setup();
}

Starting Sensor Reading

void loop() {

  if(eState == E_STATE::INIT) {
    sns_sht3x.begin();
    eState = E_STATE::CAPTURE;
  }

}

To begin reading sensor values, call .begin(). It takes a few milliseconds to complete.

※ The above loop() assumes a state-machine design using the eState variable. (Reference)

Waiting for Sensor Reading

void loop() {

  if(eState == E_STATE::CAPTURE) {
    if (sns_sht3x.available()) {
      // Sensor values are ready
    }
  }

}

Check .available() to determine whether the sensor values are ready.

Reading Sensor Values

void loop() {

  if(eState == E_STATE::CAPTURE) {
    if (sns_sht3x.available()) {
      Serial << crlf << "SHT3X:"
            << " T=" << sns_sht3x.get_temp() << 'C'
            << " H=" << sns_sht3x.get_humid() << '%';
    }
  }

}

Once the sensor values are ready, you can read them.

.get_temp() and .get_humid() use floating point operations. You can also retrieve values as integers scaled by 100.

auto temp = div100(sns_sht3x.get_temp_cent());
auto humd = div100(sns_sht3x.get_humid_per_dmil);

Serial << crlf << "SHT3X:"
  << format(" T=%c%d.%02d", temp.neg ? '-' : ' ', temp.quo, temp.rem)
  << format(" H=%c%d.%02d", humd.neg ? '-' : ' ', humd.quo, humd.rem);

In this example, div100() is used to split the x100 values into integer and fractional parts.

Methods

get_temp(), get_temp_cent()

double get_temp()
int16_t get_temp_cent()

Reads the temperature. get_temp() returns °C, while get_temp_cent() returns the value scaled by 100 as an integer.

On error, it returns a value between -32760 and -32768.

get_humid(), get_humid_per_dmil()

double get_humid()
int16_t get_humid_per_dmil()

Reads the humidity. get_humid() returns %RH, while get_humid_per_dmil() returns the value scaled by 100 as an integer.

On error, it returns a value between -32760 and -32768.

Common Methods

setup()

void setup(uint32_t arg1 = 0UL)

Allocates memory and initializes the sensor.

You can specify the I2C address in the lower 8 bits of arg1. If unspecified, it defaults to 0.

##include <SNS_SHT3X>
SNS_SHT3X sns_sht3x;
bool b_found_sht3x = false;

void setup() {
    sns_sht3x.setup();
    if (!sns_sht3x.probe()) {
        delayMicroseconds(100); // just in case, wait for devices to listen further I2C comm.
        sns_sht3x.setup(0x45); // alternative ID
        if (sns_sht3x.probe()) b_found_sht3x = true;
    } else {
        b_found_sht3x = true;
    }
}

In this example, it first tries to initialize with the default I2C ID. If no response, it retries with address 0x45.

begin(), end()

void begin()
void end()

Begins sensor reading. It takes a few milliseconds before values become ready, during which .available() returns false.

end() is not supported.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors that require waiting, provide E_EVENT_TICK_TIMER or E_EVENT_START_UP in arg1 to notify the passage of time. If enough time has elapsed after calling this method, .available() will return true and the sensor value can be read.

available()

bool available()

Returns true when the sensor is ready to be read.

probe()

bool probe()

Returns true if the sensor is connected.

sns_stat()

uint32_t sns_stat()

Stores various information about the sensor device.

  • The stored value is undefined for this device.

sns_opt()

uint32_t& sns_opt()

Returns the value passed to setup(uint32_t arg1).

  • The lower 8 bits store the specified I2C device address.

10.3 - LTR-308ALS

Illuminance sensor
This is an illuminance sensor that uses the I2C bus.

Processing Flow

  1. Wire.begin(): Initialize the bus
  2. .begin(): Start sensor operation
  3. Wait for 50ms
  4. .available() becomes true
  5. .get_luminance(): Read the value

Required Procedures for Operation

Wire Bus

Before calling the .begin() method, ensure the Wire is initialized using Wire.begin().

Procedures When Waking from Sleep

Ensure the Wire bus is active just before entering sleep (it will automatically recover Wire after waking from sleep).

Method

get_luminance()

uint32_t get_luminance()

Returns the illuminance [lx] as an integer value.

Returns -1 in case of an error.

Common Methods

setup()

void setup()

Allocates memory area for the sensor and performs initialization.

begin(), end()

void begin()
void end()

Starts sensor acquisition. A wait time of about 50ms is required before reading the sensor value.

end() is not supported.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors that require wait time processing, provide E_EVENT_TICK_TIMER or E_EVENT_START_UP in arg1 to notify elapsed time. After this method is called and the required time has elapsed, it will become available and the sensor value can be read.

available()

bool available()

Returns true when the sensor satisfies the read condition.

probe()

bool probe()

Returns true when the sensor is connected.

10.4 - MC3630

Accelerometer
This is an accelerometer that uses the SPI bus.

Operation Flow

  1. .begin(): Start sensor operation
  2. PIN_SNS_INT interrupt or available(): FIFO queue reaches the specified number of samples
  3. .get_que(): Retrieve data from the FIFO queue

Required Procedures for Operation

SPI Bus

No special setup is required.

Sleep Procedure

To allow wake-up via PIN_SNS_INT interrupt, configure the following before entering sleep:

pinMode(PAL_MOT::PIN_SNS_INT, WAKE_FALLING);

Procedure Upon Wake-up

Call the .wakeup() method. This process is handled within the <PAL_MOT> board behavior.

Data Structure

Each sample is stored in a queue smplque of axis_xyzt structures. The x, y, and z members represent the X, Y, and Z axes, respectively.

struct axis_xyzt {
  int16_t x;
  int16_t y;
  int16_t z;
  uint16_t t;
};

Each axis value is stored with 1G equivalent to 1000. The t value represents the sample index, starting from 0 and incrementing with each sample.

Methods

read()

uint8_t read()

Reads data from the semiconductor’s FIFO queue. The return value is the number of bytes read. Use .get_que() to access the queue and retrieve the number of samples stored.

get_que()

smplque<axis_xyzt>& get_que()

Retrieves acceleration samples. The queue is a smplque of axis_xyzt. Once available is true, promptly clear the queue.

Common Methods

setup()

void setup()

setup() is not used for this sensor.

begin(), end()

void begin(uint32_t conf)
void end()

Initializes the sensor with the specified conf.

conf[0:15] (bit 0–15): Sampling mode, conf[16:23] (bit 16–23): Acceleration range, conf[24:31] (bit 24–31): Number of samples before triggering interrupt.

conf[0:15] Sample ModeDescription
MODE_LP_1HZ_UNOFFICIAL1Hz Low Power (unofficial)
MODE_LP_2HZ_UNOFFICIAL2Hz Low Power (unofficial)
MODE_LP_7HZ_UNOFFICIAL7Hz Low Power (unofficial)
MODE_LP_14HZ14Hz Low Power (default)
MODE_LP_28HZ28Hz Low Power
MODE_LP_54HZ54Hz Low Power
MODE_LP_105HZ105Hz Low Power
MODE_LP_210HZ210Hz Low Power
MODE_LP_400HZ400Hz Low Power
MODE_ULP_25HZ25Hz Ultra Low Power
MODE_ULP_50HZ50Hz Ultra Low Power
MODE_ULP_100HZ100Hz Ultra Low Power
MODE_ULP_190HZ190Hz Ultra Low Power
MODE_ULP_380HZ380Hz Ultra Low Power
conf[16:23] Acceleration RangeDescription
RANGE_PLUS_MINUS_8G±8G (default)
RANGE_PLUS_MINUS_4G±4G
RANGE_PLUS_MINUS_2G±2G
RANGE_PLUS_MINUS_1G±1G

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

process_ev() is not used for this sensor.

available()

bool available()

Returns true when data has been read into the internal queue.

probe()

bool probe()

probe() is not used for this sensor.

wakeup()

void wakeup()

Reinitializes the SPI bus after waking from sleep and reads acceleration data.

10.5 - BMx280

Environmental sensor
This is a pressure, temperature, and humidity (humidity only for BME280) sensor using the I2C bus.

Process Flow

  1. Wire.begin(): Initialize the bus
  2. .setup(): Initialize the sensor
  3. .begin(): Start sensor operation
  4. Wait for several milliseconds
  5. .available() becomes true
  6. .get_press(), .get_temp(), .get_humid(): Read values

Procedures Required for Operation

Wire Bus

Before calling the setup() method, ensure Wire is operational by calling Wire.begin().

Procedures for Sleep Wake-up

Before entering sleep, keep the Wire bus operational (Wire will automatically recover after waking from sleep).

Code Example

##include <TWELITE>
##include <SNS_BME280>

SNS_BME280 sns_bme280; // Declare the object

You need to include #include <SNS_SHT3X> and declare an SNS_SHT3X class object.

Initialization

void setup() {
    Wire.begin();
    sns_bme280.setup();
}

Start Getting Sensor Values

void loop() {

  if(eState == E_STATE::INIT) {
    sns_bme280.begin();
    eState = E_STATE::CAPTURE;
  }

}

Call .begin() to start getting sensor values. It takes several milliseconds to complete.

Note: The above loop() is designed to branch processing based on the state variable eState. (Reference)

Waiting for Sensor Value Acquisition

void loop() {

  if(eState == E_STATE::CAPTURE) {
    if (sns_bme280.available()) {
      // Sensor values ready to read
    }
  }

}

You can check if sensor values are ready by .available().

Reading Sensor Values

void loop() {

  if(eState == E_STATE::CAPTURE) {
    if (sns_bme280.available()) {
      Serial << crlf << "BMx280:"
            << " P=" << int(sns_bme280.get_press()) << "hPa";
            << " T=" << sns_bme280.get_temp() << 'C'
						<< " H=" << sns_bme280.get_humid() << '%';
    }
  }

}

Once sensor values are ready, you can read them.

.get_temp(), get_humid() involve floating-point operations. You can also get values as integers multiplied by 100.

auto temp = div100(sns_bme280.get_temp_cent());
auto humd = div100(sns_bme280.get_humid_per_dmil);

Serial << crlf << "BMx280:"
  << " P=" << int(sns_bme280.get_press()) << "hPa";
  << format(" T=%c%d.%02d", temp.neg ? '-' : ' ', temp.quo, temp.rem)
  << format(" H=%c%d.%02d", humd.neg ? '-' : ' ', humd.quo, humd.rem);

Here, div100() is used to split the value multiplied by 100 into integer and fractional parts.

Methods

get_press()

int16_t get_press()

Reads pressure. The unit is hectopascal (hPa), usually around 1000.

get_temp(), get_temp_cent()

double get_temp()
int16_t get_temp_cent()

Reads temperature. get_temp() returns °C as a double, and get_temp_cent() returns temperature multiplied by 100 as an integer.

Returns values between -32760 and -32768 on error.

get_humid(), get_humid_per_dmil()

double get_humid()
int16_t get_humid_per_dmil()

Reads humidity. get_humid() returns % as a double, and get_humid_per_dmil() returns humidity multiplied by 100 as an integer.

Returns values between -32760 and -32768 on error.

Common Methods

setup()

void setup(uint32_t arg1 = 0UL)

Allocates memory and initializes the sensor.

The lower 8 bits of arg1 can store the I2C address. If not specified, it defaults to 0.

##include <SNS_BME280>
SNS_BME280 sns_bme280;
bool b_found_bme280 = false;

void setup() {
  ...
  sns_bme280.setup();
	if (!sns_bme280.probe()) {
			delayMicroseconds(100); // device needs small time for further I2C comm.
			sns_bme280.setup(0x77); // alternative ID
			if (sns_bme280.probe()) b_found_bme280 = true;
	} else {
			b_found_bme280 = true;
	}
	...

The code first tries the default I2C ID to check device response; if no response, tries 0x77 as an alternative.

begin(), end()

void begin()
void end()

Starts sensor acquisition. It takes several milliseconds to read sensor values; wait until available() returns true.

end() is not supported.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors with wait time processing, pass E_EVENT_TICK_TIMER or E_EVENT_START_UP in arg1 to notify elapsed time. After this call, if enough time has passed, available() will return true and sensor values can be read.

available()

bool available()

Returns true when sensor reading conditions are met.

probe()

bool probe()

Returns true when the sensor is connected.

sns_stat()

uint32_t sns_stat()

Contains various sensor device information.

  • The lower 8 bits store the chip model of BME280/BMP280. 0x60 means BME280, and 0x58 means BMP280.

sns_opt()

uint32_t& sns_opt()

Stores the value passed to setup(uint32_t arg1).

  • The lower 8 bits store the specified I2C device address.

10.6 - PCA9632

LED Driver
This is the LED driver used in NOTICE PAL.

Processing Flow

  1. Wire.begin(): Initialize the bus
  2. .setup(): Initialize the class object
  3. .reset(): Initialize the driver
  4. Execute various operations

About PCA9632

It is a 4-channel LED driver.

  • Each channel can be set to OFF, ON, PWM, or BLINK mode
  • Each channel supports independent brightness control via PWM
  • All channels set to blink will share the same blink pattern
  • Individual brightness control (via PWM) is available even in blink mode

Required Procedures for Operation

Wire Bus

Ensure the Wire is initialized via Wire.begin() before calling the setup() method.

Procedure Upon Wake from Sleep

Ensure the Wire bus is active right before entering sleep (Wire will be automatically recovered after waking up).

Code Example

##include <TWELITE>
##include <SNS_PCA9632>

SNS_PCA9632 pca;

Include #include <SNS_PCA9632> and declare an instance of the SNS_PCA9632 class.

Initialization & Reset

void setup() {
    Wire.begin();
    pca.setup();
    pca.reset();
}

Lighting Up

...
   pca.set_led_duty_all(
      127,
      127,
      127,
      127
   );

   pca.set_led_status(
      SNS_PCA9632::LED_PWM,
      SNS_PCA9632::LED_NOP,
      SNS_PCA9632::LED_PWM,
      SNS_PCA9632::LED_NOP);

In the example above, LED1 and LED3 are lit using PWM control.

Methods

Constructor, setup()

SnsPCA9632(uint8_t i2c_addr = DEFAULT_I2C_ADDRESS)
void setup(uint8_t i2c_addr = DEFAULT_I2C_ADDRESS)

The constructor allows specifying the i2c_addr.

If the class object is defined globally, the constructor is not called automatically, so ensure to call setup().

reset()

bool reset()

Initializes the device.

Writes the following values to registers starting at address 0x0: {0x81, 0x35, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x0B, 0x00}

set_mode2()

bool set_mode2(uint8_t u8var = 0x35)

Writes the specified value to the MODE2 register.

set_power_mode()

bool set_power_mode(bool b_pow_on)

Set b_pow_on to true for normal operation, or false to enter sleep mode.

bool set_blink_cycle(uint8_t u8var)
bool set_blink_cycle_ms(uint16_t u16ms)

Sets the blink (group PWM) cycle.

If u8var is specified, the cycle is (u8var+1)/24 seconds.

If u16ms is specified, it sets the cycle in milliseconds.

bool set_blink_duty(uint8_t u8duty);

Sets the duty ratio of the blink (group PWM). The lit duration becomes u8duty/256, where 0 is OFF and 255 is fully ON.

set_led_duty()

bool set_led_duty(uint8_t port, uint8_t duty)

Sets the brightness (PWM duty ratio).

port specifies the target LED (SNS_PCA9632::LED1..4).

duty specifies a value from 0 to 255, with a brightness ratio of duty/256.

set_led_duty_all()

bool set_led_duty_all(uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4)

Sets the brightness (PWM duty ratio) for all LEDs.

p1, p2, p3, p4 correspond to LED1..4. Each takes a value from 0 to 255, with a brightness ratio of duty/256.

set_led_status()

bool set_led_status(uint8_t u8led1, uint8_t u8led2, uint8_t u8led3, uint8_t u8led4)

Sets the ON/OFF status for all LEDs.

u8led1..4 specify the state of LED1 to LED4.

Available states:

Description
SNS_PCA9632::LED_OFFOFF
SNS_PCA9632::LED_ONON
SNS_PCA9632::LED_PWMPWM brightness control
SNS_PCA9632::LED_BLINKBlink control (group PWM)
SNS_PCA9632::LED_NOPDo not change the current state

probe()

bool probe()

Returns true if the device is present on the I2C bus.

show_registers()

void show_registers()

Displays values of registers (0x0–0x8).

10.7 - SHT4x

Temperature and Humidity Sensor
This is a temperature and humidity sensor that uses the I2C bus.

Processing Flow

  1. Wire.begin(): Initialize the bus
  2. .begin(): Start sensor operation
  3. Wait a few milliseconds
  4. .available() becomes true
  5. .get_temp(), .get_humid(): Read the values

Required Procedures for Operation

Wire Bus

Ensure the Wire is active using Wire.begin() before calling the begin() method.

Procedure After Wake-up from Sleep

Ensure the Wire bus is active just before entering sleep (it will be automatically recovered after waking up).

Methods

get_temp(), get_temp_cent()

double get_temp()
int16_t get_temp_cent()

Reads the temperature. get_temp() returns the value in °C, and get_temp_cent() returns the temperature multiplied by 100 as an integer.

On error, a value between -32760 and -32768 is returned.

get_humid(), get_humid_per_dmil()

double get_humid()
int16_t get_humid_per_dmil()

Reads the humidity. get_humid() returns the value in %, and get_humid_per_dmil() returns the humidity multiplied by 100 as an integer.

On error, a value between -32760 and -32768 is returned.

Common Methods

setup()

void setup()

Allocates memory and performs initialization for the sensor.

begin(), end()

void begin()
void end()

Starts sensor data acquisition. It requires approximately 5ms of wait time before values can be read.

end() is not supported.

process_ev()

void process_ev(uint32_t arg1, uint32_t arg2 = 0)

For sensors requiring wait-time processing, pass E_EVENT_TICK_TIMER or E_EVENT_START_UP to arg1 to notify time progression. After calling this method and enough time has passed, the sensor becomes available and its value can be read.

available()

bool available()

Returns true when the sensor meets the read condition.

probe()

bool probe()

Returns true when the sensor is connected.

11 - Network Behavior (NWK)

Abstraction layer for wireless communication using TWELITE NET
Network Behavior defines addressing and delivery control for sending and receiving IEEE802.15.4 MAC layer packets.
  • <NWK_SIMPLE> - A very simple relay network.
  • <NWK_LAYERED> - A simplified tree structure network with layers. (Note: MWX only supports parent nodes that receive data.)

11.1 - Simple Relay Network <NWK_SIMPLE>

A simple relay network
This is a network behavior implementing a simple relay network.
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE)
        << NWK_SIMPLE::repeat_max(3);

The above is an example of network usage declaration and configuration. The basic concept of network addresses and other fundamental content will be explained first, with details provided later.

Each wireless station in this network is identified by an 8-bit logical ID. This value is independently set by each wireless station at startup. Logical IDs can be duplicated, but communication must be conducted assuming duplication.

Set the ID of each wireless station. Usually, the network consists of a wireless station acting as a parent and wireless stations acting as children. It is also possible to operate a network with only child stations.

Child stations also serve as relay stations.

Wireless Station IDRole
0x00Parent Station
0x01..0xEFChild Station
0xFEChild Station without assigned ID

Logical IDs can be specified as destinations, but 0xFE and 0xFF have special meanings. The following table summarizes destination specifications.

Destination IDMeaning
0x00Specifies the parent from a child. Invalid when specified from the parent.
0x01..0xEFSpecifies a specific child station.
0xFEBroadcast to all child stations.
0xFFBroadcast to all wireless stations.

Also, a 32-bit serial number can be used to identify wireless stations.

Packet delivery uses IEEE802.15.4 broadcast. Since ACK is not used, the sender cannot determine delivery success, but an appropriate number of retries is set to achieve the required success rate. If confirmation of arrival is necessary, normal packet communication is used.

For large-scale or frequent communication, this may seem inefficient, but it can be more efficient in networks primarily performing data collection with relatively few relay hops.

Also, since communication for network construction is not required, communication stopping entirely due to failures or exceptional situations is theoretically less likely. If the parent station is in receive mode and within wireless range of the child station, and the child station transmits packets, the parent can receive them in most cases. Networks requiring communication for construction must complete communication to re-establish connection after configuration information is lost. The network behavior <NWK_SIMPLE> is named “simple” for this reason.

To operate this simple network, it is often necessary to ignore multiple received retransmitted packets (identical packets). Identification of identical packets in <NWK_SIMPLE> is done using the sender’s serial number and the packet sequence number (called the duplicate checker). The sequence number ranges from 0 to 63 and is assigned sequentially, assuming packets received close in time have close sequence numbers. Sequence numbers considered distant after a certain timeout are excluded from duplication checks.

Considerations for the duplicate checker are as follows.

  • Number of elements that can be checked (increasing this increases memory usage and processing time)
  • Timeout settings

By default, the timeout is 1 second, and the number of wireless stations checked is 16. That means packets relayed around and older than 1 second are no longer considered duplicates. If packets arrive from more than 16 stations in a short time, duplicate checks cannot be performed for the excess stations.

If there are many relay stations or retransmissions occur at very long intervals, settings may need to be considered.

Declaration and Registration

An example of using the network behavior <NWK_SIMPLE>.

##include <TWELITE>
##include <NWK_SIMPLE>

void setup() {
  ...

  auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
}

Line 2 includes the definition of <NWK_SIMPLE>. Line 7 registers <NWK_SIMPLE> with the_twelite.

Configuration

Configure after registering <NWK_SIMPLE>.

void setup() {
  auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
  nwksmpl << NWK_SIMPLE::logical_id(0xFE);
}

Configuration is done using the << operator.

<< Operator (Configuration)

The << operator is used to perform initial settings on the object the_twelite.

The following configuration class objects are used as inputs. If not configured, default values apply.

NWK_SIMPLE::logical_id(uint8_t id)

Sets the logical device ID specified by parameter id. Default is 0xFE (child without assigned ID).

NWK_SIMPLE::retry_default(uint8_t val)

Sets the default number of retransmissions on send to the value specified by val.

NWK_SIMPLE::repeat_max(uint8_t val)

Sets the maximum relay count to the value specified by val. Default is 2.

Specify 0 to disable relaying.

NWK_SIMPLE::dup_check(uint8_t maxnodes, uint16_t timeout_ms, uint8_t tickscale)

Parameters for the duplicate packet detection algorithm.

  • maxnodes is the number of wireless stations (nodes) whose history is kept. If set too low, duplicate exclusion will fail for nodes exceeding this number in a short time, causing multiple data receptions or excessive retransmissions. Default is 16. Each node consumes 21 bytes of memory.
  • timeout_ms is the timeout in milliseconds before history is erased. Timeout is managed in sequence number blocks and processed per block. Default is 1000 ms.
  • tickscale is the time unit for timeout management, 2^tickscale ms. Time is managed with 7 bits, so set so that 127*(2^tickscale) > timeout_ms. Default is 5 (32 ms).

NWK_SIMPLE::secure_pkt(const uint8_t *pukey, bool b_recv_plain_pkt = false)

Enables encrypted packets.

  • pukey specifies the encryption key as 16 bytes (128 bit).
  • b_recv_plain_pkt set to true allows receiving plaintext packets with the same application ID and channel.

STG_STD

Reflects interactive mode settings. The following values are applied.

auto&& set = the_twelite.settings.use<STG_STD>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
...
set.reload(); // Load settings
nwk << set;   // Apply interactive mode settings
  • logical_id
  • retry_default

Methods

prepare_tx_packet()

// The type name is packet_tx_nwk_simple<NWK_SIMPLE>, but auto&& is used here.
auto&&  preare_tx_packet()

// Example
if (auto&& pkt =
  the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
  ...
  pkt.transmit();
}

Obtains a transmission object. The object is derived from packet_tx. This object stores the transmission address and payload and sends packets with the .transmit() method.

This object has a bool operator. If TWENET cannot accept the transmission request at object creation, it returns false.

Transmission Object

Methods of the transmission object obtained by .prepare_tx_packet().

bool Operator

operator bool()

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

Returns false if TWENET cannot accept the transmission request at object creation.

transmit()

MWX_APIRET transmit()

// Example
uint8_t txid;

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

  ...

  MWX_APIRET ret = pkt.transmit();
  if (ret) {
    txid = pkt.get_value();
  }
}

Performs packet transmission. MWX_APIRET is true if the transmission request succeeds, but the transmission process does not start at this point.

The packet transmission ID is stored in the value part obtained by .get_value() of MWX_APIRET. Transmission completion can be confirmed by the_twelite.tx_status.is_complete() or transmit_complete().

Maximum Packet Length and Structure

The maximum packet length is shown below. When the destination is LID (logical device ID), up to 91 bytes can be included without encryption.

Network LayerEncryptionMaximum Payload
<NWK_SIMPLE>None91
<NWK_SIMPLE>Enabled89
  • Two bytes are reserved for future use. Users may use these reserved bytes if desired.

The packet structure is as follows.

|MacH:XX[........................................]MacF:2|
         TWENET:3[.....................]TwenetF:1
                  NWK_SIMPLE:11|PAYLOAD
                                        (:n is bytes)

1: MacH is the IEEE802.15.4 MAC header information
2: TwenetH is information for TWENET identification
3: NWK_SIMPLE is NWK_SIMPLE network control information
  |Type:1|Src LID:1|Src Addr:4|Dest LID:1|Dst Addr:4|Repeat:1|
4: PAYLOAD is the payload
5: TwenetF is the CRC8 checksum (for TWENET packet discrimination)
6: MacF is the CRC16 MAC layer checksum

11.2 - <NWK_LAYERED>

Layered Tree Network
This is a network behavior that implements a simple relay network.

Initialize in setup() as follows. Assign the role as a parent using NWK_LAYERED::ROLE_PARENT.

##include <NWK_LAYERED>
void setup() {
  ...
  auto&& nwk_ly = the_twelite.network.use<NWK_LAYERED>();
  nwk_ly << NWK_LAYERED::network_role(NWK_LAYERED::ROLE_PARENT);
            // set a role as parent.
}

When a packet is received, on_rx_packet() is called similarly to NWK_SIMPLE.

void on_rx_packet(packet_rx& rx, bool_t &handled) {
  auto type = rx.get_network_type();

  if (type == mwx::NETWORK::LAYERED) {
    ; // Packet of layered tree network
    handled = true; // Mark as handled
  }
}

rx is a class that wraps packet information. Internally, except for setting an internal flag for the processing of _get_network_type(), no modification of packet information is performed.

In other words, by referring to rx.get_psRxDataApp(), which returns tsRxDataApp*, you can obtain the same packet information as in the TWENET C library. packet_rx defines some procedures to access this information, but the information obtained does not change.

Using with NWK_SIMPLE

When used together with NWK_SIMPLE, assign NWK_LAYERED to the_twelite.network and NWK_SIMPLE to the_twelite.network2.

##include <NWK_LAYERED>
##include <NWK_SIMPLE>

void setup() {
    ...
	auto&& nwk_ly = the_twelite.network.use<NWK_LAYERED>();
	auto&& nwk_sm = the_twelite.network2.use<NWK_SIMPLE>();
}

void on_rx_packet(packet_rx& rx, bool_t &handled) {
  auto type = rx.get_network_type();

  if (type == mwx::NETWORK::LAYERED) {
      ; // Packet of layered tree network
  }
  else if (type == mwx::NETWORK::SIMPLE) {
      ; // Packet of NWK_SIMPLE
  }
  else if (type == mwx::NETWORK::NONE) {
      ; // Normal app (such as App_Twelite)
  }
  else {
      ; // Uninterpretable packet
  }

  // Mark the packet as handled, and prevent further intervention by the MWX library.
  handled = true;
}

Each packet type is identified by .get_network_type() as shown above.

  • mwx::NETWORK::LAYERED : Refer to the packet information as is.
  • mwx::NETWORK::SIMPLE : Follow the processing of NWK_SIMPLE.
  • mwx::NETWORK::NONE : No network processing or duplicate packet handling is performed. For example, in the standard App_Twelite application, three packets including retransmissions are sent per transmission. If all packets are successfully received, on_rx_packet() will be called three times. Usually, receiving three times does not mean the data from the 2nd and 3rd receptions is needed. You need to add processing for duplicate packets.

For examples, please refer to Act_Samples Rcv_Univsl. It handles reception of packets with the same wireless channel and application ID but different types in TWELITE PAL, Act_samples, and App_Twelite. Additionally, duplicate check processing is provided for App_Twelite.

12 - Configuration Interface by Setting Behavior

Abstraction layer for interactive mode
Setting behavior is a behavior for using configuration CUI via interactive mode. The interface for configuration is performed through serial port input and output. Interactive configuration using terminal software such as TWELITE STAGE APP / TeraTerm / screen is possible.

12.1 - <STG_STD>

Minimal configuration behavior
<STG_STD> is a configuration behavior with minimal configuration items.
               Example configuration screen
[CONFIG/MY_APP:0/SID=8102ECE3]
a: (0x1234ABCD) Application ID [HEX:32bit]
i: (         1) Device ID [1-100,etc]
c: (        13) Channel [11-26]
o: (0x00000000) Option Bits [HEX:32bit]

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

Usage

Registration

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

Add #include <STG_STD> as above.

Reading with setup()

uint32_t APP_ID;
uint8_t CHANNEL;
uint8_t LID;

void setup() {
   ...
   auto&& set = the_twelite.settings.use<STG_STD>();

   // call reload() before reading values.
   set.reload();

   // read value
   APP_ID = set.u32appid();
   CHANNEL = set.u8ch();
   LID = set.u8devid();

   // apply them
	 the_twelite
		<< TWENET::appid(APP_ID)
		<< TWENET::channel(CHANNEL);

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

In most cases, reading the settings is done at an early stage in setup().

In the above example, first, the configuration behavior is registered by the_twelite.settings.use<STG_STD>().

Next, call set.reload() to actually read data from EEPROM and interpret it. Note that it does not read automatically.

set.u32appid(), set.u8ch(), and set.u8devid() retrieve the application ID setting, channel setting, and logical device ID setting, respectively. Here, the settings are stored in variables.

Then, the application ID, channel, and other values are applied using the setting values.

List of Settings

Below is the list of setting IDs (enum class E_STGSTD_SETID) definitions.

Setting IDContent
APPIDApplication ID
LOGICALIDLogical Device ID (8bit)
CHANNELChannel
CHANNELS_3Channel (up to 3 channels)
POWER_N_RETRYPower and retry count
OPTBITSOption 1
OPT_DWORD2Option 2
OPT_DWORD3Option 3
OPT_DWORD4Option 4
ENC_MODEEncryption mode
ENC_KEY_STRINGEncryption key (string input)

<STG_STD> defines representative settings and four freely usable 32bit values. These can be used freely by the user.

Customizing the Configuration Behavior

Customize all items before calling .reload().

Application Name

	auto&& set = the_twelite.settings.use<STG_STD>();
	set << SETTINGS::appname("MY_APP");
	...
	set.reload();

The application name is displayed on the first line of the interactive mode.

[CONFIG/MY_APP:0/SID=8102ECE3]

Please specify a string pointer. Since no copy is made internally, local variables cannot be specified.

Default Values

	auto&& set = the_twelite.settings.use<STG_STD>();
	set << SETTINGS::appid_default(0x13579be);
	set << SETTINGS::ch_default(18);
	set << SETTINGS::lid_default(7);
		...
	set.reload();

Default values for application ID, frequency channel, and logical device ID (LID) can be changed.

Multiple Channel Setting Menu

	auto&& set = the_twelite.settings.use<STG_STD>();
	set << SETTINGS::ch_multi();
	...
	set.reload();

Specifying SETTINGS::ch_multi() makes the channel setting multiple. When multiple settings are made, use .u32chmask() to read the setting value.

Immediately Display the Configuration Screen

auto&& set = the_twelite.settings.use<STG_STD>();
	set << SETTINGS::open_at_start();
	...
	set.reload();

Default values for application ID, channel, and logical ID can be changed.

Changing Item Names and Description Content

const TWESTG_tsMsgReplace SET_MSGS[] = {
	{ int(E_STGSTD_SETID::OPTBITS),    "Option 1",
			"Please set option 1" },
	{ int(E_STGSTD_SETID::OPT_DWORD2), "Option 2",
			"Please set option 2\r\nOption 2 is such and such" },
	{ 0xFF } // terminator
};

setup() {
  auto&& set = the_twelite.settings.use<STG_STD>();
	set.replace_item_name_desc(SET_MSGS);
	...

You can change the item name to another one. The above example uses Japanese in UTF-8, but it may not display properly unless conditions such as terminal display are met.

This array ends with { 0xFF }.

The first entry is the setting ID, the second is the item name, and the third is the explanation displayed during setting input. You can insert line breaks with \r.

Determine Whether the Configuration Screen is Open

		auto&& set = the_twelite.settings.use<STG_STD>();
		if (!set.is_screen_opened()) {
		   // Processing when the configuration screen is not displayed
		}

Outputting to serial during the configuration screen output may cause the screen to collapse. Confirm that the screen is not open with .is_screen_opened().

Deleting Items

auto&& set = the_twelite.settings.use<STG_STD>();
set.hide_items(E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4);

...
if(set.is_hidden(E_STGSTD_SETID::OPT_DWORD3) {
  ; // OPT_DWORD3 is hidden
}

Delete unnecessary items. .hide_items hides unnecessary items by specifying item IDs as parameters (multiple can be specified as variadic arguments). Whether an item is hidden can be checked with .is_hidden().

Methods

reload()

	auto&& set = the_twelite.settings.use<STG_STD>();
	set << SETTINGS::appname(APP_NAME)
		  << SETTINGS::appid_default(DEF_APP_ID)
   		<< SETTINGS::open_at_start();

	set.reload();

Reads the settings. Execute after all customizations are finished.

Methods (Data Reading)

Call the following methods to read data.

MethodContent
uint32_t u32appid()Application ID
uint8_t u8devid()Logical Device ID
uint8_t u8ch()Configured channel (11..26)
uint32_t u32chmask()Channel setting mask (bitmask, e.g., for 13, set bit 1UL « 13)
uint8_t u8power()Power setting (0..3)
uint8_t u8retry()Retry count
uint32_t u32opt1()Option 1
uint32_t u32opt2()Option 2
uint32_t u32opt3()Option 3
uint32_t u32opt4()Option 4
uint8_t u8encmode()Encryption mode (0: none, 1: enabled, 2: enabled, plaintext packet also shown)
const uint8_t * u8enckeystr()Get encryption key

Applying Settings

Settings can be directly applied to the_twelite or <NWK_SIMPLE> objects using this object.

auto&& set = the_twelite.settings.use<STG_STD>();
...
set.reload(); // Actual reading from EEPROM happens here

the_twelite << set; // Apply settings (such as APPID)

auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();

nwk << set; // Apply settings (such as LID)

The applied settings are as follows. Items hidden by .hide_items() are not applied.

TargetItem IDContent
the_tweliteAPPIDReflected as TWENET::appid(value)
CHANNELReflected as TWENET::channel(value). ※ Not applied when SETTINGS::ch_multi() is specified
CHANNELS_3Reflected as TWENET::chmask(value). ※ Applied only as channel mask when SETTINGS::ch_multi() is specified
POWER_N_RETRYReflected as TWENET::tx_power(value) and TWENET::mac_retry_count(value). Note: <NWK_SIMPLE> retransmission uses the same value.
<NWK_SIMPLE>LOGICALIDReflected as NWK_SIMPLE::logical_id(LID)
POWER_N_RETRYReflected as NWK_SIMPLE::repeat_max(LID)

Item IDs

Item IDs are specified in .hide_items() and other places. These item IDs are defined in enum class E_STGSTD_SETID.

E_STGSTD_SETID::Content
APPIDApplication ID
LOGICALIDLogical ID (0..100)
CHANNELChannel
CHANNELS_3Channel (multiple)
POWER_N_RETRYPower and retry settings
OPTBITSOption bits
UARTBAUDUART baud rate setting
OPT_DWORD2Option bits 2
OPT_DWORD3Option bits 3
OPT_DWORD4Option bits 4
ENC_MODEEncryption mode
ENC_KEY_STRINGEncryption key

Extra Menu

[ROOT MENU/BAT1/SID=810B645E]
0: CONFIG
1: EEPROM UTIL
2: H/W UTIL

 [ESC]:Back [!]:Reset System

Press the M key to access the additional menu.

  • CONFIG : Returns to the configuration screen
  • EEPROM UTIL : Menu for EEPROM maintenance
  • H/W UTIL : Menu to check hardware status

EEPROM UTIL

[EEPROM UTIL/BAT1/SID=810B645E]
r: Read sector.
R: Read ALL sectors.
e: Erase sector.
E: Erase ALL sectors.

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

Read and erase sectors. When reading or erasing all sectors, input the uppercase “YES” (3 characters).

H/W UTIL

[H/W UTIL/BAT1/SID=810B645E]

No functionality is provided in the current version.