OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

RealSense D435(距離計測カメラ) & T265(自己位置認識カメラ) をROS上で動かした

f:id:motokiinfinity8:20190605001720j:plain

はじめに

現在、息子の遊び相手になってくれる自律移動ロボット 及び 息子の手動車いすを電動化かつ半自動運転化する仕組みを個人で検討中。
その中で、今更ながら Intel Realsenseカメラを入手することができたので、導入に向けて早速立ち上げてみました。

Realsenseの様な深度計測カメラは2年ほど前に、Realsense R200 / SR300 を使ったことはあり、これをROS上に組み込んで、ペットボトルを高速で追従させたり、
しまいには子供の顔位置を認識させてその口元に正確にスプーンを運ぶ「自動ご飯食べさせアームロボット」を本気で制作したりしていました(遠い目)

しかし、R200もSR300 も Intel提供のドライバが不安定だったのか、一度開発環境が崩れてしまって、その後に復旧できないという悲劇に襲われてしまい、それきりになってしまいました。
ファームウェアエンジニアの友人に解析協力を依頼しましたが、結局ハマってしまい、復活不可という自体になってました)

今回のD435 / T265のドライバはだいぶ安定しているという噂を聞いたので、以前の悪夢を忘れられたらよい、ですが。。。

Realsense D435

普通のカメラ同様のRGB映像の取得に加えて、ステレオビジョンによる距離計測が可能なカメラです。最小11cm〜最大10mまでの各映像の奥行き値を計測可能とのことです。
そのため、主な用途は
 ・物体検出時に物体との距離が分かる
なので、ものを掴むための位置を把握したり、障害物との距離を検知したり、かなり広い用途での活躍が期待できます。
近距離用だったSR300と比べても圧倒的にコンパクトなので、幅広いロボットに取り付け可能なのも利点。

ちなみに、IMU(加速度/ジャイロ)搭載のD435i という品番もあります。今から買うならこちらの方が良いかも。
(単独でVisual SLAMができます)

Realsense T265

2対の魚眼カメラと、IMU(加速度/ジャイロ)搭載しており、カメラだけで完結してVisual SLAMが実行できます。
つまり、主な用途は、
 ・カメラが移動した際の現在位置情報を把握できる
なので、移動ロボットに搭載しておくだけで簡単に現在位置が把握できるということですね。Visual SLAMなので、これだけだと絶対的な位置が分からない(あくまで実行開始場所からの相対位置)のと、特徴の少ない場所(例えば、真っ白な部屋)では使えないなどのデメリットはあるものの、日常的な場所で使いモノになるのかは、じっくり見ていきたいと思います。

初期確認 @ D435

Windows上でまずは動作確認をしてみました。
ツールは以下を参照。
Releases · IntelRealSense/librealsense · GitHub

RealSense SDK もしくはViewerをダウンロード して確認します。普通のRGBカメラ映像 と 距離画像(グレイスケール)の映像が見えれば、カメラ自体は正しく動いているのがわかりました。

D435のファームアップ

D435購入時期が古かったため、念の為もカメラのファームウェアバーションを上げておきます。

以下のサイトを参照にして、バージョンを5.9.2→5.10.13(当時では最新)に変更しました。

Download Latest Firmware for Intel® RealSense™ D400 Product Family
からZIPファイルをダウンロード。Signed_Image_UVC_5_10_13_0.binファイルがあったので、
Download Windows* Device Firmware Update Tool for Intel® RealSense™ D400 Product Family
にあるIntel RealSense D400 Series DFU Tool for Windows.zipツールもダウンロードして、これを起動する。
③ ②でダウンロードしたintel-realsense-dfu.exeを「管理者として実行」を行う
④ Realsense D435をPCのUSB3.0に刺した状態で、「2」を選択。カメラバージョンが読める事を確認する
⑤ 「1」を選択してバージョンアップ作業開始。「1」

Ubuntu上でのカメラ動作確認 (D435/T265)

インストール環境

私の開発環境に合わせて以下を入れました
 ・開発PC : Intel Core i5-8250U
 ・開発OS : Ubuntu16.04 LTS

インストール手順

naonaorange.hatenablog.com

上記サイトを参考にしながら設定を進めました。
まずは関連パッケージのインストール

sudo apt-key adv --keyserver keys.gnupg.net --recv-key C8B3A55A6F3EFCDE || sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C8B3A55A6F3EFCDE
sudo apt-get update

そして、ここからrealsenseドライバのインストールになります。D435だけで使用していた際には、aptから realsense2関連パッケージを入れていたのですが、
今回はT265で SDK1.22.0 以降が要求されております。 (なぜかaptから入れると 1.18.1から更新されない…)
なので、私は一旦、公式gitサイトからSDK一式を落としてきて、buildしておりますのでご注意を!

github.com
にしたがってインストール。

git clone https://github.com/IntelRealSense/librealsense.git
sudo apt-get install git libssl-dev libusb-1.0-0-dev pkg-config libgtk-3-dev
sudo apt-get install libglfw3-dev
./scripts/setup_udev_rules.sh
./scripts/patch-realsense-ubuntu-lts.sh
sudo make install
カメラ画像確認方法

realsense-viewer で SDK起動。
まず、D435を接続して… 普通のRGBカメラ映像 と 距離画像の映像が見えることを確認します。
f:id:motokiinfinity8:20190605013557p:plain
左側が距離画像、右側がRGBカメラ画像になります。左側の距離画像を見ると…前方のおもちゃの土台までの距離は32cmとなっています。
実際にものさしで測ってみると…33cm程度なので誤差は1cm程度、とても優秀です!!
RGB映像と距離画像は画角が違う様で近距離になる程、ずれ量がある点には注意。おそらく画素位置の補正は必要だろうなぁ〜

次に T265 を接続。普通に魚眼カメラ映像×2が見えます。同時に画面右側にはソフト起動時を原点とした座標を表示しています。
f:id:motokiinfinity8:20190605014559p:plain
こちらは座標の軌跡を3D表示しています。座った状態でカメラを起動した後、立ち上がってぐるっとリビングを移動したので、その軌跡が残っています。
しっかりとloop closeできているので、それなりに自己位置認識の精度はありそうですね。
f:id:motokiinfinity8:20190605014642p:plain

ROSでの起動方法

以下のサイトからダウンロード。
github.com

あとは、catkin_make のビルドでOK

D435確認方法

RGB映像の確認方法は以下

roslaunch realsense2_camera rs_camera.launch
rosrun image_view image_view  image:=/camera/color/image_raw

上記に加えて、深度映像を確認します。

rosrun image_view image_view image:=/camera/depth/image_rect_raw

で深度映像が見えます。

次に、D435の特徴である画像をPointCloud で取得できます。これをRviz上に表示してみます。

roslaunch realsense2_camera rs_rgbd.launch enable_pointcloud:=true align_depth:=false depth_registered_processing:=true align_depth:=true

別ターミナルにて

cd ~/catkin_ws/src/realsense2_camera/rviz
rviz -d pointcloud.rviz

おぉ、各画素位置が深度情報に応じて立体的にマッピングされている! なんだか面白い(月並みなセリフ)
[RGB画像]
f:id:motokiinfinity8:20190605215252p:plain
[PointCloud画像]
f:id:motokiinfinity8:20190605215329p:plain
これを使えば Visual SLAMが綺麗に可視化される訳ですね!

T265確認方法

asukiaaa.blogspot.com
を参照しています。

roslaunch realsense2_camera rs_t265.launch

で起動して、その後に

rostopic echo /tf

