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

Utility Functions

Other utility functions

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.

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.

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.

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.

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

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

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.

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.

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