Arduinoで非同期処理（Linuxからの処理依頼を非同期にさばく）

別記事で [[16x8 LEDマトリクスの表示を矯正する](https://qiita.com/zb_freestyle/items/e366b03b08d66613e87b "16x8 LEDマトリクスの表示を矯正する")] を書きました。
その LED Matrix を使って、Linux からメッセージ表示 request を送れば、メッセージをスクロール表示させて response を返す Arduino、を作っていきます。
この記事のポイントは「Arduino での request 受信とメッセージ表示を非同期(厳密には疑似非同期)で行う」です。

使うもの。

* Arduino NANO (NANO Every)
* Adafruit LED Backpack Library
* ArduinoJson
* Ubuntu
* pySerial3

***
### Linux <-> arduino の USBシリアル接続確認

<details><summary>Arduino NANO を Linux 接続した時の Kernel Message</summary><div>

```
usb 1-12: new full-speed USB device number 3 using xhci_hcd
usb 1-12: New USB device found, idVendor=0403, idProduct=6001
usb 1-12: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-12: Product: FT232R USB UART
usb 1-12: Manufacturer: FTDI
usb 1-12: SerialNumber: ALxxxxxx
usbcore: registered new interface driver usbserial_generic
usbserial: USB Serial support registered for generic
usbcore: registered new interface driver ftdi_sio
usbserial: USB Serial support registered for FTDI USB Serial Device
ftdi_sio 1-12:1.0: FTDI USB Serial Device converter detected
usb 1-12: Detected FT232RL
usb 1-12: FTDI USB Serial Device converter now attached to ttyUSB0
```

</div></details>
<details><summary>Arduino NANO Every を Linux 接続した時の Kernel Message</summary><div>

```
usb 1-6: new full-speed USB device number 4 using xhci_hcd
usb 1-6: New USB device found, idVendor=2341, idProduct=0058
usb 1-6: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-6: Product: Arduino Nano Every
usb 1-6: Manufacturer: Arduino LLC
usb 1-6: SerialNumber: FAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cdc_acm 1-6:1.0: ttyACM0: USB ACM device
usbcore: registered new interface driver cdc_acm
cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
```

</div></details>

```c++:serial_test.ino
#include <Wire.h>
 
void setup()
{
  Wire.begin();
  Serial.begin(115200);
  Serial.println("serial test");
}

void loop()
{
  String str;  
  if(Serial.available()>0) {
    str = Serial.readString();
    Serial.println("[" + str + "]");
  }
}
```

```:screen&nbsp;で動作確認
$ screen /dev/ttyUSB0 115200
serial test
hogehoge
[hogehoge]
```

***
### テキストをスクロール表示してみる
Linux から arduino に request を渡すと、内容の実行とともに response を返してくれるようにします。
とりあえず手始めに以下の機能を実装。データは JSON で受け渡します。

|機能|request|response|
|:----|:----|:----|
|テキストのスクロール表示|{"textscr":"hello world !"}|{"status":200, "msg":"..."}|
|Arduino のヘルスチェック|{"status":{}}|{"status":200, "msg":"..."}|

以下 Arduino 主要コード抜粋

```c++:test.ino
int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int ExecCmd(String cmd, JsonVariant value) {
  if(cmd == "status") {
    sendResponse(100, "alive");
   
  } else if(cmd == "textscr") {
    JsonObject obj2 = value.as<JsonObject>();
    
    String msg = obj2["msg"];
    int size = obj2["size"];

    if(size < 1 || size > 2) {
      size = 1;
    }
    matrix.setTextSize(size);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);

    int l = msg.length();
    for(int16_t x = 7; x >= -6*l; x--) {
      matrix.clear();
      matrix.setCursor(x,0);
      matrix.print(msg);
      matrix.writeDisplay();
      delay(100);
    }

    sendResponse(200, "msg:" + msg + ",size:" + (String)size);

  } else { // "status" : 404
    sendResponse(404, "command not found:" + cmd);
  }
}

StaticJsonDocument<200> request;

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      ExecCmd((String)p.key().c_str(), p.value());
    }
  }
}
```

<details><summary>Arduino コード全体はここ</summary><div>

```c++:test.ino
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>
#include <ArduinoJson.h>

#ifndef _swap_int16_t
#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; }
#endif

class aitendo_KLED1608K33D_8x16matrix : public Adafruit_LEDBackpack, public Adafruit_GFX {
 public:
  aitendo_KLED1608K33D_8x16matrix(void);

  void drawPixel(int16_t x, int16_t y, uint16_t color);

 private:
};

aitendo_KLED1608K33D_8x16matrix::aitendo_KLED1608K33D_8x16matrix(void) : Adafruit_GFX(16, 8) {
}

void aitendo_KLED1608K33D_8x16matrix::drawPixel(int16_t x, int16_t y, uint16_t color) {
  if ((y < 0) || (x < 0)) return;
  if ((getRotation() % 2 == 0) && ((x >= 16) || (y >= 8))) return;
  if ((getRotation() % 2 == 1) && ((y >= 16) || (x >= 8))) return;

 // check rotation, move pixel around if necessary
  switch (getRotation()) {
  case 0:
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 1:
    y = 16 - y - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  case 2:
    x = 16 - x - 1;
    y = 8 - y - 1;
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 3:
    x = 8 - x - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  }

  if (color) {
    displaybuffer[x] |= 1 << y;
  } else {
    displaybuffer[x] &= ~(1 << y);
  }
}

aitendo_KLED1608K33D_8x16matrix matrix = aitendo_KLED1608K33D_8x16matrix();

void setup() {
  Serial.begin(115200);
  Serial.println("16x8 LED Matrix");

  matrix.begin(0x70);
  matrix.setBrightness(5);
  matrix.setRotation(0);
  matrix.clear();
  matrix.writeDisplay();
}

int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int ExecCmd(String cmd, JsonVariant value) {
  if(cmd == "status") {
    sendResponse(100, "alive");
   
  } else if(cmd == "textscr") {
    JsonObject obj2 = value.as<JsonObject>();
    
    String msg = obj2["msg"];
    int size = obj2["size"];

    if(size < 1 || size > 2) {
      size = 1;
    }
    matrix.setTextSize(size);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);

    int l = msg.length();
    for(int16_t x = 7; x >= -6*l; x--) {
      matrix.clear();
      matrix.setCursor(x,0);
      matrix.print(msg);
      matrix.writeDisplay();
      delay(100);
    }

    sendResponse(200, "msg:" + msg + ",size:" + (String)size);

  } else { // "status" : 404
    sendResponse(404, "command not found:" + cmd);
  }
}

StaticJsonDocument<200> request;

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      ExecCmd((String)p.key().c_str(), p.value());
    }
  }
}
```

</div></details>

以下 Linux 側コード
最初に Linux箱と Arduino の USB 接続確認までやっといてナンですが、ここではテスト環境 Windows Linux Subsystem の Ubuntu を使っています。

```python:test.py
import serial
import time
import json

s = serial.Serial()
s.port = "/dev/ttyS4" # COM4 の arduino に接続
s.baudrate = 115200
s.timeout = 1
s.dtr = False   # serial 接続時に arduino にリセットがかかるのを抑止
s.open()
time.sleep(1)   # 気持ち待つ

s.reset_input_buffer()  # シリアルポートの受信バッファ掃除

def request(data):
    print("request:", data)
    s.write(json.dumps(data).encode())  # encode() でバイナリ列にする必要あり
    while True: # response が返ってくるのを待つ
        msg = s.readline().decode()
        if(len(msg) > 0):
            print("response:", msg)
            break

request({"textscr" : {"msg":"Hello World !!!"}})
request({"status" : {}})
```

実行結果
![led.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567640/2d962555-81e2-20e7-a6ba-90e09177c01d.gif)
"textscr" request の response は {"status":200,"msg":Hello World !!!"} と出るのが正しいのですが、この時は先頭の文字をこぼしたようです。エラーハンドリングも考えるか。。

***
### テキストスクロール表示中の処理ブロッキングをなんとかする
実行結果見るとわかるのですが、textscr の表示が完了するまで response が返ってこないし、待ってる間は別のコマンドも受け付けてくれません。ちょっと扱いづらい。
Arduino 側のテキストスクロールは、文字表示しては 100ms 待って1ドット横にずらすというのを for ループで回しているので、テキストを 1ドットずらすごとに loop() に処理を戻してスクロール中でも次のコマンドを受け取れるようにします。加えて、スクロール表示中に新たな textscr が送られてきたら既存の表示実行は破棄して上書きするようにします。

<details><summary>Arduino 修正版コード全体はここ</summary><div>

```
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>
#include <ArduinoJson.h>

#ifndef _swap_int16_t
#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; }
#endif

class aitendo_KLED1608K33D_8x16matrix : public Adafruit_LEDBackpack, public Adafruit_GFX {
 public:
  aitendo_KLED1608K33D_8x16matrix(void);

  void drawPixel(int16_t x, int16_t y, uint16_t color);

 private:
};

aitendo_KLED1608K33D_8x16matrix::aitendo_KLED1608K33D_8x16matrix(void) : Adafruit_GFX(16, 8) {
}

void aitendo_KLED1608K33D_8x16matrix::drawPixel(int16_t x, int16_t y, uint16_t color) {
  if ((y < 0) || (x < 0)) return;
  if ((getRotation() % 2 == 0) && ((x >= 16) || (y >= 8))) return;
  if ((getRotation() % 2 == 1) && ((y >= 16) || (x >= 8))) return;

 // check rotation, move pixel around if necessary
  switch (getRotation()) {
  case 0:
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 1:
    y = 16 - y - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  case 2:
    x = 16 - x - 1;
    y = 8 - y - 1;
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 3:
    x = 8 - x - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  }

  if (color) {
    displaybuffer[x] |= 1 << y;
  } else {
    displaybuffer[x] &= ~(1 << y);
  }
}


aitendo_KLED1608K33D_8x16matrix matrix = aitendo_KLED1608K33D_8x16matrix();

#define DR_UNRELATED 0
#define DR_STOP 1
#define DR_OVERRIDE 2
#define DR_NEW 10
#define DR_CONTINUE 11

typedef struct {
  const char *cmd;
  int (*drawfunc)(int, JsonVariant);
} Cmds;
Cmds *drawing;

void setup() {
  drawing = NULL;

  Serial.begin(115200);
  Serial.println("16x8 LED Matrix");

  matrix.begin(0x70);
  matrix.setBrightness(5);
  matrix.setRotation(0);
  matrix.clear();
  matrix.writeDisplay();
}

int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int cmdStatus(int stat, JsonVariant value) {
  sendResponse((drawing ? 102 : 100), (drawing ? "drawing" : "free time"));
  return(DR_UNRELATED);
}

int cmdTextscr(int stat, JsonVariant value) {
  static String sMsg;
  static int16_t l, x;

  if(stat == DR_NEW) {
    JsonObject obj = value.as<JsonObject>();

    String msg = obj["msg"];

    sMsg = msg;
    l = msg.length();
    x = 7;

    matrix.setTextSize(1);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);
  }

  if(x >= -6*l) {
    matrix.clear();
    matrix.setCursor(x,0);
    matrix.print(sMsg);
    matrix.writeDisplay();
    delay(100);
    x--;
  } else {
    //sendResponse(200, "finish textscr msg:" + msg);
    return(DR_STOP);
  }

  if(stat == DR_NEW) {
    sendResponse(200, "textscr msg:" + sMsg);
    return(DR_OVERRIDE);
  }
  return(DR_UNRELATED);
}

Cmds cmds[] = {
  {"status", cmdStatus},
  {"textscr", cmdTextscr},
  {"", NULL},
};

StaticJsonDocument<200> request;
JsonVariant JVNULL = JsonVariant();

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      String cmd = (String)p.key().c_str();
      int i;
      for(i = 0; cmds[i].cmd != ""; i++) {
        if((String)cmds[i].cmd == cmd) {
          int r = (*cmds[i].drawfunc)(DR_NEW, p.value());
          switch(r) {
            case DR_OVERRIDE:
              drawing = &cmds[i];
              break;
            case DR_STOP:
              drawing = NULL;
              break;
          }
          break;
        }
      }
      if(cmds[i].cmd == "") {
        sendResponse(404, "command not found:" + cmd);
      }
    }

  } else {
    if(drawing) {
      int r = drawing->drawfunc(DR_CONTINUE, JVNULL);
      switch(r) {
        case DR_STOP:
          drawing = NULL;
          break;
      }
    }
  }
}
```

</div></details>

以下抜粋。

```c++:test2.ino
#define DR_UNRELATED 0  // cmdxxxx の処理が、実行中の他の request を上書きしない場合に返す
#define DR_STOP 1       // cmdxxxx の処理が完了したときに返す
#define DR_OVERRIDE 2   // cmdxxxx の処理が、実行中の他の request を上書きする場合に返す
#define DR_NEW 10       // 新たな request が来たときに cmdxxxx に渡す
#define DR_CONTINUE 11  // 引き続きの処理で cmdxxxx を呼ぶ時に渡す

int cmdStatus(int stat, JsonVariant value) {
  ...
}

int cmdTextscr(int stat, JsonVariant value) {
  ...
}

typedef struct {
  const char *cmd;
  int (*drawfunc)(int, JsonVariant);
} Cmds;

Cmds *drawing;

Cmds cmds[] = {
  {"status", cmdStatus},
  {"textscr", cmdTextscr},
  {"", NULL},
};

void loop() {
  if(Serial.available() > 0) {
    ...
    for (JsonPair p : obj) {
      String cmd = (String)p.key().c_str(); // 受け取った request 文字列を cmd に格納
      int i;
      for(i = 0; cmds[i].cmd != ""; i++) { // cmd に一致する cmds[] メンバーを探して呼ぶループ
        if((String)cmds[i].cmd == cmd) {
          int r = (*cmds[i].drawfunc)(DR_NEW, p.value());
          switch(r) {
            case DR_OVERRIDE:
              drawing = &cmds[i]; // 今よびだした関数は以降も処理を継続するので記憶しておく
              break;
            case DR_STOP:
              drawing = NULL; // 今よびだした関数はもう完了したので継続処理する関数はなし
              break;
          }
          break;
        }
      }
      ...
    }

  } else {
    if(drawing) { // 実行中の request 関数があれば、その関数を呼ぶ
      int r = drawing->drawfunc(DR_CONTINUE, JVNULL);
      ...
    }
  }
}
```

1. request コマンド（今は "textscr" "status" の二つだけ）毎にLED表示関数 cmdxxxx を用意して Cmds 構造体のリストである cmds[] にコマンド名と一緒につっこんでおく。
1. 例えば request "textscr" を Linux から送ると loop() 内から cmdTextscr が stat = DR_NEW で呼ばれてスクロール表示を始める。
1. cmdTextscr はテキスト表示して 100ms 待って、まだスクロールの終わりまで行ってなかったら DR_OVERRIDE か DR_UNRELATED を返す。スクロール表示最後まで完了してたら DR_STOP を返す。
1. 返事をもらった loop() 側は、DR_STOP だったら drawing = NULL, DR_OVERRIDE だったら実行中ということで drawing = &cmds[1] を覚えておく。
1. loop() は上記を for ループで繰り返して、drawing != NULL だったら実行中の処理を呼ぶ、また新しい request が来れば実行するを繰り返す。

これで Linux 側では request を送ったあと待たされることもないし、任意のタイミングで request が送れるようになった。

```python:test2.py
import serial
import time
import datetime
import json

s = serial.Serial()
s.port = "/dev/ttyS4"
s.baudrate = 115200
s.timeout = 1
s.dtr = False   # serial 接続時に arduino にリセットがかかるのを抑止
s.open()
time.sleep(1)   # 気持ち待つ

s.reset_input_buffer()  # シリアルポートの受信バッファ掃除

def request(data):
    print(datetime.datetime.now().strftime('%H:%M:%S'), "request:", data)
    s.write(json.dumps(data).encode())  # encode() でバイナリ列にする必要あり
    while True: # response が返ってくるのを待つ
        msg = s.readline().decode()
        if(len(msg) > 0):
            print(datetime.datetime.now().strftime('%H:%M:%S' ), "response:", msg)
            break

request({"textscr" : {"msg":"Hello World !!!"}})
time.sleep(3)
request({"status" : {}})
time.sleep(1)
request({"textscr" : {"msg":"\\(^_^)/"}})
time.sleep(6)
request({"status" : {}})
```

```shell:test2.py実行結果
ubuntu:~$ python3 test.py
00:25:39 request: {'textscr': {'msg': 'Hello World !!!'}}
00:25:39 response: {"status":200,"msg":"textscr msg:Hello World !!!"}

00:25:42 request: {'status': {}}
00:25:42 response: {"status":102,"msg":"drawing"}

00:25:43 request: {'textscr': {'msg': '\\(^_^)/'}}
00:25:43 response: {"status":200,"msg":"textscr msg:\\(^_^)/"}

00:25:49 request: {'status': {}}
00:25:49 response: {"status":100,"msg":"free time"}
```

わかりやすいように request, response の前に時刻を付加してます。
テキストスクロールを始めたらすぐに response 返ってくるし、まだスクロール中に別の textscr request を送れば新しい request を実行します。ばっちり。
おわり。