West Gate Laboratory

人生を少しでも,面白く便利にするモノづくり

3Dプリンタでネジ用ボスを印刷する

概要

3Dプリンタ(FlashForge Adventurer3)でネジ用ボスを印刷する方法などの備忘録。

背景

電子工作で何らか完成品を作ろうとすると、基板を収める筐体が必要になる場合が多い。
昔はタカチのプラスチックケースを選んで、ケースに合わせて基板を設計していたものだ。
だが、今は3Dプリンタがある。3Dプリンタがあれば、基板に合わせて筐体を作ることもできる。

筐体に基板を収める時に必要なのが、基板や蓋固定のネジ用のボスである。
例えば、タカチのTWシリーズでは以下の写真のようなボスがついている。

f:id:kaname_m:20200418210403p:plain
ネジ用ボス(赤枠、他も同様)。写真はタカチのTWシリーズ

ネットで調べるといくつか同じようなことを試している先駆者がいる。

voltechno.com

ohmic-electronics.hatenablog.com

いくつか調べるとインサートナットを使っているものもあった。

今回手持ちのFlashForge Adventurer3を使って、M3の小ねじとタッピングネジを対象にボスを作成した。
結果、PLA樹脂で十分な強度のボスが作成できることを確認した。
備忘録を兼ねて、ここに記録する。

環境

  • 3Dプリンタ:FlashForge Adventurer3
  • 素材:PLA
  • ネジ:M3x10なべ小ねじ、タッピングネジ

テストボード作成

ボスの試験用のボードをFusio360で設計する。
ボスの外径は6mm, 7mm, 8mmの3種類。下穴の内径は全て2.5mmである。
(手持ちのAdventurer3だと、内径2.5mmで印刷すると仕上がりは2.0mmくらいになる)

f:id:kaname_m:20200418211441p:plain
ボス試験用ボード。ボス外径6, 7, 8mmが各3つずつ

テストボード印刷

3Dプリンタで印刷する。このときのポイントは充填率を100%にすること。(デフォルトは15%)
Adventurer3のスライサーソフトFlashprintだと、スライス設定の その他のオプション→充填率から設定可能。

f:id:kaname_m:20200418211951p:plain

後加工

下穴径2.5mmで3Dプリンタで印刷すると若干小さくなって2.0mmくらいになるので、金属にやる場合と同じようにΦ2.5で下穴を開ける。

f:id:kaname_m:20200418212400j:plain

下穴を開けたら、なべ小ねじ用ボスに対しては、M3タップを切る。
樹脂の積層が剥がれたり割れたりということはなかった。

f:id:kaname_m:20200418212403j:plain

ネジ締め

なべ小ねじは、タップを切ってしまえば普通に問題なく締めることができる。
どの外径のボスであっても、スプリングワッシャが潰れる力で締めても樹脂はびくともしない。

タッピングネジはタップを切るよりも強い力でネジを切りながら締めていくことになるが、これに関してもどのボス外径でも問題なく締めることができた。

f:id:kaname_m:20200418212406j:plain

この後、最も外径6mmの最も細いボスに対して、なべ小ねじを限界まで締め上げる破壊試験を行った。
結果、1mmちょっと樹脂を潰しながら進んでいった後、鉄のネジの頭がナメた。
下の画像の手前右がそれである。ネジ下の樹脂が潰れているのがわかるだろうか。

f:id:kaname_m:20200418212353j:plain

PLAでもきちんと下穴開けて、タップを切れば十分な強度のボスができるようである。

まとめ

3Dプリンタ(FlashForge Adventurer3)でPLA樹脂を使ってM3ネジ用のボスを印刷した。
印刷するときは充填率100%で印刷し、 なべ小ねじに関しては金属と同様下穴を開けて、タップを切ればM3に対して外径6mmのボスでも十分な強度が出ることを確認した。
タッピングネジに関しても、同様にΦ2.5mmの下穴を開ければ、問題なくネジを切れることを確認した。

これでEagleとFusion3603Dプリンタを連携させた回路・基板・筐体設計が捗る捗る。

M5Stackからスマホ/PCにCO2濃度上昇を通知する(Pushbullet)

概要

先日の記事で紹介した、M5StackによるCO2濃度モニタはただ数値とグラフを表示するだけだった。
このままではCO2濃度が高いのか低いのか、人が忘れないようにチェックする必要がある。

westgate-lab.hatenablog.com

そこで今回は、それに改良を加えCO2濃度が一定レベルを超えたらスマホ/PCに通知を送るようにした。
通知にはPushbulletを使っているが、M5StackからPushbulletを使って通知を送る方法について述べる。

ソースコードGithubで公開しているので、記事を読むのが面倒な人はこちらを参照のこと。 (この記事に対応するソースコードのバージョンはv2.0である)

github.com

最終的にはPushbulletのアプリを通じてこんな感じにスマホへプッシュ通知される。PushbulletにPC(Chrome拡張)を登録しておけば、PCにも同時に通知される。

f:id:kaname_m:20200405153127j:plain