でカメラの現在位置(
カメラを動かすと x,y位置が変わって、自己位置(相対位置)が取得できることが分かります。

また、

roslaunch realsense2_camera demo_t265.launch

を使えば、 起動時の場所を原点として、カメラの現在の位置と角度がrviz上に表示されます。おぉ、素晴らしい!
f:id:motokiinfinity8:20190605221924p:plain

今後の参考文献 (都度更新)

本ブログをアップする直前のタイミングで、からあげ先生のサイトで「D435+Jetson Nano」の設定方法がアップされてました(光栄です!)
今後のJetson移植時に参考にします。
qiita.com

D435でVisual SLAMをするなら参考になりそうなサイトです。
qiita.com

まとめ

周囲の障害物との距離を算出できる距離計測カメラD435 と 周囲の画像を元に自己位置を認識するカメラT265 をROS上にて動かすための情報を整理してみました。
今回は一旦はノートPC上で実装しましたが、本当はロボットにのせるエッジデバイスでカメラは使用するべきなので、最終的にはそちらで再度環境を立ち上げようと考えています。
ちょうどいま、Jetson Nanoを注文しているので、入手次第 そちらでの環境セットアップを進めて、簡単な自律移動ロボットで遊んでみたいと思います!

ROS2 crystalをUbuntu16.04にインストールして動かしてみた

f:id:motokiinfinity8:20190518103128j:plain

はじめに

ロボット向けミドルウェアROS(Robot OS)。
複数センサー/デバイスを分散処理しやすい、センサー可視化ツールが豊富、オープンソースで利用者が多い等、現在ロボットソフトのデファクトスタンダードになっているソフトウェアです。個人でも、業務でも、ROSを使った開発をする機会も増えてきています。ロボットは関連技術分野が広く、すべてを自分で作るのは本当に大変なので、この様なオープンソースパッケージを活用して簡単に自立移動が実装できるため、個人開発/研究用途に非常便利です。

ROS2は、ROSの思想を汲みながら、ROSの課題であるコンポーネント間通信の改善(リアルタイム性、信頼性)を行ったものになるそうです。
特に、ロボットを展示会・研究の世界から、実用の世界に持っていくのにここは重要と思ったので、ちょうどROS Japan主催の勉強会に参加してきたので、勉強メモがてらに残したいと思います。

rosjp.connpass.com

ROS2について

撮影した写真をいくつか抜粋
f:id:motokiinfinity8:20190519211835j:plainf:id:motokiinfinity8:20190519211906j:plain
f:id:motokiinfinity8:20190519211921j:plain

ROS2の特徴は
①新しい通信プロトコル(Data Distribution Service)
  - ROS Masterがいらない、信頼性が高い、速度アップ
  - 様々なベンダー実装品と入れ替え可能

②リアルタイム性の管理
 - ノードの実行順も管理可能

③組み込み機器/Ubuntu以外のOSもサポート

だそうです。

ROSだと、リアルタイム性が保証できていない点や、通信信頼性が低い点もあり、なかなか導入に踏み切れないのが現状なのかなぁ、と。
ROSを導入するとしても、結局自前で信頼性を上げる様に時間と人をかけて独自カスタマイズしたり、じっくり検証せざるを得なくなり、開発費がかさみ事業性が悪くなってしまう(=普及を阻害する要因になる)んですよね…
せっかくのオープンソースなのに勿体ない…

という訳で、ロボットの社会実装を目指す上でROS2の考え方は個人的には良さそうに感じました。

ROS2の現状

ディストリビューションとしては、2017/12に最初のリリース版Ardent、2018/6に2nd版 Bouncy、2018/12に3rd版 Crystal と更新されている様です。本記事記載時ではCrystalが最新版なのですが、2週間後(2019/5/31)には 4th版 dashingがリリースされる様です。
ちなみに、Dashingは2年サポートなので、今後ROS2を触ってみるなら Dasingの方がよいかも

なので、現時点でのCrystal版の記載内容がどこまで今後役に立つか分かりませんが(笑)、ROS2のイメージを学ぶにはちょうど良いかと。

2019/5/18時点でのROS2の稼働状況ですが
 ・メッセージ、サービス : 一通りばっちり動いている状況
 ・アクション通信 : 動いているがAPIはDasingで変更になる模様(何!?)
 ・Launchファイル : 大変更(xmlpython)の影響でまだ未実装
 ・Rviz : 一部機能のみ
上記以外のROSで愛用されてきたNavigationなどの汎用パッケージもどこまで動くかは不明なので、正直なところ、ROS同等の機能を実現するまではあと2,3年かかりそうな印象ですね。


とはいえ、2,3年したらROS2移行の流れになりそうな印象なので、今のうちから少しずつ情報キャッチしていくのが良いかも。

インストール

環境

私の開発環境に合わせて以下を入れました
 ・開発PC : Intel Core i5-8250U 1.8GHz
 ・開発OS : Ubuntu16.04 LTS
 ・導入ROS2 : ROS2 crystal

手順

Ubuntu18.04だとaptから入る様ですが、残念ながらUbuntu16.04だとcrystalはaptで入らない様なので、
ソースからビルドする形で立ち上げます。以下のサイトを参考にしました。

gbiggs.github.io
qiita.com

①PC設定変更
ROS 1が起動中の環境(source /opt/ros/kinetic/setup.bash等を実行した環境)でROS 2をインストールするとビルドでコケるっぽいので、まずは .bashrc から ROS1関連のファイルをコメントアウトして再起動しておきます。

②パッケージのインストール

$ sudo apt update
$ sudo apt install curl gnupg2 lsb-release
$ curl http://repo.ros2.org/repos.key | sudo apt-key add -
$ sudo sh -c 'echo "deb [arch=amd64,arm64] http://packages.ros.org/ros2/ubuntu `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list'

③ツールのインストール

udo apt update && sudo apt install -y build-essential cmake git python3-colcon-common-extensions python3-pip python-rosdep python3-vcstool wget
$ sudo -H python3 -m pip install -U argcomplete flake8 flake8-blind-except flake8-builtins flake8-class-newline flake8-comprehensions flake8-deprecated flake8-docstrings flake8-import-order flake8-quotes git+https://github.com/lark-parser/lark.git@0.7b pytest-repeat pytest-rerunfailures pytest pytest-cov pytest-runner setuptools
$ sudo apt install --no-install-recommends -y libasio-dev libtinyxml2-dev

④ROS2コードの取得

$ mkdir -p ~/ros2_ws/src
$ cd ~/ros2_ws
$ wget https://raw.githubusercontent.com/ros2/ros2/release-latest/ros2.repos
$ vcs import src < ros2.repos

⑤依存関係インストール

$ sudo rosdep init
$ rosdep update
$ rosdep install --from-paths src --ignore-src --rosdistro bouncy -y --skip-keys "console_bridge fastcdr fastrtps libopensplice67 libopensplice69 python3-lark-parser rti-connext-dds-5.3.1 urdfdom_headers"

⑥ビルド

colcon build --symlink-install --packages-ignore qt_gui_cpp rqt_gui_cpp

最後のビルドが時間かかって、だいたい1時間くらいかかりました。

⑦.bashrcへ設定追加
ROS1同様にパス設定が必要なので、.bashrcに追加します。

source /opt/ros/crystal/setup.bash

上記をホームディレクトリ内の.bashrcに追加すればOK

ROS2の動かし方

ROS2に関しては、現時点ではまだ参考書・書籍がありません。なので、勉強する場合は以下のサイトを活用すればよさそうです。
講習会でも、以下のサイトを使って演習を行いました。

gbiggs.github.io

インストール確認

サンプルデモソフトがあるので、そちらを使って動作確認します。

■1つ目のターミナルで以下を入力

ros2 run demo_nodes_cpp talker

すると、以下の様に1秒ごとにメッセージがpublish(送信)されます。

[INFO] [talker]: Publishing: 'Hello World: 1'
[INFO] [talker]: Publishing: 'Hello World: 2'
[INFO] [talker]: Publishing: 'Hello World: 3'
[INFO] [talker]: Publishing: 'Hello World: 4'

■2つ目のターミナルで以下を入力

ros2 run demo_nodes_cpp listener

すると、以下の様に1秒ごとにメッセージがsubscribe(受信)できればOKです。

[INFO] [listener]: I heard: [Hello World: 1]
[INFO] [listener]: I heard: [Hello World: 2]
[INFO] [listener]: I heard: [Hello World: 3]
[INFO] [listener]: I heard: [Hello World: 4]

ちなみに、ros core(Master)設定は不要です。また同じWifi環境で複数PCで同時に実行すると当然ながら混信するので、混信を避けるには以下のコマンドでID設定を変える必要がある

export ROS_DOMAIN_ID=xxx
ROS2ワークスペースの作り方 & ビルドの仕方

インストール時にビルドして作成したので、実は実行済でした。ポイントとしては、
・ パッケージ単位で管理 (package.xmlがあるフォルダがパッケージとして扱われる)
・パッケージ内に必要なファイル(CMakeLIsts.txt)やディレクトリ構成はROS1とあまり変わらない。
 (記載の仕方は異なっているが)
・ colcon でパッケージビルド
 colcon build
・ビルドし直すたびにアクティベートしなおさないといけない(忘れると更新されないのでハマる)
 source install/local_setup.bash

ROS2 ノードの記載方法

・ノードの実装はヘッダーファイルとソースファイルの2つのファイルにある
・ノードの実装をネームスペースに置く記載の仕方をしている
・独自メッセージ、独自サービスの作成イメージは ROS1と同じイメージ
・ROS2の新機能として、ノードのコンポジションがある。ノードの実行順番を厳密に守ることができる。
executorというオブジェクトの中で実行されるタイマーイベント、データ受信コールバック、サービス等の実行は必ずexecutorが管理できるらしい

ROS2サービスの定義と利用

・サービスのイメージはROS1と変わらない

ROS2アクションの定義と利用

・考え方はROS1と同じ。サービスと違って、アクションは必ず非同期に実行
・記載の仕方は非常に複雑だが、DasingでAPIが変更になるので、ここでは雰囲気を掴んでおく程度でよさそう

ROS2コマンド

msg/srv/node/pkg/run が使えるので基本はROS1と同じ(作法が違うのみ)

launchファイル

ROS 2のシステム起動するツールはROS 1のroslaunchとかなり違うことになるらしい。ROS 2のlaunchフィあるはXMLではなくてPythonで記述される予定だが、まだ未実装なのでここは保留

ROS 1とROS 2のメッセージ通信

ROS 1とROS 2の環境を有効にして、ros1_bridgeパッケージ内のdynamic_bridgeノードを起動する。dynamic_bridgeは定期的にROS 1側とROS 2側に利用中のトピックを探す。 トピックがあったら、そのトピックで流れているデータを反対側で送信するらしい。
どの程度使えるのかはまだ未知数ですが、ROS1での開発資産が活用できる観点では期待したいところですね。

最後に

数カ月前までは 「ROS2とROS1は非互換で全く別ものだ」という噂を聞いていてドキドキしてましたが、いざ勉強してみるとROS1の思想がそのまま継承されているので、ROS1経験者は素直に移行できそうな雰囲気を感じました。
まだ開発途上のROS2ですが、日常世界にロボットを展開していく上では必要なアップデートだと思うので、引き続き情報キャッチしていきたいと思います。

ひとまず、Dasingがリリースされたら再度遊んでみたいと思います

Maker Faire Kyoto 2019 に出展 & プレゼンします

出展情報


5/4(土)~5(日)に京都・けいはんなで開催される
メーカーイベント Maker Faire Kyoto 2019に
個人として出展
します。

日時 :

2019年5月4日(土)12:00~18:00、 5月5日(日)10:00~17:00

場所

京都・けいはんなオープンイノベーションセンター(KICK)
kick.kyoto


決してアクセスの良い場所ではありませんが、都市主要部はGWの時期はどこ行っても混雑が予想されるので、案外ここは穴場かもしれません。
約200もの個人・団体モノづくりをしてる人が出展していて、面白い多種多様な工作品・ガジェットも多数あり、子供のためのモノづくりワークショップもあります。モノづくりに関係く、素直にガチャガチャ楽しい場所やと思うので、予定の空いてる方は是非お越しくださいー!
なお、私は、『家族のためのモノづくり』というテーマで、
昨年から取り組み始めた 息子や家族のために作った自作リハビリ機器・改造おもちゃを多数展示予定です。
アシスティブテクノロジー&キッズ向けのカテゴリーで展示します。
以下が公式サイトでの私のブース説明になります。
ブースは【E-12-04】になります

makezine.jp

そして、今回のMaker Faire Kyoto 2019内のプレゼンステージにて、まさかの『個人プレゼン』の機会を頂きました
【5/5(金) 16:30~16:50】

makezine.jp

個人活動としては、小規模セミナーや限定コミュ内で何度かショートピッチをさせて頂きましたが、展示会という巨大オープンスペースでの20分プレゼンは初チャレンジ!
(エントリー時の勢いでプレゼン公募にも応募したら、見事に採用されてしまいました)
我ながら大それたチャレンジなのではないか、と、今さらながらビビりはじめてます(笑)
普段、息子や家族を巻き込んだモノづくりの奮闘体験を、成功談・失敗談交えて、おもしろ楽しくプレゼン出来たら、と思ってますー(^-^)

展示作品紹介

(1) 歩行リハビリを楽しくするための「歩くと音の鳴る&光る&振動する靴」

動作デモを含めて現物展示予定です。ただし、無線接続は会場では厳しそうなので、有線接続できる様に改造済です。
ogimotokin.hatenablog.com



(2) ワンボタンでじゃんけんが出来るじゃんけん義手

動作デモを含めて現物展示予定です。また、本機器の開発背景などについてはプレゼンでも触れる予定です!


(3) 電動車椅子の操作練習のための自作ジョイスティック+二輪走行ロボット

動作デモはもちろん、操作コントローラ3種類×操作ロボット3種類持っていきます!
操作体験もできますよ!
ogimotokin.hatenablog.com
ogimotokin.hatenablog.com
ogimotokin.hatenablog.com


(4) ボタンを押すと暴走するワンワン

動作デモを含めて現物展示予定です。また、本機器の開発背景などについてはプレゼンでも触れる予定です!



(5)おかん分身ロボット+遠隔説明員

不幸な事に、足をケガして外出困難な妻(おかん)が、自宅から分身ロボットを使って遠隔説明員を行うという取り組みにチャレンジします。
せっかくの機会なので、ロボットに接客される斬新な体験をブースで出来るかも!?(ただし、おかんのやる気次第ですが…)




意気込み

障害を持った当事者や家族をサポートするアシスティブテクノロジー
大事なのは
『当事者に近い位置にいる事』
であると、この1年で強く感じました。そして、一品モノづくりができるメーカーズ活動と、個々の症例に対してカスタマイズが必要な障害支援テクノロジーは本当に相性が良いと思ってますー

・個人の超具体的な課題に対して解決案を考えれる
 (謎解き好きなエンジニアには格好のテーマがたくさん)
・実際に自分が手を動かして仕上げていける
 (モノづくりの楽しさ)
・自分が作ってくれたモノで喜んでくれる、その姿が目の前で見れる
 (エンジニアのやりがい)

エンジニアと当事者が繋がってテクノロジーを作っていく活動・世界をこの1年でたくさん見てきました!
そんな世界がこれからどんどん広がっていく事を願いつつ、自分もその一つのきっかけが出来れば、と考えてます。
そんな、一番身近な人の困り事を解決するモノづくり活動の一例として、何かを一つでも伝えれれば、と思いますー

上記の展示物を見て頂ければ分かる通り、決してすごく難しい技術は使っていません。
なので、Maker Faireというモノづくりのプロが集まる祭典で、難しい技術を求める人にとって弊ブースは少し物足りないかもしれません。
あくまで、幣ブースの特徴は、

「必要とする人のニーズ目線で
 日常的に使える事を目指した
  個人モノづくり活動」

だと思っているので、その視点で暖かい目で見て貰えれば、と思います~


業務で展示会参加する機会はたくさんあったけど、個人として出展する機会は初めて。
色々と不慣れなとこもあるとは思いますが、日常使用を目指した暖かみある自作アシスティブテクノロジー
是非とも触って感想等頂けると嬉しいです!!

M5Stack+Blynkを使って、お父さん帰宅連絡「今から帰る~」を簡易化してみた

f:id:motokiinfinity8:20190413222345j:plain

はじめに

先日作成した『おとんスケジューラ』をキッチン横に導入して、少しずつお父さんのスケジュールが家族と共有できる様になってきました。そこで、その続きとして、お父さんが退社後の帰宅連絡「今から帰るよ~」を簡易化する手段を検討し始めました。

普段の平日、仕事を終えて会社を出る際にLINE等で妻に「今から帰るよ~」を送っていたのですが、メッセージを送付する事を忘れる事が多いんですよね。また、ちょうど妻が子供にご飯を介助している時などはスマホを手放してる事も多く、タイムリーに伝わらない事が多発してました。LINEメッセージを日々送れば済む話なのですが、LINEを開いてメッセージを送るって操作も意外と忘れがち。
特に私の場合、仕事後は結構考え事をしたり明日のアクションを整理しながら帰るので、ついつい連絡メッセージを送る事を忘れてしまうのです。

たかが一言の連絡! されど一言の連絡!

こういう日々のすれ違いが積み重なると色々とトラブルになりかねないので、ここを少し対策してみよう!
  
という事で、忘れんぼうな自分をサポートするため、
「『今から帰る』連絡を簡単にするツール」を作ってみました。



完成品
完成したモノはこちら
f:id:motokiinfinity8:20190413222345j:plain


  
スマホアプリの1ボタンを押すと自宅リビングに設置した液晶ロボットに音声お知らせスマホGPSを連動させて最寄駅に着いたら自動で通知もできる!
・言い出しにくい「今日ごはんいらない」も1ボタンで通知可(笑)

通知先の液晶付ロボットは以前にM5Stackを使って開発した
『お湯張り押しボタンロボット+おとんスケジューラ表示』
を機能拡張する事にしました。

ogimotokin.hatenablog.com
ogimotokin.hatenablog.com


新しく製作する場合でも、M5Stackを購入するだけで作成できるので、とても便利です。

システム全体図

自宅外から自宅のM5Stackへ情報通知するにはいくつか手段があります。
前回の『おとんスケジューラ』では、AWS Lambdaを経由して情報通知しました。最初は前回同様のシステム案を考えていましたが、Web上で調査してみたところ、もっと簡単にスマホ~M5Stack間を連結できるアプリとして『Blink』を見つけたので、今回はこれを使ってみようと思います!

f:id:motokiinfinity8:20190420003404j:plain

結果として、Blynkを使って正解でした!
わずか2時間程度の試行錯誤で、あっさりとスマホからボタン通知が出来たので、本当に驚きました!すごいよ、これ!!

何がすごいかというと……
 ・M5Stack側ソースはほとんどサンプルコードを埋め込んだのみ (Blynkサーバーとの認証のみ)
 ・スマホ側は、Blynkアプリを使ってドラッグ&ドロップ作業程度で通知ボタンを作成できる

という簡単操作で、宅外からのスマホによる遠隔操作を実現できるので、これは本当にすごい!!


スマホ制御は難しそうに見えますが、これは世の中に簡単にマイコンスマホ連結できるツールがあります。(Blynk というアプリを使いましたが、すごく便利!)
  

Blynk導入

Blynkの使用にあたって、基本的に下記サイトに従って試してみました。
www.mgo-tec.com

ざっくりした手順としては
 ①スマホにBlynk アプリをインストール
 ②Blynkアプリにてアカウントの登録
 ③Blynkアプリにてプロジェクト作成
 ④Blynkから自分のメールアドレスに送ってきたメールを
  元にAuthコード入手 & Arduino用ライブラリ入手
 ⑤BlynkライブラリをAruduino開発環境に入れる
 ⑥M5Stack用Arduinoコードを作成する。その際に④で入手したAuthコードを記載する

という流れ。
そして、⑥後にサンプルコードをコンパイル、と思いきや、、、エラー発生しました。。。

C:\***\Arduino\libraries\M5Stack\src/utility/M5Timer.h:34:35: note: previous declaration as 'typedef class std::function<void()> timer_callback'
 typedef std::function<void(void)> timer_callback;

むむむ、とgoogle先生に聞いたところ、どうやらtimer_callbackがかぶっている模様。

M5Stack and Blynk | M5Stack Community

上記サイトを参考にArduino/libraries/M5Stack/src/utility/M5Timer.h 関数を修正してみました

//typedef std::function<void(void)> timer_callback;
typedef void (*timer_callback)(void);
typedef void (*timer_callback_p)(void *);

これで無事に使用できる様になりました。良かった~

M5Stack側コード

以下がBlynkサーバーから通知情報を受信するコード。
簡易化のため、我が家ロボット固有のサーボ動作ソフトや、お父さんのスケジュール通知ソフト(from AWS Lambda)は省略しています。
ちなみに、AWS Lambdaからの通知処理も並列して実行可能です。素晴らしい。

コードのイメージとしては、
 ・仮想ピンV0 : スマホの「今から帰るボタン」を押された通知
 ・仮想ピンV1: スマホGPSが自宅に近づいた事を検知する通知
 ・仮想ピンV2: スマホの「今日ごはんいらないボタン」を押された通知
になります。

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <Avatar.h>
#include "time.h"
#include <BlynkSimpleEsp32.h>

#define BLYNK_PRINT Serial
const char* auth   = "xxxxxxxxxxxxxxxxxxxx"; //Blynkから送られてきたメール内にある Auth コード
const char* ssid     = "xxxxxxxxxxxxx";
const char* password = "xxxxxxxxxxxxxxx";

//M5 Avatar
using namespace m5avatar;
Avatar avatar;

// Audio speaker
#include "AudioFileSourceSD.h"
#include "AudioFileSourceID3.h"
#include "AudioOutputI2S.h"
#include "AudioGeneratorWAV.h"
AudioGeneratorWAV *wav;
AudioFileSourceSD *file;
AudioOutputI2S *out;
AudioFileSourceID3 *id3;

void setup(){
  M5.begin();
  Serial.begin(115200);
  delay(10);

  // Blink起動
  Blynk.begin(auth, ssid, password );

  // アバター設定
  delay(500);
  M5.update();
  M5.Lcd.clear();
  avatar.init();
}

void loop(){
    // Blynk更新
    Blynk.run();

    M5.update();
}

// BLYNKサーバーから帰宅通知(v0)を受けた場合
BLYNK_WRITE(V0){
  int val = param[0].asInt();
  if(val == 1){
    display_information("return_home"); 
  delay(3000);
  view_change = 1;
  }
}

// BLYNKサーバーからGPSトリガ通知(v1)を受けた場合
BLYNK_WRITE(V1){
  int val = param[0].asInt();
  if(val == 1){
    display_information("return_home");
  delay(3000);
  view_change = 1;
  }
}

// BLYNKサーバーからごはん不要通知(v2)を受けた場合
BLYNK_WRITE(V2){
  int val = param[0].asInt();

  if(val == 1){
    display_information("cancel_dinner");

  delay(3000);
  view_change = 1;
  }
}

void display_information(String str){
  M5.Lcd.drawJpgFile(SD, "/avatar2.jpg");
  Serial.println("お父さん予定音");
  //お父さん情報音再生①(ピンポンパン)
  file = new AudioFileSourceSD("/pinpon.wav");
  wav = new AudioGeneratorWAV(); 
  wav->begin(file, out);
  while(wav->isRunning()){
         if (!wav->loop()) wav->stop();
  }

  //お父さん帰宅音再生
  M5.Lcd.drawJpgFile(SD, String("/" + str + ".jpg").c_str());
  file = new AudioFileSourceSD(String("/" + str + ".wav").c_str()); 
  wav = new AudioGeneratorWAV(); 
  wav->begin(file, out);
  while(wav->isRunning()){
         if (!wav->loop()) wav->stop();
  }
}


かわいいイラストは前回の『おとんスケジューラ』と同様に、
表示画像についてはパワポで作成してJPEG形式で保存&縮小して使用しています。

f:id:motokiinfinity8:20190420002146j:plainf:id:motokiinfinity8:20190420002150j:plain

Blynk側アプリ設定

Blynk側は大きく3つのウィジェットを使いました。
ちなみに、Blynkのウィジェットは2000Point分は無料で使えます。そこから使ったウィジェット分だけ差し引かれてしまい、2000Point以上使いたい場合のみ課金するという仕組みです。一度使ったウィジェットも使わなくなった場合は再度ポイントに還元できるので、お試し導入もしやすく敷居低く始める事ができるのもメリットですね。

①今から帰るボタン
 これは「Styled Button」ウィジェットを使いました。仮想ピンをV0に設定して、「0: まだ帰れない」と「1: もうすぐ帰る」をボタン押すたびに交互にスイッチする仕様としました。

f:id:motokiinfinity8:20190420004521j:plain
f:id:motokiinfinity8:20190420004528j:plain



GPS通知ボタン
 これはGPS Trigger」ウィジェットを使いました。仮想ピンをV1に設定して、「自宅近辺の設定エリア内にスマホGPSが入った場合」に1を通知するスイッチです。(写真上は、大阪の中心場所を自宅と仮定して設定しています)

f:id:motokiinfinity8:20190420004546j:plain
f:id:motokiinfinity8:20190420004550j:plain




③ごはんいらないボタン
 これは①同様に「Style Button」ウィジェットをつかっています。仮想ピンはV2に設定する事で、①とは異なるアクション(音声呼び出し)を実現しています。

最後に

今回は、M5Stack に Blynkアプリを使う事で、自宅内のM5Stackデバイスを自宅外から遠隔操作する手段を実現しました。
同様の手段は、Obnizというデバイスでも実現できますが、液晶&スピーカを活かした遠隔制御をすぐに作れるという観点で『M5Stack+Blynkアプリ』はとても良い組み合わせだと思います。

このBlynkアプリの仕組みを応用すれば、自宅外のM5Stack等のマイコンバイスと、自宅内のM5Stackデバイスを遠隔同期や遠隔制御する事も可能そうです。(既に簡易実験済)
マイコン同士の宅外連携が出来れば応用の幅が一気に広がるので、どんなアプリケーションの応用ができるのか、実際に作りながら試していきたいと思います。既に応用事例に取り組んでいますので、また次回に記載したいと思います。

お父さんの予定(Googleカレンダ) を自宅リビング(M5stack)に表示する『おとんスケジューラ』の開発

f:id:motokiinfinity8:20190310224536j:plain

はじめに

11月から東京単身赴任になりつつも、業務都合で自宅(大阪)に週の半分近く滞在している生活を続けています。
そんな中で、現在困っているのは、業務都合により流動的なお父さんのスケジュールの家族への賢い共有方法。

普段は、LINE等を使ってスケジュール変更を連絡してますが、ついつい連絡漏れがあったりして、「えっ!今日帰ってくるんじゃなかったっけ?」とか「あれ?明日から東京?」みたいな事がしばしばありました。

このままでは、家族の間に、すれ違いやら溝が埋まれてしまうと危機感を覚え、少しでも連絡漏れを防ぎたいと考えた次第です。

そこで、お父さんの予定、特に『いつに大阪(自宅)に帰ってくるのか?』『いつから東京(単身赴任先)に移動するのか』を簡単に&自動で家族と共有するツールを今回開発してみました。

開発要件

・リビング or キッチンにおとうさんの帰省予定が分かりやすく表示される事
・おとうさんの場所に関する情報のみを表示 (不要な情報はいらない)
・お父さん側は出来る限り入力の手間を減らしたい
 お父さんが日常使ってるスマホのスケジュール帳(ジョルテ)からは変えたくない

幸いなことに、お父さんの使っているジョルテはGoogleカレンダに同期する機能があったため、GoogleカレンダAPIにアクセスするガジェットを作る方針で開発を進めました。

完成品

完成したモノはこちら

f:id:motokiinfinity8:20190310224518j:plain
f:id:motokiinfinity8:20190310224529j:plain
f:id:motokiinfinity8:20190310224547j:plain

・おとんのスマホのスケジュール帳のデータをGoogleカレンダーに同期
Googleカレンダーから「東京」「大阪」のキーワードと日付情報を抜き出す
・大阪自宅リビングに置いてる液晶付マイコン(M5Stack)に滞在残り時間を表示

   
おとうさんのスケジュール張を更新したら、勝手におうちリビングのディスプレイも更新されるのです(^-^)/
  
更に、家族への音声通知機能も追加。
・朝8時(娘の通学前)、夜20時(夕食後)に、おとんの今後の予定を音声でお知らせ


ちょうどリビングには、昨年に開発した「お風呂ゆわかしボタン押しロボット」がいるのですが、
ボタンを押す以外には今のところ役目がなかったので、これをアップデートしました。

システム全体図

図にすると以下の感じです。

f:id:motokiinfinity8:20190323145450j:plain

開発にあたり、一番参考にさせていただいたのは、M5Stackで作成された「電車タイマー」でした。
開発の考え方はもちろんの事、システム構成やAWS活用方法、実際のコード記述など、かなり勉強になりました。


qiita.com

また、以下のサイトも参考になります。
blog.akanumahiroaki.com

私はM5StackでのMicroPythonは触った事はなかったので、こちらはあくまで初期検討用。
「電車タイマー」さんの方は、AWS Lambdaでhttp get命令に変換する事で、既存で私が作ってきたArduino環境による開発物の延長で考える事ができました。結果として約1週間の短期間で開発しきれたので、良かったかなぁ、と。

ハードウェア

ハードウェアとしては、液晶付き無線マイコンボード M5Stackを使いました。

理由は2つあって、1つは現在リビングに置いている「お湯張りボタン押しロボット」がそのまま使える事。ogimotokin.hatenablog.com

もう一つは、おしゃれなデザインを表示するのが簡単な事。M5StackはSDカードから画像を読み込んで簡単に表示できるのが魅力!
表示画像についてはパワポで作成してJPEG形式で保存。その後、画像エディタ等で320×240サイズに縮小してマイクロSDカードに保存。
なので、カワイイ表示デザインをすぐに作れます!

イラストにはおなじみの「いらすとや」さんも使わせて頂いてます。

f:id:motokiinfinity8:20190310222846j:plain

ソフトウェア

Google Calendar側の設定

上記でも紹介したこちらのサイトを参考にしました。
M5Stack で Google Calendar のスケジュールを表示する(MicroPython) - Tech Blog by Akanuma Hiroaki

これを参考にGoogle Calendar API の設定を行いました。
GCP のコンソールから API とサービスを追加 をクリック
API のリストの中から Google Calendar API をクリック
API の認証情報を作成、サービスアカウントキー を選択
→ 認証情報(JSON)ファイルはダウンロードしておく

加えて、 Google Calendar の共有設定を変更して、上記で作成したサービスアカウントIDを共有対象にしておきます。

次に Goole Calendar APIの動作確認をします。
手元のLinux(Ubuntu)環境のPCを使って、以下のサイトの手順に沿って、お父さんのGoogle Calendarの予定を表示してみたいと思います。

pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
gedit quickstart.py  → サンプルコードを作成
python quickstart.py

これにより、お父さんのこれからの予定10件が表示されたらOKです。

AWS Lanbda側の設定

AWS Lambdaの設定そのものはこちらを参考にしました。
qiita.com

AWS Lambdaの中に転送する index.js (Java Script)は以下のものを参考にしました。
github.com

私は AWSはもちろん Java Script もほとんど使った事がなかったので、色々と見様見真似で頑張りました。

仕様イメージとしては、
 ・10日前から30個分のスケジュールを抜き出す
 ・そのうち、「東京」「大阪」と記載されたキーワードを抜き出し、その開始時間を保存
 ・その保存情報の組み合わせと現在時刻から、以下の情報を計算する
  「現在が大阪か東京(大阪以外)か」「大阪からの次回出発日時」「大阪への帰還日時」

これをhttp情報として出力する事によって、端末側(M5stack)での処理を軽くしました。

上記仕様のため、以下の仕様は非対応と割りきっています。
・ 「大阪」と「東京」以外の場所はわからない。あくまで自宅視点になるので、「大阪にいるか」「大阪にいないか」に絞る
・ スケジュール帳における終了期間は見ない

下記Index.js と node_modulesディレクトリ privatekey.json
を以下のコマンドでzipに圧縮して、AWS Lambdaにアップロードします

$ ls
 index.js   node_modules  privatekey.json
$ zip -r ../oton_scheduler_js .

index.js

const {google} = require('googleapis');
const privatekey = require('./privatekey.json');

exports.handler = (event, context, callback) => {
  Promise.resolve()
  .then(function(){
    return new Promise(function(resolve, reject){
      //JWT auth clientの設定
      const jwtClient = new google.auth.JWT(
             privatekey.client_email,
             null,
             privatekey.private_key,
             ['https://www.googleapis.com/auth/calendar']);
      //authenticate request
      jwtClient.authorize(function (err, tokens) {
        if (err) {
          reject(err);
        } else {
          console.log("認証成功");
          resolve(jwtClient);
        }
      });
    })
  })
  .then(function(jwtClient){
    return new Promise(function(resolve,reject){
      const calendar = google.calendar('v3');
      const params = {
          'auth': jwtClient
      };

      var now_date = new Date();
	  var before_week = new Date()- 10*24*60*60*1000;	//現在から10日前
      calendar.events.list({
        auth: jwtClient,
    	calendarId: 'xxxxx@gmail.com',
   timeMin: (new Date(before_week)).toISOString(),
	maxResults: 30,
    	singleEvents: true,
    	orderBy: 'startTime',
      }, (err, res) => {
        if (err) return console.log('The API returned an error: ' + err);
        const events = res.data.items;
        if (events.length) {
          console.log('Upcoming 30 events:');
          events.map((event, i) => {
            const start = event.start.dateTime || event.start.date;
          });

          var nowPlace = 4;
          var returnOsakaDate = 0;
          var leaveOsakaDate = 0;
          let osaka_array = [];
          let tokyo_array = [];
          let shutyo_array = [];

          for (var i = 0; i < events.length; i++) {
             if (events[i].summary == '大阪') {
                 osaka_array.push(events[i].start.dateTime);
                 if(new Date(events[i].start.dateTime).getTime() < now_date.getTime()){
                    nowPlace = 0;
                 } else {
                    if(returnOsakaDate == 0){
                        returnOsakaDate = events[i].start.dateTime;
                    }
                 }
                 //console.log('大阪');
              }
             if (events[i].summary == '東京') {
                 tokyo_array.push(events[i].start.dateTime);
                 if(new Date(events[i].start.dateTime).getTime() < now_date.getTime()){
                    nowPlace = 1;
                 } else {
                    if(leaveOsakaDate == 0){
                        leaveOsakaDate = events[i].start.dateTime;
                    }
                 }
                 //console.log('東京');
              }
             if (events[i].summary == '出張') {
                 shutyo_array.push(events[i].start.dateTime);
                 //if (events[i].start.date.getTime() < now_date.getTime()){
                 if(new Date(events[i].start.dateTime).getTime() < now_date.getTime()){
                    nowPlace = 2;
                 } else {
                    if(leaveOsakaDate == 0){
                        leaveOsakaDate = events[i].start.dateTime;
                    }
                 }
              }
          }
          console.log(osaka_array);
          console.log(tokyo_array);
          let oton_array = {
             nowPlace : nowPlace,
             returnOsakaDate : returnOsakaDate,
             leaveOsakaDate : leaveOsakaDate,        
             OsakaDate: osaka_array,
             TokyoDate: tokyo_array,
           }

           const response = {
	      statusCode : 200,
	      body : JSON.stringify({
                nowPlace : nowPlace,
                returnOsakaDate : returnOsakaDate,
                leaveOsakaDate : leaveOsakaDate,        
                OsakaDate: osaka_array,
                TokyoDate: tokyo_array
              }),
           };
           resolve(response);
        } else {
          console.log('No upcoming events found.');
        }
      });
    });
  })
  .then(function(result){
    callback(null, result);
  })
  .catch(function(err){
    callback(err);
  });
};

送付されてくるデータ例は以下のイメージになります。

{"nowPlace":0,"returnOsakaDate":"2019-03-23T00:00:00+09:00","leaveOsakaDate":"2019-03-23T13:00:00+09:00","OsakaDate":["2019-03-16T20:00:00+09:00",(中略),"2019-03-25T00:00:00+09:00"],"TokyoDate":["2019-03-13T00:00:00+09:00",(中略),"2019-03-27T00:00:00+09:00"]}
M5Stack側

M5stack側の処理はシンプルに、http getで受信したメッセージ情報をもとに、SDカード上のイラストの上に残時間表示を追記表示するというシンプルな機能です。
ただし、苦労したのはそのメッセージの受信。AWS Lambdaから送られてきたjson方式(のはず)の文字列をArduinoJsonで処理しようとしたのですが、、、なぜかうまくいかず苦戦。仕方がないので、今回は送付データが固定長のため、sscanf読み出しで対応しました。(将来的には、再度バグ要因を確認する必要がありそうだ。。。)

#include <Arduino.h>
#include <ArduinoJson.h>
#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include "time.h"

class OtonSchedule{

  public:
     int nowPlace;
     struct tm returnOsakaDate;
     struct tm leaveOsakaDate;
       
    OtonSchedule(){
       getNewSchedule();
    };
    bool getNewSchedule(){
        HTTPClient http;
        http.begin(url);
        int httpCode = http.GET();
        if (httpCode == HTTP_CODE_OK) {
            String body = http.getString();
            body.toCharArray(httpResponceBuff,64);
            Serial.print("Response Body: ");
            Serial.println(body);

            //古いデータを更新
            nowPlace_old = nowPlace;
            returnOsakaDate_old= returnOsakaDate;
            leaveOsakaDate_old = leaveOsakaDate;
            
            //なぜかArduinoJsonが使えないので、sscanf読み出しに変更
            //StaticJsonBuffer<500> jsonBuffer;
            //JsonObject& root = jsonBuffer.parseObject(body);
            nowPlace = body.substring(12,13).toInt();
            //String returnOsakaDate = body.substring(33,49);
            returnOsakaDate.tm_year = body.substring(33,37).toInt();
            returnOsakaDate.tm_mon = body.substring(38,40).toInt();
            returnOsakaDate.tm_mday = body.substring(41,43).toInt();
            returnOsakaDate.tm_hour = body.substring(44,46).toInt();
            returnOsakaDate.tm_min = body.substring(47,49).toInt();
            leaveOsakaDate.tm_year = body.substring(78,82).toInt();
            leaveOsakaDate.tm_mon = body.substring(83,85).toInt();
            leaveOsakaDate.tm_mday = body.substring(86,88).toInt();
            leaveOsakaDate.tm_hour = body.substring(89,91).toInt();
            leaveOsakaDate.tm_min = body.substring(92,94).toInt();             

            return true;

        }else{
          return false;
        }
    };
     void isDisplayed(){
            Serial.print("現在のおとん位置 : ");
            if(nowPlace==0) Serial.println("Osaka");
            else if(nowPlace==1) Serial.println("Tokyo");
     };
     bool isOsaka(){
         if(nowPlace == 0) return true; 
         else return false; 
     };
     bool isChanged(){
        if(nowPlace_old != nowPlace)  return true; 
        else if(difftime(mktime(&returnOsakaDate), mktime(&returnOsakaDate_old))!=0)  return true; 
        else if(difftime(mktime(&leaveOsakaDate), mktime(&leaveOsakaDate_old))!=0)  return true; 
        else return false;
     };
     struct tm stayOsakaTime(){
        struct tm timeInfo, stayTime;
        getLocalTime(&timeInfo);
        timeInfo.tm_year += 1900;
        timeInfo.tm_mon  += 1;
        
        double stayTime_sec  = difftime(mktime(&leaveOsakaDate), mktime(&timeInfo));
        stayTime.tm_mday = stayTime_sec  / (60*60*24);
        stayTime_sec  -= stayTime.tm_mday * (60*60*24);
        stayTime.tm_hour = stayTime_sec  / (60*60);
        stayTime_sec  -= stayTime.tm_hour * (60*60);
        stayTime.tm_min = stayTime_sec  / (60);
        stayTime_sec  -= stayTime.tm_min * (60);
        stayTime.tm_sec = stayTime_sec;
        if(stayTime.tm_mday >=100){
          stayTime.tm_mday = 0;
          stayTime.tm_hour = 0;
          stayTime.tm_min = 0;
          stayTime.tm_sec = 0;
        }
        return stayTime;                  
     };
     struct tm stayTokyoTime(){
        struct tm timeInfo, stayTime;
        getLocalTime(&timeInfo);
        timeInfo.tm_year += 1900;
        timeInfo.tm_mon  += 1;

        double stayTime_sec  = difftime(mktime(&returnOsakaDate), mktime(&timeInfo));
        stayTime.tm_mday = stayTime_sec  / (60*60*24);
        stayTime_sec  -= stayTime.tm_mday * (60*60*24);
        stayTime.tm_hour = stayTime_sec  / (60*60);
        stayTime_sec  -= stayTime.tm_hour * (60*60);
        stayTime.tm_min = stayTime_sec  / (60);
        stayTime_sec  -= stayTime.tm_min * (60);
        stayTime.tm_sec = stayTime_sec;
        if(stayTime.tm_mday >=100){
          stayTime.tm_mday = 0;
          stayTime.tm_hour = 0;
          stayTime.tm_min = 0;
          stayTime.tm_sec = 0;
        }
        return stayTime; 
     };    
    private:
      int nowPlace_old;
      bool changeflg;
      struct tm returnOsakaDate_old;
      struct tm leaveOsakaDate_old;
};

OtonSchedule otonSchedule = OtonSchedule();
int view_mode;  //0: アバターモード、1:スケジュールモード
bool view_change;
uint32_t time_cnt;


void setup(){
  M5.begin();
  Serial.begin(115200);
  delay(10);

  // WiFi設定
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  //WiFi.config(ip,gateway,subnet,DNS);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected.");
  M5.Lcd.println("WiFi connected.");
  Serial.println("IP address: ");
  M5.Lcd.println("IP address: ");
  Serial.println(WiFi.localIP()); 
  M5.Lcd.println(WiFi.localIP());
  delay(1000);

  //WAVの場合
  out = new AudioOutputI2S(0,1); 
  out->SetOutputModeMono(true);
  out->SetGain(0.8); 
  wav = new AudioGeneratorWAV(); 
  
  //NTP設定
  // https://wak-tech.com/archives/833
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
  
  //NTP時刻情報取得
  struct tm timeInfo;
  getLocalTime(&timeInfo);
  timeInfo.tm_year += 1900;
  timeInfo.tm_mon  += 1;
 
  Serial.print(timeInfo.tm_year);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mon);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mday);
  Serial.print(" ");
  Serial.print(timeInfo.tm_hour);
  Serial.print(" ");
  Serial.print(timeInfo.tm_min);
  Serial.print(" ");
  Serial.println(timeInfo.tm_sec);

  // おとんスケジューラ起動
  while(otonSchedule.getNewSchedule() != true){
    delay(500);
    Serial.print(".");
  }
  otonSchedule.isDisplayed();
  time_cnt = millis();
  view_mode =0;
  view_change =0;

  //サーボ起動
  ledcSetup(LEDC_CHANNEL_3, LEDC_SERVO_FREQ, LEDC_TIMER_BIT) ; // 16ビット精度で制御
  ledcAttachPin(SERVO_PIN, LEDC_CHANNEL_3) ; // CH3をRC SERVOに
  ledcWrite(LEDC_CHANNEL_3, servo_pwm_count(-45)) ; // ニュートラル
  Serial.println("start set Position : -45°");

  // アバター設定
  delay(500);
  M5.update();
  M5.Lcd.clear();
  avatar.init();
  avatar.setExpression(expressions[4]);

  server.begin();
  
  M5.Lcd.drawJpgFile(SD, "/avatar1.jpg");
   delay(1000);
}

