/      日本語

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.