スマホ・PCへのプッシュ通知はPushbulletが便利

Pushbulletは、様々な端末同士をつないでチャットしたりメッセージや写真を送ったり通知を共有するサービスである。
情報は、テキスト・URL・写真など何でも良い。

www.pushbullet.com

スマホとPC間のちょっとしたデータの共有に便利なので、電子工作に限らず私はよく使っている。
また、APIが豊富に用意されているため、インターネットに繋がってHTTPが使えるデバイスからも利用可能である。

以前ブログに書いた「RaspberryPiで再配達を撲滅するシステム」でも、スマホ/PCへの通知にPushbulletを使っている。

westgate-lab.hatenablog.com

M5Stackからプッシュ通知を送る

さて、ここからM5Stackからスマホ・PCへPushbulletを使ってプッシュ通知を行う方法を述べる。

Pushbulletアカウント、Access Tokenの取得

この後は、すでにPushbulletのアカウントを持ち、Access Tokenを取得している前提とする。
Access Tokenの取得方法については以下の過去記事参照。

westgate-lab.hatenablog.com

プッシュ通知タイミングの設計

今回は、以下のしきい値でプッシュ通知を送ることとした。

  • 1000ppmを超えたら、注意レベルとし、換気を促す。

  • 2000ppmを超えたら、危険レベルとし、速やかな換気を要請する。

しきい値付近では値がふらつくことが予想されるため、一度通知をプッシュしたら一定時間は通知をしないようにした。

ソースコード

ソースコードは全てGithub上で公開している。詳細はそちらを参照のこと。

github.com

なお、この記事では詳細は書いていないが、Ambientを使ったセンサ値のログも同時に実装している。ESPによるAmbientの使い方は公式チュートリアルが豊富にあるのでそちらを参照のこと。 以下では、ソースコードのうち特に重要な点について抜粋して述べる。

まず、Pushbulletではhttpsを使うため、WiFi.hに加えてWiFiClientSecure.hが必要である。

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>

const char *ssid = "YOUR-WIFI-SSID";        // write your WiFi SSID (2.4GHz)
const char *password = "YOUR-WIFI-PASSWORD";     // write your WiFi password
WiFiClientSecure secureClient;
#define PB_APIKEY "YOUR-PUSHBULLET-API-KEY"      // write your Pushbullet API key

ESP32は2.4GHzのWiFiのみ対応しているので、SSIDには2.4GHzの方を記述すること。

setup()内で、WIFIをセットアップしておく。Pushbullet用関数(後述)はWIFIさえセットアップしておけば、他にセットアップは必要ない。

    // Wifi setup
    M5.Lcd.print("WiFi setup...");
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    M5.Lcd.println("done");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.print(WiFi.localIP());

次に、loop()内でセンサの値を取得したら、前回のCO2濃度と比較し、濃度しきい値を超えたらユーザへ通知を送るnotifyUser()、後述)。
co2_ppmにCO2濃度が格納されているものとする。

ここでは、CO2濃度をNORMAL, CAUTION, WARNINGの三段階に分け、前回取得時よりもレベルが上がっていたら通知を出している。
ただし、一度通知を出すと通知がpauseされ(pause_notify_[caution|warning])、一定時間通知は出されない。また、プログラム開始からも一定時間は通知を出さない(起動直後の値のふらつき対策)。

#define CO2_CAUTION_PPM 1000
#define CO2_WARNING_PPM 2000

int notify_timer_caution = 0;
int notify_timer_warning = 0;
bool pause_notify_caution = true;
bool pause_notify_warning = true;
#define PAUSE_LENGTH 600 // do not notify PAUSE_LENGTH [s] once notified

enum
{
    LEVEL_NORMAL,
    LEVEL_CAUTION,
    LEVEL_WARNING
};

int co2_level_last = LEVEL_NORMAL;

// (中略)
void loop()
{
    if (airSensor.dataAvailable())
    {
        // get sensor data(前回記事参照)
        co2_ppm = airSensor.getCO2();

        int co2_level_now;
        // check co2 level
        if (co2_ppm < CO2_CAUTION_PPM)
            co2_level_now = LEVEL_NORMAL;
        else if (co2_ppm < CO2_WARNING_PPM)
            co2_level_now = LEVEL_CAUTION;
        else
            co2_level_now = LEVEL_WARNING;

        // notify user when co2 level exceed threshold
        if (co2_level_now > co2_level_last)
        {
            if (co2_level_now == LEVEL_CAUTION && !pause_notify_caution)
            {
                if(notifyUser(co2_level_now)){
                    Serial.println("notifyUser(): CAUTION");
                }else{
                    Serial.println("notifyUser(): failed!");
                }
                pause_notify_caution = true;
            }
            if (co2_level_now == LEVEL_WARNING && !pause_notify_warning)
            {
                if(notifyUser(co2_level_now)){
                    Serial.println("notifyUser(): WARNING");
                }else{
                    Serial.println("notifyUser(): failed!");
                }                
                pause_notify_warning = true;
            }
        }

        co2_level_last = co2_level_now;
    }

    delay(SENSOR_INTERVAL_S * 1000); // SENSOR_INTERVAL_S [秒]ごとにセンサ値取得

    // 一度通知を出したら一定時間通知を出さないためのタイマー
    if (pause_notify_caution)
    {
        notify_timer_caution += SENSOR_INTERVAL_S;
        Serial.printf("notify_timer_caution: %d\n", notify_timer_caution);
        if (notify_timer_caution > PAUSE_LENGTH)
        {
            notify_timer_caution = 0;
            pause_notify_caution = false;
            Serial.println("notify_timer_caution set false");
        }
    }
    if (pause_notify_warning)
    {
        notify_timer_warning += SENSOR_INTERVAL_S;
        Serial.printf("notify_timer_warning: %d\n", notify_timer_warning);
        if (notify_timer_warning > PAUSE_LENGTH)
        {
            notify_timer_warning = 0;
            pause_notify_warning = false;
            Serial.println("notify_timer_warning set false");
        }
    }
}