int cnt=0;
struct tm nowTime, otonStayTime;

void loop(){
    //ループの方針
    // ・NTP時間は10秒ごと
    // ・サーバーからのデータ取得は10分ごと
    // NTPから現在時刻情報の取得
    getLocalTime(&nowTime);    
    
    M5.update();
    if(M5.BtnB.wasPressed()){
      //otonSchedule.nowPlace = (otonSchedule.nowPlace+1)%2;  //場所の変更 (デモ用)
      view_change = 1;  //アバター⇔スケジューラの変更
      time_cnt = millis();
    }

    // 30秒以上経過した場合
    if ((millis() -time_cnt)>30000){
      view_change = 1;  //アバター⇔スケジューラの変更
      time_cnt = millis();
    }

    // サーバーからのデータ取得(毎時10分)
    if(nowTime.tm_min % 10 == 0){  
        otonSchedule.getNewSchedule();
        otonSchedule.isDisplayed();
        if(otonSchedule.isChanged()){
           Serial.println("おとんのスケジュールが変更された!"); 
        }
    }

    //描画設定
    // 30秒ごとに表示
    if(view_change){
      if(view_mode ==0){
        avatar.stop();
        delay(500);
        if(otonSchedule.isOsaka()){
           M5.Lcd.drawJpgFile(SD, "/osaka.jpg");
           Serial.println("おとんは大阪にいるよ");
           otonStayTime = otonSchedule.stayOsakaTime();
        }else{
           M5.Lcd.drawJpgFile(SD, "/tokyo.jpg");
           Serial.println("おとんは東京にいるよ");
           otonStayTime = otonSchedule.stayTokyoTime();  
        }
        char str[20];
        M5.Lcd.setTextColor(WHITE); 
        M5.Lcd.setTextSize(5);
        M5.Lcd.setCursor(40,180);
        sprintf(str,"%02d", otonStayTime.tm_mday);
        M5.Lcd.print(str);
        M5.Lcd.setCursor(120,180);
        sprintf(str,"%02d", otonStayTime.tm_hour);
        M5.Lcd.print(str); 
        M5.Lcd.setCursor(220,180);
        sprintf(str,"%02d", otonStayTime.tm_min);
        M5.Lcd.print(str);
      // スケジューラからアバターに変更
      }else if(view_mode ==1){
         Serial.println("view_change : Avatar");
         M5.Lcd.clear();
         delay(500);
         avatar.start();
       }
    view_mode = (view_mode+1)%2;  //場所の変更 (デモ用)
    view_change = 0;
    }

    
    // 朝8時のアラーム
    // 今日のお父さんの予定を表示 (「今日はずっと大阪」「今日はずっと東京」「今日は大阪に帰ってくる」「今日は東京にいってしまう」)
    if((nowTime.tm_hour == 8 || nowTime.tm_hour == 20) && nowTime.tm_min == 1){

         //お父さん情報音再生①(ピンポンパン)
         file = new AudioFileSourceSD("/pinpon.wav");
         wav = new AudioGeneratorWAV(); 
         wav->begin(file, out);
         while(wav->isRunning()){
           if (!wav->loop()) wav->stop();
         }
         M5.Lcd.drawJpgFile(SD, "/oton_info.jpg");

         //お父さん情報音再生②(今日のお父さん情報)         
         if(nowTime.tm_hour == 8){
            file = new AudioFileSourceSD("/today_info.wav");
         }else{
            file = new AudioFileSourceSD("/tomorrow_info.wav");          
         }
         wav = new AudioGeneratorWAV(); 
         wav->begin(file, out);
         while(wav->isRunning()){
           if (!wav->loop()) wav->stop();
         }
         delay(2000); 

         int remainTime_hour  =  otonStayTime.tm_mday*24+otonStayTime.tm_hour;
         
         if(otonSchedule.isOsaka()){
            // 朝8時点で15時間以内 or 夜20時時点で29時間以内に東京に帰る場合
           if((nowTime.tm_hour == 8 && remainTime_hour <= 15) || (nowTime.tm_hour == 20 && remainTime_hour <= 29)){
                M5.Lcd.drawJpgFile(SD, "/goto_tokyo.jpg");
                file = new AudioFileSourceSD("/goto_tokyo.wav");
           // 大阪に1日滞在する場合
           }else{
                M5.Lcd.drawJpgFile(SD, "/stay_osaka.jpg");
                file = new AudioFileSourceSD("/stay_osaka.wav");            
           }
        }else{
            // 朝8時点で15時間以内 or 夜20時時点で29時間以内に大阪から帰る場合
           if((nowTime.tm_hour == 8 && remainTime_hour <= 15) || (nowTime.tm_hour == 20 && remainTime_hour <= 29)){
                M5.Lcd.drawJpgFile(SD, "/goto_osaka.jpg");
                file = new AudioFileSourceSD("/goto_osaka.wav");
           // 東京に1日滞在する場合
           }else{
             M5.Lcd.drawJpgFile(SD, "/stay_tokyo.jpg");
             file = new AudioFileSourceSD("/stay_tokyo.wav");              
           }
        }
        wav = new AudioGeneratorWAV(); 
        wav->begin(file, out);
         while(wav->isRunning()){
           if (!wav->loop()) wav->stop();
        }
        delay(3000);
        view_change = 1;
    }
}

