West Gate Laboratory

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

3Dプリンタで我が家専用チューブ調味料スタンドを作成する

概要

家の冷蔵庫のチューブ調味料ゾーンがあまりにも混沌としていたので、3Dプリンタ我が家専用のオーダーメイドチューブ調味料スタンドを作って整理整頓した話。

背景

料理にチューブ調味料は欠かせない。

特に、私はカレーを作るのが趣味でよくカレー粉やスパイスを使って作るのだが、しょうがやにんにくをすりおろすのがだんだん面倒になってきて、最近はチューブ調味料を使っている。
しかし、こうした調味料が増えてくるとその整理整頓が課題になる。
特にチューブ調味料の場合、中身が少なくなってくると逆さにして保管するわけだが、チューブ形状によっては逆さにすると不安定なものも多い。

結果、先日までの我が家のチューブ調味料及びドレッシング等のゾーンはこの状態。(汚くて申し訳ない)

f:id:kaname_m:20200524134249j:plain

生にんにくは倒れ、一つが倒れると他もいいやとばかりに生しょうがが倒れ、なぜか安定しているはずの奥の味ポンまで倒れている。 冷蔵庫の扉部分に収納されるため、扉の開閉の際にも倒れる力が加わり、固定されていないとこうした細長い形状のものは倒れてしまうのだ。

ちなみに、探してみるとチューブ調味料整理のためのホルダーが売っていたりする。

薬味チューブホルダー クリアー

薬味チューブホルダー クリアー

  • メディア: ホーム&キッチン

別にこれを使ったっていいのだが、せっかくなので今回は我が家のレギュラー調味料専用のスタンドを作成してみる。

オーダーメイドチューブ調味料スタンド

特に難しい問題はないのだが、順番としては

  1. チューブのサイズ計測

  2. チューブの配置決定

  3. 3Dモデル設計、印刷

となる。

チューブのサイズ計測

我が家のレギュラーチューブ調味料は、「生しょうが」「生わさび」「生にんにく」「マンゴーチャツネ」この4つである。
マンゴーチャツネはあまり聞かないかもしれないが、カレーに入れるとおいしいのだ。

さて、チューブ調味料だが、メーカーや中身によってサイズは様々である。
そのため、ひとつひとつノギスで計測していく。

チューブスタンドではおもにフタ部分を固定するため、フタの中で最も太い、根元部分を計測する。
例えばマンゴーチャツネのフタは、最も太い部分で22.5mm程度だ。

f:id:kaname_m:20200524135231j:plain

その他も同様に計測すると、結果は以下の通りであった。

生にんにく、マンゴーチャツネ:約22.5mm
生しょうが:約29mm
生わさび:約33mm

チューブの配置決定

サイズが計測できたら、次に配置を決めていく。

冷蔵庫の調味料置き場の面積はそれほど広いわけではいので、なるべくきっちり詰める必要がある。
また、手前に小さい調味料がある方が取りやすいだろう。

隣と調味料が干渉しない間隔を計測する。

f:id:kaname_m:20200524135932j:plain

こうして我が家にとって最適な配置を決定していく。

こうして決まったのがこの配置である。(上が冷蔵庫奥側)

f:id:kaname_m:20200524140442p:plain

手前に小さめの調味料2つ、奥側に大きめの調味料2つ。手前にはもう1つ分スペースがあるので、それは予備とした。

3Dモデルの設計、印刷

さて、ここまでできたら後はCADで設計するのみである。

といっても形自体は非常に単純なので設計はすぐに終わる。
完成図がこちら。

f:id:kaname_m:20200524140619p:plain

奥の方に薄板が延びているのは、そこに重量のあるビン調味料を置くことでスタンド自体の安定性を上げる目的だ。
スタンド自身は樹脂で軽いので、それだけだと倒れてしまう可能性がある。
また、チューブの入れ口を適度に面取りすることで、チューブを入れやすくしている。

これを3Dプリンタで印刷する。サイズが大きいので、4~5時間かかった。
我が家ではFlashForgeのAdventurer3を使っている。

f:id:kaname_m:20200524140940j:plain

できあがったスタンドに実際にチューブ調味料を入れてみる。

f:id:kaname_m:20200524141333j:plain

うん。ピッタリである。

さらに、これを冷蔵庫に入れてみる。

f:id:kaname_m:20200524141041j:plain