最後に、ユーザへ通知をプッシュする関数は以下の通りである。

PushbulletへのHTTP通信は、コード中にも記載の通りこちらのサイトを使わせていただいた。

PushbulletはREST APIが利用でき、HTTP POSTでプッシュ通知を実装できる。
通知中の情報としては、title(通知タイトル、ここではCO2 Monitor)、body(通知文)を与えられる。

// reference: https://fipsok.de/Esp32-Webserver/push-Esp32-tab
bool pushbullet(const String &message)
{
    const char *APIKEY{PB_APIKEY};
    const uint16_t timeout{5000};
    const char *HOST{"api.pushbullet.com"};
    String messagebody = R"({"type": "note", "title": "CO2 Monitor", "body": ")" + message + R"("})";
    uint32_t broadcastingTime{millis()};
    if (!secureClient.connect(HOST, 443))
    {
        Serial.println("Pushbullet connection failed!");
        return false;
    }
    else
    {
        secureClient.printf("POST /v2/pushes HTTP/1.1\r\nHost: %s\r\nAuthorization: Bearer %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s\r\n", HOST, APIKEY, messagebody.length(), messagebody.c_str());
        Serial.println("Push sent");
    }
    while (!secureClient.available())
    {
        if (millis() - broadcastingTime > timeout)
        {
            Serial.println("Pushbullet Client Timeout !");
            secureClient.stop();
            return false;
        }
    }
    while (secureClient.available())
    {
        String line = secureClient.readStringUntil('\n');
        if (line.startsWith("HTTP/1.1 200 OK"))
        {
            secureClient.stop();
            return true;
        }
    }
    return false;
}

bool notifyUser(int level)
{
    const char *title = "CO2 Monitor";
    char body[100];

    switch (level)
    {
    case LEVEL_CAUTION:
        sprintf(body, "CO2 exceeded %d ppm. Ventilate please.", CO2_CAUTION_PPM);
        return pushbullet(body);

    case LEVEL_WARNING:
        sprintf(body, "CO2 exceeded %d ppm. Ventilate immediately.", CO2_WARNING_PPM);
        return pushbullet(body);

    default:
        return false;
    }
}

ここまでできれば、CO2が一定レベルを超えるとプッシュ通知される。

スマホへは記事トップのように通知されるが、PC(Windows10)の場合は以下のような感じである。

f:id:kaname_m:20200405154931p:plain

まとめ

前回作成したCO2濃度モニタに、Pushbulletを使ったプッシュ通知機能を実装した。
これでより意識的に換気できるだろう。

さぁ、空気を入れ替えよう。

換気のすゝめ ~M5StackでCO2濃度モニタを作る~

概要

集中力を保ったり、生産性を上げたりするのに換気は本当に大事だよ、意識的に換気するためにCO2濃度を監視しよう、という話。

先日、多くの方から反響をいただいた、CO2濃度モニターのツイート。
このツイートにあるCO2濃度モニターの作り方の紹介。

暇な休日の午後に戯れで作ったものがこんなに反響を呼ぶとは思っていなかった・・・。

(2021年2月13日追記)
スイッチサイエンスさんでM5StackのCO2モニター化キットを販売することになりました! 近日中に発売予定!

www.switch-science.com

(2021年2月26日追記)
発売しました!

背景

会議中、運転中、仕事中、そして流行りのテレワーク中。寝不足でもないのに眠くなったり、集中力が全く出ないことがないだろうか。 私は特に家にいるとそれが起きる事が多い。

昔から「これ、CO2のせいじゃね?」とは思っていたが、そのためにわざわざ環境計測機を買うわけにも行かず、そうなると定量的にわからないので行動を起こせない。
1,2年ほど前に一度本気でCO2モニタを作ろうと思ったこともあったが、適当なセンサが売っておらず諦めていた。

ところが、先日千石電商で買い物をしていると、こんなセンサを見つけた。

www.sengoku.co.jp

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とセンサで異なるので注意。

f:id:kaname_m:20200401221646p:plain
Groveを介したM5Stackへの接続

f:id:kaname_m:20200401222453j:plain Groveケーブルは別途用意し、このように接続すればOK。