最後に

この自動ツールにすべて頼るつもりはなく、基本の予定通知はLineで直接連絡したり、通信手段を使っての日々の会話コミュニケーションになります。
ただ、どうしても共有が漏れてしまったりする事もあるし、Lineする程でもないけど気になる瞬間もあったりする。すれ違いの生じるスタートは相手が何やってるか分からなくなるところからなので、そんなすれ違いをカバーするツールとして捉えてます。
スマホを開ける手間なしで、気軽に予定が確認できる点が良いところ!
  
この様なスケジュール共有ツールによって得られる事は、おとんの動きが常に分かる安心感、家族の心理距離の近さを保てる事、それが価値かなぁ、と感じてます!(^^)!
  

このおとんスケジューラの続きを作っていくならば、
 ・おとんの一週間の予定をぱっと表示する & 一見して分かる手段
 ・おとんの帰宅連絡「今から帰るよ~」を自動化する手段

を追加実装したいなぁ、と考えてます!

しばらく使ってみて、使用感を見ながらアップデートしていきます

息子の歩行リハビリを楽しくするメロディ靴 Ver.4 を作ってみた

f:id:motokiinfinity8:20190216161758j:plain

はじめに

昨年7月に息子の歩行リハビリのモチベーションを上げるため、
足を踏み込むと楽しい音の鳴る電子工作靴の試作3号機を製作しました。