スタンド自体も冷蔵庫のサイズに合わせて作ったので、ピッタリである。良い。

まとめ

冷蔵庫のチューブ調味料が混沌とする問題を、専用のスタンドを3Dプリンタで作ることで解決した。
非常に小ネタではあるが、精神衛生上良い影響がある気がする。

このスタンドを作ったのは2週間ほど前だが、この記事を書いた今日現在もチューブは倒れることなく冷蔵庫で立っている。

EagleとFusion360を連携させて回路設計から筐体設計まで一気通貫で行う

概要

回路設計ソフトウェアであるEagleと、3DCADソフトウェアのFusion360を連携させる方法について。
特に、Eagle-Fusion360間の部品の3Dパッケージの連携方法について述べる。

背景

今や、ホビーユースであればEagleも、Fusion360も無料で利用することができる。とてもいい時代だ。(Autodeskありがとう)
特に、EagleがAutodeskに吸収されてから、EagleとFusion360の連携機能が強化されてきた。
今では簡単にEagleで設計した回路を3Dの状態でFusion360にエクスポートし、自分の基板専用の筐体をそのまま設計することができる。
さらに、3Dプリンタが家にあれば、その場で筐体を印刷することも可能だ。

例えば、こんなものができる。
先日記事にしたピアノ演奏可視化装置の回路を例に取ってみる。

westgate-lab.hatenablog.com

Eagleで回路設計をし、

f:id:kaname_m:20200509170524p:plain

それをFusion360に取り込み、

f:id:kaname_m:20200509170533p:plain

f:id:kaname_m:20200509170539p:plain

それに合わせて筐体を設計できてしまう。

f:id:kaname_m:20200509170551p:plain

筐体のモデリングができたら、3Dプリンタで専用ケースも作れてしまう。

f:id:kaname_m:20200606190641j:plain

フタも印刷すれば専用デバイスの出来上がりだ。

f:id:kaname_m:20200606190644j:plain

ただ、3Dの情報を含めきちんと両者を連携させるには、3DパッケージをEagleのライブラリに正しく取り込む必要がある。
この記事では、Eagleに3Dパッケージを取り込み、Fusion360と連携させる方法について述べる。

Eagleで3Dパッケージを扱う場合、既存の3Dパッケージをダウンロードして使う方法と、自分で3DCADを使って3Dパッケージを作成し、それをEagleに取り込む方法がある。

既存の3Dパッケージをダウンロードして使う場合

Eagleにも一部の表面実装コンデンサなど、デフォルトで3Dパッケージが入っているデバイスもあるが、大半は3Dパッケージが存在しない。そのため、自分で用意する必要がある

今回は、ピアノ演奏可視化でも使っているESP32-DevkitCボードを題材にする。

まず最初に3Dパッケージをインターネットからダウンロードするか、自分で作成するかの分岐がある。
ESP32-DevkitCのようなメジャーなモジュールはたいてい誰かが3Dパッケージを作ってくれているので、それを探して使わせてもらうことにする。

3Dパッケージを探す

3Dパッケージの検索には、以下のサイトが便利だ。

www.snapeda.com

grabcad.com

まずは、既存の3Dパッケージが存在しないか、上のようなサイトを使って検索することになる。
ESP32-DevkitCボードについては、例えば「ESP32」と検索してつらつら見ていくと、ESP32-DEVKITC-32Dのパッケージが存在する。

www.snapeda.com

上のページを見ていただくとわかるように、ありがたいことにシンボルとフットプリント(.lbr)・3Dモデル(.step)の全てがダウンロードできる。
SnapEDAはアカウントが必要なので、アカウント登録後、シンボル・フットプリント・3Dモデルをダウンロードする。

f:id:kaname_m:20200509170556p:plain
ESP32-DEVKITC-32Dの2Dデータ(SnapEDAより)

f:id:kaname_m:20200509170559p:plain
ESP32-DEVKITC-32Dの3Dデータ(SnapEDAより)

なお、今回はシンボル・フットプリントもない状態から始めたため、運良く見つかった上のライブラリを使わせてもらったが、3Dモデルはあってもライブラリがない、という場合もある。(GrabCADは3Dモデルのみ)
そうした場合は自分でシンボルやフットプリントをEagleで新規作成する必要がある。
Eagleの詳しい使い方は、以下の書籍が詳しい。
Eagleを使って回路設計する人は1冊持っておくと便利である。

EAGLEによるプリント基板製作の素

