フローで扱うセンサーをマイコンでちょっとインテリジェントにしてみる


enebular中の人アドベントカレンダーも9日目となりました!

9日目のネタは、enebular-agentで扱うセンサー値についてです。

例えば、加速度センサーの値をミリ秒オーダーで解析して動きを知ったり、ノイズ混じりのセンサー値をフィルタをかけて正常な値を取り出したりする場合があると思います。これには、高速な周期かつ正確なタイミングでセンサー値を処理する必要があります。しかし、Node-REDのフローでは、リアルタイムに処理することがなかなか難しいところがあります。そんな場合にマイコンを使って、事前にセンサーの値を必要な形に処理する方法を試してみたいと思います。
簡単に図にすると以下のような感じです。

構成

とりあえず、今回は光センサーを使いました。光の強弱がアナログ電圧で出力されますので、この値にフィルタをかけ、上限のしきい値を超えたら1(ON)、下限のしきい値を下回ったら0(OFF)というよう処理します。上限と下限のしきい値を分けているのは、不感帯を作りセンサーの値がしきい値付近で揺らついてもON/OFFを繰り返さないようにするためです。

マイコンはRaspberry Pi Picoを使いました。Raspberry PiとRaspberry Pi PicoはUSBで接続し、USB-Serialで通信を行います。Node-RED側ですぐに値が扱えるようにJSONで入出力するようにしました。

構成としては、以下のとおりです。(Cloud上のアプリでは、単純に部屋の照明が点いているか消えているかを可視化してます)

Raspberry Pi Picoのソフトウェア

Raspberry Pi Picoのソフトウェアはライブラリが豊富なArduino IDEで開発します。初期セットアップ方法等は著書で解説しています。
フィルタはmovingAvgという移動平均を取るライブラリを使いました。つまりローパスフィルタです。ArduinoでJSON型を扱うライブラリArduinoJsonも使用しています。
上限のしきい値と下限のしきい値はNode-REDから設定できるようにしました。

#include <ArduinoJson.h>
#include <movingAvg.h> 
#include "Scheduler.h"

const int LIGHT_SENSOR = A0;      // センサーを接続しているポート
const int ADV_MAX = 10;           // 移動平均のポイント数
const unsigned long INTERVAL = 10;// センサーの周期(ms)

int on_threshold = 500;           // 上限しきい値のデフォルト値
int off_threshold = 499;          // 下限しきい値のデフォルト値

int light_enable = false;

movingAvg avgLight(ADV_MAX);
StaticJsonDocument<256> send_buff;
StaticJsonDocument<256> recv_buff;

// シリアル入力を監視する処理
void serial_in(){
  if (Serial.available()){
    int count = 0;
    char c[256];
    while(Serial.available() && count < 256){
      c[count++] = Serial.read();
    }
    DeserializationError error = deserializeJson(recv_buff, c);
    on_threshold = recv_buff["onThreshold"];
    off_threshold = recv_buff["offThreshold"];
  }
  yield();
}

// センサー値を扱うループ処理
void sensor_loop(){
  static int prev_light_enable = false;
  static unsigned long prev_time = 0;

  // センサからアナログ値を読む
  int light = analogRead(LIGHT_SENSOR);
  // 移動平均を求める
  int avg = avgLight.reading(light);

  // 上限、下限判定
  if(avg >= on_threshold){
    light_enable = true;
  } else if(avg <= off_threshold){
    light_enable = false;
  } else {

  }

  // 前回値と異なればSerialにJSON出力
  if(prev_light_enable != light_enable){
    prev_light_enable = light_enable;

    send_buff["light"] = light_enable;

    String output;
    serializeJson(send_buff, output);
    Serial.println(output);
  }

  yield();

  // 周期を保つ
  unsigned long diff_time = millis() - prev_time;
  prev_time = millis();
  if(diff_time < INTERVAL){
    delay(INTERVAL - diff_time);
  }
}

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

  Scheduler.startLoop(serial_in);
  Scheduler.startLoop(sensor_loop);
}

void loop() {

}

Arduino IDEのシリアルモニタで確認すると部屋の照明を消すと0、点けると1がJSON型で出力されています。

Raspberry Piのフロー

フローは単純です。serial通信を扱うために、node-red-node-serialportをパレットに追加して、serialポート経由でRaspberry Pi Picoから受け取ったデータをJSON型に変換し、http requestノードで、Heroku上のフローにHTTP POSTしているだけです。

※Raspberry PiはUSB接続されたRaspberry Pi Picoを/dev/ttyACM0という仮想シリアルポートとして認識するはずですが、接続に失敗する場合は、権限を以下のコマンドで変更して下さい。

$ sudo chmod 666 /dev/ttyACM0

デバッグノードで、HTTP POSTする値を確認してみると部屋の照明を消すと0、点けると1が送られていることがわかります。

Herokuのフロー

とりあえず、確認するだけなので単純なフローです。

以下のような形で表示されます。

最後に

Raspberry Pi側でやっているような処理をフローでやろうとすると複雑なフローになってしまうので、そういった意味でも今回実験したような形でセンサー値を処理するのは良いと思いました。また、以前の記事「Edge Impulseを試してみる」で書いた様にマイコン上でも機械学習が簡単にできるサービスもあるので、こちらも同じ様に試してみたいと思います。