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

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

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.

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.