換気のすゝめ ~M5StackでCO2濃度モニタを作る~
概要
集中力を保ったり、生産性を上げたりするのに換気は本当に大事だよ、意識的に換気するためにCO2濃度を監視しよう、という話。
先日、多くの方から反響をいただいた、CO2濃度モニターのツイート。
このツイートにあるCO2濃度モニターの作り方の紹介。
先日作ったCO2濃度モニタ見ながらテレワークしてるんだけど、1500ppmに近づくと一気に集中力が無くなる。換気すると5分程度で600ppm近くまで下がって、するとほんとにすぐ集中力が戻る。
— 水田かなめ (@kmizta) March 30, 2020
テレワークしてるみんな、換気しろ、換気。マジで大事。 pic.twitter.com/zlQYh821Qn
暇な休日の午後に戯れで作ったものがこんなに反響を呼ぶとは思っていなかった・・・。
(2021年2月13日追記)
スイッチサイエンスさんでM5StackのCO2モニター化キットを販売することになりました!
近日中に発売予定!
(2021年2月26日追記)
発売しました!
背景
会議中、運転中、仕事中、そして流行りのテレワーク中。寝不足でもないのに眠くなったり、集中力が全く出ないことがないだろうか。 私は特に家にいるとそれが起きる事が多い。
昔から「これ、CO2のせいじゃね?」とは思っていたが、そのためにわざわざ環境計測機を買うわけにも行かず、そうなると定量的にわからないので行動を起こせない。
1,2年ほど前に一度本気でCO2モニタを作ろうと思ったこともあったが、適当なセンサが売っておらず諦めていた。
ところが、先日千石電商で買い物をしていると、こんなセンサを見つけた。
CO2+温湿度センサ、SCD30を使ったセンサモジュールである。(2020年4月1日現在欠品中)
手元にM5Stack Basicが転がっていたので、この2つを合わせてCO2濃度モニタを作成した。この記事ではその作り方を示す。
CO2濃度基準
屋内のCO2濃度については、望ましいとされている数値を省庁が基準を定めている。
基準 | 濃度 |
---|---|
建築物環境衛生管理基準(厚生労働省) | 1000ppm以下 |
学校環境衛生基準(文部科学省) | 1500ppm以下 |
およそ2000ppmを超えると、頭痛や眠気、倦怠感、注意力散漫などの症状が出るようである。
なお、大気中の二酸化炭素濃度は2018年時点で407.8ppmということである。
CO2+温湿度センサSCD30
このセンサは「SensirionのSCD30は高精度非分散型赤外線(NDIR)ベースのCO2センサーで400~10000ppmを±(30ppm + 3%)の精度で検出することができます。」ということである。
このセンサはI2Cで通信ができ、電源電圧も3.3V,5VいずれもOKなのでM5StackのGroveコネクタから接続するだけでも良い。ただし、ピンの並び順がM5Stackとセンサで異なるので注意。
Groveケーブルは別途用意し、このように接続すればOK。
Groveでなくても、M5Stackの下面・上面いずれにもI2Cのピン(21:SDA,22:SCLピン)がでているので、そこからつないでも良い。ただし、こちらもピンの並び順が異なるので注意。
変換基板を作って、ピンから直接つなぐとこんな感じになる。
プログラム
SparkFunのライブラリ
ありがたいことに、このセンサについてはSparkFunがArduino用ライブラリを公開してくれている。 M5StackはArduino環境を利用できるので、このライブラリを使えば、あっという間にできてしまう。
Arduinoに上のライブラリをインストールしたら、Exampleに入ったBasicReadingsを試してみよう。 ピンの接続が問題なければUARTでとりあえず値が読めるはずである。
なお、このセンサは最初の数分は値が不安定なので、しばらく試運転するのが良い。(どの二酸化炭素センサもそうだと思うけど)
センサの最小読み取り間隔は2秒である。デフォルトは2秒となっているので、まずは2秒毎に読み出すこととした。
画面設計
値が読めたらあとは画面設計だ。
画面については結果はトップのツイートの通りだが、今回は以下のようなレイアウトとした。
CO2濃度+温湿度を表示
CO2濃度の履歴を表示。グラフはスクロールする。
1000ppm以上を注意、2000ppm以上を危険レベルとし、それぞれ黃、赤で表示する(グラフ、文字ともに)
文字表示部分、グラフ部分それぞれSpriteで表示しており、グラフはセンサを取得するごとに1pxずつプロットしながらスクロールしていく。
ソースコードはGithubで公開しているので、もし作ってみたいという人がいたら自由に使ってもらって構わない。(初めて公開したのでうまくできているかどうか・・・)
(2020年4月18日追記)
この記事に対応したソースのバージョンは1.0である。
使った実感
コロナウイルスの影響でテレワークが始まったので、テレワーク中に上のモニタでCO2濃度を監視してみた。
6畳の自室を閉め切って仕事を続けると、1時間かそこらで1000ppmを超えてしまう。
さらに作業を続けると1500ppm近くに達し、そうなると一気に集中力がなくなる。
ここで、換気をすると5分ほどで600ppm程度にCO2濃度は下がる。 すると不思議と集中力が回復するのである。
そう、集中を切らす最大の原因は二酸化炭素なのかもしれない。
また、CO2濃度を監視しながら仕事すると、「1000ppmになるまでに資料仕上げなきゃ!」という謎の締め切り効果が生まれてそれはそれで仕事が捗るのでオススメである。
まとめ
M5StackにCO2濃度センサを接続し、CO2濃度監視モニタを作った。
ツイートに書いた通りだが、換気は本当に大事である。
あなたが会社・家・車など、生活している中で不意に集中力がなくなるときがあるとしたら、それはCO2濃度が高いせいかもしれない。
意識的に換気しよう。
(2020年4月5日更新)
後日、機能を更新してスマホ・PCへのプッシュ通知とAmbientを使ったデータ保存・閲覧を実装した。(ver2.0)
詳細はこちらの記事を参照のこと。
(2020年5月6日追記)
CO2濃度モニタを定常運用のためリビングへ置くことにした。
幸い、M5Stackにはネオジム磁石が入っているため、冷蔵庫に貼ることが可能である。
定常運用のためにはUSB-Cによる電源が必要なので、電源として磁石で冷蔵庫に貼り付けることができるUSB充電器を使用した。
磁石で冷蔵庫に貼り付けられるUSB充電器はあまり種類がなく、これがちょうど良かった。
また、このUSB充電器からCO2モニタにUSB-Cで電源を供給するが、USB-Cは一般的に太く、屈曲がしにくいケーブルが多い。
ただ、今回に関してはM5Stackの分しか消費しないため細くて取り回しの良いこちらのケーブルを使用した。
エレコム USBケーブル Type C (USB A to USB C) 0.5m USB2.0認証品 3A出力 最大480Mbps ブラック U2C-AC05NBK
- 発売日: 2015/11/09
- メディア: Personal Computers
このケーブルは細く取り回しがよいわりに3A流せる優れモノである。
USB充電器とCO2モニタを貼り付け、今ではこのように常時運用中である。
M5Stackでマルチタスクを使ってセンサ取得・ログ記録を行う
概要
センサ値の取得とログの記録は切っても切れない関係である。
M5Stack Grayに内蔵された加速度センサやジャイロセンサや磁気センサ、外付けの気圧センサの値を高頻度に取得、SDへ記録をしたいが、 何も工夫をせずに取得→記録のプログラムを実行すると、SDへの書き込み時間の変動のため、周期が変動してしまい、不都合である。
そこで、FreeRTOSの機能を使いセンサ値の取得とログの書き込みをそれぞれマルチタスクで処理させることで、 高頻度かつ安定した時間でセンサ値取得・ログ記録を行う方法を述べる。
背景
M5Stack Grayには加速度センサ・ジャイロセンサ・磁気センサが内蔵されている。
これらを使ってなにかやろうと思うと、まずは値をログしたくなる。
今回はそれらに加え、Groveで外付けの気圧センサをつなぎ、加速度・ジャイロ・磁気・気圧の4つのセンサ値をログすることにした。
幸いM5StackにはSDカードスロットがついているのでSDカードにログを記録できる。
だが、単純にセンサ取得、記録をしていくと問題が起きる。
例えば、下のグラフは単にセンサ値取得→SDへログ記録を25Hz(40ms)で繰り返したときの一周期あたりの処理時間である。
およそ4.5秒に1回、処理時間が伸びていることがわかる。
原因を見るために、各処理ごとに処理時間を計測した結果がこちら。
凡例は上から「加速度ジャイロ取得」「磁気取得」「姿勢計算」「気圧取得」「ログ書き込み」だが、
処理時間が伸びる原因は一目瞭然で、ログ書き込みである。
SDは一般的にある書き込み量の単位を超えると書き込みに時間がかかってしまう。
これでは一定周期を前提とする制御などには不都合である。
1秒ごとにまとめて書き込む(失敗)
最初に思いつくのは、「毎周期SDに書き込むのは無駄だから、ログを一定周期分まとめて書き込めばいいんじゃね?」だが、 結局まとめて書き込む際にSDの一定の書き込み量を超えると処理時間遅延が生じる。
以下はこの処理を実装したときの周期あたりの処理時間。結局書き込みに時間がかかっている。
割り込みを使ってセンサ取得する(失敗)
SDに書き込んでいる間センサ取得が止まってしまうのが問題なので、
割り込みを使ってセンサ値取得を優先的に行ってみる。
こうすると、SDへのログ書き込み中でも優先的にセンサ値が取得される。
幸い、SDは書き込みを中断しても処理的に問題ないため、割り込みセンサ取得も可能である。
だが、M5Stackでこれを行うと、以下のエラーが生じて再起動してしまう。
[IGuru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
どうやら割り込み中にセンサ値取得などの長ったらしい処理をしているとWDTが発動して再起動してしまうようだ。
WDTを停止するという方法もあるかもしれないが、他の処理に悪影響を及ぼす可能性もあるので、やめておく。
マルチタスクを使う(成功)
さて、ここで活躍するのがタスク機能である。
ESP32-Arduino-coreではオープンソースのリアルタイムOSである、freeRTOSが使われている。
リアルタイムOSの詳細やその使い方については以下のウェブサイトが非常に詳しい。
リアルタイムOSが何たるかは上のページを参照していただくとして、 ここではごくごく簡単に使い方を書くと、
タスク用関数を定義する。
xTaskCreatePinnedToCore()でタスクを生成する。その際、タスク用関数とその優先度などを指定する。
Arduino環境でESP32の開発をすると、setup()やloop()の関数を使ってプログラムを書くことになるが、 これらの関数もfreeRTOSのタスクを使って実装されている。
以下はesp32-1.0.4\cores\esp32\main.cpp
のコードの一部である。
メイン関数でloopTaskがタスクとして生成され、loopTask関数内でsetup()とloop()のループが呼ばれていることがわかる。
setup()が一度だけ呼ばれ、その後loop()が無限ループするのはこのためである。
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_task_wdt.h" #include "Arduino.h" TaskHandle_t loopTaskHandle = NULL; #if CONFIG_AUTOSTART_ARDUINO bool loopTaskWDTEnabled; void loopTask(void *pvParameters) { setup(); for(;;) { if(loopTaskWDTEnabled){ esp_task_wdt_reset(); } loop(); } } extern "C" void app_main() { loopTaskWDTEnabled = false; initArduino(); xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE); } #endif
また、M5Stackに内蔵されているESP32はデュアルコアのため、コア0、コア1の2つを使うことができる。
これらのコアを有効利用するためにも、freeRTOSを有効に使う必要がある。
xTaskCreateUniversal
がタスク生成の関数だが、最後の引数がそのタスクを実行するコアを指定している。
CONFIG_ARDUINO_RUNNING_COREは1と定義されているため、loopTaskはコア1で実行されることになる。
これと同様に、センサ取得タスクとログ記録タスクを作成し、各コアに割り付けてやる。
タスク関数の作成
センサ取得タスクとログ記録タスク関数を記述する。
タスク関数は、今回のように周期的に行うタスクの場合内部にwhileループを持つ。
返り値はvoid, 引数はvoid*だ。引数の型からわかるように、何でも受け取れる。
以下はセンサ取得タスク関数。
#define AHRS_SAMPLE_RATE 50 #define AHRS_SAMPLE_MS (1000.0 / AHRS_SAMPLE_RATE) typedef struct _log_data_ { unsigned long time; float accX, accY, accZ; float gyroX, gyroY, gyroZ; float magX, magY, magZ; float roll, pitch, yaw; float temperature; float pressure; } LOG_DATA; LOG_DATA log_data[2][AHRS_SAMPLE_RATE]; // ダブルバッファ unsigned long ms_begin; void prvGetSensorTask(void *pvParameters) { LOG_DATA *buf; lid = 0; double_buffer = 0; buf = log_data[double_buffer]; portTickType xLastWakeTime; xLastWakeTime = xTaskGetTickCount(); ms_begin = millis(); while (1) { // ここで色々センサからデータを取得し、バッファに格納する・・・ // (中略) buf[lid].time = millis() - ms_begin; // 経過時間 lid++; if (lid == AHRS_SAMPLE_RATE) // ある程度バッファに溜めてからSDに書き込む { double_buffer = !double_buffer; // ダブルバッファを使う buf = log_data[double_buffer]; lid = 0; logged = true; // ログ記録タスクで参照するフラグ } // このタスクは周期的に実行される vTaskDelayUntil(&xLastWakeTime, AHRS_SAMPLE_MS / portTICK_PERIOD_MS); } }
上の関数では、センサの値を周期的に読み取るため、vTaskDelayUntil()
を使っている。
今回の場合、AHRS_SAMPLE_MS[ms]ごとにこのwhileループが回ることになる。
原理的には、周期タスクとせずにメイン関数からタイマで毎回タスクを生成してもいいのだが、タスク生成のオーバーヘッドが大きいのでおすすめしない。というか多分正しい使い方ではない。
また、センサ取得とログ記録をマルチタスクで行うため、ダブルバッファを使っている。
ダブルバッファ自体は簡易的に実装していて、SDに書き込むデータ量の2回分のバッファを用意し、1回分が溜まったら裏側に切り替え、それが埋まったら表側に切り替え、ということをしている。
次に、ログ記録タスク関数はこちら。
void prvWriteLogTask(void *pvParameters) { LOG_DATA *ld; while (1) { if (logged) { logged = false; ld = (LOG_DATA *)pvParameters + AHRS_SAMPLE_RATE * (!double_buffer); // 書き込むバッファの表or裏 File f = SD.open(log_filepath, FILE_APPEND); if (f) { for (int i = 0; i < AHRS_SAMPLE_RATE; i++) { sprintf(logtext, "%lu,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f", ld->time, ld->accX, ld->accY, ld->accZ, ld->gyroX, ld->gyroY, ld->gyroZ, ld->pitch, ld->roll, ld->yaw, ld->magX, ld->magY, ld->magZ, ld->pressure); f.println(logtext); ld++; } f.close(); } else { M5.Lcd.println("Failed!"); } } delay(1); // これ大事 } }
センサ取得タスクで、バッファが溜まったらloggedフラグを立て、それを記録タスクで参照する。
バッファにデータが溜まったことをフラグで確認したら、バッファの表裏を間違えないようにして、SDのに書き込む。
この中で特に大事なのは最後のdelay(1)
である。
マルチタスクをする場合、タスクにdelay()を入れないと優先度によってはRTOSの他の処理を行うことができず、WDTが作動してしまったりする。
なので、タスクの最後にdelay()を入れて、他の処理ができるようにする。
ちなみに、ESP32のArduinoライブラリでは、delay()が内部ではタスク用のvTaskDelay()で実装されている。
// esp32-hal-misc.c void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
タスクを生成する
タスク用関数ができたら、メインの関数からそれらを生成してやる。
// MultiTask setting #define SENSORTASK_CORE 0 #define SENSORTASK_PRI 1 #define LOGTASK_CORE 1 #define LOGTASK_PRI 0 void logging() { TaskHandle_t h_WriteLogTask, h_GetSensorTask; M5.Lcd.fillScreen(BLACK); M5.Lcd.setCursor(0, 0); M5.Lcd.println("Press B to stop logging!"); setLogFileName(); logWriteHeader(); // Multi task start xTaskCreatePinnedToCore(prvGetSensorTask, "GetSensorTask", 4096, NULL, SENSORTASK_PRI, &h_GetSensorTask, SENSORTASK_CORE); xTaskCreatePinnedToCore(prvWriteLogTask, "WriteLogTask", 4096, (void *)log_data, LOGTASK_PRI, &h_WriteLogTask, LOGTASK_CORE); while (1) // ボタンが押されたら終了 { M5.update(); if (M5.BtnB.wasReleased()) break; delay(10); } M5.Lcd.printf("\nSaved as %s", log_filepath); vTaskDelete(h_GetSensorTask); vTaskDelete(h_WriteLogTask); delay(3000); }
ESP32の場合は、デュアルコアのため明示的にコアを指定してタスクを生成するxTaskCreatePinnedToCore()
を使う。
引数は左からタスク関数ポインタ、タスクの名前、スタックメモリ量、タスクへ渡すパラメータ、タスク優先度、タスクハンドラ、割り当てるコア となっている。詳細は公式ドキュメント(以下)を参照。
ESP32の場合コア0とコア1が使えるので、好きな方にタスクを割り当てる。
優先度は、0が最も低く、数字が増えるほど高くなる。あまり高くしすぎると内部の他の処理を妨げるので、必要最低限の優先度とするのが良さそう。
今回重要なのは、ログ記録タスクの優先度(LOGTASK_PRI)よりセンサ取得タスクの優先度(SENSORTASK_PRI)を高くすること。
こうすることで、ログ記録中でも優先度の高いセンサ取得をしてくれる。
以下が、マルチタスクでセンサ値取得タスクの優先度を上げて25Hz(40ms)でセンサ取得し、1秒毎(25回分ごと)にSDへ記録したときの、1周期あたりの処理時間である。
図からわかるように、きれいに25Hzでセンサが取得できていることがわかる。
ちなみに、センサ取得とログ記録の優先度を逆転してみると、処理時間は以下の通り。
センサ取得タスクが優先度の高いログ記録タスクに邪魔され、処理時間がめちゃめちゃになっていることがわかる。
優先度を一緒にすると、今度は全体的に周期が遅くなる。これもダメ。
やはり、センサ取得タスクの優先度をログ記録タスクのそれより高くする必要がある。
「センサ取得タスクの周期をvTaskDelayUntil()で限界以上に早く設定しちゃうとどうなるの?」という疑問もあろうと思うので、それをやってみると以下の通り。
100Hzでは処理しきれていないが、できるだけ早く処理するよう健闘していることがわかる。なお、これでもSDへのログ記録自体は問題なかった。
なお、今回100Hzではセンサが処理しきれていないが、処理時間の大半はGroveで外付けしている気圧センサである。
もしM5Stack内部の加速度・ジャイロ・磁気のみの取得であればもっと高い周期でも処理できるはずである。
気圧センサの使い方については以下の過去記事参照。
まとめ
高頻度のセンサ取得とSDへのログ記録を実現させるため、M5Stack内蔵のデュアルコアを使ってマルチタスク化した。
センサ取得タスクとログ記録タスクを生成し、それぞれのコアに割付けセンサ取得タスクの優先度を上げることで乱れなく周期的なセンサ取得・記録ができるようになった。
おまけ
高頻度にセンサを処理することは、例えば姿勢のフィルタ・制御では必須である。
姿勢のフィルタでよく使われるMadgwickフィルタやMahonyフィルタなどでは、最低でも30Hz以上あったほうが良さそうである。
気圧センサは関係ないが、内部のセンサの処理により、今はようやくここまでできている。
M5Stackの姿勢フィルタ、だいぶいい感じになってきた。よく聞くMadgwickフィルタよりMahonyフィルタの方が何か安定してる pic.twitter.com/N6Qe876wOU
— 水田かなめ (@kmizta) March 1, 2020
姿勢の可視化については、こちらのページを参照した。
M5StackにGroveでセンサつないで気圧測定する
概要
M5Stackにオムロンの気圧センサ(2SMPB-02E)を使ったモジュールをつないで、気圧を計測した。
ついでに、センサに平均機能とIIRフィルタ機能がついていたので、それを使ってセンサの値にフィルタをかけてみた。
背景
先日、M5Stackを購入した。
PICよりも周波数が高く、ライブラリが多くて扱いやすそうというイメージで購入してみたが、初期化にしろ何にしろとにかくコーディングが楽。(一方で内部で何してるのかわからなくても動いてしまうということでもあるが・・・)
さて、M5Stackの派生でM5Stack Grayがある。
これにはBasicのM5Stackに加え、9軸IMU(加速度、ジャイロ、磁気)が搭載されており、動きモノの制御に使ったりするのに最適である。
また、Basicと同じくLCD・3つのボタン・SDカードスロットがあるのでちょっとしたUIやログの記録もできる。プロトタイプ開発にはもってこいである。
今回、M5Stack Grayに内蔵されているセンサで取得できる値(加速度・角速度・磁場)以外に、高度の取得が必要になったので、M5Stack GrayにGroveで気圧センサを接続し、値を取得した。
オムロン絶対圧センサ2SMPB-02E
今回、使用した気圧センサは、以下のページで紹介されていたオムロンの気圧センサ2SMPB-02Eである。
基本的なソースコードは上記ページを参照のこと。
スイッチサイエンスからモジュールが出ているので、それを使った。
M5Stack Grayに接続するとこんな感じ。
センサのデータシートはこちら。
また、ありがたいことにArduino環境でこのセンサを使うためのライブラリがGithubで公開されている。
M5Stackでも、このライブラリを使用した開発ができる。
ライブラリインストール
Arduino環境にこの気圧センサのライブラリをインストールする。
上記Githubからライブラリをzip形式でダウンロード、Arduinoから スケッチ→ライブラリをインクルード→.zip形式のライブラリをインストール でOK。
M5Stackで読み込む
ライブラリが読み込めたら、最低限は以下の関数を使って値を読み込める。
// ライブラリインクルード #include "Omron2SMPB02E.h" #include <M5Stack.h> // センサのインスタンス Omron2SMPB02E prs; // 初期化・リード prs.begin(); M5.begin(); prs.set_mode(MODE_NORMAL); float tmp = prs.read_temp(); // 温度[deg]も読める float pressure = prs.read_pressure(); // 気圧[Pa]も読める
なお、MODE_NORMALとは測定→スタンバイ→測定を繰り返すモード。通常の使用ではこれを使えば良い。
その他に、SleepモードやForcedモードといった省電力のためのモードが存在する。(データシート参照)
センサ値にフィルタをかける
このセンサ、デフォルトではセンサ出力値になんのフィルタもかかっておらず、結構ノイズが乗る。
例えば以下のグラフは部屋の地面と天井の間をセンサを動かしたときの気圧の生データである。
まぁ動いていることはわかるが、ノイズ多いよね、という感じ。
そこで、センサ値にフィルタをかけるわけだが、このセンサには機能として「複数回の測定平均を出力」「IIRフィルタ」が内蔵されている。今回は内蔵されたこれらの機能を使うこととする。
こうした機能がない場合はCPU側でIIRフィルタなどのプログラムを組んでやる必要があるが、このセンサはその機能自体が内蔵されているのでCPU側としては楽である。
複数回のセンサ値の測定平均を出力
データシートに測定モード別の測定回数の記述がある。測定回数が増えるほど精度は上がるが、変換時間が伸びたり平均電流が大きくなる。
例えば、Standard Modeでの測定をする場合は、温度/ 圧力(気圧)それぞれの平均回数を1,8に設定する。
prs.set_average(AVG_1, AVG_8); // 第1引数が温度の平均回数、第2引数が気圧の平均回数 // 引数は以下の定義 // arg of set_average() #define AVG_SKIP 0x0 #define AVG_1 0x1 #define AVG_2 0x2 #define AVG_4 0x3 #define AVG_8 0x4 #define AVG_16 0x5 #define AVG_32 0x6 #define AVG_64 0x7
IIRフィルタ
さらに、IIRフィルタをかけてノイズを低減する。
IIRフィルタとは、無限インパルス応答を利用した信号処理によるフィルタである。詳細はWikipedia等参照。
このセンサはIIRフィルタによるローパスフィルタが内蔵されているので、それを設定する。
IIRフィルタを設定することで、さらにノイズを低減できる。データシートには以下のような表が記載されており、例えばStandard Modeではフィルタ係数を4に設定することでRMSノイズ[Pa]が2.6→0.8に低減できる。
なお、フィルタ係数を大きくするとノイズ自体は小さくなるが、原理上出力の遅れが大きくなるので、使うシステムに応じて適切な係数を設定することが必要である。
IIRフィルタ係数を設定する関数は以下の通り。
prs.set_filter(FILTER_4); // IIRフィルタ係数を設定(4) // 引数は以下の定義 // arg of set_filter() #define FILTER_OFF 0x0 #define FILTER_2 0x1 #define FILTER_4 0x2 #define FILTER_8 0x3 #define FILTER_16 0x4 #define FILTER_32 0x5
これらのset_average()やset_filter()はset_mode()の前に呼ぶと良い。
測定結果
上記の手順で温度の平均回数8(Standard mode)、IIRフィルタ係数を4と設定し、センサを部屋の地面から天井まで動かしたときの出力値は以下の通り。ノイズが低減されていることがわかる。
部屋の天井までは2.5m程度なので、見た感じ±10cmくらいの精度で相対高度が測定できそうである。
平均回数を上げたり、フィルタ係数を大きくするとさらに精度は上がるだろう。
まとめ
オムロンの気圧センサ2SMPB-02EのモジュールをGroveでM5Stackに接続して値を取得した。
さらに、センサ内蔵の平均値出力・IIRフィルタ機能を利用してノイズを低減し、±10cm程度の精度で相対高度が取れることを確認した。
ラズベリーパイを使ったIoTシステムに脆弱性があると指摘があったためセキュリティ対策を施した話
概要
先日のブログで記事を書いた、ラズベリーパイを使って作ったIoTシステムに「脆弱性がある」と指摘を受けたので、素人ながら調べつつ最低限のセキュリティ対策を施した話。
(ちなみに、上の記事ははてなブログの週間ランキング2位になってしまった)
今週のはてなブログランキング〔2020年2月第1週〕 - 週刊はてなブログ
背景
先日上記のブログ記事を公開したところ、「システムに脆弱性がある」という指摘を多々頂いた。システムというのは、「ラズベリーパイでインターホンを監視して、呼出音を検知したら条件に応じて解錠ボタンを押す」というものである。
もともとは、予定された配達か否かで2通りの解錠方法を考えていた。
受けた指摘は主に2つ。
もともと予定された配達時間帯に万が一悪意を持った人物が我が家のインターホンを鳴らすと中央玄関が開いて、中へ入れてしまう
解錠コマンド用URLにアクセスした相手を識別しておらず、悪意を持った人物がコマンドを送ることが可能
というものである。
1つ目に関しては、確かにそうなので、「予定された配達の場合」のモードは廃止した。一方で、2つ目に関しては何らか認証が必要そうだ。しかしやり方がわからない。
そんなときに、IPA(情報処理推進機構)から連絡があった。どうやら脆弱性関連情報の届出受付システムを通じてこのシステムの脆弱性を届出てくれた人がいたようだ。
脆弱性関連情報の届出受付:IPA 独立行政法人 情報処理推進機構
届出られた内容を読むと脆弱性の詳細が事細かに書いてあり、素人の自分にとっては非常にありがたい内容だった。届出内容には対処方法も書いてあり、どうやらアクセス制御機能を実装する必要がありそうだ。
「アクセス制御・・・?」状態だった私はまずそれを調べるところから始め、ようやくID/パスワードによる認証ができるようになったため、その過程を述べる。今回はDigest認証を使用した。
Basic認証、Digest認証
アクセス制御とは、要はあるシステムに対し誰か何をしていいのかを設定したり識別したりして、それに基づいて操作を拒否したり許可したりすることである。
思いつくのはID/パスワードによる認証だが、ID/パスワードによる最も基本的な認証方法にBasic認証とDigest認証がある。
私はアクセス制御については素人なので、詳細な説明は他の記事を読んでもらうとして、ここではごくごく簡単に説明する。
Basic認証:実装が容易で、簡易的な認証方法。ログインIDとパスワードを入力すると、それらがBase64でエンコードされてサーバへ送信される。サーバはその情報をデコードし、ID/パスワードが一致していたらOKとする。
→Basic認証はとても実装が楽だが、認証情報がただBase64でエンコードされているだけで、誰でもデコードできるので、実質平文で送っていることになる。公衆Wifiで認証情報を入力するな、というのはこういうことのようだ。なので、一般的に通信を暗号化するSSL(HTTPS)と一緒に使うようである。
Digest認証:Basic認証が実質平文でID/パスワードを送っているのに対し、Digest認証ではパスワードをハッシュ化して送信する。ハッシュ化、というのは一方向関数を使ってある固定長のメッセージに置き換えることで、ハッシュ値からもとの値(ID/パスワード)を解析することを困難にするもの。サーバ側ではID/パスワード情報があるので、同様にハッシュ値を計算して、それらが一致すれば許可する、といった具体のようである。
→今回はDigest認証を使ってアクセス制御を実装することとした。
なお、Digest認証ではアクセス先のURLやユーザ名はハッシュ化されず平文で送られるため、それらも隠したい場合はHTTPSによる通信暗号化をする必要がある。
最低限の対策
ポート変更
このシステムではラズベリーパイ側のサーバはFlaskで実装している。Flaskはデフォルトで5000番ポートを使用するが、まずはこれを変更する。49513~65535が自由に使えるポート番号のようだ。
もちろん、これだけでは対策にはなり得ないわけだが、デフォルトポートを狙ってくるようなアクセスには効果があるだろう。
Flaskでポート番号を変更する場合は、起動時の引数にport=(好きなポート番号)
を加えてやれば良い。
(以下の例ではFlaskの処理をスレッド化して起動している、これについては過去記事参照)
rest_service_thread = threading.Thread(name='rest_service', target=app.run, args=('0.0.0.0',), kwargs=dict(debug=False,port=PORT_NUM)) rest_service_thread.start()
URL変更
以前の記事ではモザイクを掛けていたとはいえほとんどURL丸見えで公開してしまったため、そりゃ見る人が見ればアクセスしてくるよね、という状態だった。(実際にアクセスログを見るといくつか外部からのアクセスが見られた。もちろんインターホンが鳴った時以外は何も反応しないわけだが)
なので、URLはより複雑な名前に変更した。(個人使用のシステムなのでURLは非公開)
(ただ、先程述べたように今回実装したDigest認証はURLは平文で送られるため、もしパケットキャプチャなどされた場合にはURLはわかってしまう)
Digest認証の実装
FlaskにDigest認証を実装する。今回は以下のページを参照した。
Digest認証はFlask拡張を入れれば簡単に実装できる。
pip3 install flask-httpauth
で、スクリプト上でインポートする。
from flask_httpauth import HTTPDigestAuth
Flaskインスタンスを生成する際は以下の通り。
# Flaskインスタンス生成 app = Flask(__name__) app.config['SECRET_KEY'] = 'your secret key' auth = HTTPDigestAuth()
で、実際にアクセスがあったときの処理部分に以下を記述。
@app.route("/YOUR_URL/") @auth.login_required def your_function():
次に、ユーザネームを引数とし、あらかじめサーバに保存してある認証用の情報からパスワードを返す関数を実装する。ID/パスワードはDictionaryで持っておくのが楽で、スクリプト上に直接書いてもよいが、今回は外部ファイルから読み込んでいる。
@auth.get_password def get_pw(username): # ユーザデータ読み込み users = pickle_load('./users.pickle'); if username in users: return users.get(username) return None
ID/パスワードなどの認証用情報は以下の記事を参照してあらかじめ外部ファイルに出力しておいた。
ここまで実装すると、http://[IPaddress]:[PORT_NUM]/[YOUR_URL]/
にアクセスした際にID/パスワードの入力を要求される。
これで最低限のアクセス制御が実装できた。なお、アクセスする度ID/パスワードを打っていては配達員が帰ってしまうため、それらの情報はブラウザに保存した。
その他
セキュリティ対策にはならないのだが、私はこのシステムにアクセスする端末を決めているので、その機種以外でのアクセスをhttpヘッダのUser-Agentを使って弾いてみた。
User-Agentについては以下の記事などが詳しいので、詳細はそちらを参照のこと。
from Flask import request
して、アクセスがあった際にrequest.headers.get('User-Agent')
すると、例えば以下のような文字列が得られる。
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/xxx.xx (KHTML, like Gecko) Chrome/xx.x.xxx.xxx Safari/xxx.xx
(xxxはバージョン番号)
文字列を見るとわかるように、アクセス元のOSなどがここからわかる(上の例の場合はWin10 64bit)。
今回はこのシステムにアクセスする端末を限定し、それ以外の種類のものからのアクセスは弾くことにした。 (もちろん、これは個人を識別できるIDでは全く無いため、気休めである)
その他の例として、iPadから接続すると以下のようなUser-Agentが得られる。
Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/xxx.xxx (KHTML, like Gecko) CriOS/xxx.xxx Mobile/xxxx Safari/xxx.xxx
「iPad」の文字列が入っており、iPadからのアクセスであることがわかる。
まとめ
ラズベリーパイを使った再配達撲滅システムに最低限のセキュリティ対策を行い、アクセス制御(Digest認証)を実装した。
個人で使うシステムとしてはこれで十分と思うが、もし足りない部分があれば追加で実装していこうと思う。
余談
暗号の歴史について、以前読んだこの本が非常に面白かったのでここで紹介しておく。
- 作者:サイモン シン
- 出版社/メーカー: 新潮社
- 発売日: 2001/07/31
- メディア: 単行本
スイッチの長押しで電源をON/OFFする(ように見える)回路を作る
今どきのモバイル機器は、電源をON/OFFするとき特定のスイッチを長押しすることが多い(気がする)。 身近にあるところだと、例えばモバイルルータやガラケーは長押しで電源ON/OFFである。
スマホやiPadも、長押しで直接OFFではないが、長押しがOFFの動作に紐付いている。
このように、ある種今どきのモバイル機器は長押しが電源ON/OFFの動作につながっている。
趣味の電子工作でモバイルデバイスを作る際も、電池・バッテリによる電源供給をON/OFFするために何らかのスイッチを設けるのが普通である。私は昔はよくスライドスイッチなどで物理的にON/OFFしていたが、こだわりはじめると段々と実際の製品のように長押しでON/OFFしたくなってくる。
今回は、そんなスイッチ長押しで電源をON/OFFする(ように見える)(可能な限り最小構成の)回路について述べる。 ※電源回路の先にはマイコンがある前提。今回はPICを想定する。
「ように見える」と書いたのは、長押しで実際に電源供給がON/OFFするわけではなく、マイコンを利用してインジケータを長押しでON/OFFして、「あたかも長押しでON/OFFしたように見える」回路だからである。以降省略して「ON/OFFする」と書く。
この回路を使うと以下のような長押しON/OFFができる。
ペンに付けて、ただタップ数を数えてくれるデバイスが完成したぞ! pic.twitter.com/LwsdW3kIy6
— 水田かなめ (@kmizta) May 25, 2019
これは「ペンに付けてただタップ数を数えてくれるデバイス」だが、中身はさておきタクトスイッチの長押しで電源がON/OFFする(ように見える)。
この回路はできるだけ最小構成で組んだつもりだが、「もっとここ省略できる」などあれば是非教えて欲しい。
スイッチ長押し 電源ON/OFF回路
結論から言うと、以下のような回路でスイッチ1つの長押しで回路全体の電源供給をON/OFFできる。
図中の、FETの型番や抵抗値はあくまで例である。この回路はモバイル機器に使うことを想定しているため、この電源回路自体の消費電力をできるだけ下げるため、大きめの抵抗値を使っている。
また、FETやトランジスタ、いずれも秋月電子で入手できるものを選んでいる。マイコンの接続ピンはGPIOならどこでも良い。
動作説明
ユーザが長押しする物理的なスイッチは回路図中のSWである。そして、回路全体をON/OFFする電気的なスイッチがQ1のPchFETである。
例えば、LEDやディスプレイなどのインジケータが接続された回路なら、一定時間長押ししたらインジケータが付き、一定時間長押ししたらインジケータが消灯するのが理想だろう。この回路はそれを実現する。
以下に順序立てて動作を説明する。
(以下、H:電源電圧、L:GND電圧を示す。また、図中の赤線は電圧Hレベル、青線は電圧Lレベル、黒矢印は電流を示す)
電源OFF→ON
(SW押す前)Q1ゲートはHのため、Q1はOFF。電源は供給されていない。
①ユーザがSWを押す
②SWを介してQ2のゲートがHになり、Q2が導通する。
③Q2が導通することでQ1のゲートがLになり、実は一旦ここで回路としてはONになる。
「え?押した瞬間ONになっちゃってるじゃん?」という意見はもっともだが、この時点ではインジケータは(マイコンが)OFFにしているため、ユーザは「電源OFF」と認識している。
④マイコンは、電源ONしたらすぐに12ピン(RC1)をL出力に初期化する。よってT1はOFF状態。マイコンはその状態で一定時間何もせず待つ。
⑤一定時間後もマイコンの電源が入ったまま(ユーザがSWを押したまま)であれば、12ピンをHにする。よってはT1はON状態。
⑥T1がONしたことによりT1も介してQ1のゲートはLになる。ここでLEDやディスプレイといったインジケータを動作開始する。ユーザは電源ONを認識する。
⑦ユーザがSWを離しても、T1がONのためQ1ゲートはLとなり、Q1はON状態。回路に電源が供給される。
ポイントは、④⑤でマイコンが一定時間何もせず待つことである。
SWを押してから一定時間経つまではマイコンは何も動作を開始しないため、ユーザから見ると、あたかも一定時間長押ししたら電源がONしたように見える、というわけである。
もしユーザが長押しでなく、ちょっと押しただけでSWを離すと、Q2がOFFになり、またT1もマイコンによりOFF状態のため、Q1ゲートはHになり、結果としてQ1はOFF、回路全体の電源供給はOFFになる。
電源ON→OFF
さて、晴れて電源がONできたら次はOFFである。全体の電気的スイッチであるQ1はT1によりON状態になっているため、正しく長押しを検知できれば、T1をOFFしてやることで回路全体をOFFすることができる。
そこで使うのがR3である。
電源ON→OFF時の動作は以下の通り。
①SWを押していない時、R3がつながっている11ピン(入力ピン)はR1を介してLレベルである。
②ユーザがSWを押すと、11ピンはHになる。同時にQ2がONになるが、もともとT1を介してQ1がONしているので関係ない。
③11ピンのHレベルを検知したら、マイコンは一定時間12ピン(T1)をHのまま待つ。
④一定時間後も11ピンがHのままだったら、LEDやディスプレイなどのインジケータをOFFする。ユーザは電源OFFを「認識」する。同時に12ピンをLにする。T1がOFFになる。(実際にはまだQ2がONのため、電源供給は続いている)
⑥ユーザはインジケータのOFFを見て、SWを離す。Q2ゲートはLになり、Q2はOFFになるため、Q1ゲートはHになり、Q1がOFFする。結果として回路全体の電源供給はOFFする。
ここでも同様にポイントは③④でマイコンが一定時間何もせずに待つことであり、一定時間経ってインジケータをOFFすることであたかも長押しで電源OFFしたかのように見える。
もし、ユーザが長押しでなく、短時間でSWを離した場合は、12ピンはHのまま、すなわちT1はONのままなので電源供給は続く。
これで、長押しによる電源OFFが実現した。
この回路を実際に使ってみる
この回路は、長押しの長さをマイコンで設定できるので、ユーザの好きな長押し時間が作れる。ちょっと便利。
また、手に入れやすい部品で思いつく限り最小構成で組んでいるため、チップ部品を使えばかなり電源回路を小さく作れる。
最初に示した動画を再掲するが、上の回路を入れており長押しで電源ON/OFFする。(このペンについてはいずれ記事にしたい)
ペンに付けて、ただタップ数を数えてくれるデバイスが完成したぞ! pic.twitter.com/LwsdW3kIy6
— 水田かなめ (@kmizta) May 25, 2019
スイッチ長押しでON/OFFできると、なんか「それっぽい」感じがする。
世の中のモバイル機器も同じような回路を組んでいるんだろうか??
ラズベリーパイ使って宅配便の再配達を撲滅した話(総集編)
概要
外出時でも荷物を受け取れるように、ラズベリーパイを使って受取までのプロセスをほぼ自動化した話。
我が家がオートロックマンションのため、共同玄関をどうセキュアに開けるかがキモ。
背景
私は宅配便の受取が苦手である。
時間指定できるならまだマシだが、指定したその2~3時間どのタイミングで来るかもわからず、待っていなければならないのは苦痛である。
以前はコンビニ受け取りもしていたが、やはり段ボールを抱えて家まで帰るのは面倒である。(我が家は3件隣がコンビニだがそれでも面倒は面倒)
時間指定ができない宅配便などは、もう諦めて一度不在票入れてもらってその上で時間指定で受け取っていた。配達員の方々には申し訳ないがそうせざるを得ない。
日本の宅配便の約6件に1件が再配達
一人暮らし、共働きが多い現在、再配達率は相当多いのでは?と思い調べてみたところ、国土交通省の発表では平成31年度4月の段階で16%、都市部に限れば18%に上るそうである。
5~6回に1回が再配達ということになる。正直もっと多いと思っていた。
配達業界もこれは問題と思っているのだろう、最近はAmazonなどで置き配も始まっている。
我が家はオートロックマンション
置き配はセキュリティ上不安な面も残るが、再配達問題の解決の一歩にはなるだろう。私も是非利用してみたい。だが、我が家はオートロックマンションのため、インターホンで「解錠」ボタンを押さないと共同玄関が開かない。共同玄関が開かないことには配達員は玄関にたどり着くことができない。これでは置き配してもらうこともできない。
そこで、ラズベリーパイを使ってインターホンを監視、共同玄関を解錠したりユーザに配達を通知するシステムを作ってみた。
インターホン監視システム概要
今回構築したシステムの概要は以下の通りである。あらかじめ配達がわかっている場合と、予定外の配達の場合で動き方が分かれる。
まずは予定された配達の場合。
時間指定便など、あらかじめ配達時間がわかっている場合は、事前にGoogle Calendarに配達予定を登録しておく。ラズベリーパイは配達予定を取得し、その時間帯にインターホンが鳴ったら共同玄関を開け、配達員を中に入れる。ラズベリーパイは同時にユーザへ配達の旨をプッシュ通知する。
(2020年1月27日 19時50分追記)
「指定時間帯は誰でも入れてしまうセキュリティホールになる」とのコメントが多くありましたので,このモードは廃止しました.
次が予定外の配達の場合。
時間指定ができなかったものなど、事前にいつ届くかわからなかいものの場合、ラズベリーパイはインターホンを検知するとその通知とインターホンのモニタ画面をスマホへプッシュする。ユーザはモニタ画面を見て配達員と判断したら、ラズベリーパイに解錠コマンドを送る。今回、Flaskでラズパイ上にサーバを立て、そこにコマンドを送ることにした。
オートロックマンションの共同玄関はセキュリティのためにあるため、来る者拒まずすべて通していたらオートロックの意味はなくなってしまう。そのため、「事前にわかっている配達」か、「ユーザが判断して許可した配達」のみ、通すようにした。
なお、部屋の玄関までたどり着いたら、簡易宅配ボックスであるOKIPPAを使って受け取る。
ラズパイでインターホン音検知
このシステムのトリガは全てインターホン音である。なにはともあれ、ラズパイでインターホン音を検知する。
家に転がっていたウェブカメラのLogicool C270をラズパイに接続し、インターホンが押されたときの「ピロピロピロ・・・」という音を検知している。具体的には、音声データを常に取得、FFTで最大音量の周波数を取得し、インターホン音と一致するかを判定する、というもの。詳細は以下の記事を参照。なお、プログラミング言語にはPythonを使っている。
インターホンの音を検知したら、同時にユーザのスマホでプッシュ通知を送っている。プッシュ通知にはPushbulletを使った。PushbulletはPython APIも用意されており、アカウントを作れば簡単にラズパイ→スマホへのプッシュ通知を実装できる。
ラズパイで解錠ボタンを押す
インターホン音が検知できたら、次にサーボモータで解錠ボタンを押せるようにしてみる。サーボモータにはマイクロサーボSG92Rを使った。秋月電子にも売っている。pigpioを使い、pythonで動かしている。サーボモータはインターホンに強力両面テープで貼り付けた。解錠ボタンをちょうどよく押せる角度を調整するのがキモである。
ここの詳細は以下の記事を参照。
ここまでできると、こういうことができるようになる。
ラズパイ+ウェブカメラでインターホン検知→サーボモータで解錠。あまりに反応が早いと怪しいので少し間を空けて。 pic.twitter.com/BGr4zJplWk
— 水田かなめ (@kmizta) 2019年12月31日
ラズパイでGoogle Calendarから配達予定を取得する
次に、Google Calendarから配達予定を取得してみる。なお、予定自体はあらかじめユーザがスマホで設定する。 PythonからGoogle Calendar APIを使う手順はQuickstartが整備されているので、それに従って設定していけば良い。
今回のシステムでは、CRONで1時間に1回Google Calendarを読みに行って新しい配達予定がないかを確認している。
読み取った予定をもとに「予定された配達」か「予定外の配達」かを識別している。
ここの詳細は以下の記事を参照。
ここまでくれば、一応「予定された配達」は自動で受け取れることができるようになる。
ラズパイ+ウェブカメラでインターホンのモニタを抽出、通知する
もし、予定外の配達だった場合、ユーザへインターホンのモニタ画像を通知する必要がある。今回は、OpenCVを使った。ただモニタを撮影してプッシュするとモニタ以外の無駄な領域が多いため、輝度値からモニタ部分のみを検出し、射影変換して向きを補正した上でPushbulletを使ってユーザへ通知している。
ここの詳細は以下の記事を参照。
外出先からラズパイに接続されたサーボモータを操作する
さて、ここまで来たら、ユーザは通知されたモニタ画像を見て配達か否かを判断できる。もしこれが予定外の配達であると判断した場合は、ラズパイに解錠コマンドを送って共同玄関を開ける必要がある。しかし、ただコマンドを送るだけではない、「即座に判断し、即座に解錠コマンドを送る」必要がある。なぜならば、配達員はインターホンを鳴らして十数秒もすれば不在票を入れて帰ってしまうからである。
リモートでラズパイを操作するので有名なのはWebIOPiだが、これは基本GUIによる操作を前提としており、操作に時間がかかってしまう。今回のような用途には不向きである。
そこで、今回のシステムではFlaskを使ってラズパイにサーバを立ち上げ、そこにコマンドを送ることにした。
また、家のルータにポートマッピング設定を行い、Flaskで使うポートへのコマンドをラズパイに転送することで、外出先からのラズパイ操作を実現した。
ここまで来ると、スマホへの通知画面は以下のようになる。モニタ画像とともにプッシュされたURLを1タップすれば、サーボモータが動作、解錠ボタンが押され共同玄関が開く。
(2020年2月2日 追記) まとめのところにも同じ内容を追記しているが、解錠コマンドはインターホンが鳴った時のみ反応すれば良いため、インターホンを検知してから30秒間のみ、応答するようにしている。四六時中コマンドで開けられるわけではない。そもそもインターホンが鳴ってないときに解錠ボタンを押しても共同玄関は開かない。
ここの詳細は以下の記事を参照。
まとめ
ようやく、ここまででラズパイを使った再配達撲滅システムが完成した。
再配達撲滅システム(オートロックマンション用)、完成した。 pic.twitter.com/vLucri4NpH
— 水田かなめ (@kmizta) January 26, 2020
完成以来、再配達を依頼することはなくなった。ハッピーハッピー。
(2020年1月27日 19時50分追記)
普段2,3人/日しか来ないブログのPV数がえげつないことになっていてビックリ・・・
上にも追記しましたが,「指定した時間帯は誰でも入れるセキュリティホールとなる」といったコメントが多くありましたので,このモードは廃止しました.
こうするとインターホンが鳴るとスマホに通知が飛び,その場でインターホンモニタを見てスマホから中央玄関を開けることもできるシステムになります.
なお,「解錠コマンド」はインターホンが鳴ってから30秒程度しかコマンドに応答しないようになっており,普段はコマンドを送ったところで何もしません.念の為.
(2020年2月9日 14時05分追記)
このシステムに対してアクセス制御を実装し、セキュリティ対策とした。 以下の記事参照。
ESP32でBluetooth通信してるとASSERT_WARNが出る件
最近ESP-WROOM-32を買って弄っている。
ESP-WROOM-32にはBluetoothが搭載されているので、スマホなどBluetooth搭載機器と簡単に通信を行うことができる。
単純にBluetoothを試してみるだけなら、Arduino IDEでファイル→スケッチ例→BluetoothSerial→SerialToSerialBT
で手軽に通信することができる。
このスケッチ例を使って、ESP32とスマホとつないでBluetooth通信を試していたのだが、シリアルモニタにASSERT_WARN(1 9), in lc_task.c at line 5054
というメッセージが定期的に生じる(ひどいと数秒に1回)。
別にメッセージが出るだけで、Bluetooth通信自体はできるのでいいっちゃいいのだが、かなり目障り。
対処方法
どうやら原因は使っていたノートPCのBluetoothドライバだったようだ。
自分のPCのデフォルトのドライバはRealtekのものだった。
こいつをWindows標準のドライバに変更すると症状が収まった。
ドライバを変更するには、デバイスマネージャーから件のドライバを右クリック→ドライバの更新→コンピューターを参照してドライバーソフトウェアを検索→コンピューター上の利用可能なドライバーの一覧から選択します をクリックする。
すると、以下の画面になるので、Generic Bluetooth Adapter
を選択し、次へ、からインストールする。
デバイスマネージャー上でドライバが切り替わっていればOKだ。これでASSERT_WARNメッセージは出なくなった。