Groveでなくても、M5Stackの下面・上面いずれにもI2Cのピン(21:SDA,22:SCLピン)がでているので、そこからつないでも良い。ただし、こちらもピンの並び順が異なるので注意。

変換基板を作って、ピンから直接つなぐとこんな感じになる。

f:id:kaname_m:20200401223030j:plain

プログラム

SparkFunのライブラリ

ありがたいことに、このセンサについてはSparkFunがArduino用ライブラリを公開してくれている。 M5StackはArduino環境を利用できるので、このライブラリを使えば、あっという間にできてしまう。

github.com

Arduinoに上のライブラリをインストールしたら、Exampleに入ったBasicReadingsを試してみよう。 ピンの接続が問題なければUARTでとりあえず値が読めるはずである。

なお、このセンサは最初の数分は値が不安定なので、しばらく試運転するのが良い。(どの二酸化炭素センサもそうだと思うけど)

センサの最小読み取り間隔は2秒である。デフォルトは2秒となっているので、まずは2秒毎に読み出すこととした。

画面設計

値が読めたらあとは画面設計だ。

画面については結果はトップのツイートの通りだが、今回は以下のようなレイアウトとした。

  • CO2濃度+温湿度を表示

  • CO2濃度の履歴を表示。グラフはスクロールする。

  • 1000ppm以上を注意2000ppm以上を危険レベルとし、それぞれ黃、赤で表示する(グラフ、文字ともに)

文字表示部分、グラフ部分それぞれSpriteで表示しており、グラフはセンサを取得するごとに1pxずつプロットしながらスクロールしていく。

ソースコードGithubで公開しているので、もし作ってみたいという人がいたら自由に使ってもらって構わない。(初めて公開したのでうまくできているかどうか・・・)

github.com

(2020年4月18日追記)
この記事に対応したソースのバージョンは1.0である。

使った実感

コロナウイルスの影響でテレワークが始まったので、テレワーク中に上のモニタでCO2濃度を監視してみた。
6畳の自室を閉め切って仕事を続けると、1時間かそこらで1000ppmを超えてしまう。
さらに作業を続けると1500ppm近くに達し、そうなると一気に集中力がなくなる。

ここで、換気をすると5分ほどで600ppm程度にCO2濃度は下がる。 すると不思議と集中力が回復するのである。

そう、集中を切らす最大の原因は二酸化炭素なのかもしれない。

また、CO2濃度を監視しながら仕事すると、「1000ppmになるまでに資料仕上げなきゃ!」という謎の締め切り効果が生まれてそれはそれで仕事が捗るのでオススメである。

まとめ

M5StackにCO2濃度センサを接続し、CO2濃度監視モニタを作った。

ツイートに書いた通りだが、換気は本当に大事である。

あなたが会社・家・車など、生活している中で不意に集中力がなくなるときがあるとしたら、それはCO2濃度が高いせいかもしれない。

意識的に換気しよう。

f:id:kaname_m:20200401221809j:plain

(2020年4月5日更新)
後日、機能を更新してスマホ・PCへのプッシュ通知とAmbientを使ったデータ保存・閲覧を実装した。(ver2.0)
詳細はこちらの記事を参照のこと。

westgate-lab.hatenablog.com

(2020年5月6日追記)

CO2濃度モニタを定常運用のためリビングへ置くことにした。

幸い、M5Stackにはネオジム磁石が入っているため、冷蔵庫に貼ることが可能である。
定常運用のためにはUSB-Cによる電源が必要なので、電源として磁石で冷蔵庫に貼り付けることができるUSB充電器を使用した。

磁石で冷蔵庫に貼り付けられるUSB充電器はあまり種類がなく、これがちょうど良かった。

また、このUSB充電器からCO2モニタにUSB-Cで電源を供給するが、USB-Cは一般的に太く、屈曲がしにくいケーブルが多い。
ただ、今回に関してはM5Stackの分しか消費しないため細くて取り回しの良いこちらのケーブルを使用した。

このケーブルは細く取り回しがよいわりに3A流せる優れモノである。

USB充電器とCO2モニタを貼り付け、今ではこのように常時運用中である。

f:id:kaname_m:20200506213906j:plain
USB充電器を冷蔵庫側面に磁石で取り付け

f:id:kaname_m:20200506213910j:plain
CO2モニタ(M5Stack)も磁石で貼り付け。

M5Stackでマルチタスクを使ってセンサ取得・ログ記録を行う

概要

センサ値の取得とログの記録は切っても切れない関係である。

M5Stack Grayに内蔵された加速度センサやジャイロセンサや磁気センサ、外付けの気圧センサの値を高頻度に取得、SDへ記録をしたいが、 何も工夫をせずに取得→記録のプログラムを実行すると、SDへの書き込み時間の変動のため、周期が変動してしまい、不都合である。

そこで、FreeRTOSの機能を使いセンサ値の取得とログの書き込みをそれぞれマルチタスクで処理させることで、 高頻度かつ安定した時間でセンサ値取得・ログ記録を行う方法を述べる。