EAGLEによるプリント基板製作の素

  • 作者:後閑 哲也
  • 発売日: 2009/11/27
  • メディア: 単行本(ソフトカバー)

シンボル・フットプリントをEagleに読み込む

まずは、Eagleに2Dデータ(シンボル・フットプリント)、つまりダウンロードした.lbrファイルを読み込む

EagleとFusion360を連携させる場合、シンボルや3Dパッケージなどの部品データは全てAutodeskのクラウドLIBRARY.IO)上に保存する必要がある。(ローカルにあるライブラリは連携に使えない)
まずは、EagleのControl Panel上のLibrariesにあるアカウント名のフォルダを右クリックすると、"View on web"とあるので、これを選択し、LIBRARY.IOの自分のアカウントページを開く。

f:id:kaname_m:20200509173846p:plain

LIBRARY.IOのマイページ上に”Import Library”があり、”Upload File”とあるので、そこから先程ダウンロードした.lbrファイルを選択する。

すると、その下の”Library from (アカウント名)”のところにアップロードしたライブラリが表示されるはずだ。

f:id:kaname_m:20200509174033p:plain

次に、LIBRARY.IOにアップロードしたライブラリを、Eagleにダウンロードする。

EagleのControl Panelの左ペインにあるLibrariesを右クリック、Open Library Managerを選択すると以下の画面になるので、AvailableタブでESP32などと検索すると、先程アップロードしたライブラリが現れる。(以下の画像ではローカルに保存した別のESP32のライブラリが出ているが、ここでは無視する)

f:id:kaname_m:20200509170612p:plain

ダウンロードマークがついているライブラリを選択、右下のUseを押すとライブラリがダウンロードされ、Control Panel上でも確認できるようになる。

f:id:kaname_m:20200509170616p:plain

3Dデータ(STEPファイル)をライブラリに取り込む

次に、先程ダウンロードしたSTEPファイルをこのライブラリに関連付ける。
ダウンロードした"ESP32-DECKITC-32.lbr"を開くと、ライブラリのデバイス一覧が開ける。
フットプリントやシンボルは正しく表示されるが、3Dパッケージはまだ取り込んでいないため、ただの四角い板のようなモデルになっている。

f:id:kaname_m:20200509170620p:plain

それでは、ここに先程のSTEPファイルを取り込む。

関連付けたい3Dパッケージ名を右クリック、Editを選ぶと以下のパッケージ編集画面が開く。

f:id:kaname_m:20200509170625p:plain

f:id:kaname_m:20200509170628p:plain

まだ3Dパッケージが反映されていないので、板状のモデルが配置されている。
ここで、上の”Upload”から先程のStepファイルを選ぶ。するとLIBRARY.IOのマイアカウント上にStepファイルが取り込まれ、編集中のライブラリにも自動で取り込まれる。
(すでにStepファイルがアップロード済みの場合は、”Remove 3D Model"で板状モデルを削除し、”Add"からモデルを選択しても良い)

f:id:kaname_m:20200509170633p:plain
UploadしたSTEPファイルが取り込まれた状態

3Dパッケージの位置・向きの調整

さて、向きや位置をここから調整する。

3Dモデルをクリックすると向きや位置を調整できる。
まずは向きを90度変更し、Z軸で高さを調整する。

(2020年6月6日追記)
ピアノ演奏可視化装置で使ったESP-WROOM-32の基板厚さが1.6mmなのに対し、今回使ったモデルは基板厚みが0.9mmだった。
そのためZ軸をぴったり合わせると0.7mmZ方向にずれてしまい、後の筐体設計のときにUSBコネクタ位置がずれてしまう。
そのため、このモデルを使ってESP-WROOM-32を模擬する際は、0.7mmオフセットさせるのが良い。

f:id:kaname_m:20200509170638p:plain

下から見てピン位置がぴったりになるようXY軸を調整。

f:id:kaname_m:20200509170642p:plain

位置調整が完了したら、OKをクリックし、保存する。

f:id:kaname_m:20200509170647p:plain

EagleのLibrary画面に戻るので、Ctrl+Sで保存。すると、「Managed Libraryが更新されたからLibraryメニューのCreate New Versionを選択してね」と出る。
忠告に従いLibrary→Create New Versionを選択。するとここの変更内容がLIBRARY.IOにアップロードされる。
(このとき、3Dパッケージのプレビュー画面が歯車マークのままの場合があるが、Fusion360では正しく反映されるので、そのまま進んで構わない)