ogimotokin.hatenablog.com

作って以降、おうちリハビリで定期的に使ってきましたが、
息子自身の認知反応が薄かった事、足の成長に伴い靴のサイズが合わなくなった事もあり、少し様子見をしていました。
(別の音の鳴る市販おもちゃ等をリハビリ時に使用してなんとかカバーしてました)

そんな中、昨年秋ごろに周囲の環境に対する息子の認識力、好奇心がぐっと上がってきた事を感じ、
『自分の意思で前に進む体験をたくさん得て欲しい!』
という親の想いから、「歩行器練習に適した靴」として再度 メロディ靴を見直す事にしました!

前試作における良かった点/改善点と対応方針

OKだった点

・靴を無線にした事で歩行練習しやすくなった
・「好きな音」+ 「光」「振動」で息子の多少の興味を掴んだ!

NGだった点

・靴を光らせると下ばかり見てしまう、前に注目するモノが必要
・「歩きたい!」と思わせるほどの爆発的な面白さが足りない
・スピーカのボリュームが小さい、音質が悪い

対応方針

以上の事を踏まえて、前試作の靴部分は変更なし(靴のサイズのみ修正)として、
『制御部+スピーカ部をアップデート』する事にしました。
試行錯誤として、単なる足踏みすると楽しい音が鳴る機能に加えて、以下の2種類の追加機能を試しました。