背景

M5Stack Grayには加速度センサ・ジャイロセンサ・磁気センサが内蔵されている。
これらを使ってなにかやろうと思うと、まずは値をログしたくなる。
今回はそれらに加え、Groveで外付けの気圧センサをつなぎ、加速度・ジャイロ・磁気・気圧の4つのセンサ値をログすることにした。

幸いM5StackにはSDカードスロットがついているのでSDカードにログを記録できる。
だが、単純にセンサ取得、記録をしていくと問題が起きる。

例えば、下のグラフは単にセンサ値取得→SDへログ記録を25Hz(40ms)で繰り返したときの一周期あたりの処理時間である。
およそ4.5秒に1回、処理時間が伸びていることがわかる。

f:id:kaname_m:20200328104700p:plain
M5Stackでセンサ値取得→SDへログ記録を25Hzで繰り返した際の処理時間

原因を見るために、各処理ごとに処理時間を計測した結果がこちら。

f:id:kaname_m:20200328104702p:plain
処理時間内訳

凡例は上から「加速度ジャイロ取得」「磁気取得」「姿勢計算」「気圧取得」「ログ書き込み」だが、 処理時間が伸びる原因は一目瞭然で、ログ書き込みである。 SDは一般的にある書き込み量の単位を超えると書き込みに時間がかかってしまう。
これでは一定周期を前提とする制御などには不都合である。

1秒ごとにまとめて書き込む(失敗)

最初に思いつくのは、「毎周期SDに書き込むのは無駄だから、ログを一定周期分まとめて書き込めばいいんじゃね?」だが、 結局まとめて書き込む際にSDの一定の書き込み量を超えると処理時間遅延が生じる。

以下はこの処理を実装したときの周期あたりの処理時間。結局書き込みに時間がかかっている。

f:id:kaname_m:20200328104706p:plain
1秒ごとにまとめて書き込んで見る(失敗)

割り込みを使ってセンサ取得する(失敗)

SDに書き込んでいる間センサ取得が止まってしまうのが問題なので、 割り込みを使ってセンサ値取得を優先的に行ってみる。
こうすると、SDへのログ書き込み中でも優先的にセンサ値が取得される。
幸い、SDは書き込みを中断しても処理的に問題ないため、割り込みセンサ取得も可能である。

だが、M5Stackでこれを行うと、以下のエラーが生じて再起動してしまう。