実際に基板を設計してみる

さて、ここまでで作成した3Dパッケージ入のESP32-DEVKITCボードを使って回路を設計してみる。
といってもFusion360との連携の試しなので、まずは回路上にただESP32-DEVKITCを置いただけだ。

f:id:kaname_m:20200509170651p:plain
Eagleで基板上に1つだけESP32-DEVKITCを配置する

基板設計が終わったら、Eagleの基板設計エディタの右端に「FUSION 360」というボタンがあるので、それをクリックすると、Fusion360との同期画面がポップアップされる。

f:id:kaname_m:20200509170704p:plain

一番下に"Push to Fusion..."とあるので、これをクリック。どうやら内部ではGitのようなバージョン管理が行われているようだ。
すると既存のモデルにリンクさせるか?と聞かれる。今回は新しいモデルなので、”Create new Fusion 360 design”を選択する。次にPush先のプロジェクトを選択し、Pushする。(これが少し時間がかかる)

f:id:kaname_m:20200509170654p:plain

Pushが終了したら、Fusion360に移る。
Pushしたプロジェクトを見てみると、先程Pushした基板の3Dモデルがあるはずだ。

f:id:kaname_m:20200509172006p:plain

3Dモデルも正しく反映されている。良い良い。

Fusion360へ基板をPushした後、連携が切れてしまった場合

Fusion360やEagleを再起動すると、デザイン同士のリンクが切れてしまうようである。
一度Pushして3DモデルをFusion360に取り込んだのに、Eagle基板設計エディタのFUSION 360をクリックすると以下の画面が出てくる場合がある。

f:id:kaname_m:20200510231351p:plain

このときは、すでにあるデザインに再度リンクすれば良いので、上の"Link to an existing Fusion 360 design"を選択し、リンクしたい既存のデザインを選択する。すると再度Pushできるようになる。

回路の修正を反映する

一発で設計終了すればいいが、実際には回路を何度も修正することになるだろう。
その場合、Fusion360に反映させるにはその度に先程のPushをすれば良い。

例えば、ボードの位置が気に食わなかったので、基板の左側に移動したとする。

f:id:kaname_m:20200509170716p:plain

すると、画面右の”FUSION 360"が緑色になり、要Pushのステータスになるので、先程と同様の手順でPushする。
同期画面では、修正がFusion360の3Dモデルに未反映のため”OUT OF SYNC”となっている。

f:id:kaname_m:20200509170719p:plain

再度Push to Fusionし、Fusion360に戻ると「モデルが更新されました」と出るので”更新”をクリック(これがない場合でも、一度スケッチを閉じて再度開けば更新される)。

f:id:kaname_m:20200509170519p:plain

ボードが基板左側に正しく移動したことが反映された。素晴らしい。

今回は例として1デバイスのみの場合を示したが、同じ作業を抵抗やコンデンサ、その他の部品に対して行うことで頭に示したような回路の3Dモデルが作成できる。

自前で3Dモデルを作成する場合

マイナーな部品の場合はネット上に3Dモデルがないため、自分でモデルを作る必要がある。
シンボルやフットプリントがない場合はそこから自分で作る必要があるが、それはすでにあるものとする。

今回は、秋月電子で売っているこのブザー(PB04-SE12HPR)を例にとってみる。

f:id:kaname_m:20200510231406j:plain
PB04-SE12HPR(秋月電子HPより)

電子ブザー 14mm PB04−SE12HPR: パーツ一般 秋月電子通商-電子部品・ネット通販

流れとしては、以下の通り。

  • データシートに合わせて3DモデルをFusion360で作成する。
  • STEPファイル形式でエクスポートする。

以降は上述したダウンロードして使う場合の、LIBRARY.IOにインポートするとこから同じである。

データシートを入手する

上で述べた3Dモデルのサイトを散々探してどうにも見つからない、となれば、外形の情報を得るため、データシートを入手する。
秋月電子の商品ページから飛べるデータシートの中に、パッケージの情報が含まれている。

f:id:kaname_m:20200510231412p:plain
ブザーのパッケージ図(データシートより)

この情報に従い、Fusion360でモデルを作成する。
どこまで詳細にモデリングするかは好みだが、最低限端子部分の寸法と縦横高さが合っていれば問題ないだろう。今回はボディの色と端子の色はざっくり合わせてみた。

