Arduino 開発を支える地味なライブラリたち


こちらは [Arduino Advent Calender 2020](https://qiita.com/advent-calendar/2020/arduino) の 12 日目の記事です。

私は普段から Arduino を活用して[デバイス開発](https://hideakitai.github.io/)をしています。そんな中で自作してきた、**地味だけど無いと困るライブラリ達**を紹介していきたいと思います。少しでも皆さんの参考になれば幸いです。そして皆さんの設計ノウハウも、ぜひ聞かせていただきたいです。


## はじめに

今回は Arduino 開発特にハマりがちな (面倒な) 下記の 4 つのカテゴリについて、ちょっとだけ開発を楽にしてくれる地味ライブラリたちを紹介していきます (Arduino 特有の問題でもないかもしれませんが)。

- 通信 (とくにシリアル通信)
- タスク・タイミング管理
- アニメーション (時間軸での操作)
- C++ 標準ライブラリ (っぽいもの) の利用

今回紹介する地味ライブラリを使うと、**複雑なプロジェクトでも多少は見通しよく設計ができる**んじゃないかな、と思います。こうしたらもっと良くなるよ、こういうライブラリもあるよ、などご意見いただけたら嬉しいです。

また、これらのライブラリたちは、[openFrameworks](https://openframeworks.cc/) とその [アドオン](https://ofxaddons.com/categories) や [Processing](https://processing.org/) のようなオープンソースなクリエイティブコーディングフレームワークから様々な知見をいただいて開発しています。偉大なる先人たちに感謝…。

## 通信 (シリアル通信)

**シリアル通信は特に Arduino における鬼門**で (ぼくだけでしょうか…)、ここをクリアできればいろいろなプロジェクトを楽に進めることができます。**シリアル通信は決まった通信プロトコルがなく、自分でプロトコル決めて実装していかなければならない**ため、はじめは本当に大変でした。では、どうやってシリアル通信するのが良いのか？を考えていった記事を [2017 年のアドベントカレンダー](https://qiita.com/hideakitai/items/347985528656be03b620) に書いていますので、こちらも合わせて見てみてください。

個人的な開発方針として、**通信バイト数をなるべく小さくしたい**ので、通信モジュールなどに必要な場合を除いて、**基本的に ASCII でのデータ送受信は行いません**。以下はバイナリ形式のみを前提としています。


### [Packetizer](https://github.com/hideakitai/Packetizer)

バイナリデータを [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) もしくは [SLIP](https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol) という形式にエンコード・デコードし、それらに `index` と `crc8` を付与することで、軽量＋安全に通信を行うためのライブラリです。

- **COBS / SLIP** に変換することで**パケット形式を明確**にし、**なるべく軽量に**データの送受信可能
- `index` バイトを追加することで、**パケット種別の判別**が可能
- `crc8` バイトを追加することで、**パケットの誤りを検出**が可能
- これらの特徴を持つパケットを、**一行で送信**することが可能
- `index` バイトを活用して、**ラムダ式などで受信コールバックを簡単に記述**可能

```C++
// define these macros before include, to enable indexing and verifying
#define PACKETIZER_USE_INDEX_AS_DEFAULT
#define PACKETIZER_USE_CRC_AS_DEFAULT

#include <Packetizer.h>
uint8_t recv_index = 0x12;
uint8_t send_index = 0x34;

void setup() {
    // you can add callback depending on index value
    Packetizer::subscribe(Serial, recv_index,
        [&](const uint8_t* data, const size_t size) {
            Packetizer::send(Serial, send_index, data, size); // send back packet
        }
    );

    // you can also add callback called every time packet comes
    Packetizer::subscribe(Serial,
        [&](const uint8_t index, const uint8_t* data, const size_t size) {
            // send back to same index
            Packetizer::send(Serial, index, data, size);
        }
    );
}

void loop() {
    Packetizer::parse(); // automatically incoming packets are verified by crc
}
```


### [MsgPack](https://github.com/hideakitai/MsgPack)

実際にプログラム上で使うクラスや変数などを、[MessagePack](https://msgpack.org/ja.html) 形式のバイナリデータにシリアライズするためのライブラリです。

- **オリジナルの MessagePack で扱える型はほぼ網羅** (独自のクラスや `std::vector<T>` なども)
- 型推論によって、**様々な型の変数を一発エンコード・デコード**できる

世の中には他にも色々なシリアライザがありますが、個人的には MessagePack が好きなので、これをいつも使っています。みなさんのおすすめのシリアライザも教えてほしいです。

```C++
#include <MsgPack.h>

// input to msgpack
int i = 123;
float f = 1.23;
MsgPack::str_t s = "str"; // std::string or String
MsgPack::arr_t<int> v {1, 2, 3}; // std::vector or arx::vector
MsgPack::map_t<String, float> m {{"one", 1.1}, {"two", 2.2}, {"three", 3.3}}; // std::map or arx::map

// output from msgpack
int ri;
float rf;
MsgPack::str_t rs;
MsgPack::arr_t<int> rv;
MsgPack::map_t<String, float> rm;

void setup() {
    // serialize to msgpack
    MsgPack::Packer packer;
    packer.serialize(i, f, s, v, m);

    // deserialize from msgpack
    MsgPack::Unpacker unpacker;
    unpacker.feed(packer.data(), packer.size());
    unpacker.deserialize(ri, rf, rs, rv, rm);

    if (i != ri) Serial.println("failed: int");
    if (f != rf) Serial.println("failed: float");
    if (s != rs) Serial.println("failed: string");
    if (v != rv) Serial.println("failed: vector<int>");
    if (m != rm) Serial.println("failed: map<string, int>");
}
```



### [MsgPacketizer](https://github.com/hideakitai/MsgPacketizer)

**個人的なシリアル通信の定番ライブラリ**を模索して、現状一番よく使っているのがこちらになります。上述の 2 つのライブラリを組み合わせることで、どんな変数でも簡単で安全にシリアル通信ができます。

- パケットサイズをなるべく小さくするため、[MessagePack](https://msgpack.org/ja.html) を使用
- MessagePack にシリアライズされたデータを、`Packetizer` を使ってさらに加工
  -  `index` バイトでパケット種別を判別し、`crc8` バイトで誤り判定
  -  [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) に変換し、小さいデータ量で安全に送受信
- 型推論により、様々な型の変数を自動的にエンコード・デコード
  - 様々な型の変数を一行でエンコード＋送信
  - 様座な型の変数を一行で受信＋デコード
  - ラムダ式などでコールバックを書くこともももちろん可能

```C++
#include <MsgPacketizer.h>

int i;
float f;
MsgPack::str_t s; // std::string or String
MsgPack::arr_t<int> v; // std::vector or arx::vector
MsgPack::map_t<String, float> m; // std::map or arx::map

uint8_t recv_index = 0x12;
uint8_t send_index = 0x34;

void setup() {
    // update received data directly
    MsgPacketizer::subscribe(Serial, recv_index, i, f, s, v, m);

    // send varibales periodically (default 30[times/sec])
    MsgPacketizer::publish(Serial, send_index, i, f, s, v, m);
}

void loop() {
    // must be called to trigger callback and publish data
    MsgPacketizer::update();
}
```

## タスク・タイミング管理

プロジェクトが大きく・複雑なものになっていくと、**さまざまな周期的なタスクや単発のタスクの管理**が大変になってきます。それらをなるべくシンプルに記述し、見通しを良くするために活用している地味ライブラリがこちらになります。

ここでも個人的な開発方針があるのですが、**基本的には割り込み処理は最小限しか使いません**。理由はプログラムの見通しが悪くなるからです。**基本的にはポーリングのみで設計を行い、どうしても必要なところにだけ割り込み処理を使います** (音系の処理やマイクロ秒単位の処理など)。個人的にはこの方が、見通しの立ちやすいタスク制御ができると思っています (~~割り込みでバグったときのデバッグが面倒なだけ~~)。


### [TaskManager](https://github.com/hideakitai/TaskManager)

**あまり複雑ではない、周期的なタスクを管理**することができます。JavaScript の `setTimeout` 的なことも `once(after_ms, function)` を使うと実現できます。

```C++
#include <TaskManager.h>

void setup() {
    // task is executed only once after 5000[ms]
    Tasks.once(5000, []{
        Serial.print("once task: now = ");
        Serial.println(millis());
    });

    // task framerate is 1 and repeat forever
    Tasks.framerate(1, []{
        Serial.print("framerate forever task: now = ");
        Serial.println(millis());
    });

    // task framerate is 2 and 10 times only
    Tasks.framerate(2, 10, []{
        Serial.print("framerate limited task: now = ");
        Serial.println(millis());
    });

    // task interval is 1000[ms] and repeat forever
    Tasks.interval(1000, []{
        Serial.print("interval forever task: now = ");
        Serial.println(millis());
    });

    // task interval is 500[ms] and 10 times only
    Tasks.interval(500, 10, []{
        Serial.print("interval limited task: now = ");
        Serial.println(millis());
    });
}

void loop() {
    Tasks.update(); // automatically execute tasks
}
```


### [SceneManager](https://github.com/hideakitai/SceneManager)

**より複雑な一連のタスクを「シーン」という名前でひとつのクラスにまとめ**、そのシーンの生成から破棄までの一連の流れにおける様々なタスクを柔軟に管理することができます。`TaskManager` の拡張版と言えます。例えば、下記のように**複数のタスクを個別のシーンクラスに分けて記述**することで、`loop()` に全ての処理を書くよりも、見通しを良くすることができます。

- センサ A の値を 1 秒に 1 回取得し、それを LCD に反映する
- センサ B の値を 1 秒に 30 回取得し、それに応じてサーボを駆動する
- 1 秒に 60 回ポーリングし、通信を受信したときはすぐに LED を光らせる

```C++
#include <SceneManager.h>

// make your scene class
class Blink : public Scene::Base {
    bool b;
public:
    virtual ~Blink() {}

    Blink(const String& name, double fps)
    : Base(name, fps)
    , b(false)
    {
        pinMode(13, OUTPUT);
        digitalWrite(13, LOW);
    }

    virtual void update() override {
        digitalWrite(13, b);
        b = !b;
    }
};

void setup() {
    Scenes.add<Blink>("blink", 1); // add scene to run in 1 [fps]
    Scenes.start();                // start scenes
}

void loop() {
    Scenes.update(); // automatically Blink::update() is called in 1 [fps]
}
```


### [PollingTimer](https://github.com/hideakitai/PollingTimer)

上記のような、時間に応じたタスク管理を行うライブラリには `PollingTimer` が使用されています。割り込みは使わず、名前の通りポーリングのみでタスクの実行タイミングを制御します。下記のような種類があります。

- `PollingTimer` : シンプルなタイマ
- `IntervalCounter` : インターバル指定でコールバックを呼び出す
- `FrameRateCounter` : フレームレート指定でコールバックを呼び出す
- `OneShotTimer` : 指定時間後に一度だけコールバックを呼び出す

```C++
#include <FrameRateCounter.h>
FrameRateCounter fps(30); // set framrate to 30[Hz]

void setup() {
    fps.addEvent([&]() {
        Serial.print("frame no. = ");
        Serial.print(fps.frame());
        Serial.print(", time = ");
        Serial.println(fps.msec());
    });
    fps.start();
}

void loop() {
    fps.update(); // event occurs if frame has changed
}
```


### [DebugLog](https://github.com/hideakitai/DebugLog)

プロジェクトが大きく複雑になってくるにつれて、デバッグ作業がどんどんしんどくなってきます。そんなときに、**柔軟にシリアルデバッグ出力とそのロギング、Arduino 標準では使えない Assertion** のようなものが使えると捗るな、、、と思って作ったライブラリです ~~(ICE を使いましょう)~~。

- **可変長引数で楽にシリアル出力**ができる
- `LOG_ERROR` `LOG_WARNING` `LOG_VERBOSE` `PRINTLN` などでログを出力
- `LOG_SET_LEVEL` で表示するログレベルを選択
- `#define NDEBUG` を定義することで**すべて表示のログを非表示 (リリースモード)**
- ログ出力データを **SD カードへ自動で保存**

```C++
// uncommend NDEBUG disables ASSERT and all debug serial (Release Mode)
//#define NDEBUG

#include <DebugLog.h>

void setup() {
    Serial.begin(115200);

    // you can change target stream (default: Serial, only for Arduino)
    // LOG_ATTACH_SERIAL(Serial2);

    // set log level (default: DebugLogLevel::VERBOSE)
    LOG_SET_LEVEL(DebugLogLevel::ERRORS); // only ERROR log is printed
    LOG_SET_LEVEL(DebugLogLevel::WARNINGS); // ERROR and WARNING is printed
    LOG_SET_LEVEL(DebugLogLevel::VERBOSE); // all log is printed

    // set log output format options (show file, line, and func)
    // default: true, true, true
    LOG_SET_OPTION(false, false, true);

    LOG_ERROR("this is error log");
    LOG_WARNING("this is warning log");
    LOG_VERBOSE("this is verbose log");

    // you can change delimiter from default " " to anything
    LOG_SET_DELIMITER(" and ");
    LOG_VERBOSE(1, 2, 3, 4, 5);

    int x = 1;
    ASSERT(x != 1); // if assertion failed, Serial endlessly prints message
}
```


### [TimeProfiler](https://github.com/hideakitai/TimeProfiler)

これまで紹介してきたポーリングに基づくタスク管理では、そのタスク実行周期を超えた重い処理があると、すべてのタスクのタイミングが狂ってしまいます。そのような場合に備えて、**タイムプロファイリングを行えばボトルネックを可視化**することが可能です。

- `TIMEPROFILE_BEGIN(hoge)` `TIME_PROFILE_END(hoge)` で任意の部分の時間を計測
- `SCOPED_TIMEPROFILE(hoge)` で任意のスコープの時間を計測

```C++
#include <TimeProfiler.h>

void setup()
{
    TIMEPROFILE_BEGIN(one); // about 1000 [ms]
    TIMEPROFILE_BEGIN(two); // about 3000 [ms]
    TIMEPROFILE_BEGIN(three); // about 6000 [ms]

    // create scope for test
    {
        SCOPED_TIMEPROFILE(all);
        {
            SCOPED_TIMEPROFILE(a);
            delay(1000);
            TIMEPROFILE_END(one);
        }
        {
            SCOPED_TIMEPROFILE(b);
            delay(2000);
            TIMEPROFILE_END(two);
        }
        {
            SCOPED_TIMEPROFILE(c);
            delay(3000);
            TIMEPROFILE_END(three);
        }
        // "all" ends here
    }

    Serial.print("all   : ");
    Serial.println(TimeProfiler.getProfile("all"));
    Serial.print("one   : ");
    Serial.println(TimeProfiler.getProfile("one"));
    Serial.print("two   : ");
    Serial.println(TimeProfiler.getProfile("two"));
    Serial.print("three : ");
    Serial.println(TimeProfiler.getProfile("three"));
    Serial.print("a     : ");
    Serial.println(TimeProfiler.getProfile("a"));
    Serial.print("b     : ");
    Serial.println(TimeProfiler.getProfile("b"));
    Serial.print("c     : ");
    Serial.println(TimeProfiler.getProfile("c"));
}
```


## アニメーション

こちらはグラフィックではなく、値をアニメーションさせるライブラリです。Arduino に限った話ではありませんが、**時間軸によって変化していく値を作るのは `if` 文の嵐になったりして、非常にめんどくさい**です。これをなるべく楽に書けるようにしたいな、と思って下記のライブラリを作りました。


### [Easing](https://github.com/hideakitai/Easing)

[Andy Brown's Easing Function Library](https://andybrown.me.uk/2010/12/05/animation-on-the-arduino-with-easing-functions/) という基本的なイージングのライブラリを Arduino 用に移植したものです。イージングがどいうものなのか、については[こちら](https://easings.net/ja)を見るとイメージしやすいと思います。

```C++
#include <Easing.h>
EasingFunc<Ease::Sine> e;
float start;

void setup() {
    e.duration(duration); // default duration is 1.0
    e.scale(scale); // default scale is 1.0
    start = millis() / 1000.;
}

void loop() {
    // value moves from 0.0 to scale in duration [sec]
    float now = millis() / 1000.;
    float value = e.get(now - start);
    Serial.println(value);
}
```


### [Tween](https://github.com/hideakitai/Tween)

上記の `Easing` を内部で使用し、Tween アニメーションをサクッと作れるライブラリです。**値の連続的なアニメーションをそれなりに簡単に作る**ことができます。モータの動作や LED の光り方を良い感じにするなど、いろんな場面で使えると思います。

![tween.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37296/2303ae56-5392-fe0f-8802-9a2ab66b1f8f.gif)


```C++
#include <Tween.h>
Tween::Timeline timeline;
float target = 0.f;

void setup() {
    timeline.add(target) // target tweens
        .then(10, 5000)  // to 10 in 5000[ms]
        .then(5, 5000)   // then to 5 in 5000[ms]
        .wait(1000)      // then stops 1000[ms]
        .then(0, 5000);  // then to 0 in 5000[ms]

    timeline.start();
}

void loop() {
    timeline.update(); // must be called to tween target
    Serial.println(target); // target value tweens automatically
}
```


## C++ 標準ライブラリ (っぽいもの) の利用

ここまで、普通に `std::vector<T>` のような C++ の標準ライブラリにあるコンテナクラスを使ってきました。ESP 系や ARM 系のチップであれば使うことは可能なのですが、**残念なことに Arduino UNO や Mega などの AVR 系チップのものは、標準ライブラリを使えません…**。マイコンはメモリの容量が限られているため致し方ないことではあるのですが、ライブラリを書いていると、やはり **Uno などへの対応をお願いされることが多々あります**。

そんな経験を経て、一念発起して C++ 標準ライブラリのような挙動をするライブラリを、C++ の勉強だと思って作りました。Arduino 用のライブラリを作っているような方々は、これらを使うとちょっと幸せになれるかもしれません。

これらのライブラリは、**標準ライブラリが使用可能な場合には標準ライブラリを優先して使う**ようになっており ([ArduinoSTL](https://github.com/mike-matera/ArduinoSTL) などの [uClibc++](https://cxx.uclibc.org/) ベースのライブラリとも共存可能)、プラットフォーム間の差異を意識せずにとりあえず `include` しておけば良いものになっています。


### [ArxContainer](https://github.com/hideakitai/ArxContainer)

`std::vector` `std::deque` `std::map` の主要なメソッドを使用可能な `arx::vector` `arx::deque` `arx::map` を使えるようになります。内部では固定長のリングバッファを使って上記の機能を模倣しているため、可変長っぽく見えてもコンパイル時にサイズが固定され、必要な分のメモリしか確保せずに済みます。

```C++
#include <ArxContainer.h>

// vector
arx::vector<int> vs {1, 2, 3};
for (size_t i = 4; i <= 5; ++i)
    vs.push_back(i);
for (size_t i = 0; i < vs.size(); ++i)
    Serial.println(vs[i]);
for (const auto& v : vs)
    Serial.println(v);

// deque
arx::deque<int> dq {1, 2, 3};
for (int i = 4; i <= 5; ++i)
    dq.push_back(i);
for (int i = 0; i < dq.size(); ++i)
    Serial.print(dq[i]);

// map
arx::map<String, int> mp {{"one", 1}, {"two", 2}};
mp.insert("three", 3);
mp["four"] = 4;
for (const auto& m : mp) {
    Serial.print("{");
    Serial.print(m.first); Serial.print(",");
    Serial.print(m.second);
    Serial.println("}");
}
Serial.print("one   = "); Serial.println(mp["one"]);
Serial.print("two   = "); Serial.println(mp["two"]);
Serial.print("three = "); Serial.println(mp["three"]);
Serial.print("four  = "); Serial.println(mp["four"]);
```


### [ArxSmartPtr](https://github.com/hideakitai/ArxSmartPtr)

カウンタ付きスマートポインタの `std::shared_ptr` が使えるようになります。[Boost.SmartPtr](https://github.com/boostorg/smart_ptr) を簡略化して移植したものです。

```C++
#include <ArxSmartPtr.h>

{
    Serial.println("start");
    std::shared_ptr<Base> t1(new Base(4));
    std::shared_ptr<Base> t2;
    {
        std::shared_ptr<Base> t3(new Base(5));
        std::shared_ptr<Base> t4(new Base(6));
        t2 = t3;
    }
    Serial.println("end");
}
// start
// Base::Constructor 4
// Base::Constructor 5
// Base::Constructor 6
// Base::Destructor 6
// end
// Base::Destructor 5
// Base::Destructor 4
```


### [ArxTypeTraits](https://github.com/hideakitai/ArxTypeTraits)

テンプレートメタプログラミングを使いやすくする `type_traits` の一部が使えるようになります。それに付随して、`std::function` `std::tuple` も使えるようになります。ある程度のテンプレートメタプログラミングが可能になるため、そのような技法を使ったライブラリを AVR などのプラットフォーム間の差異を意識せずに作れるようになります。

標準ライブラリが使えるときはそれを優先して使うように設計されており、[ArduinoSTL](https://github.com/mike-matera/ArduinoSTL) などの [uClibc++](https://cxx.uclibc.org/index.html) ベースのライブラリとも補完し合って使うことが可能です。これらと組み合わせて使うことで、C++ 標準ライブラリの多くを使用することが可能となります。ただ、その分プログラム領域やメモリを食うことになるので、非力な AVR マイコンを使用する場合はご注意下さい。

Arduino では基本的に C++14 でビルドできるようになっていますが、`ArxTypeTraits` ではごく一部の C++17/20 のものもカバーしています。Pull Request もお待ちしております。

```C++
#include <ArxTypeTraits.h>

template <class T>
auto f(T)
-> typename std::enable_if<std::is_integral<T>::value>::type {
    Serial.println("T is integral");
}

template <class T>
auto f(T)
-> typename std::enable_if<std::is_floating_point<T>::value>::type {
    Serial.println("T is floating point");
}

template <class T>
auto f(T)
-> typename std::enable_if<!std::is_arithmetic<T>::value>::type {
    Serial.println("T is not arithmetic");
}

void setup() {
    f(1);
    f(1.1);
    f("1.11");
}
// T is integral
// T is floating point
// T is not arithmetic
```


## その他のライブラリ

せっかくなので、その他の使用頻度が高いライブラリも羅列しておきます。

### 通信系

- [ArduinoOSC](https://github.com/hideakitai/ArduinoOSC) : Arduino で OSC 通信を簡潔に実現します。詳細は [以前の記事](https://qiita.com/hideakitai/items/f50f99e38b520a0ace37) をご覧ください。
- [ArtNet](https://github.com/hideakitai/ArtNet) : Arduino で Art-Net の送受信を可能にし、受信データを直接 [FastLED](https://github.com/FastLED/FastLED) に流し込むこともできます。
- [CRCx](https://github.com/hideakitai/CRCx) : CRC を計算します。普通の C++ でも使えるように、[FastCRC](https://github.com/FrankBoesing/FastCRC) と [CRCpp](https://github.com/d-bahr/CRCpp) を自動で切り替えます。
- [ESP32DMASPI](https://github.com/hideakitai/ESP32DMASPI) : ESP32 で DMA による大容量・高速な SPI 通信を実現します (例では 8192byte)。
- [TsyDMASPI](https://github.com/hideakitai/TsyDMASPI) : ESP32DMASPI の Teensy 版です。送信側しかないので、PRお待ちしております。


### フィルタリング

- [Debouncer](https://github.com/hideakitai/Debouncer) : ボタン押下時のリップルを無視し、安定した入力値を取得します。エッジコールバックつき。
- [Filters](https://github.com/hideakitai/Filters) : シンプルなフィルタ集です。種類が少ないですが、LPF・HPF・疑似微分・疑似積分が使えます。


### 物理演算

- [ArduinoEigen](https://github.com/hideakitai/ArduinoEigen) : C++ の線形代数ライブラリ [Eigen](http://eigen.tuxfamily.org/index.php?title=Main_Page) が使えます (AVR 非対応 : 代替品 => [EigenArduino](https://github.com/vancegroup/EigenArduino))。
- [VectorXf](https://github.com/hideakitai/VectorXf) : [openFrameworks の ofVec2f/3f/4f](https://openframeworks.cc///documentation/math/ofVec3f/) の Arduino 移植版、簡易ベクトル演算に便利です。


## まとめ

だいぶ長くなってしまいましたが、個人的に開発・公開している Arduino 用のライブラリを、ちょっとしたノウハウと一緒に紹介してきました。これらを活用して、機能をたくさん詰め込んでも見通しが良い設計に、多少は貢献できるといいなあと思っております。


## 参考

- [Arduino Library List](https://www.arduinolibraries.info/)
- [Awesome Arduino](https://github.com/Lembed/Awesome-arduino)
- [Awesome ESP](https://github.com/agucova/awesome-esp)
- [The Top 124 Arduino Library](https://awesomeopensource.com/projects/arduino-library)