[IGuru Meditation Error: Core  1 panic'ed (Interrupt wdt timeout on CPU1)

どうやら割り込み中にセンサ値取得などの長ったらしい処理をしているとWDTが発動して再起動してしまうようだ。
WDTを停止するという方法もあるかもしれないが、他の処理に悪影響を及ぼす可能性もあるので、やめておく。

マルチタスクを使う(成功)

さて、ここで活躍するのがタスク機能である。

ESP32-Arduino-coreではオープンソースリアルタイムOSである、freeRTOSが使われている。
リアルタイムOSの詳細やその使い方については以下のウェブサイトが非常に詳しい。

miqn.net

リアルタイム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"8192NULL1, &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()を使う。
引数は左からタスク関数ポインタ、タスクの名前、スタックメモリ量、タスクへ渡すパラメータ、タスク優先度、タスクハンドラ、割り当てるコア となっている。詳細は公式ドキュメント(以下)を参照。

docs.espressif.com

ESP32の場合コア0とコア1が使えるので、好きな方にタスクを割り当てる。
優先度は、0が最も低く、数字が増えるほど高くなる。あまり高くしすぎると内部の他の処理を妨げるので、必要最低限の優先度とするのが良さそう。

今回重要なのは、ログ記録タスクの優先度(LOGTASK_PRI)よりセンサ取得タスクの優先度(SENSORTASK_PRI)を高くすること。
こうすることで、ログ記録中でも優先度の高いセンサ取得をしてくれる。

以下が、マルチタスクでセンサ値取得タスクの優先度を上げて25Hz(40ms)でセンサ取得し、1秒毎(25回分ごと)にSDへ記録したときの、1周期あたりの処理時間である。

f:id:kaname_m:20200328104708p:plain
マルチタスク化して実行した場合の処理時間

図からわかるように、きれいに25Hzでセンサが取得できていることがわかる。

ちなみに、センサ取得とログ記録の優先度を逆転してみると、処理時間は以下の通り。

f:id:kaname_m:20200328104711p:plain
センサ取得タスクの優先度を下げた場合の処理時間

センサ取得タスクが優先度の高いログ記録タスクに邪魔され、処理時間がめちゃめちゃになっていることがわかる。

優先度を一緒にすると、今度は全体的に周期が遅くなる。これもダメ。

f:id:kaname_m:20200328104714p:plain
センサ取得とログ記録の優先度を同一(0)にした場合

やはり、センサ取得タスクの優先度をログ記録タスクのそれより高くする必要がある。

「センサ取得タスクの周期をvTaskDelayUntil()で限界以上に早く設定しちゃうとどうなるの?」という疑問もあろうと思うので、それをやってみると以下の通り。

f:id:kaname_m:20200328104716p:plain
限界以上(例:100Hz)の周期でタスク実行した場合の処理時間

100Hzでは処理しきれていないが、できるだけ早く処理するよう健闘していることがわかる。なお、これでもSDへのログ記録自体は問題なかった。

なお、今回100Hzではセンサが処理しきれていないが、処理時間の大半はGroveで外付けしている気圧センサである。
もしM5Stack内部の加速度・ジャイロ・磁気のみの取得であればもっと高い周期でも処理できるはずである。

気圧センサの使い方については以下の過去記事参照。

westgate-lab.hatenablog.com

まとめ

高頻度のセンサ取得とSDへのログ記録を実現させるため、M5Stack内蔵のデュアルコアを使ってマルチタスク化した。
センサ取得タスクとログ記録タスクを生成し、それぞれのコアに割付けセンサ取得タスクの優先度を上げることで乱れなく周期的なセンサ取得・記録ができるようになった。

おまけ

高頻度にセンサを処理することは、例えば姿勢のフィルタ・制御では必須である。
姿勢のフィルタでよく使われるMadgwickフィルタやMahonyフィルタなどでは、最低でも30Hz以上あったほうが良さそうである。

気圧センサは関係ないが、内部のセンサの処理により、今はようやくここまでできている。

姿勢の可視化については、こちらのページを参照した。

ambidata.io

M5StackにGroveでセンサつないで気圧測定する

概要

M5Stackにオムロンの気圧センサ(2SMPB-02E)を使ったモジュールをつないで、気圧を計測した。

ついでに、センサに平均機能IIRフィルタ機能がついていたので、それを使ってセンサの値にフィルタをかけてみた。

www.switch-science.com

背景

先日、M5Stackを購入した。

PICよりも周波数が高く、ライブラリが多くて扱いやすそうというイメージで購入してみたが、初期化にしろ何にしろとにかくコーディングが楽。(一方で内部で何してるのかわからなくても動いてしまうということでもあるが・・・)

さて、M5Stackの派生でM5Stack Grayがある。

これにはBasicのM5Stackに加え、9軸IMU(加速度、ジャイロ、磁気)が搭載されており、動きモノの制御に使ったりするのに最適である。

www.switch-science.com

また、Basicと同じくLCD・3つのボタン・SDカードスロットがあるのでちょっとしたUIやログの記録もできる。プロトタイプ開発にはもってこいである。

今回、M5Stack Grayに内蔵されているセンサで取得できる値(加速度・角速度・磁場)以外に、高度の取得が必要になったので、M5Stack GrayにGroveで気圧センサを接続し、値を取得した。

オムロン絶対圧センサ2SMPB-02E

今回、使用した気圧センサは、以下のページで紹介されていたオムロンの気圧センサ2SMPB-02Eである。

qiita.com

基本的なソースコードは上記ページを参照のこと。

スイッチサイエンスからモジュールが出ているので、それを使った。

M5Stack Grayに接続するとこんな感じ。

f:id:kaname_m:20200322110206j:plain
M5Stack Grayにオムロン気圧センサモジュールを接続

センサのデータシートはこちら

また、ありがたいことにArduino環境でこのセンサを使うためのライブラリがGithubで公開されている

github.com

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モードといった省電力のためのモードが存在する。(データシート参照)

センサ値にフィルタをかける

このセンサ、デフォルトではセンサ出力値になんのフィルタもかかっておらず、結構ノイズが乗る。

例えば以下のグラフは部屋の地面と天井の間をセンサを動かしたときの気圧の生データである。

f:id:kaname_m:20200322111907p:plain
2SMPB-02Eの生データ(単位はPa

まぁ動いていることはわかるが、ノイズ多いよね、という感じ。

そこで、センサ値にフィルタをかけるわけだが、このセンサには機能として「複数回の測定平均を出力」「IIRフィルタ」が内蔵されている。今回は内蔵されたこれらの機能を使うこととする。

こうした機能がない場合はCPU側でIIRフィルタなどのプログラムを組んでやる必要があるが、このセンサはその機能自体が内蔵されているのでCPU側としては楽である。

複数回のセンサ値の測定平均を出力

データシートに測定モード別の測定回数の記述がある。測定回数が増えるほど精度は上がるが、変換時間が伸びたり平均電流が大きくなる。

f:id:kaname_m:20200322112629p:plain
測定モード別特性(2SMPB-02Eのデータシートより)

例えば、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に低減できる。

f:id:kaname_m:20200322113433p:plain
IIRフィルタ適用時のRMSノイズ使用(2SMPB-02Eデータシートより)

なお、フィルタ係数を大きくするとノイズ自体は小さくなるが、原理上出力の遅れが大きくなるので、使うシステムに応じて適切な係数を設定することが必要である。

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くらいの精度で相対高度が測定できそうである。

f:id:kaname_m:20200322111910p:plain
測定値平均・IIRフィルタを使用したセンサ出力値(単位はPa

平均回数を上げたり、フィルタ係数を大きくするとさらに精度は上がるだろう。

まとめ

オムロンの気圧センサ2SMPB-02EのモジュールをGroveでM5Stackに接続して値を取得した。

さらに、センサ内蔵の平均値出力・IIRフィルタ機能を利用してノイズを低減し、±10cm程度の精度で相対高度が取れることを確認した。

ラズベリーパイを使ったIoTシステムに脆弱性があると指摘があったためセキュリティ対策を施した話

概要

先日のブログで記事を書いた、ラズベリーパイを使って作ったIoTシステムに「脆弱性がある」と指摘を受けたので、素人ながら調べつつ最低限のセキュリティ対策を施した話。

westgate-lab.hatenablog.com

(ちなみに、上の記事ははてなブログの週間ランキング2位になってしまった)

今週のはてなブログランキング〔2020年2月第1週〕 - 週刊はてなブログ

背景

先日上記のブログ記事を公開したところ、「システムに脆弱性がある」という指摘を多々頂いた。システムというのは、ラズベリーパイでインターホンを監視して、呼出音を検知したら条件に応じて解錠ボタンを押す」というものである。

もともとは、予定された配達か否かで2通りの解錠方法を考えていた。

f:id:kaname_m:20200126140827p:plain
予定された配達の場合(廃止済み)

f:id:kaname_m:20200126140822p:plain
予定していなかった配達の場合(現行版は常にこれ)

受けた指摘は主に2つ。

  • もともと予定された配達時間帯に万が一悪意を持った人物が我が家のインターホンを鳴らすと中央玄関が開いて、中へ入れてしまう

  • 解錠コマンド用URLにアクセスした相手を識別しておらず、悪意を持った人物がコマンドを送ることが可能

というものである。

1つ目に関しては、確かにそうなので、「予定された配達の場合」のモードは廃止した。一方で、2つ目に関しては何らか認証が必要そうだ。しかしやり方がわからない。

そんなときに、IPA情報処理推進機構から連絡があった。どうやら脆弱性関連情報の届出受付システムを通じてこのシステムの脆弱性を届出てくれた人がいたようだ。

脆弱性関連情報の届出受付:IPA 独立行政法人 情報処理推進機構

届出られた内容を読むと脆弱性の詳細が事細かに書いてあり、素人の自分にとっては非常にありがたい内容だった。届出内容には対処方法も書いてあり、どうやらアクセス制御機能を実装する必要がありそうだ。

「アクセス制御・・・?」状態だった私はまずそれを調べるところから始め、ようやくID/パスワードによる認証ができるようになったため、その過程を述べる。今回はDigest認証を使用した。

f:id:kaname_m:20200209114348p:plain
アクセス制御機能を追加したシステム。Digest認証を使用

Basic認証、Digest認証

アクセス制御とは、要はあるシステムに対し誰か何をしていいのかを設定したり識別したりして、それに基づいて操作を拒否したり許可したりすることである。

思いつくのはID/パスワードによる認証だが、ID/パスワードによる最も基本的な認証方法にBasic認証Digest認証がある。

私はアクセス制御については素人なので、詳細な説明は他の記事を読んでもらうとして、ここではごくごく簡単に説明する。

Basic認証:実装が容易で、簡易的な認証方法。ログインIDとパスワードを入力すると、それらがBase64エンコードされてサーバへ送信される。サーバはその情報をデコードし、ID/パスワードが一致していたらOKとする。

Basic認証はとても実装が楽だが、認証情報がただBase64エンコードされているだけで、誰でもデコードできるので、実質平文で送っていることになる。公衆Wifiで認証情報を入力するな、というのはこういうことのようだ。なので、一般的に通信を暗号化するSSLHTTPS)と一緒に使うようである。

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認証を実装する。今回は以下のページを参照した。

qiita.com

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/パスワードなどの認証用情報は以下の記事を参照してあらかじめ外部ファイルに出力しておいた。

neuryo.hatenablog.com

ここまで実装すると、http://[IPaddress]:[PORT_NUM]/[YOUR_URL]/にアクセスした際にID/パスワードの入力を要求される。

f:id:kaname_m:20200209135658p:plain
認証画面

これで最低限のアクセス制御が実装できた。なお、アクセスする度ID/パスワードを打っていては配達員が帰ってしまうため、それらの情報はブラウザに保存した。

その他

セキュリティ対策にはならないのだが、私はこのシステムにアクセスする端末を決めているので、その機種以外でのアクセスをhttpヘッダのUser-Agentを使って弾いてみた。

User-Agentについては以下の記事などが詳しいので、詳細はそちらを参照のこと。

qiita.com

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ができる。

これは「ペンに付けてただタップ数を数えてくれるデバイス」だが、中身はさておきタクトスイッチの長押しで電源がON/OFFする(ように見える)。

この回路はできるだけ最小構成で組んだつもりだが、「もっとここ省略できる」などあれば是非教えて欲しい。

スイッチ長押し 電源ON/OFF回路

結論から言うと、以下のような回路でスイッチ1つの長押しで回路全体の電源供給をON/OFFできる。

f:id:kaname_m:20200130232439p:plain
スイッチ長押し 電源ON/OFF回路

図中の、FETの型番や抵抗値はあくまで例である。この回路はモバイル機器に使うことを想定しているため、この電源回路自体の消費電力をできるだけ下げるため、大きめの抵抗値を使っている。

また、FETやトランジスタ、いずれも秋月電子で入手できるものを選んでいる。マイコンの接続ピンはGPIOならどこでも良い。

動作説明

ユーザが長押しする物理的なスイッチは回路図中のSWである。そして、回路全体をON/OFFする電気的なスイッチがQ1のPchFETである。

例えば、LEDやディスプレイなどのインジケータが接続された回路なら、一定時間長押ししたらインジケータが付き、一定時間長押ししたらインジケータが消灯するのが理想だろう。この回路はそれを実現する。

以下に順序立てて動作を説明する。

(以下、H:電源電圧、L:GND電圧を示す。また、図中の赤線は電圧Hレベル青線は電圧Lレベル黒矢印は電流を示す)

電源OFF→ON

(SW押す前)Q1ゲートはHのため、Q1はOFF。電源は供給されていない。

f:id:kaname_m:20200201100400p:plain
電源OFF状態

①ユーザがSWを押す

②SWを介してQ2のゲートがHになり、Q2が導通する。

③Q2が導通することでQ1のゲートがLになり、実は一旦ここで回路としてはONになる。

f:id:kaname_m:20200201100408p:plain
電源供給開始。ただしインジケータはOFF

「え?押した瞬間ONになっちゃってるじゃん?」という意見はもっともだが、この時点ではインジケータは(マイコンが)OFFにしているため、ユーザは「電源OFF」と認識している。

マイコンは、電源ONしたらすぐに12ピン(RC1)をL出力に初期化する。よってT1はOFF状態。マイコンはその状態で一定時間何もせず待つ。

⑤一定時間後もマイコンの電源が入ったまま(ユーザがSWを押したまま)であれば、12ピンをHにする。よってはT1はON状態。

f:id:kaname_m:20200201100412p:plain
T1をON

⑥T1がONしたことによりT1も介してQ1のゲートはLになる。ここでLEDやディスプレイといったインジケータを動作開始する。ユーザは電源ONを認識する。

f:id:kaname_m:20200201100416p:plain
インジケータがON。ユーザは電源ONと認識する。

⑦ユーザがSWを離しても、T1がONのためQ1ゲートはLとなり、Q1はON状態。回路に電源が供給される。

f:id:kaname_m:20200201100420p:plain
ユーザがSWを離した。電源は供給され続ける。

ポイントは、④⑤でマイコンが一定時間何もせず待つことである。

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時の動作は以下の通り。

f:id:kaname_m:20200201100424p:plain
電源ON状態

①SWを押していない時、R3がつながっている11ピン(入力ピン)はR1を介してLレベルである。

f:id:kaname_m:20200201100427p:plain
SWを押していない時、11ピンはLレベル

②ユーザがSWを押すと、11ピンはHになる。同時にQ2がONになるが、もともとT1を介してQ1がONしているので関係ない。

f:id:kaname_m:20200201100432p:plain
SWを押すと11ピンはHレベル

③11ピンのHレベルを検知したら、マイコンは一定時間12ピン(T1)をHのまま待つ。

④一定時間後も11ピンがHのままだったら、LEDやディスプレイなどのインジケータをOFFする。ユーザは電源OFFを「認識」する。同時に12ピンをLにする。T1がOFFになる。(実際にはまだQ2がONのため、電源供給は続いている)

f:id:kaname_m:20200201100437p:plain
一定時間SW ON(11ピンH)を検知したらT1とインジケータをOFF。ユーザは「電源OFF」を認識する。

⑥ユーザはインジケータのOFFを見て、SWを離す。Q2ゲートはLになり、Q2はOFFになるため、Q1ゲートはHになり、Q1がOFFする。結果として回路全体の電源供給はOFFする。

f:id:kaname_m:20200201100400p:plain
SWが離され、本当に電源供給が止まる

ここでも同様にポイントは③④でマイコンが一定時間何もせずに待つことであり、一定時間経ってインジケータをOFFすることであたかも長押しで電源OFFしたかのように見える。

もし、ユーザが長押しでなく、短時間でSWを離した場合は、12ピンはHのまま、すなわちT1はONのままなので電源供給は続く。

これで、長押しによる電源OFFが実現した。

この回路を実際に使ってみる

この回路は、長押しの長さをマイコンで設定できるので、ユーザの好きな長押し時間が作れる。ちょっと便利。

また、手に入れやすい部品で思いつく限り最小構成で組んでいるため、チップ部品を使えばかなり電源回路を小さく作れる。

最初に示した動画を再掲するが、上の回路を入れており長押しで電源ON/OFFする。(このペンについてはいずれ記事にしたい)

スイッチ長押しでON/OFFできると、なんか「それっぽい」感じがする。

世の中のモバイル機器も同じような回路を組んでいるんだろうか??