① 液晶画面に好きな映像を表示
 ・息子の大好きなピタゴラスイッチの音源を再生
 ・数歩歩くと「ピタッゴラスイッチ~♪」とフィニッシュ表示
 ・ネタとして、妻や娘が好きなドラクエ・マリオのコイン音を再生

② カラフルLEDイルミネーションを光らせる
 ・右足を踏むとLED右側、左足を踏むとLED左側を光らせる
 ・足の踏み込む力(重さ)に応じて、LED光る量を変える
 ・音源は、息子が好きな妻のピアノ生音に変更
 ・左右交互に足踏みした時だけ正しく音が鳴る。失敗すると失敗のイヤな音

という事で両方を作ってみた
①の動画

②の動画


それぞれを使ってみて息子自身の反応を見たところ、①は画面がちっちゃすぎてあまり惹きつけ効果が薄かったです。
一方で、②の方で特にLEDに息子の目が釘付けだったので、最終的には②を採用して、実際に歩行器に搭載して試しました。

その結果、
不器用ながらも左右交互に足を出せる様に!!

もちろんこの発明の成果だけではなく、冬休み前から地道に歩行器練習を繰り返してきた経験が大きいとは思いますが、


父ちゃん、嬉しいであります!!!


全体ハードウェア

まずは、①の取り組みですが、制御+液晶表示+スピーカ再生手段として【M5Stack】に置き換えました。
過去のスピーカソフト@Arduinoとの互換性、私自身が開発に慣れたデバイスである事、内蔵スピーカ等を自作せずに済む事による開発のスピード感を優先したためです。