f:id:kaname_m:20200510231415p:plain

f:id:kaname_m:20200510231341p:plain

STEPファイル形式でエクスポートする

さて、モデルができあがったらそれをエクスポートする
Fusion360のデザイン画面からファイル→エクスポートで.stepを選択し任意の場所にエクスポートする。

ここから先は上述の”3Dデータ(STEPファイル)をライブラリに取り込む”以降と同じだ。

f:id:kaname_m:20200510231355p:plain

ライブラリをアップデートする場合

ライブラリそのもの、例えばシンボルやフットプリント、3Dモデルの位置調整などを行った場合はライブラリをアップデートし、それを反映する必要がある。

例えば、先程作成した3Dパッケージの位置を微修正する場合は、まず該当する3Dパッケージが含まれるライブラリをEagleで開き、 3Dパッケージ→Editでパッケージの編集画面に入る。
編集画面で先程と同様に位置・向きを修正し、終わったらOKで保存。
Eagleのライブラリ画面に戻ると3Dパッケージを登録したときと同様に「ライブラリが更新されたから”Create new version”してね」と出るので、Library→Create new versionでライブラリのバージョンを上げる。

ライブラリのアップデートが終わったら、次にこの変更をEagleの設計に反映する必要がある。 Eagleの回路または基板エディタでLibrary→Update allで使用しているライブラリを含めてアップデートする。

f:id:kaname_m:20200510231403p:plain

アップデートが正しく完了すると、またFusion360に反映しなさいということでエディタ右側のFUSION360ボタンが緑色になるので、同様にPushする。

f:id:kaname_m:20200510231400p:plain

Fusion360に戻り、デザインを更新すると、修正されたモデルが反映されているはずだ。(デザインが開かれている場合は、一度閉じて再度開くこと)

まとめ

回路設計ソフトウェアのEagleと3DCADソフトウェアのFusion360を連携させて回路基板の3Dモデルを作成する方法を述べた。
これを使うと回路設計をEagleで行い、Fusion360で取り込んでその基板に合わせた筐体設計が可能になる3Dプリンタを持っていればその場で専用の筐体を印刷することも可能である。

ピアノ演奏を可視化する装置作ってみた(それを使って弾いてみた)

概要

私は電子工作と別にピアノも趣味なのだが、最近は有名どころのクラシックを弾いている。
例えば、ショパンベートーヴェン・リストなどだ。

ただ弾くだけでも楽しいのだが、演奏が音だけでなく視覚でも楽しめると良いかな、と思い、ピアノ演奏を可視化する装置(以下、ピアノディスプレイ)を作ってみた。

こんな感じ。

また、このピアノディスプレイはMaker Faire Kyoto Online 2020にもオンライン出展した。この記事ではその製作過程を述べる。

背景

よくYoutubeでクラシックピアノ演奏動画を見たりするのだが、ある日Rousseau氏の動画を見かけた。

www.youtube.com

Rousseau氏はピアノの演奏とその演奏を可視化した動画を組み合わせて非常にきれいな作品を投稿している。
どうやら、動画の上半分は記録したMIDIをもとに動画を生成し、上下で動画を組み合わせているようである。

めっちゃきれいやん、と思い、早速自分でも作ってみることにした。
ただ、動画で後から組み合わせるのではなく、リアルタイムに可視化するディスプレイとした。

CADでハードウェア設計

Fusion360を使って設計をすすめる。
今回、ピアノの実寸大でディスプレイを開発するため、横幅は1mを超えるサイズになる。

f:id:kaname_m:20200502143222p:plain

鍵盤それぞれに対し、15個のフルカラーLEDを配置した。(後述:NeoPixel)
ピアノの鍵盤は88鍵あるので、全部で1320個のフルカラーLEDを使用している。 また、光らせた時に見栄えが良くなるよう、前面にはスモークアクリル板を設置している。

f:id:kaname_m:20200502143434p:plain

ちなみに、Fusion360レンダリング機能を使うといい感じの画像も作れる。それっぽいそれっぽい。

f:id:kaname_m:20200502143504j:plain

できるだけスタイリッシュな見た目にするため、極力前面にはネジなどの機構部品が見えないようにした。
アクリルの四隅の支持材も同様にFusion360で設計して、3Dプリンタで印刷した。

実際に作っていく

ハードウェア設計が終わったら、実際にものを集めて作っていく。

