A low-memory, fast-switching, cooperative multitasking library using stackless coroutines on Arduino platforms.
MIT License
NEW: Profiling in v1.5: Version 1.5 adds the ability to profile the
execution time of Coroutine::runCoroutine()
and render the histogram as a
table or a JSON object. See Coroutine
Profiling for details.
A low-memory, fast-switching, cooperative multitasking library using stackless coroutines on Arduino platforms.
This library is an implementation of the
ProtoThreads library for the
Arduino platform. It emulates a stackless coroutine that can suspend execution
using a yield()
or delay()
functionality to allow other coroutines to
execute. When the scheduler makes its way back to the original coroutine, the
execution continues right after the yield()
or delay()
.
There are only 2 core classes in this library:
Coroutine
class provides the context variables for all coroutinesCoroutineScheduler
class handles the scheduling (optional)The following classes are used for profiling:
CoroutineProfiler
interfaceLogBinProfiler
provides an implementation that tracks the execution timeLogBinTableRenderer
prints the histogram as a tableLogBinJsonRenderer
prints the histogram as a JSON objectThe following is an experimental feature whose API and functionality may change considerably in the future:
Channel
class allows coroutines to send messages to each otherThe library provides a number of macros to help create coroutines and manage their life cycle:
COROUTINE()
: defines an instance of the Coroutine
class or anCoroutine
COROUTINE_BEGIN()
: must occur at the start of a coroutine bodyCOROUTINE_END()
: must occur at the end of the coroutine bodyCOROUTINE_YIELD()
: yields execution back to the caller, oftenCoroutineScheduler
but not necessarilyCOROUTINE_AWAIT(condition)
: yield until condition
becomes true
COROUTINE_DELAY(millis)
: yields back execution for millis
. The millis
uint16_t
.COROUTINE_DELAY_MICROS(micros)
: yields back execution for micros
. Themicros
parameter is defined as a uint16_t
.COROUTINE_DELAY_SECONDS(seconds)
: yields back execution forseconds
. The seconds
parameter is defined as a uint16_t
.COROUTINE_LOOP()
: convenience macro that loops foreverCOROUTINE_CHANNEL_WRITE(channel, value)
: writes a value to a Channel
COROUTINE_CHANNEL_READ(channel, value)
: reads a value from a Channel
Here are some of the compelling features of this library compared to others (in my opinion of course):
Coroutine
consumes about 230 bytes of flashCoroutine
consumes 170 bytes of flashCoroutine
consumes 16 bytes of static RAMCoroutineScheduler
consumes only about 40 bytes of flash andCoroutine
consumes between 120-450 bytes of flashCoroutine
consumes about 130-160 bytes of flash,Coroutine
consumes 28 bytes of static RAMCoroutineScheduler
consumes only about 40-60 bytes of flashCoroutine::runCoroutine()
directly)
CoroutineScheduler::loop()
):
switch
statements in the coroutinesCoroutine
class is easy to subclass to add additional variables andSome limitations are:
Coroutine
cannot return any values.Coroutine
is stackless and therefore cannot preserve local stack variablesmalloc()
and free()
which increases flashChannel
is an experimental feature and has limited features. It isAfter I had completed most of this library, I discovered that I had essentially
reimplemented the <ProtoThread.h>
library in the
Cosa framework. The difference is that
AceRoutine is a self-contained library that works on any platform supporting the
Arduino API (AVR, Teensy, ESP8266, ESP32, etc), and it provides a handful of
additional macros that can reduce boilerplate code.
Version: 1.5.1 (2022-09-20)
Changelog: CHANGELOG.md
This is the HelloCoroutine.ino sample sketch which
uses the COROUTINE()
macro to automatically handle a number of boilerplate
code, and some internal bookkeeping operations. Using the COROUTINE()
macro
works well for relatively small and simple coroutines.
#include <AceRoutine.h>
using namespace ace_routine;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;
COROUTINE(blinkLed) {
COROUTINE_LOOP() {
digitalWrite(LED, LED_ON);
COROUTINE_DELAY(100);
digitalWrite(LED, LED_OFF);
COROUTINE_DELAY(500);
}
}
COROUTINE(printHelloWorld) {
COROUTINE_LOOP() {
Serial.print(F("Hello, "));
Serial.flush();
COROUTINE_DELAY(1000);
Serial.println(F("World"));
COROUTINE_DELAY(4000);
}
}
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Leonardo/Micro
pinMode(LED, OUTPUT);
}
void loop() {
blinkLed.runCoroutine();
printHelloWorld.runCoroutine();
}
The printHelloWorld
coroutine prints "Hello, ", waits 1 second, then prints
"World", then waits 4 more seconds, then repeats from the start. At the same
time, the blinkLed
coroutine blinks the builtin LED on and off, on for 100 ms
and off for 500 ms.
The HelloScheduler.ino sketch implements the same
thing using the CoroutineScheduler
:
#include <AceRoutine.h>
using namespace ace_routine;
... // same as above
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Leonardo/Micro
pinMode(LED, OUTPUT);
CoroutineScheduler::setup();
}
void loop() {
CoroutineScheduler::loop();
}
The CoroutineScheduler
can automatically manage all coroutines defined by the
COROUTINE()
macro, which eliminates the need to itemize your coroutines in the
loop()
method manually. Unfortunately, this convenience is not free (see
MemoryBenchmark):
CoroutineScheduler
singleton instance increases the flash memory byCoroutineScheduler::loop()
method calls the Coroutine::runCoroutine()
virtual
dispatch instead of directly, which is slower andCoroutine
instance consumes an additional ~70 bytes of flashCoroutineScheduler
.On 8-bit processors with limited memory, the additional resource consumption can
be important. On 32-bit processors with far more memory, these additional
resources are often inconsequential. Therefore the CoroutineScheduler
is
recommended mostly on 32-bit processors.
The HelloManualCoroutine.ino program shows what
the code looks like without the convenience of the COROUTINE()
macro. For more
complex programs, with more than a few coroutines, especially if the coroutines
need to communicate with each other, this coding structure can be more powerful.
#include <Arduino.h>
#include <AceRoutine.h>
using namespace ace_routine;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;
class BlinkLedCoroutine: public Coroutine {
public:
int runCoroutine() override {
COROUTINE_LOOP() {
digitalWrite(LED, LED_ON);
COROUTINE_DELAY(100);
digitalWrite(LED, LED_OFF);
COROUTINE_DELAY(500);
}
}
};
class PrintHelloWorldCoroutine: public Coroutine {
public:
int runCoroutine() override {
COROUTINE_LOOP() {
Serial.print(F("Hello, "));
Serial.flush();
COROUTINE_DELAY(1000);
Serial.println(F("World"));
COROUTINE_DELAY(4000);
}
}
};
BlinkLedCoroutine blinkLed;
PrintHelloWorldCoroutine printHelloWorld;
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Leonardo/Micro
pinMode(LED, OUTPUT);
}
void loop() {
blinkLed.runCoroutine();
printHelloWorld.runCoroutine();
}
Version 1.5 added support for profiling the execution time of
Coroutine::runCoroutine()
through the CoroutineProfiler
interface. Currently
only a single implementation (LogBinProfiler
) is provided.
The HelloCoroutineWithProfiler.ino
program shows how to setup the profilers and extract the profiling information
using the Coroutine::runCoroutineWithProfiler()
instead of the usual
Coroutine::runCoroutine()
:
#include <AceRoutine.h>
using namespace ace_routine;
const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;
COROUTINE(blinkLed) {
COROUTINE_LOOP() {
digitalWrite(LED, LED_ON);
COROUTINE_DELAY(100);
digitalWrite(LED, LED_OFF);
COROUTINE_DELAY(500);
}
}
COROUTINE(printHelloWorld) {
COROUTINE_LOOP() {
Serial.print(F("Hello, "));
Serial.flush();
COROUTINE_DELAY(1000);
Serial.println(F("World"));
COROUTINE_DELAY(4000);
}
}
COROUTINE(printProfiling) {
COROUTINE_LOOP() {
LogBinTableRenderer::printTo(
Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
LogBinJsonRenderer::printTo(
Serial, 3 /*startBin*/, 14 /*endBin*/);
COROUTINE_DELAY(5000);
}
}
LogBinProfiler profiler1;
LogBinProfiler profiler2;
LogBinProfiler profiler3;
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Leonardo/Micro
pinMode(LED, OUTPUT);
pinMode(PIN, INPUT);
// Coroutine names can be either C-string or F-string.
blinkLed.setName("blinkLed");
readPin.setName(F("readPin"));
// Manually attach the profilers to the coroutines.
blinkLed.setProfiler(&profiler1);
readPin.setProfiler(&profiler2);
printProfiling.setProfiler(&profiler3);
}
void loop() {
blinkLed.runCoroutineWithProfiler();
printHelloWorld.runCoroutineWithProfiler();
printProfiling.runCoroutineWithProfiler();
}
Every 5 seconds, the printProfiling
coroutine will print the profiling
information in 2 formats on the Serial
port:
LogBinTableRenderer
LogBinJsonRenderer
name <16us <32us <64us<128us<256us<512us <1ms <2ms <4ms <8ms >>
0x1DB 16921 52650 0 0 0 0 0 0 0 0 1
readPin 65535 1189 0 0 0 0 0 0 0 0 0
blinkLed 65535 830 0 0 0 0 0 0 0 0 0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}
The HelloSchedulerWithProfiler.ino sketch
implements the same thing as HelloCoroutineWithProfiler
using 2 techniques to
handle more than a handful of coroutines:
LogBinProfiler::createProfilers()
to automatically create the profilersCoroutineScheduler::loopWithProfiler()
method instead of theCoroutineScheduler::loop()
method.#include <AceRoutine.h>
using namespace ace_routine;
const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;
COROUTINE(blinkLed) {
COROUTINE_LOOP() {
digitalWrite(LED, LED_ON);
COROUTINE_DELAY(100);
digitalWrite(LED, LED_OFF);
COROUTINE_DELAY(500);
}
}
COROUTINE(printHelloWorld) {
COROUTINE_LOOP() {
Serial.print(F("Hello, "));
Serial.flush();
COROUTINE_DELAY(1000);
Serial.println(F("World"));
COROUTINE_DELAY(4000);
}
}
COROUTINE(printProfiling) {
COROUTINE_LOOP() {
LogBinTableRenderer::printTo(
Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
LogBinJsonRenderer::printTo(
Serial, 3 /*startBin*/, 14 /*endBin*/);
COROUTINE_DELAY(5000);
}
}
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Leonardo/Micro
pinMode(LED, OUTPUT);
pinMode(PIN, INPUT);
// Coroutine names can be either C-string or F-string.
blinkLed.setName("blinkLed");
readPin.setName(F("readPin"));
// Create profilers on the heap and attach them to all coroutines.
LogBinProfiler::createProfilers();
CoroutineScheduler::setup();
}
void loop() {
CoroutineScheduler::loopWithProfiler();
}
The printProfiling
coroutine will print the same information as before every 5
seconds:
name <16us <32us <64us<128us<256us<512us <1ms <2ms <4ms <8ms >>
0x1DB 16921 52650 0 0 0 0 0 0 0 0 1
readPin 65535 1189 0 0 0 0 0 0 0 0 0
blinkLed 65535 830 0 0 0 0 0 0 0 0 0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}
The latest stable release is available in the Arduino IDE Library Manager. Two libraries need to be installed as of v1.5.0:
The development version can be installed by cloning the following git repos:
You can copy these directories to the ./libraries
directory used by the
Arduino IDE. (You should see 2 directories, named ./libraries/AceRoutine
and
./libraries/AceCommon
). Or you can create symlinks from /.libraries
to these
directories.
The develop
branch contains the latest working version.
The master
branch contains the stable release.
The source files are organized as follows:
src/AceRoutine.h
- main header filesrc/ace_routine/
- implementation filessrc/ace_routine/testing/
- internal testing filestests/
- unit tests which depend onexamples/
- example programsThe following programs are provided under the examples
directory:
HelloCoroutine
CoroutineScheduler
instead of manually running theHelloCoroutine
except the Coroutine
subclasses and instances areHelloCoroutineWithProfiler
using CoroutineScheduler
Coroutine
subclassesCOROUTINE_DELAY()
macroreset()
function to interrupt the sound generator.Channel
to allow a Writer to sendChannel
to allow a Writer to sendChannel
by using 2 coroutines to ping-pong anThere are several interesting and useful multithreading libraries for Arduino. I'll divide the libraries in to 2 camps:
Task managers run a set of tasks. They do not provide a way to resume
execution after yield()
or delay()
.
In order of increasing complexity, here are some libraries that provide broader abstraction of threads or coroutines:
switch
statements don't work.yield()
for a seamless experience.setjmp()
and longjmp()
.<ProtoThread.h>
library in the Cosa framework uses basically theAceRoutine
library.The AceRoutine library falls in the "Threads or Coroutines" camp. The
inspiration for this library came from
ProtoThreads and
Coroutines in C
where an incredibly brilliant and ugly technique called
Duff's Device
is used to perform labeled goto
statements inside the "coroutines" to resume
execution from the point of the last yield()
or delay()
. It occurred to me
that I could make the code a lot cleaner and easier to use in a number of ways:
PROGMEM
attribute). In return, switch
statements would workstruct
. ItI looked around to see if there already was a library that implemented these
ideas and I couldn't find one. However, after writing most of this library, I
discovered that my implementation was very close to the <ProtoThread.h>
module
in the Cosa framework. It was eerie to see how similar the 2 implementations had
turned out at the lower level. I think the AceRoutine library has a couple of
advantages:
COROUTINE()
and EXTERN_COROUTINE()
) toAll objects are statically allocated (i.e. not heap or stack).
On 8-bit processors (AVR Nano, Uno, etc):
sizeof(Coroutine): 16
sizeof(CoroutineScheduler): 2
sizeof(Channel<int>): 5
sizeof(LogBinProfiler): 66
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1
On 32-bit processors (e.g. Teensy ARM, ESP8266, ESP32):
sizeof(Coroutine): 28
sizeof(CoroutineScheduler): 4
sizeof(Channel<int>): 12
sizeof(LogBinProfiler): 68
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1
The CoroutineScheduler
consumes only 2 bytes (8-bit processors) or 4 bytes
(32-bit processors) of static memory no matter how many coroutines are created.
That's because it depends on a singly-linked list whose pointers live on the
Coroutine
object, not in the CoroutineScheduler
. But using the
CoroutineScheduler::loop()
instead of calling Coroutine::runCoroutine()
directly increases flash memory usage by 70-100 bytes.
The Channel
object requires 2 copies of the parameterized <T>
type so its
size is equal to 1 + 2 * sizeof(T)
, rounded to the nearest memory alignment
boundary (i.e. a total of 12 bytes for a 32-bit processor).
The examples/MemoryBenchmark program gathers flash and memory consumption numbers for various boards (AVR, ESP8266, ESP32, etc) for a handful of AceRoutine features. Here are some highlights:
Arduino Nano (8-bits)
+--------------------------------------------------------------------+
| functionality | flash/ ram | delta |
|---------------------------------------+--------------+-------------|
| Baseline | 1616/ 186 | 0/ 0 |
|---------------------------------------+--------------+-------------|
| One Delay Function | 1664/ 188 | 48/ 2 |
| Two Delay Functions | 1726/ 190 | 110/ 4 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis) | 1804/ 212 | 188/ 26 |
| Two Coroutines (millis) | 1998/ 236 | 382/ 50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros) | 1776/ 212 | 160/ 26 |
| Two Coroutines (micros) | 1942/ 236 | 326/ 50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds) | 1904/ 212 | 288/ 26 |
| Two Coroutines (seconds) | 2130/ 236 | 514/ 50 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler | 1874/ 212 | 258/ 26 |
| Two Coroutines, Profiler | 2132/ 236 | 516/ 50 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis) | 1928/ 214 | 312/ 28 |
| Scheduler, Two Coroutines (millis) | 2114/ 238 | 498/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros) | 1900/ 214 | 284/ 28 |
| Scheduler, Two Coroutines (micros) | 2058/ 238 | 442/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds) | 2028/ 214 | 412/ 28 |
| Scheduler, Two Coroutines (seconds) | 2246/ 238 | 630/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup) | 1978/ 214 | 362/ 28 |
| Scheduler, Two Coroutines (setup) | 2264/ 238 | 648/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup) | 1956/ 214 | 340/ 28 |
| Scheduler, Two Coroutines (man setup) | 2250/ 238 | 634/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler | 1992/ 214 | 376/ 28 |
| Scheduler, Two Coroutines, Profiler | 2178/ 238 | 562/ 52 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler | 2112/ 286 | 496/ 100 |
| Scheduler, LogBinTableRenderer | 3514/ 304 | 1898/ 118 |
| Scheduler, LogBinJsonRenderer | 3034/ 308 | 1418/ 122 |
|---------------------------------------+--------------+-------------|
| Blink Function | 1948/ 189 | 332/ 3 |
| Blink Coroutine | 2118/ 212 | 502/ 26 |
+--------------------------------------------------------------------+
ESP8266 (32-bits)
+--------------------------------------------------------------------+
| functionality | flash/ ram | delta |
|---------------------------------------+--------------+-------------|
| Baseline | 264981/27984 | 0/ 0 |
|---------------------------------------+--------------+-------------|
| One Delay Function | 265045/27992 | 64/ 8 |
| Two Delay Functions | 265109/27992 | 128/ 8 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis) | 265177/28028 | 196/ 44 |
| Two Coroutines (millis) | 265337/28060 | 356/ 76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros) | 265209/28028 | 228/ 44 |
| Two Coroutines (micros) | 265369/28060 | 388/ 76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds) | 265209/28028 | 228/ 44 |
| Two Coroutines (seconds) | 265385/28060 | 404/ 76 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler | 265257/28028 | 276/ 44 |
| Two Coroutines, Profiler | 265433/28060 | 452/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis) | 265241/28036 | 260/ 52 |
| Scheduler, Two Coroutines (millis) | 265385/28060 | 404/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros) | 265257/28036 | 276/ 52 |
| Scheduler, Two Coroutines (micros) | 265401/28060 | 420/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds) | 265257/28036 | 276/ 52 |
| Scheduler, Two Coroutines (seconds) | 265417/28060 | 436/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup) | 265273/28036 | 292/ 52 |
| Scheduler, Two Coroutines (setup) | 265433/28060 | 452/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup) | 265257/28036 | 276/ 52 |
| Scheduler, Two Coroutines (man setup) | 265433/28060 | 452/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler | 265321/28036 | 340/ 52 |
| Scheduler, Two Coroutines, Profiler | 265449/28060 | 468/ 76 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler | 265465/28100 | 484/ 116 |
| Scheduler, LogBinTableRenderer | 267381/28100 | 2400/ 116 |
| Scheduler, LogBinJsonRenderer | 266789/28104 | 1808/ 120 |
|---------------------------------------+--------------+-------------|
| Blink Function | 265669/28064 | 688/ 80 |
| Blink Coroutine | 265801/28100 | 820/ 116 |
+--------------------------------------------------------------------+
Comparing Blink Function
and Blink Coroutine
is probably the most
fair comparison, because they implement the exact same functionality. The code
is given in
Comparison To NonBlocking Function.
The Blink Function
implements the asymmetric blink (HIGH and LOW having
different durations) functionality using a simple, non-blocking function with an
internal prevMillis
static variable. The Blink Coroutine
implements the
same logic using a Coroutine
. The Coroutine
version is far more readable and
maintainable, with only about 220 additional bytes of flash on AVR, and 130
bytes on an ESP8266. In many situations, the increase in flash memory size may
be worth paying to get easier code maintenance.
See examples/AutoBenchmark. Here are 2 samples:
Arduino Nano:
+---------------------------------+--------+-------------+--------+
| Functionality | iters | micros/iter | diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop | 10000 | 1.700 | 0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling | 10000 | 2.900 | 1.200 |
| DirectSchedulingWithProfiler | 10000 | 5.700 | 4.000 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling | 10000 | 7.100 | 5.400 |
| CoroutineSchedulingWithProfiler | 10000 | 9.300 | 7.600 |
+---------------------------------+--------+-------------+--------+
ESP8266:
+---------------------------------+--------+-------------+--------+
| Functionality | iters | micros/iter | diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop | 10000 | 0.200 | 0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling | 10000 | 0.500 | 0.300 |
| DirectSchedulingWithProfiler | 10000 | 0.800 | 0.600 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling | 10000 | 0.900 | 0.700 |
| CoroutineSchedulingWithProfiler | 10000 | 1.100 | 0.900 |
+---------------------------------+--------+-------------+--------+
Tier 1: Fully Supported
These boards are tested on each release:
Tier 2: Should work
These boards should work, but they are not tested frequently by me, or I don't own the specific hardware so they were tested by a community member:
Tier 3: May work, but not supported
Tier Blacklisted
The following boards are not supported and are explicitly blacklisted to allow the compiler to print useful error messages instead of hundreds of lines of compiler errors:
This library was developed and tested using:
This library is not compatible with:
It should work with PlatformIO but I have not tested it.
The library works on Linux or MacOS (using both g++ and clang++ compilers) using the EpoxyDuino emulation layer.
I use Ubuntu 20.04 for the vast majority of my development. I expect that the library will work fine under MacOS and Windows, but I have not explicitly tested them.
If you have any questions, comments, or feature requests for this library, please use the GitHub Discussions for this project. If you have bug reports, please file a ticket in GitHub Issues. Feature requests should go into Discussions first because they often have alternative solutions which are useful to remain visible, instead of disappearing from the default view of the Issue tracker after the ticket is closed.
Please refrain from emailing me directly unless the content is sensitive. The problem with email is that I cannot reference the email conversation when other people ask similar questions later.
Created by Brian T. Park ([email protected]).