ブロック図は以下の通り。
f:id:motokiinfinity8:20190216145633j:plain

M5Stack 1つで素早く色々と実装できるので、やはり便利なデバイスだなぁ~!

一方で、②の取り組みですが、制御にはESP32 DevKit、LED表示にはLEDテープ(NeoPixel)、音再生にはDFplayerMiniモジュールを使用しています。前記事にも書きましたが、液晶ディスプレイを使わない場合、この組み合わせの方が非常にコスパ良く、色々と応用出来て良さそうです!

ブロック図は以下の通り。
f:id:motokiinfinity8:20190216150053j:plain

上記の①②を簡単に入れ替えて試せるのも、靴側は特にハード変更しなくて良い事、両者ともESP32ベースのソフトコードだからです。
モジュール共通化の考え方は、この様に色々と試してみたい場合に向いてますね。

②については、自作スピーカーとして音の改善を少しだけ検討してみました。
結果として、秋月電子に売っていた3W8Ωのスピーカを使い、スピーカ筐体は100均で売ってるプラスチックのタッパを使いました。
プラスチックの筐体内で音の響きあって、なかなか質の良い音が鳴りました。
見た目は悪いですが、コスパは素晴らしいので是非試してみてください。

f:id:motokiinfinity8:20190216162005j:plain


工夫した点は、スピーカ部のTWILIteモジュールとHost(M5stack/ESP32)との接続方法

本試作では、TWILITE DIPからの靴の圧力センサ値をUART信号で受信する方法にしました。
以前の3号機では、有線式試作とスピーカ部を共用させるという設計制約を設けていたので、
 ・靴にある圧力センサ値はスピーカ側のTWELITE DIPからPWMとして出力
 ・途中でRCフィルタを経由してアナログ電圧値に変換、マイコン側のアナログ端子で電圧を図る
という面倒くさいことをやっていました。
しかし、今回のスピーカ側の改定に伴い、一旦再度見直したところ、TWELITE DIPからのUART信号を使うのが簡単&応用しやすそうでした。

全体ソフトウェア

TWILITE DIPからの受信信号

TWELITEからのUART信号は、以下の様な信号が送信されてきます。

:7881150178810000380027D3000C05230000FFFFFFFFFF97
:788115017881000038002813000C02230000FFFFFFFFFF59
:78811501758100003800284F000C02230000FFFFFFFFFF20
:788115017581000038002899000C04230000FFFFFFFFFFD4
:0281150145810D0AFD000567000B171A8000FFFFFF007FE9 [右足]
:018115019C81003C170006F3000CDE18000041FFFFFFFDC2 [左足]

^1^2^3^4^5^^^^^^^6^7^^^8^9^^^a^b^c^de1e2e3e4ef^g
 
データフォーマット
 1: 1バイト:[0-1]送信元の論理デバイスID (0x78 は子機からの通知)
 2: 1バイト:[2-3]コマンド(0x81: IO状態の通知)
 3: 1バイト:[4-5]パケット識別子 (アプリケーションIDより生成される)
 4: 1バイト:[6-7]プロトコルバージョン (0x01 固定)
 5: 1バイト:[8-9]LQI値、電波強度に応じた値で 0xFF が最大、0x00 が最小
 6: 4バイト:[10-17]送信元の個体識別番号
 7: 1バイト:[18-19]宛先の論理デバイスID
 8: 2バイト:[20-23]タイムスタンプ (秒64カウント)
 9: 1バイト:[24-25]中継フラグ(中継回数0~3)
 a: 2バイト:[26-29]電源電圧[mV]
 b: 1バイト:[30-31]未使用
 c: 1バイト:[32-33]D1バイト:I の状態ビット。DI1(0x1) DI2(0x2) DI3(0x4) DI4(0x8)。1がOn(Lowレベル)。
 d: 1バイト:[34-35]DI の変更状態ビット。DI1(0x1) DI2(0x2) DI3(0x4) DI4(0x8)。1が変更対象。
 e1~e4: [36-37][38-39][40-41][42-43]  AD1~AD4の変換値。0~2000[mV]のAD値を16で割った値を格納。
 ef: [44-45] AD1~AD4の補正値  (LSBから順に2ビットずつ補正値、LSB側が  AD1, MSB側が AD4)
 g: [46-47] 1バイト:チェックサム

要は、":"記号をトリガにして送信される24Byte程度のデータから適切なものを抜き出すイメージです。
今回使うのは、
  1: 送信元の論理デバイスID →右足/左足判別用
 e1: 左足の圧力センサ値
 e4: 右足の圧力センサ値

の3つのデータになるので、これらを抜きとるコードを書けばOKです。
(e1/e2/e3/e4のどれを使うかは、回路構成に依存します)


以下のコードでは、シリアル送信信号(文字列)から左右の圧力センサ値(整数値)に変換するコードの例になります。

void loop() {

  (省略)

  // Serial受信(内部バッファへのコピー)
  rcv_buf_update();

  // Serialバッファからコマンド取得
  String cmdmsg = getCmdmsg();

  if(cmdmsg != ""){
    String device_id = (cmdmsg.substring(0,2)); //送信元の論理デバイスID
    String A1_tmp    = (cmdmsg.substring(36,38)); //左足
    String A4_tmp    = (cmdmsg.substring(42,44)); //右足

    // 左足の場合
    if(device_id.toInt()==0x1){
      //16進数文字列→Int変換
      if(A1_tmp[0] >= 0x30 && A1_tmp[0] <= 0x39)  l_sense_val = (A1_tmp[0]-48)*16*16;
      else if(A1_tmp[0] >= 0x41 && A1_tmp[0] <= 0x46)  l_sense_val = (A1_tmp[0]-55)*16*16;
      else  l_sense_val = 0;
      if(A1_tmp[1] >= 0x30 && A1_tmp[1] <= 0x39)  l_sense_val += (A1_tmp[1]-48)*16;
      else if(A1_tmp[0] >= 0x41 && A1_tmp[0] <= 0x46)  l_sense_val += (A1_tmp[1]-55)*16;

 //右足の場合
    }else if(device_id.toInt()==0x2){
      //16進数文字列→Int変換
      if(A4_tmp[0] >= 0x30 && A4_tmp[0] <= 0x39)  r_sense_val = (A4_tmp[0]-48)*16*16;
      else if(A4_tmp[0] >= 0x41 && A4_tmp[0] <= 0x46)  r_sense_val = (A4_tmp[0]-55)*16*16;
      else  r_sense_val = 0;
      if(A4_tmp[1] >= 0x30 && A4_tmp[1] <= 0x39)  r_sense_val += (A4_tmp[1]-48)*16;
      else if(A4_tmp[0] >= 0x41 && A4_tmp[0] <= 0x46)  r_sense_val += (A4_tmp[1]-55)*16;   
    }
  }

  (省略)
}



//内部バッファからコマンド命令の検索・抽出
String getCmdmsg(){
  String result = "";
  bool incmd = false;
  int start_pos, end_pos = 0;
  int i;

  //Serial.println(recv_buffer.indexOf(':')); 
  
  for( i=0; i<recv_buffer.length(); i++){
    if(incmd == false){
      // ":"捜索
      if(recv_buffer.charAt(i) == ':'){
        //Serial.println(" ");
        incmd = true;
        start_pos = i;
      }
    }else{
      // 改行コード捜索
      if(recv_buffer.charAt(i) == 0x0A){
        incmd = false;
        end_pos = i;
        // メッセージ抽出 & メッセージより前のフレーム破棄
        result = recv_buffer.substring( start_pos+1, end_pos);
        recv_buffer = recv_buffer.substring(end_pos+1, recv_buffer.length());
        break;
      }
    }
  }
  return result;
}