まずは、鍵盤サイズのアクリル板を用意する。

f:id:kaname_m:20200502143442j:plain

アクリル板の上にNeoPixelのLEDストリングを切って貼っていく。
今回使用したのはWS2812の300個/5mのものだ。

この際、鍵盤の寸法にピッタリ合うよう、コンマ1mm単位で位置を調整する必要がある。

f:id:kaname_m:20200502143449j:plain

LEDストリングを貼り終えたら、全てのストリング1つ1つ配線していく。

このとき注意しなければいけないのは、電源ラインである。
LEDストリングは接続数が増えていくと電流量が結構ハンパないことになるため、単純にストリングを全接続すると末端のLEDはまともに光らなくなる。
例えば、今回使用しているNeoPixel(WS2812B)は、LEDを白色最大光量にすると大体1個50mA消費する。
両手で10鍵押し続けると、15x10=150個のLEDが光り、単純計算で7.5Aの電流が流れることになる。

もちろん、実際には白色には光らせないのでもう少し電流量は少なくなるのだが、単純にLEDストリングをつなげると、末端のLEDに供給されるまでに電圧降下してしまい、以下のツイートのようにまともに光らなくなる。

というわけで、このピアノディスプレイでは銅テープをバスバーとしてディスプレイの上下に配置し、そこからそれぞれのLEDストリングに電源を供給することとした。

f:id:kaname_m:20200502143452j:plain

銅テープは5mm幅のものを用いた。

3M  銅箔テープ No.CU-35C 5mm幅 x 20m

3M 銅箔テープ No.CU-35C 5mm幅 x 20m

  • メディア: Tools & Hardware

ちなみに、LEDストリングの信号ラインをつなげるために合計87本の短い単線をつなげた。これが一番疲れた気がする・・・。

f:id:kaname_m:20200502143500j:plain

ソフトウェア

さて、次にソフトウェアの話。
今回はESP32を使って開発した。

MIDIをESP32で読み取る

ピアノディスプレイでは、電子ピアノのMIDI OUTをESP32で読み取っている。

ArduinoMIDIを解析する方法としては、以下の記事が詳しい。

qiita.com

MIDI OUTは、基本的には31.25kbpsのUART信号なので、マイコンで簡単に読み取ることができる。
ただ、出力信号とマイコンの入力はアイソレートする必要があり、高速フォトカプラが必要だ。

f:id:kaname_m:20200502143703p:plain
MIDI標準ハードウェア(MIDI1.0規格書より)

このあたりのハードウェア仕様はMIDI1.0規格書に詳しく記載されている。
規格書には、MIDIに適合する高速フォトカプラのリストも記載されている。

NeoPixel

みんな大好きNeoPixel。 使い方についてはネット上に多く記事が存在するので、ここでは多くは書かないが、今回はFastLEDライブラリを使用した。

github.com

NeoPixelのライブラリとしては、FastLEDの他にAdafruit_NeoPixelがあるが、Adafruitのライブラリは以下のツイートのようにLEDを多数接続した際に正しく点灯しないため、ピアノディスプレイのように多数のLEDを接続する場合はFastLEDがオススメだ。

マルチタスク

ピアノディスプレイでは1320個のNeoPixelを制御しているが、それだけたくさんのLEDをつなげるとその制御だけで(時間的に)大半のCPUリソースを食ってしまい、他の処理(MIDI入力など)ができなくなる。

そこで、ESP32はデュアルコアなので、1つのCPUをNeoPixelの制御に、もう1つのCPUをMIDI処理などに割り当てMIDIの取りこぼしやLEDの表示ミスなどがないようにする。

ESP32(M5Stack)を使ったマルチタスクの方法については以下の過去記事参照。

westgate-lab.hatenablog.com

最終形態

ディスプレイ部分まで完成したところで、Maker Faire Kyoto Online 2020に出展した。

ピアノの各鍵盤をHSV色空間の色相に割り当てると、こんな感じにゲーミングピアノのようになる。

2020年5月2日のMaker Faire Kyoto Online 2020に向けて最低限のところまで開発したため、まだ回路などは完成していない。(ブレッドボードがディスプレイの裏にぶら下がっている)

他にも、いろんな光らせ方の実装やHIDデバイスと連携させたシーケンスの実装など、色々やりたいことはあるのだが、それらは別途記事にしていきたいと思う。

さぁ、ピアノを目で楽しもう。

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