既存発明品の流用の数々

それ以外の制御については、今まで作ってきた工作品での要素技術を組み合わせて実現しました。

(1)については、過去に作ったM5Stackの音声/映像再生部を流用しました
ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

(2)については、クリスマスに子供のために作ったクリスマスツリーのイルミネーション技術
音声再生についてはじゃんけんロボットの音声再生モジュールを転用しました。

ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

今まで短期間でたくさんモノを作ってきたノウハウを組み合わせてます!

開発ソース

別途 Github に公開予定

まとめ

今回は、息子の歩行器練習のモチベーションを上げるためのメロディ靴を作り、実際のリハビリ練習で定期的に使えるものが出来ました!
ただモノを作って終わりにするではなく、
実際に継続して使い続けれること
にとにかくこだわりました。
家族で試行錯誤しながら機能を考えたり、オンラインコミュ、SNS、ピッチイベント等で試作品を公開してこれをベースに一緒に改良アイデアを検討して頂けたり、PT/OT先生と一緒に現物でチューニングを繰り返せたり……

息子という実際に使ってくれるユーザーが目の前にいるからこそ出来る
家族のためのモノづくり

私自身も楽しみながら出来ました!


息子向けとしては、もうしばらく使って貰いながら、必要に応じて機能追加&改善を試みたいと思います。
今のところ、気になっているのは、靴部分の電池。現在はCR2032×2個使いなので、使えば使うほど電池を消費してしまうので、例えば充電タイプのものに変更するなどは検討しています。(とはいえ、子供の足部分にリチウムポリマー電池を置くのはちょっと抵抗あるから、そこは悩みどころ…)

もし息子以外で同様にお困りごとを抱えている方、「作ってみたい」という方がいるならば、改めてもう少し簡単に作れる手順を模索してみようかなぁ~

簡単にスピーカ再生機能を追加! DFPlayer MiniでMP3音源再生!

はじめに

アレコレとモノを作るのに熱中しているとブログ更新をしなくなってしまいますね(-.-)
「作ったモノの情報をしっかり整理して書き残そう」と考えると、
ついつち後回しになって記事が進まない。。。

なので、改めて初心に戻り、「自分の技術メモ+備忘録を気軽に残す」事を目的として、簡単に試してみたデバイスの記録メモや試行錯誤の履歴も一緒に残していきたいと思います。
(需要なかったらスイマセン)

今回試してみたのは、『簡単にスピーカ再生できるデバイスです。
子供のおもちゃ等を作る際に、とりあえず簡単に音を鳴らしたい!」というニーズ、色々とありますよね。

私が作った工作モノでも音の鳴る機器はいくつかありますが、その実現手段は大きく2つ。

ArduinoにSDカードをSPI接続して無理やりSDカード上のWAVファイルを再生
実施例:【電子工作(Arduino)】歩くと効果音の鳴るメロディ靴 - OGIMOノート ~家族のためのモノづくり~

② M5StackでSDカード上のMP3ファイル再生
実施例:M5Stack(ESP32)を使い始めてみた - OGIMOノート ~家族のためのモノづくり~

最近は、もっぱら②を使用。
②のM5stackはとにかくお手軽だしラピットプロトにはすごく便利なのですが、
 ・液晶表示がいらない場合、音声再生のみの用途には高価 (約5000円)
 ・MP3再生はそれなりに処理量をくうので、他処理(例えばLEDイルミネーション)を並行実行させるには厳しい
  (音かイルミネーションのどちらかが処理が追い付かず、不連続になってかっこ悪くなる)
というデメリットもある訳ですね。やっぱりお値段は重要。

そんな中、最近見つけた3つ目の手段を紹介!!

f:id:motokiinfinity8:20190124015146j:plain
Arduinoマイコン/ESP32 にMP3再生モジュール『DFPlayer Mini』を接続
です!

他の①②に比べて、
[メリット1] 導入が簡単!
  モジュールにマイクロSDカードを指して、Host機器(Arduino UnoやESP32)とUARTで接続するだけ。
  ライブラリを使えばマイクロSDカード内の任意のファイルを再生可。
[メリット2] 安い!
 Amazonでは2個で900円程度で売ってます。


特に値段の観点は重要!
ESP32モジュール(1500円程度)+DFPlayer Mini(450円)+スピーカ(300円程度) = 2250円なので、
M5stack半額以下で「無線対応 簡易音声再生デバイス」が出来る
ので、素敵です!
ぬいぐるみをしゃべらせたり、玄関で音を鳴らしたり、と、『お手軽おしゃべり機器』が作れてしまう!!

モジュールの説明は、こちらのサイトが詳しそうです。
DFPlayer_Mini_SKU_DFR0299-DFRobot

なお、モジュールのピン配置はこちら
f:id:motokiinfinity8:20190124013245p:plain

ハードウェア構成図

Arduino Uno Rev.3(5V I/O)と接続する際の回路構成はこちら。
f:id:motokiinfinity8:20190124012719j:plain

モジュール側のI/Oは3.3Vとの事ですので、電圧5VのArduinoと接続するときは1kΩの抵抗を間に挟むことが推奨されているとの事。UARTは、ArduinoのRxとDFPlayerのTx、ArduinoのTxとDFPlayerのRxを繋ぎます。
電源は 5V/3.3VのどちらをつないでもOKですが、ここの電源ノイズが音質に直接影響してくるため、コンデンサ等の配慮が必要です。私の場合、子供おもちゃ用は音声ボリュームが必要なので、駆動電圧の高い5Vを使いました。

スピーカは3Ωまで対応。アンプ不要で、ダイレクトにDFPlayerに繋げるため、とても楽ですね。
なお、私は100均ショップで買ってきた安いスピーカ機器を分解してスピーカユニットのみ取り出すのが一番コスト安くすみます。

SDカードはFAT形式でフォーマットして、順番に再生音源を書き込んでいきます。
どうやらFATエントリ順(平たく言えば書き込んだ順)でindex番号が1,2…と割り振られていく様です。
(他ブログでは"0001.mp3""0002.mp3"と書く事を推奨されていますが、おそらく結果としてFATエントリー順に並ぶから、だと思われます)

なお、実際のDF Player miniの接続写真はこちら
f:id:motokiinfinity8:20190124003218j:plain


ソフトウェア

本体部とは UARTで通信します。
ボーレートは 9600kbps限定の様なので注意。
ライブラリは、”DFPlayer-Mini-mp3”と"DFRobotDFPlayerMini"の2種類がある様子。Web検索する限りでは、前者が多そうですが、どうやらこちらは現在非推奨になっている様なので、私は後者の"DFRobotDFPlayerMini"を使いました。

github.com





上記の情報を参考に。

これを使って、簡単に娘とじゃんけんをするジャンケンロボットを作ってみました。
(ボタンを長押しする時間に応じてグー/チョキ/パーを変える & 音声
今回は参考までに、ボタン押す時間に応じて、グー/チョキ/パーの再生音声を変えるサンプルコードを作ってみました。

>|c|
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

#define BUTTON_PIN 3
#define SW_RX_PIN 10
#define SW_TX_PIN 11

SoftwareSerial mySoftwareSerial(SW_RX_PIN, SW_TX_PIN);
DFRobotDFPlayerMini myDFPlayer;

bool buttonState = LOW;
bool buttonStete_old = LOW;

uint32_t pushStart_time = 0;
uint32_t push_time = 0;

void setup() {
Serial.begin(115200);
mySoftwareSerial.begin(9600);

pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(1, interruptsw, CHANGE); // Pin3をスイッチ割込みとして使用

//スピーカ出力開始
myDFPlayer.begin(mySoftwareSerial);
delay(100); //設定待ち時間は仮置き

myDFPlayer.volume(30); //Set volume value. From 0 to 30
}

void loop() {
if(jyanken_start == true){
myDFPlayer.play(1); //Play the first mp3 (0001:じゃんけん合図)
delay(1500);

// ③じゃんけんの手(グー/チョキ/パー)を出す
// (1) ボタン長押し時間が1000ms以下ならグー
if(push_time < 1000){
myDFPlayer.play(3); //Play the first mp3 (0003:グー)
// (2) ボタン長押し時間が2000ms以下ならチョキー
}else if(push_time < 2000){
myDFPlayer.play(4); //Play the first mp3 (0004:チョキ)
// (3) ボタン長押し時間が2000ms以上ならパー
}else{
myDFPlayer.play(5); //Play the first mp3 (0005:パー)
Serial.println("pa");
}
}



// スイッチボタンの割り込み処理
void interruptsw(){

delay(10); // 5ms待ち (チャタリング対策)
buttonState = digitalRead(BUTTON_PIN);

//ボタン押し判定 (押し時間の測定開始)
if(buttonState == LOW && buttonStete_old == HIGH){
pushStart_time = millis();
jyanken_start = true;
//ボタン離し判定 (押し続けた時間を測定)
}else if(buttonState == HIGH && buttonStete_old == LOW){
push_time = millis() - pushStart_time;
}
buttonStete_old = buttonState;
}
|


終わりに

使ってみて今後も使えそうな 音源再生モジュール DF Player Miniのメモを記載しました。
今回は、仮としてArduino UNO rev3+DF Player Miniとしましたが、最終的には ESP32+DF Player Miniとして使う想定で進めていく予定です。
また、今回上記で記載した「ジャンケン読み上げ機」ですが、なぜジャンケンなのか等については、次回ブログで取り上げたいと思います。ありがとうございました。