TECH PLAY

アプトポッド

アプトポッド の技術ブログ

252

はじめに こんにちは、ハードウェア/OT 製品開発グループ 1 でソフトウェア開発を担当している矢部です。 アプトポッドでは以前より、 EDGEPLANT CAN-USB Interface という自社開発製品を取り扱っています。 こちらは車載機器の通信規格であるCAN 2 データの送受信に対応したもので、CANバスに接続して使用する製品です。 EDGEPLANT CAN-USB Interface EDGEPLANT CAN-USB Interface とその周辺プロダクトについては、こちらの記事でもご紹介しておりますので、よろしければご覧ください。 tech.aptpod.co.jp これまでCAN-USB Interfaceは、 弊社のアプライアンス製品 に付属する形としてのみ提供していました。 デバイスドライバーも専用のものを開発・提供しており、専用ソフトウェアからしか利用できませんでした。 今後、幅広いお客様にこちらの製品をお使いいただけるよう、オープンソースのCANドライバーであるSocket CAN 3 への対応を進めています。 Socket CANはオープンソースのため、当社の専用ソフトウェアで直接利用する場合と比較すると、インターフェイス機器側で付与されるタイムスタンプが利用できないなど機能面で劣る部分もありますが、様々なサードパーティプロダクトと連携できる点が魅力です。 当社製品をSocket CANを介して利用可能にすることで、サードパーティプロダクトを介して誰でも簡単に当社製品を利用できるようになります。 今回は、CAN-USB InterfaceをSocket CANによってLinuxマシン上で使用する方法についてご紹介します。以降の説明では、当社の車載対応のLinux搭載エッジコンピュータ EDGEPLANT T1 を前提として解説していきます。 はじめに これまでの使われ方 EDGEPLANT CAN-USB Interfaceの特長 EDGEPLANT T1 でCANデータを見る CAN-USB Interface を準備する ドライバをインストールする インターフェースを確認する CAN の設定を行う can-utils を利用する CANデータを受信する CANデータを送信する SavvyCAN を利用する 事前準備 SavvyCAN をビルドする SavvyCAN でデータを見る Python で実装する python-can をインストールする CANデータを送受信する おわりに これまでの使われ方 前述の通り、これまでは 弊社のアプライアンス製品 という形で何らかの車載ターミナルデバイスとともに提供していました。これらの車載ターミナルデバイスには Yocto 4 ベースのOSを搭載しており、CAN-USB Interface用のドライバや、クラウドサーバーへの接続用のクライアントソフトウェアなどが標準で組み込まれています。 こちらを用いたCANデータの可視化については、この記事などをご覧ください。 tech.aptpod.co.jp EDGEPLANT CAN-USB Interfaceの特長 Y分岐ケーブルによりCANバス2チャンネル分のデータを取得 TCXOを搭載、高い周波数精度とタイマー分解能 ペリフェラル側で、受信直後の正確なタイムスタンプを付与 デイジーチェーン型のクロック共有機構 優れたコストパフォーマンス(価格) CAN-USB Interfaceは、バスからCANデータを取得した際の正確な時刻情報を記録するため、インターフェイス機器側でタイムスタンプを付与します。インターフェイスにはマイコンが搭載されており、CANバスからデータを受信するとすぐにTCXO(温度補償型水晶発振器)による正確なタイマーによってタイムスタンプを付与します。 たとえばRaspberry PiのようなSPI経由のCANインターフェイスではCPUの負荷次第でCANデータの取りこぼしも起き得ますが、本製品ではマイコンで処理しているため、CANデータを取りこぼすことなく取得できます。(もちろん、本製品からUSBでホストコンピュータに転送する際のバッファなどには注意が必要です) TCXOの周波数精度は±2.5ppmで、1時間連続計測でも9ミリ秒ほどしか時刻はずれません。また、タイマーの分解能は1マイクロ秒で、タイムスタンプのぶれは10マイクロ秒程度に収まります。さらに、複数台のインターフェイス機器を併用しても打刻ずれが発生しないクロック共有機構を備えており、複数台同期したタイムスタンプを取得できます。クロック共有はデイジーチェーン型の配線でシンプルにつなげることができます。これら高いタイムスタンプ精度と複数台での同期タイムスタンプの機能により、R&Dなど取得されたデータの品質が重要なケースで広くご利用いただいております。 この記事ではSocket CANを介した利用方法をご紹介しますが、CAN-USB Interfaceが持つこれらの機能性や性能を最大限活かすには、当社がご提供する専用ソフトウェアをご利用いただく必要があります。 たとえば、Socket CANを経由してやり取りするデータにはタイムスタンプを付与できないため、インターフェイス側で付与したタイムスタンプは利用できません。 製品の詳細については、CAN-USB Interfaceの製品ページをご覧ください。本製品含め当社のソリューションにご興味をお持ちいただけましたら、当社Webサイトより是非お問い合わせください。 www.aptpod.co.jp EDGEPLANT T1 でCANデータを見る 続いて、 EDGEPLANT T1 上で CAN-USB Interface を利用する方法についてご紹介します。 EDGEPLANT T1 は Ubuntu 18.04 ベースのOSで動作しているため、その他PCなどで Ubuntu がインストールできる環境であれば同様の手順で利用可能です。 CAN-USB Interface を準備する EDGEPLANT T1 上で CAN-USB Interface を利用するための事前準備を説明します。 ドライバをインストールする CAN-USB Interface を利用するためには、専用のドライバのインストールが必要になります。 CAN-USB Interface のページ で公開されているデバイスドライバをダウンロードして、インストールしてください。 インターフェースを確認する CAN-USB Interface はインターフェースが2つあるため、接続すると can0 と can1 として見えます。 $ ip link show type can 7: can0: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10 link/can 8: can1: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10 link/can CAN の設定を行う CANのビットレートを 500kbps に設定する場合、以下のように行います。 $ sudo ip link set can0 type can bitrate 500000 $ sudo ip link set can0 up up することで ifconfig でも確認できるようになります。 $ ifconfig can0 can0: flags=193<UP,RUNNING,NOARP> mtu 16 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 can-utils を利用する EDGEPLANT T1 にはデフォルトで can-utils がインストールされているため、これを使用してCANデータを見ることが出来ます。 CAN-USB Interface は使用していないですが、Raspberry Pi 上で can-utils を利用した記事もありますので、興味のある方はご覧ください。 tech.aptpod.co.jp CANデータを受信する candump コマンドで受信したCANデータを見ることができます。 $ candump can0 can0 000 [8] 00 00 00 00 00 00 00 01 can0 000 [8] 00 00 00 00 00 00 00 02 can0 000 [8] 00 00 00 00 00 00 00 03 can0 000 [8] 00 00 00 00 00 00 00 04 can0 000 [8] 00 00 00 00 00 00 00 05 can0 000 [8] 00 00 00 00 00 00 00 06 CANデータを送信する cansend コマンドでCANデータを送信することができます。例として、 can0 と can1 を接続して、データをループバックさせてみます。 can1 も 500kbps で設定して立ち上げます。 $ sudo ip link set can1 type can bitrate 500000 $ sudo ip link set can1 up can0 からCANパケットを送信して、 candump を実行した can1 でデータを受信することを確認できました。 $ candump can1 & $ cansend can0 001#11.22.33.44.55.66.77.88 $ can1 001 [8] 11 22 33 44 55 66 77 88 // can1 の受信データ SavvyCAN を利用する Socket CANに対応した製品は有償無償含め多数存在していますが、ここではオープンソースの Savvy CAN を利用します。EDGEPLANT T1 は ARM アーキテクチャ の Jetson TX2 モジュールで動作していますが、Savvy CANのARM用イメージは配布されていません。自前でビルド、インストールする必要があります。 事前準備 ビルドには、Qt5 と Qt Serial Bus が必要になるので、それらをまずインストールします。 Qt5 のインストール $ sudo apt install qt5-default qtdeclarative5-dev libqt5serialport5-dev qttools5-dev -y Qt Serial Bus のビルド、インストール # Overkill: get all qt-stuff $ sudo apt install qml-module-qt-labs-folderlistmodel qml-module-qtquick-extras qml-module-qtquick-controls2 qt5-default libqt5quickcontrols2-5 qtquickcontrols2-5-dev qtcreator qtcreator-doc libqt5serialport5-dev build-essential qml-module-qt3d qt3d5-dev qtdeclarative5-dev qtconnectivity5-dev qtmultimedia5-dev # To get rid of the private/qobject_p.h error $ sudo apt-get install qtbase5-private-dev # Build and install qtserialbus $ cd <work dir> $ git clone git://code.qt.io/qt/qtserialbus.git $ cd qtserialbus $ git checkout 5.9.8 $ qmake $ make -j6 $ sudo make install Use qt serialbus in ubuntu 18.04 with qt 5.9 · GitHub SavvyCAN をビルドする SavvyCAN の 1.0.20 以降は Qt5.10(QRandomGenerator) に依存していますが、L4Tのデフォルトは Qt5.9 になっており、動作できません。 そのため、1.0.20 より前のバージョンを使う必要があります。ここでは V199.1 を使ってビルドします。 $ cd <work dir> $ git clone -b V199.1 https://github.com/collin80/SavvyCAN.git $ cd SavvyCAN $ qmake $ make -j6 SavvyCAN でデータを見る ビルドした SavvyCAN を立ち上げます。 $ ./SavvyCAN ツールバーの Conecction → Open Connection Windowを選択します。 Add New Device Connection-> QT SerialBus Device-> socketcan → can* を設定します。 CANフレームを受信することが出来ました。 Python で実装する これだけではただのツール紹介なので、Pythonを用いてCANデータを扱う方法を簡単にご紹介します。 python-can をインストールする python-can というライブラリが公開されており、こちらを利用すると簡単に実装することが出来ます。 $ sudo apt install python3-pip $ pip3 install python-can CANデータを送受信する can-utils の例と同じく、 can0 と can1 を接続して、データをループバックさせてみます。 import asyncio import can def print_message (msg): print (msg) async def main (): can0 = can.interface.Bus(bustype= "socketcan" , channel= "can0" , bitrate= 500000 ) can1 = can.interface.Bus(bustype= "socketcan" , channel= "can1" , bitrate= 500000 ) listeners = [ print_message, # Callback function ] # Create Notifier with an explicit loop to use for scheduling of callbacks loop = asyncio.get_event_loop() notifier = can.Notifier(can1, listeners, loop=loop) # Send messages for id in range ( 10 ): msg = can.Message(arbitration_id= id , data=[ id , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]) await asyncio.sleep( 0.5 ) can0.send(msg) notifier.stop() can0.shutdown() can1.shutdown() loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() このコードを実行すると、以下のように can1 でデータが受信できます。 $ sudo python3 loopback_test.py Timestamp: 1623221566.564898 ID: 00000000 X DLC: 8 00 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221567.066872 ID: 00000001 X DLC: 8 01 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221567.570862 ID: 00000002 X DLC: 8 02 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221568.072851 ID: 00000003 X DLC: 8 03 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221568.574905 ID: 00000004 X DLC: 8 04 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221569.078850 ID: 00000005 X DLC: 8 05 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221569.580852 ID: 00000006 X DLC: 8 06 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221570.082847 ID: 00000007 X DLC: 8 07 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221570.584880 ID: 00000008 X DLC: 8 08 00 00 00 00 00 00 00 Channel: can1 おわりに 今回は CAN-USB Interface の紹介になりましたが、その他にもアナログデータを扱うことができる ANALOG-USB Interface も販売しております。 また、自動車からのCANデータの収集や、遠隔適合のためのソリューションとして、以下のプロダクトをご提供しております。 自動車向け遠隔計測ソリューション(CAN/CAN-FD対応) www.aptpod.co.jp 自動車ECU向け遠隔適合ソリューション www.aptpod.co.jp また、自社以外の製品を使用したカスタマイズ事例も多数ありますので、ご興味をお持ちいただけましたら、まずはお気軽に 弊社サイトのお問い合わせフォームよりご相談ください 。 www.aptpod.co.jp OT : Operational Technology の略。ハードウェアおよび組込みソフトの製品開発をミッションとするグループ ↩ Controller Area Network の略。 https://ja.wikipedia.org/wiki/Controller_Area_Network ↩ https://en.wikipedia.org/wiki/SocketCAN ↩ Linuxをベースとして独自にカスタマイズしたシステムを構成できるもの。 The Yocto Project ↩
コーポレートマーケティング室でデザインを担当しております高森です。 アプトポッドはベンチャー企業でありながら育休を取得されるパパ社員が多く、社内の勤務スレッドでも家族のお世話のためお休みや時短勤務をしますというやりとりが頻繁に行われています。とても子育てフレンドリーな会社だなと感じておりましたが、今回私自身も、社内で初の産休(産休育休合わせて8ヶ月ほど)を取得させていただくこととなりました。4月に仕事復帰して早1ヶ月が経ちましたので、情報のアップデートが激しいIT業界のベンチャー企業で8ヶ月間お休みするとどうなったかをレポートしたいと思います。 コロナ禍と重なった妊娠期間 産休&育休中の過ごし方 便利だった育児アプリ 復帰後に変わっていた社内環境 あれ?macOSが変わってる あれ?SketchのUIも変わってる 社内のデザインライブラリがたくさんできてる みんなXD使ってる 社内ツールがなんだか変わっている 出産後に変わった働き方、取り組んでること 限られた環境で集中できるようになった 共同作業を意識した働き方に レビューは半日早めを目標に おわりに コロナ禍と重なった妊娠期間 アプトポッドでは、新型コロナウィルス感染対策として2020年2月から早々にリモート勤務が推奨され、その後出社の必要がない社員は原則リモート勤務となりました。 昨日まで対面で打ち合わせをしていたメンバーとも突然音声のみのオンラインミーティングとなり初めは違和感があったものの、一気にリモート体制が整えられていきました。すっかり普通となったオンラインミーティングですが、初めは"ソワソワする"なんて思っていた時代があったことは、将来子供に語ることができそうです。 私の妊娠&出産期間は完全にこのリモート期間と重なりました。コロナ禍での出産は大変なこともありますが、つわりがひどい時は横になって休みながら作業をすることができたり、大きいお腹で満員電車に乗る必要がなかったり、妊娠の時期が標準でリモート勤務だったことで、仕事面での不安はなかったように思います。 また、リモートでのやりとりが当然となっていたため、産休中も連絡があったら対応できるだろう(実際はそんな事態は無く完全にお休みをいただいてました)と気楽に構えられていたことはとても良かったです。出産に関する届け出も、不明な点があれば人事の方といつでもやりとりできるという安心感がありました。 産休&育休中の過ごし方 デザイナーが4人しかいない中で長期のお休みをいただいていたこともあり、復帰後のブランクがやはり心配でした。復帰後もすぐに戦力になりたい...と思っていたため、ツールの感覚が鈍らないように隙を見つけてちょっとした作業ならできるのではとも考えておりました。実際に出産直後の子供がほとんど寝ている時期は、会社の会議資料やプロジェクトのスレッドを覗いたり、子供グッズを作るのにパソコンで作業したり、意外と時間取れる?と思ったりもしていたのですが...徐々に覚醒して泣いたり笑ったりするようになっていく子供のお世話で、1日があっという間に過ぎていくようになりました。この時期は子供をあやしながら流し見できるNetflixとワイドショーがお友達、起きてる時間はとにかく育児について調べまくる、という子供100%の生活になり、パソコンは部屋の隅っこでほとんど触ることはなかったです。 便利だった育児アプリ せっかくなので子育てに使って便利だったアプリを紹介します ピヨログ 育児の記録をひたすら付けていくアプリです。社内の育児サークルでパパ社員たちが絶賛していたので産後すぐから使っていました。子供が生まれて以降ブルーライトに敏感になりスマホのナイトモードの大事さを知ったのですが、当然のごとくダークモードにも対応されていて、ウィジェットにも早々に対応、使いやすいように常に細かくアップデートされていて素晴らしいなと思いました。 睡眠時間の変化です(青いところが寝ている時間) 【生後0ヶ月】→【生後5ヶ月ごろ】→【最近】 1日中寝たり起きたりだった生活リズムが整ってきて、保育園が始まってからは平日日中は親元を離れるようになりました。成長が感じられてしみじみします(泣) こうしてみると初期は本当に頑張っていました。夜間授乳で寝不足が2週間以上続くと丸1日寝てない状態と同じなのだそうです。将来子供の反抗期が訪れた際はこのスクリーンショットを印籠のように使いたいと思います。 ステップ離乳食 食べた食材を記録していくアプリです。月齢ごとに食べられる食材がわかりやすく表示され、食べたらチェックを入れていきます。調理法も確認できシンプルで使いやすいアプリだと思いました。離乳食は色々な情報が溢れていてサイトによって書いていることもバラバラなので、色々調べるのに疲れた時にはこのアプリのみで十分かと思ってます。 Notion 情報・タスク管理ツールです。子供の食べたものや成長記録など、それまでバラバラで付けていた記録をまとめて管理したいなと思い使い始めました。Notionを育児記録に使ってるtipsもたくさんあるので参考にしながら使ってます。この頃には記録を夫と共有という点はもう諦めてw、自分が見やすく記録しやすくを目標に、自分の食事管理も兼ねて使ってみてます。業務に関するメモなどあらゆるメモ帳として使っており、入力した項目を行ごと列ごとコンポーネントごとごそっと並び替えできるので、とにかくメモしておいて整理は後でといった使い方ができる点が気に入ってます。 そんなこんなで気がつくと復帰する月となっていました。慣らし保育期間は子供がもらってきた風邪菌により私自身がダウン。3週間体調不良(子供は元気)が続き、なんの準備もできないまま仕事復帰となりました。こうなることは予想してはおりましたが見事に、本当にあっという間に産休&育休は終わってしまいました。(体感では3週間くらいしか休んでないのではという感じです) 復帰後に変わっていた社内環境 あれ?macOSが変わってる 復帰後、macOSがBigSurに変わっておりました。まずはOSをアップデートしよう...と思うもそのための容量が不足しているためファイルを整理するところから、さらにファイルの保存場所を探すのもまごつくレベル...半年以上パソコンに触らないとこうなるのか、と先が思いやられ1日8時間なんてとても脳が働かなったことを覚えています。心配していたデザインツールについてはすぐに感覚が戻りました。 あれ?SketchのUIも変わってる アプトポッドでは主にSketchを使用してデザインしておりますが、SketchもBigSurに合わせてデザインが大きく変わっておりました。全体的に丸くなり色合いも変わっている他、シンボル一覧が見やすくなっていたりライブラリのシンボルが使いやすくなっていました。 社内のデザインライブラリがたくさんできてる 産休&育休前は、デザインコンポーネントのたたき台を作成した段階で長期休暇に入ったのですが、その後デザインチーム内でコンポーネントが整理され、ライブラリにまとめる作業が進められていました。Sketchの変更と合わせてコンポーネント共有化のワークフローがイメージできるようになっていました。動画作成時のテロップなども、とにかく繰り返し使いそうなものはライブラリ化されており、久しぶりに作業する際にとても助けになりました。 みんなXD使ってる AdobeのXDはこれまであまり使っていなかったのですが、ファイルの軽さと共有しやすさなどからXDが以前よりも使われるようになっていました。私も復職後から現在まで主にXDを使っているのですが、デザインファイルの共同作業がとても便利に感じています。 以前からデザインファイルの共同作業に向けて世の中がどんどんアップデートされている気配を感じてはおりましたが、私自身は黙々と一人で作業を進めたい派で、作業中のデザイン(ファイルも)ほど見られたくないものはないというくらい、共同作業についてはネガティブな印象を持っておりました。しかしリモート環境でデザインのレビューをする際に、デザイナー同士であれば同じファイルを見ながらの方が、それぞれの画面パターンをいちいち切り替えて説明せずとも各自でファイルを見渡してさっと共有できたり、パッと出たアイディアもその場で仮イメージを作成して残しておくという使い方ができてとても便利に感じています。 社内ツールがなんだか変わっている 入社以来、社内の手続きやtipsはQiitaにまとめられているものを探して確認していたのですが、ConfluenceやJiraが使われるようになっておりました。タスクや議事録をプロジェクトごとに管理する目的とのことでしたが、産休明けでも会社で進行中のプロジェクトをざっくりと把握できるようになっておりました。 また、社内情報については管理本部の方々が更新してくれている社内向けポータルサイトが更新されており、申請手続きの入り口が設けられていたので手順を調べなくても大丈夫、といった具合に整理されておりました。(保育園に必要な書類もポチッと申請できるようになっていたのを知らず...活用できなかったことが悔やまれます) ちなみに社内ポータルサイトではたまに社員へのインタビューなども掲載されており、リモート体制になってから入社された方が紹介されていたりします。リモート体制でも社内コミュニケーションが取りやすいように工夫いただいていることが、長期休暇明けの私にとっても社内の振り返りや把握にとても助けになっており、ちょっとした息抜きになっています。みんなのリモートでの仕事状況やパパたちがどのように時間を使っているのか参考になりました。 出産後に変わった働き方、取り組んでること 出産後に変わったことはズバリ、 残業ができません! 正確にいうと残業はできるのですが、自分の都合の良いタイミングまで仕事を続けることができません。私の場合は毎日17時頃に保育園のお迎え時間がやってくるため、16時ごろから作業に集中できるようになっても、"このままキリの良いところまでやるぞー"ということができません。もう少しやりたくても、中途半な状態でも、切り上げなければならない時間が毎日決まっています。 子供が寝た後に再開、週末に集中して...なども(家の間取りの関係もあり)業務時間ほど集中することができず、家にパソコンがあるからいつでも作業できるというゆるい心構えでいたことを反省しました。 限られた環境で集中できるようになった 勤務時間外にまとまった作業時間を取ることが難しいため、何がなんでも勤務時間に終わらせたい、集中しなきゃという緊張感からか、以前よりも限られた環境で集中することができるようになりました。 以前はすぐに気が散ってしまうためホワイトノイズをかけて強制的に集中できるようにしたり、気分が上がる作業スペース作りに精を出していましたが、今はリビングの片隅の60cm四方のスペースで、オムツのストックの上に足を乗せたまま黙々と作業できるようになりました。自分の中では結構変わったなと思っています。 共同作業を意識した働き方に これまでは作業ファイルの共有はプロジェクトが落ち着いたタイミングで整理してから、と後回しにしがちだったのですが、デザイン作業と並行して早い段階からコンポーネントを整理するように心がけてます。デザイン案を展開する際にもこの方が効率が良さそうだと感じてます。 その他にも、作業プロセスも可能な限りドキュメントに残しておきたいと思っているのですが、デザイン作業を優先してついつい後回しになりがちとなってしまってます。こちらは昼食前後でデザイン作業とドキュメント作成を分けるなど、週の中に強制的にドキュメント作成の時間を設けるように試してみてます。 レビューは半日早めを目標に アプトポッドはフレックス勤務がOKなので、復帰後は基本の就業時間よりも2時間早くずらして勤務しています。そのため、自分の勤務終了後に連絡を受けてもレスポンスが遅くなる、または翌日になってしまうこともあり得ます。 これまでも作成した資料に不備があってレビューをもらうもその後対応できず、上司に修正いただくということがありました....終了時間が決まっているため修正時間も考慮して、余裕を持って共有するようにしていきたいです。 おわりに 復帰後は仕事内容に制限がかかってしまうかな...と心配しておりましたが、今のところそういった不安はありません。 復帰して1ヶ月が経ちましたが、想像していたよりも、驚くほど普通に働くことができています。 これもフレックスやリモートといった柔軟なワークスタイルを作ってくれている会社のおかげであり、足りないところをサポートしてくれる上司とチームメンバーのおかげです。今のところ病気ひとつせず休まず保育園に行ってくれている子供にも感謝です。 まだ子供が熱を出した経験をしたことがないため、保育園に呼びされたらどうしようか、そもそも保育園が休園になったら(こんな時期なので)どうしようかなど心配事は尽きないのですが、進め方を一緒に模索してくれる社風であることは確信してますので、時間の使い方を試行錯誤しながらこれからも育児と仕事の両立を頑張っていきたいです。 アプトポッドへの入社に興味を持たれた方は 採用ページ をご覧ください! (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "https://platform.wantedly.com/visit_buttons/script.js"; fjs.parentNode.insertBefore(js, fjs); }(document, "script", "wantedly-visit-buttons-wjs"));
はじめに こんにちは、 SRE チームの金澤です。 弊社は intdash というIoTプラットフォームを展開しています。そのサーバサイドである intdash Server はクラウドインフラを用いた構築が多く、その一つが Amazon Web Service(AWS) です。 パブリッククラウドを使用する上で気をつけたい点の一つとして 障害の把握 が挙げられます。サービス障害の要因確認として役立ち、その内容をもとに今後のプロアクティブな対策を検討する助けにもなります。また大規模の障害の場合はお客様が把握されている可能性も高く、いただいたご質問にスムーズな回答を差し上げる一助にもなります。素早く把握していることに越したことはありません。 そのような体制を目指すべくまずは、 障害情報を一か所に集約する 影響を受けない障害内容についても通知を受ける 上記が必要と定め検討を開始しました。この記事ではAWSサービスの障害通知方法の検討とツールを利用した通知システムの構築についてご紹介します。 はじめに 障害情報の把握 PHD利用時の課題 shd notifierの利用 結果 まとめ 障害情報の把握 障害を通知するためには、まずは障害を把握する必要があります。把握の手段はいくらかありますが、以下が代表的なものかと思います。 Service Health Dashboard(SHD) SHD AWSが提供するサービスの稼働状況を示すダッシュボードです。各リージョンおよび各サービス毎の稼働状況が示されています。それぞれにRSSフィードが配信されているため、確認したいリージョンまたはサービスのRSSフィードを購読して、サービスの状況を確認することが可能です。 Personal Health Dashboard(PHD) PHD AWSアカウントに紐づくかたちでサービス障害を記録してくれる機能です。バックエンドに AWS Health API が使用されており過去90日間の障害情報が表示されます。ダッシュボードの閲覧にはAWSアカウントへのログインが必要となります。 Twitter(非公式) AWSが公式に提供しているものではないですが障害情報をツイートされています。東京リージョンに特化した @awsstatusjp と、リージョンを全体に広げた @awsstatusjp_all が存在するようです。 DownDetector(非公式) こちらもAWS非公式ですがSNSからの投稿内容を利用し各種サービスの状況を判断するWebサービスがあります。各種サービスにはAWSも含まれており、状態を確認することができます。 PHD利用時の課題 まず、上記の情報を通知に利用し、AWSだけで構成できる仕組みを考えました。 SHDを利用する場合、購読が必要になるRSSフィード数が多くなり、RSSフィードを登録した後のメンテナンス(追加・削除・確認等)に工数が掛かることが予想されたため、ここではPHDを利用した形での検討を進めました。 PHDはすでに構築されているダッシュボードを閲覧して障害状況を把握する方法の他に、AWS Cloudwatch Eventsと連携したアクションを取ることが出来ます。AWS Cloudwatch EventsのイベントトリガーをAWS Healthイベントに設定することで、PHDの更新に併せてターゲットを呼び出すことができます。これは ドキュメントにも方法が紹介されています 。このドキュメントではAWS Cloudwatch Eventsでイベントを受信する方法や、イベントを受信した後にAWS Chatbotを利用してビジネスチャットなどに障害情報を通知する方法が紹介されています。 ドキュメントを参考に構築したあとはしばらく状況を確認していましたが、障害発生しても通知がされないことがありました。確認を進めていくとHealth APIでは障害イベントの種類は2種類存在し、通知されるイベントはアカウント固有のイベントである ACCOUNT_SPECIFIC のみであったことが分かりました。これは ドキュメント にも記載があります。 Only AWS Health events that are specific to your AWS account are delivered to CloudWatch Events. For example, this can include events such as a required update to an Amazon EC2 instance and other scheduled change events that might affect your account and resources.Currently, you can't use CloudWatch Events to return public events from the Service Health Dashboard. Events from the Service Health Dashboard provide public information about the Regional availability of a service. These events aren't specific to AWS accounts, so they aren't delivered to CloudWatch Events. 上記引用にて記載されているとおり、今回通知されなかった障害イベントの種類は PUBLIC となっているようでした。 shd notifierの利用 記事冒頭で要件とした「影響を受けない障害内容についても通知を受ける」を満たすためには、 PUBLIC となる障害イベントについても通知も受けたいところです。その場合には、 AWS Health Tools - shd notifier の利用を検討することができます。こちらはAWSが提供しているツールで、SHDの更新情報を検知してAWS SNSやSlackなどのビジネスチャットに通知できるようにするものです。 shd-notifier はAWS Step Functions、AWS LambdaやAWS Cloudwatch Eventsを利用して構築されます。障害検知・通知をまとめて一つのワークフローとして定義して処理を行います。具体的には以下のような動作を行います。 SHDへの定期的なポーリング 検知した障害に対しての定期的な状況確認 確認結果をSNS/ビジネスチャットへ投稿 デプロイは上述のGithubにCloudFormationで使用するテンプレートとLambda関数をデプロイするシェルが提供されているため、そちらを利用してデプロイする方法が簡単です。(弊社ではインフラ基盤をTerraformでコード管理しているため、一部を別途書き起こししています。) 結果 今回のデプロイの結果を、先日発生した障害内容から確認してみます。なお、投稿する先としてSlackを設定しています。 2021/05/07 03:16-04:43(JST) に AWS CloudFrontにおいてコンソール表示遅延の障害が発生しました。aws healthコマンドで確認すると以下の応答が返却されます。 eventScopeCodeの値が PUBLIC となっていることが確認できます。そのためこのAWSアカウントとしては本障害は公開イベント扱いとなります。 $ aws health describe-events --region us-east-1 | jq .events[] : { "arn": "arn:aws:health:global::event/CLOUDFRONT/AWS_CLOUDFRONT_OPERATIONAL_ISSUE/AWS_CLOUDFRONT_OPERATIONAL_ISSUE_ZQLBL_1620324983", "service": "CLOUDFRONT", "eventTypeCode": "AWS_CLOUDFRONT_OPERATIONAL_ISSUE", "eventTypeCategory": "issue", "region": "global", "startTime": "2021-05-07T03:16:23.386000+09:00", "endTime": "2021-05-07T04:43:59.487000+09:00", "lastUpdatedTime": "2021-05-07T04:44:00.233000+09:00", "statusCode": "closed", "eventScopeCode": "PUBLIC" } : PUBLIC となっている通り弊社環境では幸いにも影響はありませんでした。通知状況を確認してみます。 初報 障害発生後に初報が発報されたことが確認できました。 [SHD AUTO] という文字がありますがこれは投稿時に付与することが出来るprefixで任意で設定することができます。 続報/終報 続いて、続報および終報が発報されました。 12:43 PM(PDT) に正常に回復した旨が報告されています。 この続報/終報では、初報から時間が経過しており途中経過をまとめた報告となっています。これは投稿先への投稿間隔を比較的長めに設定したことによるものです。前述のGithubリポジトリにある Health-Event-Iterator-LambdaFn.py で投稿間隔を調整可能です。 以上で、無事障害イベントの内容が通知されていることが確認できました。 まとめ 今回はAWSのサービス障害が発生した場合における、広い範囲での障害情報の収集および集約化についての手法の確認・構築を行いました。なお、AWS Step Functionsステートで動作するLambda関数はHealth APIを使用するため、AWSアカウントが ビジネスアカウント 以上である必要があります。ご注意ください。 今後は、 投稿内容のブラッシュアップ 障害対応自動化への応用検討 など引き続き改善を進め、障害対応の体制作りに貢献出来ればと考えています。
はじめに 事業開発室の小宮です。 今日は、これまであまり語られる機会の少なかった、当社のビジネスサイドの業務についてご紹介します。 はじめに 会社・チーム紹介 会社紹介 チーム紹介 アカウントマネージャー業務内容 見込み顧客獲得 提案 受注とそれ以降 アプトポッドの営業として働くことの魅力 お客様の課題解決に自社製品で貢献できる 先進的な業務に関われる 納得感のある会社である おわりに 会社・チーム紹介 会社紹介 アプトポッドは産業用IoTプラットフォーム”intdash”などのご提供を通じて、お客様のプロジェクトの成功をお手伝いさせていただいている会社です。まずは当社について、かいつまんでご紹介させてください。 昨今は様々なIoT製品があります。intdashは、以下の特徴を全て兼ね備えている、という点で、他のサービスと差異があると考えています。 モバイル回線(不安定回線)経由でも低遅延なデータ伝送の実現 モバイル回線経由でもデータの完全回収の実現 動画、各種センサーなど、プロトコルに依存しない多様なデータの取得 複数種類のセンサーデータの正確な時刻同期 可視化、分析プラットフォームの一体提供 ただ、上記の特徴を全て同時にご要望されるお客様は少なく、ケースバイケースで必要な要素に着目いただいています。 ご活用いただいている業界としては、 自動車はじめ各種モビリティが大半 、ユースケースとしては、 高頻度データの遠隔データ収集、遠隔診断、遠隔制御 などとなっております。 お客様のお仕事の性質上、研究開発に深く関わることが多いので、公開できる事例をご用意するハードルが非常に高いのが悩みどころです。実際には ほぼ全てのお客様が一部上場企業 であり、日本の未来を支える縁の下の力持ちのような存在だと思っています。 チーム紹介 当社の人員構成は、エンジニア7割、ビジネスサイド2割、管理部(マーケも内包される)1割となっております。 ビジネスサイドは、私が所属する 事業開発 を担当する部門と、商談を技術面から支援する ソリューションアーキテクト のチーム、お客様へのソリューション提供に責任を持つ プロジェクトマネージャー のチームからなります。 事業開発チームは、この記事を書いている2021年中旬時点で4名の、まだまだ小さなチームです。このチームで営業と、いわゆる事業開発を兼ねている形になります。本記事では主に、営業(アカウントマネージャー)としての業務内容をご紹介いたします。 アカウントマネージャー業務内容 以下は有名な本”The Model”を元にした、営業プロセスの図解になります。現時点では、ここに出てくる全てのプロセスを事業開発チームが担当しております。 図は Salesforceのブログ より引用 以降では、プロセスごとにもう少し詳細な業務内容を説明いたします。 見込み顧客獲得 見込み顧客の獲得は、主として自社独自の施策によるものと、販売パートナー様との協業によるものに分かれます。 自社施策としては、従来展示会等のリアルイベントからの流入が最も多くなっていました。展示会出展の際には、社内外のマーケ・デザインのチームと協力して、企画、当日の説明、終了後の見込み顧客のフォローまで一貫して行います。ただ、直近では展示会がCOVID-19の影響をもろに受け、それ以外の施策の試行錯誤を重ねているところです。 イベント出展については以下の記事も参考になります。 tech.aptpod.co.jp 販売パートナー様は、純粋な販売代理店様から、自社でintdash構築能力を持つシステム開発会社様まで多岐にわたります。共通しているのは、intdashを最初にきちんと理解していただくプロセスに気をつかうこと。多くの販売パートナー様にとってintdashとは、「一言での説明が難しい商材」であるため、きちんとご理解いただけるよう心がけています。 提案 具体的なニーズがありそうな見込み顧客とは、商談を重ねます。COVID-19以降はほぼ100%の商談がWeb会議で完結するようになりました。 ほぼ全ての商談で、ソリューションアーキテクトと協働します。当社のお客様は、普段から技術を扱っている方が多く、初回の商談から深い仕様の相談をされることも少なくないためです。また、intdashは継続して成長していく製品であるため、例えば現状の製品のカバー範囲外のご要望について、今後の製品開発でカバーしていくのか、あくまでその場限りのカスタムでご対応するのか、など、提案内容に対し製品開発的な目線も必要になります。 そのため営業目線であっても、特に現状の自社製品の限界や、カスタム開発での提案をする際のやり方など、 製品仕様だけでなく、開発に関する理解も重要になります 。単純にお客様の声をヒアリングできるだけではなく、 社内外双方の幸福度が最大になるよう 立ち回ることが求められます。 受注とそれ以降 無事、お客様のご要望を叶えられそうな提案ができ、お客様内のIT部門からのセキュリティに対する懸念も払拭され、先方のご予算確保の目処も立つと受注となります。バンザイ、お疲れさまでした。 …で終わりにはなりません。2つの観点があります。 1つ目は、 継続して案件を育てていくこと 。 当社の取り扱う案件の大半が、何か大きな目的のための実証(POC: Proof of Concept)から始めるケースが多くなっています。POCは最終的にもう少し大きなサービスを目指していることが多く、これに対してお客様側のプロジェクト全体の成功に貢献するための進め方を、受注前後を通じて継続して支援する姿勢が求められます。この姿勢は社名の由来でもある、 『a project to prove one’s delight』 という言葉に現れています。 2つ目は、 カスタマーサクセスの視点 。 現時点で当社では、サービス導入までのデリバリーと、納入後のサポートが部門として分かれていません。そのため、納入後の問い合わせ対応なども、セールス対応しているチームが受けています。この導入後の問い合わせや要望から、新たなお仕事や製品改善の種が見つかることもあります。 アプトポッドの営業として働くことの魅力 ここまでアプトポッドの営業としての業務内容をご紹介してきました。続いて当社の営業メンバーに、当社で働く魅力をヒアリングしたのでご紹介します。 お客様の課題解決に自社製品で貢献できる IoT/DX化の大きな難題を抱えている、日本を代表する製造業の方々に対し、 当社のミドルウェアを提案することで課題解決に寄与したり、協創の中で自身や会社を成長させていただけるところ 。 自社で製品を作っているので、製品が採用されてお客様の役に立てればとても嬉しい。課題にも一緒に取り組めるし、他社製品を売るだけでは味わえないかなーと思います。 お客様に対して自社製品で貢献できることは、商社など仕入れて売る系の営業では味わえない楽しさかと思います。特に当社はエンジニアとビジネスサイドの距離が近いことと、社内のエンジニアの幅が広い(ハードからスマホアプリまで)ため、お客様からのご要望に対しても幅広い対応力があります。 同時に、現場の声を製品開発にフィードバックできるハードルも低く、一緒に製品を成長させていく手触りも感じられます。新製品は営業メンバー自らが最初のユーザーになり、時には自ら車を運転して機能や性能を確かめます。 www.youtube.com (この動画も営業メンバーが実際に商品を扱いながら撮影しました) 先進的な業務に関われる 技術革新が激しい業界のため、日進月歩で新しい知識が求められる点や、 非常に幅広いレイヤーの知識が必要となる ので、常に刺激になっている。 自動運転・遠隔制御とか未来の社会システムを作る事業に、自分が関われる機会はなかなか無い。 当社のお客様は、主として大手製造業のR&D部門の方が多く、扱う案件は先進的なものばかりです。同時に自社製品に関しても、IoT、クラウドなどの技術面の他に、サブスクリプションモデルのビジネスモデルなど、売り方面でも新しい概念の説明が求められます。かっこよく言えば、 お客様とともに将来のサービスを構築していく ことが好きな方、シンプルに言うと新しいものが好きな方に向いているお仕事と言えます。 納得感のある会社である 技術を重視する会社だからかもしれませんが、会社全体の雰囲気として、非常に合理的で、働いていて各所で納得感のある会社だと思います。 例えばそれが顕著に現れたのはCOVID-19に対する対応で、当社では2020年早期の段階で、「ワクチンが出回るまで基本フルリモートワークとする」方針を打ち出していました。COVID-19という未知の課題に対し、リーズナブルな出口を早期に定義できており、このようなことは各所で見られます。 社員も謙虚で論理的な方が多く、テキストのやりとりが多くなりがちなリモートワークにおいても、コミュニケーションで困ることは全くといっていいほどありません。 このあたりについては、過去に当社のエンジニアが中途入社を振り返った記事でも同じようなことが述べられていますので、概ね間違っていないのだろうと思います。 tech.aptpod.co.jp tech.aptpod.co.jp おわりに アプトポッドは アカウントマネージャーを募集中 です! これまでSaaSプロダクトの営業(フィールド・インサイド含む)や、製造業を中心としたエンタープライズ向け営業をやっていた方などは親和性が高いと思います。お客様との会話は最初から技術的な仕様の話になる事が多いため、技術的なバックグラウンドを持つ方も歓迎です。何より様々なジャンルの新しいことが好きな方、新規事業が好きな方、お客様と一緒にプロジェクトを進めていくのが好きな方、ご応募お待ちしております。 アカウントマネージャー以外にも、ビジネスに近い職種だと、 プロダクトマーケティングマネージャー、ソリューションアーキテクト、サポートエンジニアなども募集しております 。合わせてご確認いただけますと幸いです。
はじめに こんにちは、アプトポッドでハードウェア/OT 製品開発グループ *1 のマネージャーをしている おおひら です。 昨今の半導体不足の状況、「部品が買えない」という阿鼻叫喚の声がそこかしこから聞こえておりハードウェア製品に携わるみなさまにおかれましては眠れぬ夜を過ごしていることと思います。 弊社もご多分に漏れず、この4月に量産を開始したNVIDIA Jetsonプラットフォームを採用した組込みコンピューター製品 EDGEPLANT T1 の部品調達に四苦八苦しております。 prtimes.jp 以前の記事( EDGEPLANT T1 リリースまでの軌跡 - aptpod Tech Blog )では設計者視点で商品化の流れを紹介させていただきましたが、今回の記事ではその裏側にある部品調達の現場を知っていただいたうえで、中国のB2B ECサイトのAlibaba.comで半導体部品を購入する新しい試みについてお伝えしたいと思います *2 。 はじめに 半導体不足、実際のところ現場はどうなの? 2020年夏頃 2020年秋~年末 2021年1月 2021年2月 2021年3月 2021年4月以降 Alibaba.com とは 購買の事前準備 検索 サプライヤとの交渉 問合せ メッセージのやりとり(購入意思決定まで) メッセージのやりとり(購入意思決定のあと) 着荷~検品~レビュー 関税と輸送費の支払い Tipsなど 半導体の品名についている20+や08+ってなに? "Original" って謳っているのはどういうこと? 品質面の不安はないの? 半導体部品の偽物の不安はないの? Invoiceへの価格記載は正しくしよう サプライヤとのやりとりは丁寧に、礼節を大事にしよう 即断即決しよう モバイルアプリを活用しよう おわりに 半導体不足、実際のところ現場はどうなの? 何も知らない一般の皆さんに解説すると、半導体部品が徐々に購入できなくなっています。世界中のいろいろな会社で部品の取り合いをしています。値段も上がっています。部品がないと製品が作れません。地獄です。 — なるみCYO (@queenmk) 2021年5月13日 紛うことなき地獄。2011年のタイ水害や2018年の積層セラミックコンデンサ供給不足など、過去にも部品調達の問題はありましたが、いまこの瞬間が最大の危機だと感じます。 (いま振返ると)昨年から徐々に兆候はあったのですが、時系列で弊社の状況を記載すると以下のとおりです。 2020年夏頃 新型コロナウイルスの影響で海外の半導体メーカー各社に出勤制限が生じ、工場の操業を制限しているため納期が伸びるという情報を複数の代理店様から頂く この時点では「まぁそうだよね、しょうがないよね」ということで3ヶ月ほど先の試作部品発注を実施 Digi-KeyやMouserなどの主要部品ECサイトの在庫状況は正常 2020年秋~年末 旭化成エレクトロニクス(AKM)の工場火災 が発生したこともあり、水晶発振器等のアナログ部品について在庫状況を注視 EDGEPLANT T1の重要部品であるGNSS受信モジュールについてAKMの件の影響を受けそうな兆候が見られたため先行して部品購入を実施しようとしたところ主要な部品ECサイトで在庫切れの状況を知る 慌てて代理店様に問合せるも「AKM火災の直接の影響ではなく、電子部品全般のリードタイムが伸びているためにGNSS受信モジュールも納期が28週(7ヶ月)以上になる」との報告をいただく *3 2021年1月 4月のEDGEPLANT T1の1st Lot 量産に向けた電気・半導体部品の購買を実施 この時点では品目として9割ほどの部品が問題無く確保できたが、一部の車載や産業グレード部品が主要ECサイト(Digi-Key, Mouser)で確保できず、他のECサイト(chip1stop, RSコンポーネンツ, TTI, LCSC, element14, Arrow Electoronics, onlinecomponents etc.)で確保することがあった *4 この頃から車載グレードの半導体部品の調達問題で OEM各社の減産報道 がでてくる *5 2021年2月 半導体の供給状況逼迫の根本原因として 台湾TSMCの製造ラインの逼迫 が報道される アメリカ中西部の寒波 の影響でTexas InsturumentsやInfineonが現地の半導体工場の操業を停止 *6 2021年3月 EDGEPLANT T1の2nd Lot以降の部品調達状況(特に代理店様から購入させていただいているキーコンポーネント)を確認するも既に遅しで、民生グレード部品であっても納期が28週以上に伸びている部品が発生していたためすぐに2021年の購買予定情報をまとめて開示したうえで発注書を出す とにかく発注しないと部品確保が進まない しかしそれでも納期保証はできない、という悲しい条件付き (幸いながら弊社の製品では採用していなかったものの)特定の車載グレードの半導体部品では1年以上の納期になるものが出てくる なんか分からんが鋼板や樹脂の価格も高騰しているらしいぞ?との噂 2021年4月以降 特定メーカーのMCUなどで民生グレードの半導体部品でも納期1年以上の製品がでてくる 大口顧客のまとめ買いの影響なのか半導体以外の受動部品やコネクタ製品でも主要ECサイトで在庫切れが多発 ECサイトでついさっきまで在庫があった部品がなくなったり(実体験)、「今すぐ来年までの購入予定を共有しなければ売れない」などというメーカーが現れたり(噂)。修羅の国かここは さて、このような危機的状況にあっては一定のリスクを許容した上で正規代理店以外のルートで部品を購入することも検討せざるを得ません。 一般的に大手メーカーであれば電気・半導体部品の購買に関しては正規代理店を前提とした上で、一定の審査 *7 を経て認定したパートナーから部品を調達することになると思いますので本稿のような試みは不可能と思います。あくまで我々のような小さなチーム *8 が生き延びる術だと思って以降の文章をお読みいただければ幸いです。 Alibaba.com とは japanese.alibaba.com Alibaba.comは中国の阿里巴巴集団:アリババグループが運営している海外向けのB2B ECサイトです。中国ローカルの工場や卸、部品メーカーなどが売り手として登録しており、ここにバイヤーとして登録することで電気・半導体・機構部品や各種完成品、OEM製品などの購買が可能です。同じくアリババグループが運営する 淘宝:タオバオ や AliExpress はB2CのECプラットフォームとして日本でも認知度が高まっていますね。 では早速Alibaba.comで半導体部品を購入してみましょう。 購買の事前準備 アカウントを作成します。プロフィールは名刺代わりですので会社の住所や連絡先、興味のある技術分野などを記入しておきましょう。 検索 購入したい半導体部品の型番を調べてみましょう。昨今納期が45週と恐ろしいことになっているSTMicroElectronics社のマイコンなんてどうでしょうか。 試しにSTM32F401RET6を検索してみましょう。 たくさん出てきましたね。なお検索する際には私は必ず左側タブのチェックをつけています。 Trade Assurance Verified Supplier Trade Assuranceは納期や品質に契約違反があった場合にアリババが100%の返金保証をするサービスの対象になるサプライヤであることを示します。 Verified Supplierはサプライヤの供給能力や製品品質、コンプライアンス認証などについてAlibaba.comが規定する監査に合格していることを示します。SGSやTUVなどの代表的な第三者機関の監査を受けており、一定の品質判断の目安になります。 fuwu.alibaba.com サプライヤとの交渉 問合せ さて、ここで検索して一番トップにでてきた Shenzhen Jeking Electronic Corp. に注目しましょう。 レビューの数と評価 サイト登録からの期間(6年) 問合せ後の返信待ち時間が平均4時間以下 Googleで検索すると コーポレートサイト がしっかり出てくる ということで好感が持てます。 早速この画面の右側にある Contact Supplier を押して見積依頼の問合せをしてみましょう。品名や所要数、備考を記載するフォームが現れますので記載して送信します。 フォームの下側にある下記の項目にもチェックをいれて送信します。24時間以内に返信が無い場合は自動的にRFQを提出して広く他のサプライヤに見積依頼を行うことができます。 Recommend matching suppliers if this supplier doesn't contact me on Message Center within 24 hours. Agree to share Business Card with supplier. wp-service.alibaba.co.jp なおこのRFQ機能は相見積取得ができる強力な機能であり、一度提出すると多数のサプライヤから見積回答のメッセージが殺到することもあります。 複数のサプライヤとのやりとりを通じて市場流通価格の適正値や業者の対応速度感などの重要な情報が得られることもありますので活用してみてください *9 。 メッセージのやりとり(購入意思決定まで) さてサプライヤに問合せをした後は回答を待ちます。Alibabaプラットフォーム上のメッセージ機能で回答が届きます。早い場合は数分、遅くても半日程度で見積回答が届くことが多いです。 1日以上待つことも稀にありますが、その場合は先に紹介したRFQ依頼を出しておくことで他のサプライヤからの見積回答メッセージが届くと思います。 ※ ここからは(冷やかしでSTM32F401RET6を買うふりをするわけにいかないので) 私が以前購入した他のSTM32マイコンの購買やりとりを参考として記載します このときは問合せから10分ぐらいの爆速回答。しかし$4.452と高いですね…平常時の価格の2割増しぐらいか? 100個じゃなくて400個買うのでもう少し安くしてよ、という交渉で$4.19/個に。念のため保存状態やパッケージ形態を聞きます。 そして「市場価格が不安定なので今日決済してほしい」という圧をかけてくる。商売人ですね。市況が市況だけにしょうがない。 実は私はこの裏で他の3,4社のサプライヤと同時にメッセージのやりとりをしており、最も低価格かつメッセージレスポンスが良好だったのがこちらの卸だったので購入を決定しました。 メッセージのやりとり(購入意思決定のあと) 購入することが決定したら輸送や支払いに関するやりとりを行います。 メッセージ画面の左下にある "Start Order" ボタンを押すと画面右側にオーダー情報の入力フォームが現れますが、基本的には先方にお任せしたほうがスムーズですので「Please draft order for me」と伝えてフォームを用意してもらいましょう。下記のように順次サプライヤから質問されると思いますので、希望する条件を伝えていきます。 輸送規則 (工場渡(EXW)、本船渡(FOB)などの指定) 輸送業者の指定やアカウント共有 (FedExやDHLのビジネスアカウントがあると関税の支払い等がスムーズに行える) 荷受人住所 (メッセージ画面左下の "Logistics Inquiry"から事前にプロフィールで登録しているビジネス用の住所を一発共有できるので便利) 決済情報 (電信送金(T/T)は時間もかかるため、Alibaba.comプラットフォーム上でのクレジットカードまたはPayPal決済をすることが多い) 輸送規則については関税関連の手続きの手間を減らすためにDAP(仕向地持ち込み渡・関税込み条件)が可能か聞くことが多いのですが、サプライヤによりけりという感じです。 決済に関してはAlibaba.comのサイトでも注意喚起がされていますが、必ずTrade Assuranceの保護プラグラムの適用対象になるようにAlibaba.comのプラットフォーム上で追跡できる形で決済しましょう。 一部の悪質な業者はログが残らないように他のメッセージアプリを指定してきたり直接送金の決済方法を指定してくることがあります。 サプライヤ側からAlibaba.comの決済URLが共有され、支払いできれば購入完了です。あとは荷物の発送と着荷を待つのみ。 Proforma Invoice (PI) がメッセージ上で共有されると思いますので、念のため住所等に間違いがないか目を通しておきましょう。 着荷~検品~レビュー Alibaba.comのオーダー情報で荷物の追跡状況が可視化されます。 これまで私が対応してきたサプライヤは概ね3日~1週間程度で届くことが多かったです。土曜日に発送しようとしてくれて「ごめん今日FedEx休みだったわ」とか言ってきたり、深センの人たちは仕事熱心ですね😊 着荷したら部品の品質チェックを行いましょう。外観検査(レーザー刻印、ピンの酸化、傷、曲がり)をした上で、実際の基板に実装して機能確認まで実施します。 外観検査を兼ねて長期保存のためのテーピングや防湿梱包をしてくれる業者さんにご協力いただくこともあります。 taping-service.com さて、検品が完了して品質に問題が無ければ受取り確認とサプライヤのレビューを行います。もしこの段階で製品品質が契約条件と異なれば返金申請が可能です。たまに100個注文して98個しか入っていなかったりしますので要注意。満足いく製品が購入ができればレビューコメントでお礼の気持ちを伝えておきましょう。 関税と輸送費の支払い 部品の着荷から若干遅れて輸送業者から関税と輸送費の請求書が届きます。関税については購入した品目の価格に依存し、輸送費はサプライヤと合意した契約条件に依存しますので内容を確認して支払いをしましょう。 Tipsなど 半導体の品名についている20+や08+ってなに? D/C = Date Code ということで製造年を意味しています。20+ならば2020年製、08+なら2008年製。 "Original" って謳っているのはどういうこと? そもそもオリジナル以外の半導体部品があるんかい、という話なんですが、日本国内の状況と違ってわりとカジュアルに"リサイクル品(Recycle)"や"工場仕掛かり在庫品(Factory Inventory)"の提案をされることがあります。 リサイクル品はその名のとおり既製品の廃棄基板などから引っ剥がした再生品ICのことです。価格面ではかなり安価です。気になる場合はまず少量買って検品してみましょう。 工場仕掛かり在庫は防湿梱包を開けてリール巻直しなどをして、製造用の仕掛かりとして工場に保管されていた在庫品を販売しているものです。こちらもオリジナル品より安価なことが多いです。 品質面の不安はないの? あります。毎度のことですが下記のようなメッセージのやりとりをしています。 保存状態は良好か?湿気とか大丈夫?ピン酸化してない? 多少価格が高くなってもいいので、Factory InventoryではなくてOriginal Stockが良いのだけどあるかな? なお弊社では現時点においてはまだAlibaba.comで購入した部品の量産品への適用はしておりません。 今後、自社製品の性質を考慮して適切なタイミングで採用することを想定しています。 半導体部品の偽物の不安はないの? あります。こればかりは完全な解決策はないと思うのですが私が気をつけているのは以下のことです。 全てを満たすことは難しく、いずれかを妥協することはあると思います。あくまで目安としていただければと思います。 Trade Assurance と Verified Supplier のお墨付きを得ているサプライヤを選ぶ Alibaba.comへの登録がごく近年だったり、レビューの数やスターの数が少なかったり低かったりするサプライヤを 避ける RFQによる相見積機能を活用して市場の適正価格を把握し、異常に安い価格や異常に高い価格を提示してくるサプライヤを 避ける 企業名でGoogle/Baidu検索したり、Linkedinで検索したりして、企業情報を公開しているサプライヤを選ぶ メッセージのやりとりの雰囲気や質問への回答の正確さ(真摯に答えてくれるか?)を見定める Invoiceへの価格記載は正しくしよう サプライヤによってはInvoiceに記載する製品価格を実際よりも低くするか聞いてくることがあります。これは製品価格を低く改ざんして関税を過少申告する行為にあたり関税法による罰則が科せられます。必ず正しい価格を記載してもらうようにしましょう。 サプライヤとのやりとりは丁寧に、礼節を大事にしよう RFQで相見積をとる際にお断りするサプライヤも多くでてくると思います。「申し訳ないけど今回は先に連絡をくれたところから買うね。また機会があればよろしく」ぐらいは声かけしましょう。(とはいえ毎日のように「Hi, なんか注文ない?」と聞いてくるサプライヤは適切にスルーしましょう) 即断即決しよう 一番嫌がられるのは時間の浪費。聞けることを聞いたらあとは悩んでも仕方がない。即断即決で購入しましょう。 モバイルアプリを活用しよう Alibaba.comの特性上、複数のサプライヤとのメッセージのやりとりが大変多く発生します。営業熱心な深センの卸のみなさまは昼夜問わず土日にもメッセージをしてくることも多いのでサクッと返信できるようにiOS/Androidのモバイルアプリを入れて対応すると捗ります。 おわりに 本記事では昨今の電気・半導体部品の調達難の状況と、それでもなんとかして部品を入手するためにAlibaba.comで中国のサプライヤから購入をする流れを説明させていただきました。 部品の調達状況については「少なくとも2021年中は改善しない」とか「更に悪くなるリスクもある」などという声もありますが、なんとか知恵を絞って状況を打破していきたいと思っています *10 。 ハードウェア製品を設計・製造している全ての事業者の方々、大変な状況だと思いますがなんとか頑張っていきましょう! *1 : OT : Operational Technology の略。ハードウェアおよび組込みソフトの製品開発をミッションとするグループ *2 : 当然のことながらメーカーの正規代理店を介した購買ではありませんので、保証やサポートは無いことをご承知おきください *3 : この時点で情報収集を強力に行って想像力を働かせ、他の半導体部品についても状況精査をしておくべきでした。GNSS受信モジュールの確保に気を取られすぎたことが反省点です *4 : 車載グレード、かつ新しめの部品については1品目だけどうしても調達できない部品があったため、チームメンバーにお願いして設計変更の対応をしてもらいました *5 : 私の頭の中では民生グレードの部品はまだ大丈夫だろう、という謎の希望的観測がありました *6 : 同時に中西部にあるDigi-KeyやMouserの倉庫の稼働停止によるロジスティクスの問題が発生して対応に追われたこともあり根本的な半導体部品供給状況に目を向けることができなかったことも反省点です *7 : ソニーの グリーンパートナー とかね *8 : ちなみに弊社のHW関連社員は私を入れて4人。この6月から待望の資材調達スタッフの方に来ていただけることになっています😭 *9 : 購入先のサプライヤが決定したらすぐにRFQを取り下げないと永遠にメッセージが来るので注意。マイページのRFQ管理画面から設定ができます *10 : 私はここ数ヶ月で輸出入関連の知識が足りないと痛感したこともあり、今年の試験で通関士の資格をとるべく勉強中だったりします
こんにちは。Visual M2M Data Visualizer の開発を担当している白金です。 この度、 Visual M2M Data Visualizer Ver3.0.0 のアップデートとあわせて 可視化用パーツ「 ビジュアルパーツ 」を開発するための開発キット(以下「 Visual Parts SDK 」) をリリースしました。 Visual Parts SDKを使用して可視化用パーツをカスタマイズ開発することで、Visual M2M Data Visualizerに、ユーザー様自身やパートナー企業様の手で新しい可視化方法を追加することが可能になります。 早速、Visual Parts SDKを使って フリートマップ のビジュアルパーツを作ってみたので Visual Parts SDK とあわせて紹介したいと思います。 作成したフリートマップのビジュアルパーツ Visual Parts SDK の紹介 Visual Parts SDK とは Visual Parts SDKの構成 ローカル開発環境でサンプルのビジュアルパーツを表示する Visual Parts SDK で利用可能なデータ Visual Parts SDK を使用した標準ビジュアルパーツ 単一データ 時系列データ メディアデータ 複数のビジュアルパーツを組み合わせる ローカル開発環境でフリートマップを作る 作成したビジュアルパーツの紹介 ビジュアルパーツを実装する ワークスペースを準備する 開発用のディレクトリを作成する View を実装する Container を実装する ビジュアルパーツの設定情報を定義する サムネイル画像の準備をする index.tsx を作成します。 サンプルパーツを削除する Data Visualizer で表示を確認する ビジュアルパーツをローカル開発サーバーでホストする ローカル開発サーバーのURLを入力する ビジュアルパーツを選択する デモ用計測データの定義を準備する データをバインドする 計測データを確認する まとめ Visual Parts SDK の紹介 Visual Parts SDK とは Visual M2M Data Visualizer (以下「Data Visualizer」) に表示する可視化パーツをカスタマイズ開発するためのSDKです。 Visual Parts SDK により、ユーザー様自身やパートナー企業様のご要望に対して Data Visualizer に含まれている標準ビジュアルパーツで表現が難しかった可視化方法 が、カスタマイズ開発することで 解決することが可能 になります。 Visual Parts SDKの構成 Visual Parts SDK は JavaScript の言語をサポートしている 以下2つのnpmパッケージで構成しています。 @aptpod/data-viz-visual-parts-sdk (APIライブラリー) Visual Parts SDKの本体です。Data VisualizerのAPIを提供するライブラリーです。 @aptpod/data-viz-create-visual-parts-react (ワークスペース作成ツール) ビジュアルパーツの開発に使用するワークスペース(開発用のディレクトリ)を作成するためのパッケージスクリプトです。ビジュアルパーツを開発する際に、このスクリプトを使用することは必須ではありませんが、使用するとTypeScript, React, Styled Components などのフレームワークも活用して効率的に開発を進めることができます。 Visual Parts SDK の構成図 詳細については ビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」 を参照してください。 ローカル開発環境でサンプルのビジュアルパーツを表示する @aptpod/data-viz-create-visual-parts-react のパッケージスクリプトを実行して開発用のワークスペースを作成します。 $ npx @aptpod/data-viz-create-visual-parts-react -o < workspace-directory > 作成したワークスペースにはサンプルとしていくつかのビジュアルパーツが付属しています。 ビジュアルパーツのサンプル 以下の動画では、弊社のモバイル計測アプリ intdash Motion の加速度センサの値を、ワークスペースに含まれるサンプルビジュアルパーツを使って可視化しています。 www.youtube.com 動画で確認できる情報は下記のとおりです。 ワークスペースを作る 依存パッケージをインストールする ローカル開発サーバーを起動する Data Visualizer にローカル開発サーバーのURLを設定する Sensor Value のサンプルビジュアルパーツを表示する Motion の計測を開始して、Sensor Value のビジュアルパーツでセンサーの値を可視化する Horizontal Barsのビジュアルパーツで複数のセンサーの値を可視化する ビジュアルパーツのLine Graph も同時に表示する ストアした計測したデータを表示する Visual Parts SDK で利用可能なデータ Visual Parts SDK を使用して下記データの連携が可能になります。 Data Visualizer から ビジュアルパーツへは 可視化に必要な情報、ビジュアルパーツから Data Visualizer には、変更されたビジュアルパーツの設定情報を連携することが可能です。 Visual Parts SDK で連携する情報 Visual Parts SDK を使用した標準ビジュアルパーツ Data Visualizer に含まれている標準ビジュアルパーツも Visual Parts SDK を使用して作成されています。 Visual Parts SDKを使用してどんなパーツが作成できるのかを知っていただくために、標準ビジュアルパーツの中でもよく使われているものをいくつか紹介します。 単一データ 数値・テキストの表示、またはメーターで表現します。 まずは数値で可視化したい、または最大値に対してどれぐらい近づいたかなどを確認するときに使用します。 Current Value Arc Subdivision Meter 時系列データ 時系列のデータを折れ線グラフで表示したり2つのデータから散布図を可視化します。表示している時間範囲で計測データの前後関係(傾向)や、特徴点、または突出したデータが含まれていないか確認するときに使用します。 Line Graph Scatter メディアデータ H.264形式で計測した動画の可視化、またはPCMで計測した音声を再生することができます。 *1 Audio Player には、スペクトログラムを表示する機能も含まれています。 Video Player Audio Player 複数のビジュアルパーツを組み合わせる ユーザー様自身やパートナー企業様の手で標準ビジュアルパーツを組み合わせてダッシュボードを作成することが可能です。 Visual Parts SDK を使用して作成したパーツは、その他の標準パーツと一緒にダッシュボード上に表示することができます。(下図赤枠) 複数のビジュアルパーツの表示 ローカル開発環境でフリートマップを作る ここから本題の作成したフリートマップについて紹介します。 作成したビジュアルパーツの紹介 まずは作成したパーツの紹介をします。 複数の車両の位置と走行速度、SOC (バッテリーの充電率)をリアルタイムで監視し、必要に応じて特定の車両(ドライバー)にアクションを指示するユースケースをイメージして作成しました。 また、マップ表示は、OpenStreetMap を使用して ライセンス の範囲内でお手軽に実装できました。 フリートマップ (完成図) LIVE計測中の動画はこちらです。 youtu.be 後から走行データ、SOCの残量を確認することも可能です。 youtu.be Panel Option から OpenStreetMap の表示スタイルも変更できるように実装しました。 youtu.be ビジュアルパーツを実装する では、実際にビジュアルパーツの実装の内容を紹介したいと思います。 Visual Parts SDK のワークスペース作成ツール @aptpod/data-viz-create-visual-parts-react を使用します。 ビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」 の手順のとおり、事前にサーバーの設定を完了しておきます。 ワークスペースを準備する ビジュアルパーツを開発するためのワークスペースを作成します。 # ワークスペースのディレクトリを作成します $ npx @aptpod/data-viz-create-visual-parts-react -o visual-parts-fleet-map # ワークスペースに移動します $ cd visual-parts-fleet-map # 依存パッケージをインストールします $ npm ci 開発用のディレクトリを作成する フリートマップの開発で使用するディレクトリ、ファイルを追加します。 追加作成したファイル構成は以下のとおりです。 ソースコードは GitHub を参照してください。 src - assets - images - fleet-map - th-fleet-map@3x.png ....... サムネイル画像です - entrypoint - fleet-map - parts ....................... View のReactサブコンポーネントを実装します - bar-sub-division-meter - car-maker - edge-card - edges-panel-title - map-zoom-controller - scrollbar - selector - props-selector.ts ........ Visual Parts SDK のデータから View の Component Props に変換します - component.tsx .............. View のReactコンポネントを実装します - container.tsx .............. Visual Parts SDK と Fleet Map Component を連結します - index.ts ................... ビジュアルパーツ Fleet Map を定義します - extension.tsx .............. ビジュアルパーツの設定、及びPanel Option を定義します - utils.ts ................... ヘルパーを実装します - index.ts ..................... エントリポイントの定義します (複数のビジュアルパーツを実装する場合はここにimportの行を追加します) View を実装する フリートマップ で表現したいComponent の Props を src/entrypoint/fleet-map/component.tsx に定義します。 この定義に沿って React の Component を実装します。 type Props = { size: { /** unit: px */ width: number /** unit: px */ height: number } openStreetMap: { url: string attribution: string } datasets: { edgeUUID: string edgeName: string /** unit:degree */ lat: number /** unit:degree */ lng: number /** unit:degree */ heading: number /** unit: ratio */ soc: number /** unit: Km/h */ speed: number }[] } Map の表示は OpenStreetMap を使用しています。 Leaflet 、及び React Leaflet *2 を使用してお手軽に実現しています。 import React , { memo , useMemo , useState , useCallback , useEffect } from 'react' import { renderToString } from 'react-dom/server' import { TileLayer , Marker , Map } from 'react-leaflet' import { Icon , Point } from 'leaflet' import { MapZoomController } from './parts/map-zoom-controller' import { CarMarker } from './parts/car-maker' import { Scrollbar } from './parts/scrollbar' import { EdgesPanelTitle } from './parts/edges-panel-title' import { EdgeCard } from './parts/edge-card' import * as C from './constant' import * as S from './style' import * as utils from './utils' type Props = { ... } export const FleetMap: React.VFC < Props > = memo (( props ) => { const { size , openStreetMap , datasets } = props const { width , height } = size const [ centerCoord , setCenterCoord ] = useState ( C.OPEN_STREET_MAP_LAT_LNG_DEFAULT , ) const [ zoom , setZoom ] = useState ( C.OPEN_STREET_MAP_ZOOM_DEFAULT ) const [ selectedEdgeUUID , setSelectedEdgeUUID ] = useState ( C.EDGE_UNSELECTED ) // Zoom In / Outイベントハンドラ const onZoomIn = useCallback (() => { setZoom (( prev ) => Math .min ( prev + 1 , C.OPEN_STREET_MAP_ZOOM_IN_LIMIT )) } , [] ) const onZoomOut = useCallback (() => { setZoom (( prev ) => Math .max ( prev - 1 , C.OPEN_STREET_MAP_ZOOM_OUT_LIMIT )) } , [] ) // Mapを表示するサイズ、または OpenSteetMapのURLが変更になったら // Mapを再描画するようにKeyも変更する const mapKey = `${width}-${height}-${openStreetMap.url}` // マップのコンテナスタイル const mapContainerStyle = useMemo ( () => ( { width , height: height - C.LAYOUT_HEADER_HEIGHT , } ), [ width , height ] , ) // マップの Center 座標を更新する // Edgeが未選択、または Lat, Lng が無効値なら更新しない useEffect (() => { datasets.forEach (( { edgeUUID , lat , lng } ) => { if ( edgeUUID === selectedEdgeUUID && utils.isFiniteLatLng ( lat , lng )) { setCenterCoord ( [ lat , lng ] ) } } ) } , [ datasets , selectedEdgeUUID ] ) // Edgeを選択するイベントハンドラを作成する // すでに同一のEdgeが選択済みの場合は選択を解除する const makeOnSelectEdgeUUID = useCallback (( edgeUUID: string ) => { return () => { setSelectedEdgeUUID (( prev ) => { return prev === edgeUUID ? C.EDGE_UNSELECTED : edgeUUID } ) } } , [] ) //Car Marker の Icon を作成する const makeCarMarkerIcon = useCallback ( ( selected: boolean , heading: number ) => { return new Icon ( { iconUrl: utils.toDataURISchemaSvg ( renderToString ( < CarMarker size = { C.CAR_MARKER_SIZE } selected = { selected } rotationAngle = { isFinite ( heading ) ? heading : C.CAR_MARKER_HEADING_DEFAULT } / >, ), ), iconAnchor: [ C.CAR_MARKER_SIZE / 2 , C.CAR_MARKER_SIZE / 2 ] , iconSize: new Point ( C.CAR_MARKER_SIZE , C.CAR_MARKER_SIZE ), } ) } , [] , ) // Edgeの数を判定する const numOfEdge = datasets. length const numOfActiveEdge = useMemo ( () => datasets.filter (( { lat , lng } ) => utils.isFiniteLatLng ( lat , lng )) . length , [ datasets ] , ) // Action ボタンを押したらDriverに指示を送ろう!! // eslint-disable-next-line no-alert const doAction = useCallback (() => alert( C.DO_ACTION_MESSAGE ), [] ) return ( < S.Section marginTop = { C.LAYOUT_HEADER_HEIGHT } > < link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity = "sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=='crossorigin='" / > < S.EdgesPanelArea > < Scrollbar > < S.EdgesPanelBg > < S.EdgesPanelHeaderArea > < EdgesPanelTitle numOfActiveEdge = { numOfActiveEdge } numOfTotalEdge = { numOfEdge } / > < /S.EdgesPanelHeaderArea > < S.EdgeCardsArea > { useMemo (() => { return datasets.map (( dataset , idx ) => ( < EdgeCard key = { idx } selected = { selectedEdgeUUID === dataset.edgeUUID } name = { utils.formatEdgeName ( dataset.edgeName , C.NO_NAME ) } soc = { utils.formatSocString ( dataset.soc , C.INVALID_STRING ) } socRatio = { utils.formatSocRatio ( dataset.soc , C.SOC_RATIO_DEFAULT , ) } speed = { utils.formatSppedString ( dataset.speed , C.INVALID_STRING , ) } driver = { C.DRIVER_NAME } onCardClick = { makeOnSelectEdgeUUID ( dataset.edgeUUID ) } onActionClick = { doAction } / > )) } , [ datasets , doAction , makeOnSelectEdgeUUID , selectedEdgeUUID ] ) } < /S.EdgeCardsArea > < /S.EdgesPanelBg > < /Scrollbar > < /S.EdgesPanelArea > < S.MapArea > < Map key = { mapKey } center = { centerCoord } zoom = { zoom } zoomControl = { C.OPEN_STREET_MAP_ZOOM_CONTROLS_DEFAULT } style = { mapContainerStyle } > < TileLayer url = { openStreetMap.url } attribution = { openStreetMap.attribution } / > { useMemo (() => { return datasets.map (( dataset , idx ) => ( < React.Fragment key = { idx } > { utils.isFiniteLatLng ( dataset.lat , dataset.lng ) && ( < Marker position = {[ dataset.lat , dataset.lng ]} icon = { makeCarMarkerIcon ( selectedEdgeUUID === dataset.edgeUUID , dataset.heading , ) } onClick = { makeOnSelectEdgeUUID ( dataset.edgeUUID ) } / > ) } < /React.Fragment > )) } , [ datasets , makeCarMarkerIcon , makeOnSelectEdgeUUID , selectedEdgeUUID , ] ) } < S.MapZoomControllerArea > < MapZoomController onZoomInClick = { onZoomIn } onZoomOutClick = { onZoomOut } / > < /S.MapZoomControllerArea > < /Map > < /S.MapArea > < /S.Section > ) } ) Container を実装する Visual Parts SDK から取得したデータを FleetMap Component に連携する処理を実装します。 src/entrypoint/fleet-map/container.tsx import React , { memo , useEffect , useState } from 'react' import { ExposerEvent , ViewBox , DataSpecification , Value , } from '@aptpod/data-viz-visual-parts-sdk' import { FleetMap } from './component' import { useSelectSize , useSelectOpenstreetMap , useSelectDatasets , } from './selector/props-selector' import { OPEN_STREET_MAP_URL_DEFAULT , OPEN_STREET_MAP_ATTRIBUTION_DEFAULT , } from './constant' import { parse as parseExtension , defaultExtension } from './extension' type Props = { comm: ExposerEvent } const VIEW_BOX_DEFAULT: ViewBox = { width: 100 , height: 100 } const DATA_SPECS_DEFAULT: DataSpecification [] = [] const VALUES_DEFAULT: Value [] = [] /** * data-viz-visual-parts-sdk から取得したデータを FleetMap Component に必要なPropsに連携します。 */ export const FleetMapContainer: React.FC < Props > = memo (( props ) => { const [ viewBox , setViewBox ] = useState ( VIEW_BOX_DEFAULT ) const [ extension , setExtension ] = useState ( defaultExtension ) const [ dataSpecifications , setDataSpecifications ] = useState ( DATA_SPECS_DEFAULT ) const [ values , setValues ] = useState ( VALUES_DEFAULT ) useEffect (() => { // ビジュアルパーツの表示サイズを取得します。 props.comm.viewBox.on ( setViewBox ) // ビジュアルパーツの設定情報を取得します。 props.comm.extension.on (( anyExtension: any ) => { setExtension ( parseExtension ( anyExtension )) } ) // Data Visualizer にバインドしているデータの定義リストを取得します。 props.comm.dataSpecifications.on ( setDataSpecifications ) // Data Visualizer にバインドしているデータに紐づく計測データを取得します。 props.comm.values.on ( setValues ) // Data Visualizer に、ビジュアルパーツのイベント取得初期設定が完了したことを通知します。 props.comm.loaded.emit () } , [ props.comm ] ) // Fleet Map Component に指定する Props に変換します。 const size = useSelectSize ( { viewBox } ) const openSteetMap = useSelectOpenstreetMap ( { dataset: { url: extension.openStreetMapURL , attribution: extension.openStreetMapAttribution , } , default : { url: OPEN_STREET_MAP_URL_DEFAULT , attribution: OPEN_STREET_MAP_ATTRIBUTION_DEFAULT , } , } ) const datasets = useSelectDatasets ( { dataSpecifications , values } ) // View を表示します。 return ( < FleetMap size = { size } openStreetMap = { openSteetMap } datasets = { datasets } / > ) } ) src/entrypoint/fleetmap/props-selector.tsx import { ComponentProps , useMemo } from 'react' import { ViewBox , DataSpecification , Value , } from '@aptpod/data-viz-visual-parts-sdk' import { FleetMap } from '../component' type FleetMapProps = ComponentProps <typeof FleetMap > /** * FleetMap Component Size Props に変換します。 */ export const useSelectSize = ( params: { viewBox: ViewBox } ) : FleetMapProps [ 'size' ] => { const { width , height } = params.viewBox const size = useMemo (() => { return { width , height } } , [ width , height ] ) return size } /** * FleetMap Component の OpenStreetMap Props に変換します。 */ export const useSelectOpenstreetMap = ( params: { dataset: { url: string attribution: string } default : { url: string attribution: string } } ) : FleetMapProps [ 'openStreetMap' ] => { const p = params const trimedURL = p.dataset.url.trim () const trimedAttribution = p.dataset.attribution.trim () const url = trimedURL !== '' ? trimedURL : p. default .url const attribution = trimedAttribution !== '' ? trimedAttribution : p. default .attribution const openStreetMap = useMemo (() => { return { url , attribution , } } , [ url , attribution ] ) return openStreetMap } /** * FleetMap Component の Datasets Props に変換します。 */ export const useSelectDatasets = ( params: { dataSpecifications: DataSpecification [] values: Value [] } ) : FleetMapProps [ 'datasets' ] => { const { dataSpecifications , values } = params const edgeMap: Map < string , { edgeUUID: string edgeName: string values: number [] } > = new Map () for (const [ i , { edgeUUID , edgeName }] of dataSpecifications.entries ()) { const has = edgeMap.has ( edgeUUID ) if ( !has ) { edgeMap.set ( edgeUUID , { edgeUUID , edgeName , values: [] , } ) } const value = values [ i ] if ( !value ) { continue } const numValue = Number (( value.data [ value.baseIdx ] ?? { v: NaN } ) .v ) const edge = edgeMap.get ( edgeUUID ) edge?.values.push ( numValue ) } const datasets = [ ...edgeMap.values () ] .map (( edge ) => { return { edgeUUID: edge.edgeUUID , edgeName: edge.edgeName , lat: edge.values [ 0 ] ?? NaN , lng: edge.values [ 1 ] ?? NaN , heading: edge.values [ 2 ] ?? NaN , soc: edge.values [ 3 ] ?? NaN , speed: edge.values [ 4 ] ?? NaN , } } ) return datasets } ビジュアルパーツの設定情報を定義する OpenStreetMap の URL、Attribution を定義します。 src/entrypoint/fleet-map/extension.ts で定義した EXTENSION_CONFIGS は 実装次第では、 Panel Settings の Panel Option からビジュアルパーツの設定を変更することも可能です。 今回は例として、 OpenStreetMap の表示スタイルを変更する設定を実装してみました。 ビジュアルパーツのPanel Option src/entrypoint/fleet-map/extension.ts import * as Z from 'zod' import { Metadata } from '@aptpod/data-viz-visual-parts-sdk' import { estimate , estimatePartialObject } from 'src/utils/zod' /** * Extension の型を定義 */ export type Extension = { openStreetMapURL: string openStreetMapAttribution: string } /** * Extension のDefault値を設定します。 */ export const defaultExtension: Extension = { openStreetMapURL: '' , openStreetMapAttribution: '' , } /** * Extension のスキーマ定義します。 */ export const schema = { openStreetMapURL: Z. string (), openSteetMapAttribution: Z. string (), } /** * Extension の各フィールドをチェックします。 */ export const parse = ( anyExtension: any ) : Extension => { const def = defaultExtension const ext = estimatePartialObject < Extension >( anyExtension ) // eslint-disable-next-line prettier/prettier const openStreetMapURL = estimate ( schema.openStreetMapURL , ext.openStreetMapURL , def.openStreetMapURL ) // eslint-disable-next-line prettier/prettier const openStreetMapAttribution = estimate ( schema.openStreetMapURL , ext.openStreetMapAttribution , def.openStreetMapAttribution ) return { openStreetMapURL , openStreetMapAttribution , } } /** * Data VisualizerのPanel Optionに表示する入力項目を定義します。 */ export const EXTENSION_CONFIGS: Metadata [ 'panelOptionConfig' ][ 'extensionConfigs' ] = [ { id: 'InputText' , key: 'openStreetMapURL' , label: 'OpenStreetMap URL' , option: { placeholder: 'URL' } , } , { id: 'InputText' , key: 'openStreetMapAttribution' , label: 'OpenStreetMap Attribution' , option: { placeholder: 'HTML Source Code' } , } , ] サムネイル画像の準備をする こちらの手順 に沿ってビジュアルパーツにはSVGまたはpng形式のサムネイルを準備します。 フリートマップのサムネイルは、png形式のため Retina 対応の画像も考慮して 幅150x横100 の3倍のpng形式のファイルを使用しました。 サムネイル画像 index.tsx を作成します。 Data Visualizer がフリートマップを表示するための処理を実装します。 src/entrypoint/fleet-map/index.tsx import { expose , Renderer , Metadata , ExposerEvent , } from '@aptpod/data-viz-visual-parts-sdk' import React from 'react' import { render , unmountComponentAtNode } from 'react-dom' // Shadow DOM に適用するStyle import { StyledShadowStyle } from '../../styles/shadow' // Styled Components を Shadow DOM 以下で適用するための Utility import { StyleSheetManagerWrapper } from '../../utils/components/style/stylesheet-manager-wrapper' import { FleetMapContainer } from './container' import { EXTENSION_CONFIGS , defaultExtension } from './extension' import thumbnailSrc from 'src/assets/images/fleet-map/th-fleet-map@3x.png' /** * Metadata 作成 */ const metadata: Metadata = { partsType: '@demo/fleet-map' , partsName: 'Fleet Map' , groupName: 'Demo' , panelTagName: 'x-demo-fleet-map' , getThumbnailURL: ( baseURL: string ) => `${baseURL}${thumbnailSrc}` , panelViewConfig: { displayTimestamp: true , } , panelOptionConfig: { rangeAtMost: 0 , canEditColor: false , bindDataCountMax: 250 , extensionConfigs: EXTENSION_CONFIGS , } , defaultExtension , } /** * Renderer クラスを継承したPluginRendererを定義します。 */ class PluginRenderer extends Renderer { /** * 描画を実行します。 1回だけコールします。 * 状態を変更する場合は、 ExposerEvent のイベントを利用し、 element のDOMを再描画します。 */ // eslint-disable-next-line class-methods-use-this render ( el: HTMLElement , comm: ExposerEvent ) { // Reactを使用した描画 render ( < StyleSheetManagerWrapper > <> < StyledShadowStyle / > < FleetMapContainer comm = { comm } / > < / > < /StyleSheetManagerWrapper >, el , ) } /** * element に紐づく子要素のDOMや子要素のイベントハンドラを解放します。 * HTMLElement は、 render メソッドに引数として渡された HTMLElement (コンテナ)と同じです。 */ // eslint-disable-next-line class-methods-use-this dispose ( el: HTMLElement ) { unmountComponentAtNode ( el ) } } /** * 作成した metadata, renderer を公開します。 */ expose ( { metadata , renderer: PluginRenderer , } ) フリートマップ の src/entrypoint/fleet-map/index.ts を src/entrypoint/index.ts に追加します。 src/entrypoint/index.ts import './fleet-map' サンプルパーツを削除する 付属のサンプルパーツは不要のため削除します。 src/entrypoint/index.ts から、 import './sample/...' の行を削除 付属サンプルのディレクトリを削除 src/assets/images/samples src/entrypoint/samples npm uninstall chart.js react-chartjs-2 d3 @types/chart.js @types/d3 binary-search-bound を実行して、サンプルビジュアルパーツが使用していたパッケージをアンインストール Data Visualizer で表示を確認する ビジュアルパーツをローカル開発サーバーでホストする 実装したビジュアルパーツをホストするローカル開発サーバーを起動します。 ビジュアルパーツをローカル開発サーバーでホストすることで、ビジュアルパーツの動作確認、または調整など効率的に開発を進めることができます。 $ npm run start ... -------------------------------- Set the Plugin URL under development to Visual M2M Data Visualizer. Please set the following URL in Local Plugin URL Settings of Function Menu. - http://localhost:8080/app.js -------------------------------- ローカル開発サーバーのURLを入力する ローカル開発サーバー起動時に表示されたURL ( http://localhost:8080/app.js ) を Data Visualizer の Visual Parts Plugin Settings Plg の設定画面で保存します。 Plg のメニューを表示するためには、 ビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」 を参照してください。 ローカルサーバーのURLを設定する ビジュアルパーツを選択する Data Visualizer で開発した Fleet Map のビジュアルパーツの選択が可能になっていることを確認します。 今回作成したパーツは  Demo  グループに表示されます。( index.tsx の  groupName  で変更することができます) Fleet Map のビジュアルパーツ選択 デモ用計測データの定義を準備する デモ用の計測データを表示するための定義ファイル を Data Settings にインポートします。(Data Visualizer の使用方法については、 マニュアルをご覧ください ) Fleet Map で使用する定義データ データをバインドする 作成した Fleet Map の ビジュアルパーツにデータを一つずつ追加します。 Bind Data 計測データを確認する 最後にデモの計測を開始し、Data Visualizer を LIVE再生すると表示ができました。 完成後のビジュアルパーツ まとめ Visual Parts SDK を使用することで、ユーザー様ご自身で Data Visualizer をカスタマイズできることを実感いただけましたでしょうか?少しでも実際に近い開発や運用のイメージを掴んで頂くため、今回はフリートマップをサンプルとして選んでみました。 読者の皆様に本製品を活用いただくことで、DX表現のパートナーとしてお手伝いできることを楽しみにしております。 また、Data Visualizer ビジュアルパーツをカスタマイズ開発するための一助になれば幸いです。 以上です。ありがとうございました! *1 : 別途 intdash サーバーからストリーミングでメディアデータを受け取る、またはストアしているメディアデータにアクセスするAPIが必要になります。当APIを使用するためのSDKは開発中です。 *2 : React Leafletのv3.0.0以降はライセンスがHippocratic Licenseのため、v2.8.0を使用しています
製品開発グループで主にネイティブアプリケーション開発を担当している上野です。 この度、弊社製品の intdash というデータストリーミングプラットフォームをモバイルアプリケーションで利用することができるSDK、「 intdash SDK for Swift 」をリリースしました。 このSDKはモバイルアプリの intdash Motion や VM2M StreamVideo で利用されているものです。intdash MotionやVM2M Stream Videoは、PCやターミナルデバイスを用意せずとも、お手持ちのスマートフォンやタブレットにて手軽にデータ収集や伝送、可視化を可能とするアプトポッドのプロダクトです。 公開したリポジトリにはいくつかの サンプルアプリケーション を同梱しております。 今回はサンプルアプリを用いながらどんな事が出来るのか解説したいと思います。 サンプルアプリケーションのセットアップ 同梱しているサンプルアプリはプロジェクトをXcodeで開いてビルドしたり実行したりすることができますがそのままではサーバーを経由したデータストリーミングは行えません。 まずはデータストリーミングを仲介するサーバー(intdashサーバー)の用意と README の 事前準備 と書かれている見出しの内容の対応が必要です。 ※intdashサーバーの利用をご希望の方は、弊社営業までお問い合わせください。 aptpod,Inc. Contact intdashではサーバーとの認証に OAuth2.0 と呼ばれる RFC6749 で定義されている認証(認可)フレームワークを利用しています。OAuth2.0の細かい説明は省きますが、このサンプルアプリを利用するためには、サーバーへのクライアントID 1 とコールバックスキーマ 2 の登録が必要となります。 サーバー管理者へ依頼してそれらの情報を登録及び取得できた場合はいくつかのプロジェクトファイルを更新します。 同梱しているサンプルアプリでは Samples/iOS/Classes/Common.swift に対象のサーバーとクライアントIDを登録するグローバル変数が用意されていますので必要に応じて変更してください。 // Common.swift let kTargetServer : String = "https://example.com" /// OAuth2.0認証用のクライアントID。intdashサーバー管理者に問い合わせてください let kIntdashClientId : String = "" アプリケーションスキーマの登録は各プロジェクトの Info.plist の URL Types で行えます。 URL Type設定 ※または画像の様にプロジェクトセッティングの Info からでも可能です。 intdashサーバーへのデータストリーミング 実際にintdashサーバーとストリーミングを行うアプリを紹介します。 iPhone/iPadから取得したデータををintdashサーバーへ伝送するアップストリーム処理のサンプルアプリは下記の2つです。 SensorGPSUpstreamApp VideoUpstreamApp また、伝送されたデータをリアルタイムに取得し可視化するダウンストリーム処理のサンプルアプリは下記の2つです。 SensorGPSDownstreamApp VideoDownstreamApp センサーとGPSサンプルアプリの実行動画 ビデオサンプルアプリの実行動画 サンプルアプリではセンサー及びGPSとVideoで分けて作られており、それぞれのデータ取得方法と可視化方法を理解する事ができます。もちろん必要に応じて組み合わせたり様々な用途に利用できます。 // MainViewController+GPSManager.swift func sendLocation (location : CLLocation , rtcTime : TimeInterval ) { ... DispatchQueue.global().async { do { if ! Config.GPS_IS_PRIMITIVE_DATA { // 送信する`IntdashData`を生成します。 let sensor = GeneralSensorGeoLocationCoordinate(lat : Float (location.coordinate.latitude), lng : Float (location.coordinate.longitude)) // `GeneralSensor***`は`IntdashData`に変換が可能。 let data = sensor.toData() // データ送信前の保存処理。 if let fileManager = self .gpsDataFileManager { _ = try fileManager.write(units : [data] , elapsedTime : elapsedTime ) } // 生成した`IntdashData`を送信します。 try self .intdashClient?.upstreamManager.sendUnit(data, elapsedTime : elapsedTime , streamId : streamId ) } ... } catch { print( "Failed to send location coordinate. \(error) " ) } } } 上記は SensorGPSUpstreamApp で実装されている位置情報データを送信する処理の一部です。 intdashサーバーへデータを送信する際には、SDK内部で定義している IntdashData というクラスに送信したいデータを格納して使用します。その際、iPhoneでサポートされているセンサー値を格納するには GeneralSensor*** というクラスを使用します。 このクラスを使うと位置情報の緯度・経度、加速度等のXYZ軸の3つの数値といったように、複数のデータをまとめて一つのクラスで送信する事ができます。 ※ GeneralSensor*** は toData() で IntdashData.DataGeneralSensor に変換して利用します。 // MainViewController+GPSManager.swift func sendLocation (location : CLLocation , rtcTime : TimeInterval ) { ... DispatchQueue.global().async { do { if ! Config.GPS_IS_PRIMITIVE_DATA { ... } else { // 送信する`IntdashData`を生成します。 let lat = try IntdashData.DataFloat(id : Config.GPS_PRIMITIVE_DATA_LATITUDE_ID , data : location.coordinate.latitude ) let lng = try IntdashData.DataFloat(id : Config.GPS_PRIMITIVE_DATA_LONGITUDE_ID , data : location.coordinate.longitude ) // データ送信前の保存処理。 if let fileManager = self .gpsDataFileManager { _ = try fileManager.write(units : [lat, lng] , elapsedTime : elapsedTime ) } // 生成した`IntdashData`を送信します。 try self .intdashClient?.upstreamManager.sendUnit(lat, elapsedTime : elapsedTime , streamId : streamId ) try self .intdashClient?.upstreamManager.sendUnit(lng, elapsedTime : elapsedTime , streamId : streamId ) } } catch { print( "Failed to send location coordinate. \(error) " ) } } } もちろん GeneralSensor に定義されていない一般的なデータも送信が可能です。 小数値であれば IntdashData.DataFloat 、文字列であれば IntdashData.DataString 、整数値であれば IntdashData.DataInt 、バイナリであれば IntdashData.DataBytes というクラスを使用してください。これらのクラスは、データ本体とともにデータの内容を表すIDを格納することができます。 // MainViewController+EncodeFunc.swift func sendJPEG (jpeg : Data , timestamp : TimeInterval ) { ... DispatchQueue.global().async { do { // 送信する`IntdashData`を生成します。 let data = IntdashData.DataJPEG(data : [UInt8] (jpeg)) // データ送信前の保存処理。 if let fileManager = self .intdashDataFileManager { _ = try fileManager.write(units : [data] , elapsedTime : elapsedTime ) } // 生成した`IntdashData`を送信します。 try self .intdashClient?.upstreamManager.sendUnit(data, elapsedTime : elapsedTime , streamId : streamId ) } catch { print( "Failed to send jpeg. \(error) " ) } } } IntdashData には他にも IntdashData.DataJPEG や IntdashData.DataAAC といった画像や音声などの特定のフォーマットのデータを格納するクラスもありますので必要に応じて使い分ける事ができます。 // MainViewController+GPSManager.swift func sendLocation (location : CLLocation , rtcTime : TimeInterval ) { guard let streamId = self .gpsUpstreamId else { return } self .clockLock.lock() // 計測開始時間が未送信であれば送信します。 if self .baseTime == - 1 { self .sendFirstData(timestamp : rtcTime ) } if self .locationBaseTime == - 1 { self .locationBaseTime = rtcTime self .locationSampleBaseTime = location.timestamp.timeIntervalSince1970 } self .clockLock.unlock() // 計測開始時間から経過時間を算出します。 let elapsedTime = ((location.timestamp.timeIntervalSince1970 - self .locationSampleBaseTime) + self .locationBaseTime) - self .baseTime guard elapsedTime >= 0 else { print( "Elapsed time error. \(elapsedTime) " ) return } ... // 生成した`IntdashData`を送信します。 try self .intdashClient?.upstreamManager.sendUnit(data, elapsedTime : elapsedTime , streamId : streamId ) } 生成した IntdashData は時系列データとして取り扱う為、基準時刻(ベースタイム)からの時間差分(経過時間)とともに送信します。 ※基準時刻や経過時間などのintdashの時間にまつわるコンセプトについてはこちらのドキュメント( 詳説iSCP 1.0 )を参照してください。 // MainViewController+IntdashManager.swift extension MainViewController : ... , IntdashClientDownstreamManagerDelegate { //MARK:- IntdashClientDownstreamManagerDelegate func downstreamManagerDidParseDataPoints (_ manager : IntdashClient.DownstreamManager , streamId : Int , dataPoints : [RealtimeDataPoint] ) { dataPoints.forEach { (dataPoint) in // 取得したデータポイントに含まれるデータ種別応じて処理を行います。 switch dataPoint.dataModel.dataType { case .generalSensor : guard let dataGeneralSensor = dataPoint.dataModel as ? IntdashData.DataGeneralSensor else { return } self .sensorDataLock.lock() if let sensor = sensor as ? GeneralSensorGeoLocationCoordinate { self .setUserLocation(latitude : Double (sensor.lat), longitude : Double (sensor.lng)) } ... case .nmea : guard let dataNMEA = dataPoint.dataModel as ? IntdashData.DataNMEA else { return } ... } } 上記は SensorGPSDownstreamApp に含まれる、指定したエッジのデータをリアルタイムにダウンストリームし、そのデータを参照している処理です。 リアルタイムデータの参照は IntdashClientDownstreamManagerDelegate に含まれるコールバックから可能です。 コールバックから返却された RealtimeDataPoint が先ほどデータのアップストリームにも出てきた IntdashData クラスを持っており、 IntdashData.IntdashDataType からこのデータの種別を識別できるので種別にあった IntdashData に変換して利用してください。 伝送したデータをクラウドから取得 データ伝送時にクラウドへの保存設定を有効にした場合は、伝送終了後もデータを参照することができます。 計測済みデータを参照するサンプルアプリの実行動画 AccessingMeasurementDataSample ではクラウドに保存されているデータの取得方法を知ることができます。 もしリアルタイムデータの伝送や可視化だけでなくあとから振り返ってデータを再生したい場合は、このサンプルアプリに含まれる処理を参考に追加でキャッシュ等を実装することで実現できます。 また、工夫をすれば、ファイルにエクスポートして共有するといった機能も実現可能です。 // RequestDataPointsSampleViewController.swift func updateDataPoints (elapsedTime : TimeInterval ) { ... // 要求するデータのフィルターを作成する let filter = makeRequestDataPointsFilters() self .loadingDialog = LoadingAlertDialogView. init (addView : self.app.window ! , showMessageLabel : false ) self .loadingDialog?.startAnimating() DispatchQueue.global().async { [weak self ] in ... IntdashAPIManager.shared.requestDataPoints(name : targetMeasurement.uuid , filters : filter , start : start , end : end , limit : Config.INTDASH_REQUEST_DATA_POINTS_LIMIT ) { (response, error) in var timestamp : TimeInterval = 0 var date : Date ? if let response = response { print( "Data points size: \(response.dataPoints.count) " ) self ?.dataPointList = response.dataPoints.sorted { let t0 = $0 .time?.timeIntervalSince1970 let t1 = $1 .time?.timeIntervalSince1970 if t0 == nil , t1 == nil { return false } else if t0 == nil || t1 == nil { return true } return t0 ! < t1! } ... } } } } 上記がクラウドに保存されたデータをリクエストする例です。 intdashでは時系列データは計測という単位で保存されます。この例では計測に付与されたID(計測ID)をもとにデータ取得しています。 ※intdashの計測のコンセプトについてはこちらのドキュメント( 詳説iSCP 1.0 )を参照してください。 サンプルアプリでは利用可能な各種REST APIを IntdashAPIManager にまとめていますので、必要に応じてご利用ください。 intdash Motionのプラグインアプリについて 弊社製品のintdash Motion(※以下、Motion)は同じiPhone内にインストールしたプラグインアプリと連携してMotionでサポートしているセンサーやビデオデータとは別のデータを同時に伝送する機能を持っています。 Motion Plugins この機能はBluetooth等で接続したIoTデバイスのデータを UDP を用いてプラグインアプリがMotionへ伝送することによって実現しています。 また、Motionとの接続にはUDP 3 を使用しているため複数のプラグインアプリが同時にMotionへ接続することができます 。 MotionPluginAppSample はMotionのプラグインアプリのサンプルでMotionとの接続とBluetoothデバイスとの接続方法が実装されています。 プラグインアプリとBluetoothデバイスサンプルアプリの実行動画 動画にてプラグインアプリと接続しているアプリは BluetoothDeviceSample です。 プラグインアプリと接続するBluetoothデバイスを仮想的に試す事ができるサンプルです。 MotionPluginAppSample をお手持ちのiPhoneにインストールし、別のiPhoneに BluetoothDeviceSample をインストールして接続してみてください。 なお、このサンプルはMacでも動作するのでiPhoneを2台用意できない場合は、iPhone1台とMacでも試してみることができます。 // MainViewController+BLEManager.swift func manager (_ manager : BLECentralManager , peripheral : CBPeripheral , didUpdateValueFor characteristic : CBCharacteristic , error : Error ?) { // ToDo:ここでタイムスタンプ保持?もしくはデータから取得する可能性あり // 基本的にはMotion側でDate()で取れる端末時間を元にNTPサーバーと同期を行っているのでDate()を扱うことを推奨 // 端末時間と送信元デバイスとの伝送遅延を考慮したタイムスタンプであると良い。 let time = Date().timeIntervalSince1970 // 送られてきたデータの取得 guard let data = characteristic.value else { return } // JSONのパース guard let dic = try? JSONSerialization.jsonObject(with : data , options : [] ) as ? [String : Any ] else { print( "Failed to decode data." ) return } DispatchQueue.global().async { if let vYaw = dic[ "yaw" ] as ? NSNumber, let vPitch = dic[ "pitch" ] as ? NSNumber, let vRoll = dic[ "roll" ] as ? NSNumber, let vX = dic[ "x" ] as ? NSNumber, let vY = dic[ "y" ] as ? NSNumber, let vZ = dic[ "z" ] as ? NSNumber { let yaw = vYaw.floatValue let pitch = vPitch.floatValue let roll = vRoll.floatValue let x = vX.floatValue let y = vY.floatValue let z = vZ.floatValue let angle = GeneralSensorOrientationAngle(oaa : yaw , oab : pitch , oag : roll ).toData() let gyro = GeneralSensorRotationRate(rra : x , rrb : y , rrg : z ).toData() let string = try ! IntdashData.DataString(id : "Test-Message-ID" , data : "TestMessage" ) // 送信対象データの生成方法 guard let data = try? IntdashPacketHelper.generatePackets(units : [angle, gyro, string] ) else { print( "Failed to convert unit." ) return } let strs = NSMutableString() strs.append( "{" ) strs.append( "\n \"t\": \" \(time) \"," ) strs.append( "\n \"d\": \" \(data.base64EncodedString() )\"" ) strs.append( "\n}" ) let message = String(strs) guard let messageData = message.data(using : .utf8) else { return } self .sendMessage(data : messageData ) } } } 上記は MotionPluginAppSample に送られてきた回転角情報をMotionで認識可能なフォーマットに変換してMotionへ送信している処理です。 サンプルではBluetoothデバイスはJSON形式でデータを送信しているのでintdashで認識可能なデータに変換します。 もし使用したいIoTデバイスにデータの取得用SDKやAPIが用意されている場合は、サンプルに含まれる BLECentralManager からのデータ取得処理を、SDKやAPIからの取得処理に変更することで応用が可能です。 まとめ サンプルアプリを用いた intdash SDK for Swift の解説は以上となります。 サンプルアプリで提供しているintdashサーバーとのやりとりは基本的に片側通信ですがアップストリーム、ダウンストリームを同時に繋げることで双方向通信を実現することも可能です。 双方向通信を用いることでビデオ通話や配信アプリのような実装も可能になります。 また、プラグインアプリを実装すればお手持ちのスマートデバイスをintdashに接続することができ、データの保存やリモート制御などに手軽に活用することができるようになるため、活用の幅が広がります。 プラグインアプリで行っているiOSにおける同じiPhone、端末内のアプリ間通信はファイルベースが一般的でストリーミングで行っている情報は少ないと思うので人によっては良い参考になるかもしれません。 www.aptpod.co.jp アプトポッドでは、今回ご紹介した intdash SDK for Swift の他にも、Python でintdashと繋がるアプリケーションを構築できる intdash SDK for Python、intdash と繋がるエッジデバイスを開発するためのエージェントソフトウェアを含む intdsah SDK for Edge Device など、さまざまなSDKをご提供しております。 拡張性の高いIoTプラットフォームをお探しのお客様や、協業先、共同ソリューションをお探しのパートナー企業様、弊社の案件開発にご協力いただける開発会社様など、こちらの記事で intdash というプラットフォームやそのSDKに興味をお持ちいただけましたら、ぜひ こちら のお問い合わせ先までご連絡ください。 aptpod,Inc. Contact クライアントID: リソースサーバーがこのサンプルアプリを識別するためのID ↩ コールバックスキーマ: 認証後にアプリへコールバックするために使用するアプリケーションスキーマ ↩ UDPは TCP とは違いハンドシェイクを必要としないコネクションレスプロトコル。 ↩
こんにちは。製品開発グループに所属しております、きしだです。 前回の記事 でもご紹介の通り、弊社でついにハードウェアブランドが立ち上がり、その第一弾として EDGEPLANT T1 がリリースされました! 🎉 個人的にはデザインがとてもイケててずっと見つめていたくなる製品ですが、もちろん素敵な点はそこだけではありません。 EDGEPLANT T1 (以降、T1とよびます)は、NVIDIA® Jetson™ TX2を搭載している産業向けハードウェアで、GPUを利用したエッジコンピューティングが可能になります。そのため、自動車や建機などのデバイスに搭載することで、簡単にエッジAIコンピューティングが実現できます。 とはいっても、「エッジAIコンピューティングを構築するためにT1を買ったものの、何から手をつければいいのだろう」という方もいらっしゃると思います。 今回はその方々向けに、簡単なディープラーニング環境を構築する手順をご紹介します。 簡単なサンプルも合わせて用意したので、T1を試しに動かす最初のステップとして、お手元にある方はぜひお試しください。 その前に、T1をエッジAIとして活用する意義について少しだけ触れたいと思います。早くT1を動かしたい!という方は「EDGEPLANT T1で顔検出をやってみる」までスキップしてください。 EDGEPLANT T1をエッジAIとして活用する AIデータの多様化とクラウドコンピューティングの限界 産業向けデバイスのエッジコンピューティングを実現する EDGEPLANT T1で顔検出をやってみる コンテナイメージを用意する ためしに推論してみる まとめ EDGEPLANT T1をエッジAIとして活用する 最近話題の "エッジAI" とは、センサーやデータの収集源となるデバイス上で、発生したデータにAI処理をかけてしまう考え方です。エッジ端末でAI処理が完結することがなぜ重要なのでしょうか。 それは、「AIが扱うデータの多様化」がキーワードになります。 AIデータの多様化とクラウドコンピューティングの限界 一般的にAWSやGoogleなどが提供するクラウド上でAI処理を行うケースが盛んな印象を受けますが、近年「AIが扱うデータの多様化」が進んだことで、扱うデータ量が爆発的に増加しました。そのため通信容量や通信速度などを理由にクラウド上で対応できないケースが現れてきています。 例えば、自動運転を行う自動車上で物体検知を行う場合、遅延などが認められない緊急性の高いAI処理が求められます。この場合、クラウド上で動作させる際には悩ましい課題として注視されているようです。 そのため通信タイムラグをなくすために、「端末のそばに分散配置したサーバー上で処理し、必要な情報のみクラウドに送信するような構成」あるいは「端末側で独立して処理を行う構成」をとる試みが各所で行われています。前者では近年 " MEC (Mobile/Multi-access Edge Computing)" という技術規格が進められ勢いを増していますが、後者の端末単体で独立したAI処理も「自律制御型ロボット」などで重要な要素を持っています。 そしてこのニーズは、自動車や建機などの自然環境で動作するデバイスでも同様のことが言えると思います。 産業向けデバイスのエッジコンピューティングを実現する 対してT1は、NVIDIA® Jetson™ TX2を搭載しており、GPUを利用したエッジコンピューティングが可能になります。つまり、画像処理に大きな実績のある機械学習アルゴリズムを処理するのに最適な環境を適用することができ、工場の産業ロボットや自動車などのIoT端末となりうるデバイスで発生したデータに対してAI技術を適用した分析・学習・予測を行うことができるようになります。 ここでのポイントとしては 「車載組み込みコンピューターとして利用可能」 という点です。 実際のフィールドで運用されるモビリティに搭載されるコンピューターには、電源ノイズ耐性や耐振動/衝撃性能、高低温環境下での安定した動作が求められます。 しかしながら、AIプラットフォームとして使いやすい(エコシステムができている)NVIDIAのJetsonを搭載して、かつ上記のような信頼性を保証した機器を選定しようとすると選択肢が限られるのが現状です。 弊社のT1は、上記のようなケースにあてはまる自動車や建機などの車載上で動作することができ、エッジ端末でAI処理を完結することができます。そのため動作する環境を意識することなく様々なシーンでエッジコンピューティングが実現できるというわけです。   さて、紹介としては以上になります。 ここから、今まで解説した "エッジAI" 実現の初級編として、冒頭でご紹介した内容をご紹介したいと思います。 EDGEPLANT T1で顔検出をやってみる それでは、いよいよ本題にうつりたいと思います。 EDGEPLANT T1はNVIDIA® Jetson™ TX2を搭載しており、環境構築もNVIDIA提供のツール群を駆使します。とはいっても、今までNVIDIA Jetson を扱った方々なら体験したことがあると思うのですが、これがまたしんどいんですよね...。 環境構築には、GPUドライバー、JetPackなどの使用ライブラリ、DeepLearningフレームワークの依存関係を理解して、構築する必要がある 使用しているフレームワークによっては、GPUへの最適化を行う必要がある アルゴリズムによって使用するライブラリやフレームワークが異なるので、一つのOSでの利用が厳しい フレームワークあるいはライブラリ1つにアップデートがあると、環境すべてが使えなくなり期待の動作が行えなくなる その上、環境内の依存関係が複雑なため環境を作り直すにも時間がかかる など、心あたりがある方もいると思います。自分も運用時には相当悩みました。 そこで、今回はこれらを解決すべく、Dockerを利用してみようというお話です。 まずDockerですが、T1にはデフォルトでDockerが入っており、すぐに使うことができます。 これで環境を複数立てることは可能そうです。 一方で、GPUや各フレームワークとの依存関係については、NVIDIA社が運営している NVIDIA NGC (NVIDIA GPU Cloud) というサイトで公開されている、NVIDIA GPU用のソフトウェアに特化したDockerイメージを使用しましょう。こちらは通常のイメージの他に、NVIDIAのソフトウェア向けに最適にビルドされたTensorFlowやPytorchのコンテナイメージなども公開されているので、こちらを使うことで手軽にディープラーニング環境を実現することができます。 上記を使うことで簡単に環境構築ができそうなので、実際にサンプルを試しながら動かしてみましょう。 コンテナイメージを用意する 今回は、サンプルとして顔を検出するアルゴリズムが含まれたパッケージである Face Recognition を使用します。 このパッケージが動作する環境を構築しましょう。 github.com まずは、使用しているT1のJetPackのバージョンを確認します。 $ sudo apt show nvidia-jetpack Package: nvidia-jetpack Version: 4.4.1-b50 Priority: standard Section: metapackages Maintainer: NVIDIA Corporation Installed-Size: 199 kB Depends: nvidia-cuda (= 4.4.1-b50), nvidia-opencv (= 4.4.1-b50), nvidia-cudnn8 (= 4.4.1-b50), nvidia-tensorrt (= 4.4.1-b50), nvidia-visionworks (= 4.4.1-b50), nvidia-container (= 4.4.1-b50), nvidia-vpi (= 4.4.1-b50), nvidia-l4t-jetson-multimedia-api (>> 32.4-0), nvidia-l4t-jetson-multimedia-api (<< 32.5-0) Homepage: http://developer.nvidia.com/jetson Download-Size: 29.4 kB APT-Manual-Installed: yes APT-Sources: https://repo.download.nvidia.com/jetson/t186 r32.4/main arm64 Packages Description: NVIDIA Jetpack Meta Package 上記より、バージョンは Version: 4.4.1-b50 であることが確認できました。 これをもとにして、NVIDIA NGCにて使用するイメージを探しましょう。今回はTensorFlowを使用するため、 こちら の nvcr.io/nvidia/l4t-tensorflow:r32.4.4-tf2.3-py3 を使用することにしました。 上記のイメージを参照する形でDockerfileを作成します。今回は以下のようにしました。 FROM nvcr.io/nvidia/l4t-tensorflow:r32.4.4-tf2.3-py3 RUN apt-get update && \ apt-get install -y \ g++ \ make \ cmake \ unzip \ libcurl4-openssl-dev \ libxtst6 \ unzip \ python3-dev \ libsm6 \ libxext6 \ libxrender-dev \ libgl1-mesa-dev \ python3-pip \ python3-tk COPY requirements.txt . RUN pip3 install --upgrade pip && \ pip install --no-cache-dir -r requirements.txt 上記で指定している requirements.txt は以下のラインナップです。以下は使いたいモジュールによって適宜変更してください。 numpy scipy opencv-python pillow jupyter face_recognition 上記を指定してビルドします。 $ docker build -t t1-face-detection:latest . 少し時間がかかるので、気長に待ちましょう。 Successfully built 419ed4a78c54 Successfully tagged t1-face-detection:latest どうやら完了したようです。 試しにDockerを起動し、Jupyter Notebook上で推論がうまくいくか試して見ましょう。 Docker上のJupyter Notebookにホストからアクセスするため、ポートも明示的に指定します。 $ docker run -it -p 8888:8888 --runtime=nvidia --network host t1-face-detection:latest bash 上記の注意点としては、NVIDIA DockerをRuntimeとして指定します。こちらを指定することで、ホスト側のCUDA環境をDocker上でそのまま参照することができます。 ちなみに私は以下のようなエラーがでました 。 docker: Error response from daemon: Unknown runtime specified nvidia. このときは、 Dockerデーモンの設定 にあるRuntimeが正しく指定されているか確認する必要があるので注意です。 $ vi /etc/docker/daemon.json { "runtimes": { "nvidia": { "path": "/usr/bin/nvidia-container-runtime", "runtimeArgs": [] } } } $ sudo pkill -SIGHUP dockerd 上記を実施すると、エラーが出力されることなく nvidia-docker をRuntimeとして参照しつつ起動できました。 ためしに推論してみる それでは、早速推論を試してみましょう。 今回実行するコードは以下の通りです。 import cv2 import face_recognition import numpy from PIL import Image detector = face_recognition.face_locations img = Image.open( 'image2.jpg' ) img_array = numpy.asarray(img) # 推論 results = detector(img_array, model= "cnn" ) # 結果の可視化 for v in results: top, right, bottom, left = v cv2.rectangle(img_array, (left, top),(right, bottom),( 255 , 0 , 0 ), 3 , 4 ) detected_img = Image.fromarray(img_array) detected_img.show() 上記をこちらの画像 *1 に対して実行します。 イメージサンプル 見事に顔が検出されました!🙌 検出結果 まとめ 今回は EDGEPLANT T1 のリリースに伴い、NVIDIA提供のDockerイメージを使用した簡単なディープラーニングの環境構築のやり方をご紹介しました。 今回は顔を検出するアルゴリズムを動かしただけですが、OpenCVを利用して外付けカメラから収集した動画データを検出にかけるところまでできると、車載器としての"エッジAI" の様々なユースケースにも柔軟に対応できるようになると思います。 今回の手順を踏むことで、T1を "エッジAI" として利用する難易度がぐっと減ると思うので、知らなかった!という方はぜひお試しください。 以上です、ありがとうございました。 参考 NVIDIA Jetson Nano 開発者キットに Docker を使ってディープラーニング・フレームワークをインストールする - Qiita NVIDIA NGC *1 : こちらより引用 https://unsplash.com/photos/Q_Sei-TqSlc
HW/OTチームの織江です。 今回は先日リリースされた弊社の新製品「 EDGEPLANT T1 」の製品開発の軌跡を紹介したいと思います。 開発者視点から見た製品が出来上がるまでの経緯を眺めてもらえばと思います。 製品の企画や量産に伴うアレコレは別途改めてこのブログで紹介させていただきたいと思います。 企画構想フェーズ (1ヶ月)  この段階ではNVIDIA® Jetson™ TX2を載せることが出来て、自動車のバッテリーから電源が取れて、イグニッション信号と連動して起動できる以外の要件は決まっていませんでした。想定される出荷量も読めていない状態です。出荷台数が読めない以上すんなり作れる形状を組み合わせてモックを作るしかありません。 過去に使ったことのある産業用PCを参考に既製品のアルミケースにコネクタと主要な部品を並べ、段ボールでモックを作ってどれくらいの大きさなら妥当か協議を進めます。弊社の過去の案件ではUSBケーブルの抜けやすさが問題になったこともあり、本製品ではツメ付きのロック機構を持つUSBコネクタを採用しました。このように製品の機構全体に影響を与える部品はこの段階で決めておきます。開発後期で余分なスペックを切り捨てる調整をするのは容易でなので、必要であろうスペックよりも少し余裕を持たせておきます。 この段階で必要な機能を入れていくと困ったことに防塵・防滴の冷却用のファンがどうしても収まらなかったです。それでもこの段階ではどうすることもできなかったのでとりあえず外装に乗せます。今思えばどう考えても不格好ですがプレゼンは乗り切りました。この形状で許してくれた関係各位には感謝しかないです。 初期筐体案 設計試作フェーズ (4ヶ月 ×2)  設計に入る前にやるのはコードネームを付けることです。これは製品の性能には関係ないのですが、楽しいというのとセキュリティ面で大事です。製品名は開発チームでは決められないですが、コードネームなら自由です。コードネームが決まれば試作基板を設計します。最初の試作ではJetsonの処理負荷を最大にした場合の余裕や全機能がまずは 正しく動いているか を確認します。 OEMマニュアルを読み込んだり、開発キットの回路を参照しながら回路設計します。 電源のような重要な回路については起動時や負荷変動に対するリップルや位相マージンをのシミュレーションし、入念に検討します。 初期試作品 と、すんなりいけばいいのですが、多少ミスが出ます。測定をしながらミスの原因を特定し、カット&トライで解決策を出し合います。ソフトウェアの設定で解決できる問題もたくさんあるのでハードウェアだけの知識では解決出来ないこともたくさんありますのでチームで乗り切ります。コロナ禍で出社が制限される中、基板を各社員の家に送り合ったりして解析を進めました。  2回目の試作では 使い勝手の向上や便利な機能を追加 していきました。具体的にはGPSを使った時刻同期を導入したり、壊れやすいコネクタを壊れにくいものにしたり、コネクタのツラをぴったり合わせたり、弊社の主幹製品 intdash をインストールした際の性能調査が主なものになってきます。この試作段階で以前から付き合いのあるお客様に開発機をいち早く触ってもらいながら改善点を洗い出すことが出来ました。 2回目の試作では1回目の試作で冷却性能に余裕があることが分かったので、筐体をより安価にするため、板金とアルミ鋳造で作ることにしました。今回は社内では初めて中国の工場に鋳造を依頼することにしました。鋳造を依頼する際はもちろん設計図でやり取りするのですが、正しくコミュニケーションするには図面だけだと難しいです。私は英語や図面がそれほど得意ではないので鋳造できるであろう形状を切削で作ったサンプルを使ってコミュニケーションしました。特に表面のシボや色のコミュニケーションはどうやっても言葉で伝えるのは難しいです。実物を用意してみてもらうのが確実だと改めて実感しました。 開発中盤試作機 広い公園でGPSの感度試験 量産試作(4か月)  量産試作では、 コストを安く抑える調整と認証試験 を行います。共通化できる部品をまとめたり、不要な部品を削除したり、細かいですが地道に処理します。組立のタクトタイムを短くするため位置決めピンを設けて作業者の負担を軽減したりします。弊社のデザインチームの力を借りてロゴを入れて、より一段と製品らしくなります。 量産試作 そしてこの量産試作機を使って認証試験を行うことになります。本製品では日本・アメリカ・EUの仕向けを想定して各地域の規制を満たすためのEMC試験を行いました。また、それとは別に車載機器に求められるEMC試験も実施して規格値を満たすことを確認しました。 FCC試験風景 まとめ  開発を始めてから1年以上かかりましたが、納得のいく製品に仕上がっていると思います。都立産業技術研究センターをはじめパートナー会社のみなさまには大変お世話になりました。今後は量産品質と安定供給にフォーカスし、お客様に信頼いただけるハードウェアになるよう出荷後のサポートを丁寧に行っていきたいと思います。
OTチームの大久保です。 エッジデバイス上でのデータ処理やネットワーク周りの実装に、速度と生産性の両面で優れるRust言語を利用できないかをここ最近は検討しています。特に、 tokio のバージョン1.0がリリースされたように、最近はRustの非同期関連のエコシステムが充実してきたので、エッジデバイスでも応用できそうです。Rustのasync/awaitはゼロコストを謳っているので、安心して使うことができます。しかしながら、極めて高頻度に呼び出される関数がasyncであった場合、普通の同期的(asyncではない普通のfn)な関数に比べて、asyncであることによる関数の呼び出しコストの増加は無いのでしょうか。実用上は、asyncな関数は内部で非同期IOを行うはずなので、それに比べれば関数の呼び出しコストは微々たるもので気にする必要はありませんが、以下のような場合には問題に成りえます。 async関数だが、稀にしかコストのかかるIO操作を行わない。例えば、大抵はキューに保存されているデータを返すが、キューが空の場合になって初めてファイルを読み込むなど。 内部でIO操作を行わず、async関数にする必要が無いが、他のIO操作をする関数とインターフェイスを合わせるため、asyncを指定している。 今回は、このうち2番目のような状況を想定し、何度も値を取り出すイテレータの非同期版、つまり ストリーム を複数の方法で作って、実行速度を検証してみたいと思います。 普通のイテレータ 1から1億までの数字を順番に返すイテレータ SyncTest を作ります。 const N_LOOP: usize = 100000000 ; const SUM: usize = N_LOOP * (N_LOOP + 1 ) / 2 ; mod sync_test { use super :: * ; pub struct SyncTest ( pub usize ); impl Iterator for SyncTest { type Item = usize ; fn next ( &mut self ) -> Option < usize > { self . 0 += 1 ; if self . 0 <= N_LOOP { Some ( self . 0 ) } else { None } } } } 実行時間を調べるため、次のような print_time 関数を用意します。この print_time は、与えられた関数の実行時間を測定し、1秒間に何回イテレータ(ストリーム)が処理されたかを表示します。ちなみに、nightlyの bench による測定は、async関数を何度も呼ぶ計測には向いてなさそうなので、実行時間を調べるのに自前の print_time 関数を用意しました。 fn print_time < F: FnOnce () > (f: F) { let before = Instant :: now (); f (); let duration = Instant :: now (). duration_since (before); let secs = duration. as_secs () as f64 + duration. subsec_nanos () as f64 / 1000000000.0 ; println! ( "{:?}: \t {:.0}/s" , duration, N_LOOP as f64 / secs); } SyncText イテレータを作成し、実行するコードを作成します。 println! ( "sync_test" ); let mut iter = sync_test :: SyncTest ( 0 ); print_time ( move || { let mut sum = 0 ; while let Some (result) = iter. next () { sum += result; black_box (result); } assert_eq! (sum, SUM); }); この iter.next() は1億回呼ばれ、その結果の合計値を計算し最後にそれが正しいか確認します。また、ループ毎の結果は std::hint::black_box に渡します。 black_box は、渡した値がどのような用途にも使用され得ることを示すためのnightly限定の関数で、このようなパフォーマンス測定の時に過度な最適化を抑止します。 上記のコードの実行結果は以下のようになりました。ちなみに、動作させているCPUはCore i5-8259Uです。 sync_test 61.327952ms: 1630577848/s このイテレータを1億回実行するのには60ms程度かかり、1秒間に16億回程度実行することができます。 asyncをつけてみる SyncTest の next に async をつけただけのものを実行してみます。 Streamトレイト の定義通りではありませんが、このようにasync関数で値を取り出すオブジェクトもストリームの仲間として今回扱います。 実行はtokioのランタイムを用います。 mod async_test { use super :: N_LOOP; pub struct AsyncTest ( pub usize ); impl AsyncTest { pub async fn next ( &mut self ) -> Option < usize > { self . 0 += 1 ; if self . 0 <= N_LOOP { Some ( self . 0 ) } else { None } } } } // ランタイムの用意 let rt = tokio :: runtime :: Builder :: new_current_thread () . enable_all () . build () . unwrap (); // 実行 println! ( "async_test" ); let mut iter = async_test :: AsyncTest ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; while let Some (result) = iter. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); この実行結果は以下のようになります。 async_test 202.02216 ms: 494995203 / s このasync版 next は、秒間5億回程度呼び出すことができます。普通のイテレータより3倍程度遅くなりました。もっとも、この next 関数はほとんど中身が無く、実行時間はほとんど関数呼び出しに占められていると考えると、async関数の呼び出しのコストは案外小さいと言えるのかもしれません。 futures::stream::iter futures は、ストリームを操作するための関数をいくつか提供しています。その中で、 futures::stream::iter は、イテレータからストリームを作成するための関数です。先程作ったSyncTestイテレータから、ストリームを作成してみます。 また、 Stream トレイトを Box に入れた BoxStream というものがfuturesには定義されています。中身の型を隠蔽することができるので、中身が異なる複数のストリームを扱う場合に便利です。この BoxStream を使った場合の性能も計測します。 mod stream_from_iter_test { use super :: sync_test :: SyncTest; use futures :: stream :: {iter, BoxStream, Stream}; pub fn get_stream (init: usize ) -> impl Stream < Item = usize > { iter ( SyncTest (init)) } pub fn get_stream_boxed (init: usize ) -> BoxStream < 'static , usize > { Box :: pin ( get_stream (init)) } } // 実行 println! ( "stream_from_iter_test" ); let stream = stream_from_iter_test :: get_stream ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; futures :: pin_mut! (stream); while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); // Box版を実行 println! ( "stream_from_iter_boxed_test" ); let mut stream = stream_from_iter_test :: get_stream_boxed ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); この実行結果は以下のようになります。 stream_from_iter_test 157.242894ms: 635958786/s stream_from_iter_boxed_test 189.748031ms: 527014691/s Box を用いない場合、秒間6億回と先程のasync版 next より性能が高い結果となりました。 BoxStream にした場合、それよりやや性能が落ちます。トレイトオブジェクトからの仮想関数呼び出しと同様に、 Box に入れるとやや呼び出しコストが高くなることが伺えます。 futures::stream::unfold イテレータからStreamを作る場合、当然イテレータの next はasyncでは無いのでその中でawaitは使えません。awaitを使いたい場合、 futures::stream::unfold を使います。任意のオブジェクトを state として使い回すことでStreamを実装します。先程定義した AsyncTest を使うと以下のようになります。 mod stream_unfold_test { use super :: async_test :: AsyncTest; use futures :: stream :: {unfold, BoxStream, Stream}; pub fn get_stream (init: usize ) -> impl Stream < Item = usize > { unfold ( AsyncTest (init), | mut state | async move { let result = state. next ().await; result. map ( | result | (result, state)) }) } pub fn get_stream_boxed (init: usize ) -> BoxStream < 'static , usize > { Box :: pin ( get_stream (init)) } } // 実行 println! ( "unfold_test" ); let stream = stream_unfold_test :: get_stream ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; futures :: pin_mut! (stream); while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); // Box版を実行 println! ( "unfold_boxed_test" ); let mut stream = stream_unfold_test :: get_stream_boxed ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); この実行結果は以下のようになります。 unfold_test 421.561371ms: 237213385/s unfold_boxed_test 428.991288ms: 233104967/s 秒間2億回と、 AsyncTest の next を直接呼び出すのに比べて速度は半減しています。しかし、async関数からBoxStreamの形にしたい場合は有用でしょう。 async-stream async-stream は、他の言語のyield文に相当するものをRustで使えるようにするためのマクロを提供します。Rustにもyield、そしてジェネレーターに相当するものは nightlyには存在する のですが、安定版では使えません。 async-streamを使うと、yieldで生成したい値を返すことでストリームを実装できます。 mod async_stream_test { use super :: * ; use futures :: stream :: {BoxStream, Stream}; pub fn get_stream (init: usize ) -> impl Stream < Item = usize > { async_stream :: stream! { for i in init.. = N_LOOP { // ループの中でyieldを使う yield i; } } } pub fn get_stream_boxed (init: usize ) -> BoxStream < 'static , usize > { Box :: pin ( get_stream (init)) } } // 実行 println! ( "async_stream_test" ); let stream = async_stream_test :: get_stream ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; futures :: pin_mut! (stream); while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); // Box版を実行 println! ( "async_stream_boxed_test" ); let mut stream = async_stream_test :: get_stream_boxed ( 0 ); print_time ( || { rt. block_on (async move { let mut sum = 0 ; while let Some (result) = stream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); この実行結果は以下のようになります。 async_stream_test 1.207506539s: 82815287/s async_stream_boxed_test 1.213700546s: 82392646/s 秒間8000万回と、これまでの結果よりかなり遅くなります。async_stream内のyieldは、関数を中断する命令として働きますが、これはマクロによりawaitに変換されます。await自体は値を返せないので、async-streamはyieldによって返す値を一旦スレッドローカルストレージに格納し、awaitで抜けてから取り出して呼び出し元に返すという実装になっているようです。このあたりの処理はそれなりに複雑なので、時間がかかってしまったと思われます。 とはいえ、安定版でyieldが使えるのはなかなか魅力的です。 async-trait Rustでは、asyncの付いた関数をトレイト定義に用いることは現状できません。 async-trait は、マクロを使ってasync付き関数をトレイトに含めるようにしてくれます。そしてトレイトを実装した型をトレイトオブジェクトにすることも可能になります。ここでは、async-traitを使った場合の関数呼び出しコストを検証してみます。 async-traitを使って、これまで通り1から1億までの数字を取り出せるオブジェクトを定義します。 mod async_trait_test { use super :: * ; #[async_trait::async_trait] pub trait MyStream { async fn next ( &mut self ) -> Option < usize > ; } pub struct AsyncTraitTest ( pub usize ); #[async_trait::async_trait] impl MyStream for AsyncTraitTest { async fn next ( &mut self ) -> Option < usize > { self . 0 += 1 ; if self . 0 <= N_LOOP { Some ( self . 0 ) } else { None } } } } // 実行 println! ( "async_trait_test" ); // トレイトオブジェクトにする let mut mystream: Box < dyn async_trait_test :: MyStream > = Box :: new ( async_trait_test :: AsyncTraitTest ( 0 )); print_time ( || { rt. block_on (async move { let mut sum = 0 ; while let Some (result) = mystream. next ().await { sum += result; black_box (result); } assert_eq! (sum, SUM); }); }); この実行結果は以下のようになります。 async_trait_test 1.652983415s: 60496675/s 秒間6000万回と、async-streamを使ったときより性能が低下しました。async-traitは、async関数をトレイトで扱うためにBoxを使っており、そのため関数呼び出し毎にヒープ割当が発生し、それなりの負荷になってしまったと考えられます。しかし、async-traitはトレイトに適用できるので柔軟性が高く、その利点に比べればよほど高頻度で呼び出される場面でない限り負荷は気にならないでしょう。 generator (nightly) 非同期ストリームではありませんが、nightlyのジェネレーターを使って、これまでと同等のコードを記述してみます。 // ジェネレーターを使うために必要なfeatureの指定 #![feature(generators, generator_trait)] mod generator_test { use super :: * ; use std :: ops :: Generator; pub fn get_generator (init: usize ) -> impl Generator < Yield = usize > { // yieldを含むクロージャはジェネレーターになる move || { for i in init.. = N_LOOP { yield i; } } } } // 実行 println! ( "generator_test" ); let mut gen = generator_test :: get_generator ( 0 ); print_time ( || { use std :: ops :: {Generator, GeneratorState}; use std :: pin :: Pin; let mut sum = 0 ; while let GeneratorState :: Yielded (result) = Pin :: new ( &mut gen). resume (()) { sum += result; black_box (result); } assert_eq! (sum, SUM); }); この実行結果は以下のようになります。 generator_test 152.886807ms: 654078674/s 単純なイテレータには及ばないものの、秒間6億回とそれなりの速度です。yieldが使えると、イテレータのためにstructやenumを使って自分で状態機械を定義する手間が無くなるので、なるべく早く安定版にも導入されてほしいところです。当分先の話になりそうですが。 比較 以上の測定結果をまとめると以下の表のようになりました。 実装 実行回数 [ /s ] イテレータ 1630577848 async付next 494995203 futures::stream::iter 635958786 futures::stream::iter (Box) 527014691 futures::stream::unfold 237213385 futures::stream::unfold (Box) 233104967 async-stream 82815287 async-stream (Box) 82392646 async-trait 60496675 generator (nightly) 654078674 並べて見てみると結構な差があることがわかります。 まとめ 今回は単純なイテレータ・ストリームを色々な方法で実装し、繰り返し呼び出した時の実行時間を計測・比較しました。結果として、実装方法によって実行時間には有意な差が見られました。現実には、関数呼び出しより中身の処理の方が時間がかかり、ましてや非同期IOを扱うことにくらべれば微々たる差になります。しかし、高頻度で呼び出される場合も考えられ、その場合は実装による性能の違いを検証してみる価値はあるでしょう。
はじめに こんにちは、aptpodのサーバーサイドエンジニアの宮内です。 突然ですが、APIのレートリミット実装していますか? 最近、弊社のバックエンドAPIでもレート制限を実装しました。 Generic Cell Rate Algorithm (GCRA) を使ったのですが、 このアルゴリズムが面白かったので、今回はこのGCRAについてと、GoでGCRAを利用したレートリミットについて説明します。 Leaky Bucketについて GCRAの説明に入る前に、GCRAはLeaky Bucketを再現するアルゴリズムであるため、 まずはLeaky Bucketの理解からしていきましょう。 と言っても、深くは触れません。 Wikipedia. Leaky_bucket が詳しいので、詳細はこちらで。 Leaky Bucketとはトラフィックシェーピングやポリシングで良く利用されるアルゴリズムです。 一定のバースト性を許可する Token Bucket アルゴリズムとは異なり、一定のトラフィックに制限するという点が特徴です。 Leaky(漏れ穴のある) Bucket(バケツ) の文字通り、バケツに一気に水が入っても穴からでる水の量は一定というアルゴリズムです。 GCRAとは Understanding Generic Cell Rate Limiting から一部引用します。 Leaky bucket algorithms, however, tend to rely on a separate process to leak used capacity from the bucket. If that process fails, then the bucket may fill up and users may be limited from performing any action. Herein lies the beauty of GCRA, it doesn't rely on a separate process just an accurate clock, but it still acts like a leaky bucket. かいつまんで書くと、 世間的にLeaky Bucketを実装する場合バケットからセル(HTTPで言うリクエスト)を放出する処理を別のプロセス(ドリッププロセス)にすることが多く、 その別プロセスが失敗すると、バケットが満杯になりユーザーが何もできなくなってしまうという課題があったようです。 GCRAはこのドリッププロセスを用いずに時間ウィンドウをスライドさせることでLeakey Bucketを再現するアルゴリズムです。 GCRAの詳細 (ちょっと長いので手っ取り早く使い方だけ知りたい方は GCRAの利用 まで飛ばしてください。) ここの説明は、 先程も引用しましたが Understanding Generic Cell Rate Limiting に詳しく書いてあります。 しかし、時間ウィンドウのスライドが、ややイメージしづらいと思ったので、改めて同記事をベースに解説します。 他にもGCRAの記事を探してみたのですが、数式で語られる記述が多い印象でした。 セルの到着から、制限の判定までを細かく追っていきつつ、GCRAを理解していきましょう。 はじめに変数について書いておきます。 PERIOD -> 単位時間 LIMIT -> 単位時間あたりの最大セル(バケットを埋める単位)数 EMISSION INTERVAL -> 一つのセルが開放される間隔(= PERIOD/LIMIT) ARRIVED_AT -> セルの到着時間 ALLOWED_AT -> タイムウィンドウの一番最初 TAT -> 理論到着時間(Theoredical Arraival Time) NEW_TAT -> 次の理論到着時間 DELAY_VARIATION_TOLERANCE -> 許容値時間ウィンドウの幅(=PERIOD) TATについては重要な概念なのですが、ここではひとまずそういうものだと思って読み進めてもらうと良いと思います。 初回セル到着 まずは、初回のセルが到着したときから考えます。 1. セルが到着。 -----------------x-----------------> t | ARRIVED_AT(今) 2. TATを決める。TATがないので到着時刻をTATとする。 -----------------x-----------------> t | ARRIVED_AT(今) TAT 3. TATからEMISSION_INTERVALを足した時間をNEW_TATとして定義する。 |<- EMISSION -->| | INTERVAL | -----------------x---------------x-> t | | ARRIVED_AT(今) NEW_TAT TAT 4. NEW_TATから、DELAY_VARIATION_TOLERANCE(PERIOD分)を引いた時刻をALLOWED_ATとする。 |<- EMISSION -->| | INTERVAL | -x-------------------x---------------x--> t | | | | ARRIVED_AT(今) NEW_TAT | TAT | | | | DELAY_VARIATION_TOLERANCE(PERIOD) | |<--------------------------------->| ALLOWED_AT 5. ARRIVED_ATがALLOWED_ATより後の時間だったら許可する。 6. セルを許可したときはNEW_TATをTATとして保存しておく。 ここまでが初回セルが到着したときの処理です。 ALLOWED_ATとNEW_TATの間を時間ウィンドウとし,このウィンドウの中にARRIVED_ATがあれば、そのセルを許可します。 時間ウィンドウはセルが到着する度に前方向にスライドしていき、その度にARRIVED_ATが時間ウィンドウの中にあるかでセルの許可/拒否を決めていきます。 ここからは2回目以降のセル到着時の時間ウィンドウについて説明を移します。 2回目以降のセル到着はそのセルのARRIVED_ATとTATの前後関係によってウィンドウの進み方が変わるので、それぞれ順番に見ていきます。 2回目のセル到着(TATよりARRIVED_ATが後) まず、TATより後にセルが到着した時を見ていきます。 1. セルが到着 -----------------x----------------------> t | ARRIVED_AT(今) 2. TATを取得する。 ------x----------x----------------------> t | | TAT ARRIVED_AT(今) 3. NEW_TATを決める。TATとARRIVED_ATを比較して、より後ろの時間にEMISSION_INTERVALを加算し、その点をNEW_TATとする。この場合はARRIVE_ATに加算。 |<- EMISSION -->| | INTERVAL | ------x----------x---------------+------> t | | | TAT ARRIVED_AT(今) NEW_TAT 4. 以降は初回セルと同じようにNEW_TATからDELAY_VARIATION_TOLERANCE(PERIOD)を引いてALLOWED_ATを決める。 |<- EMISSION -->| | INTERVAL | -x--------x----------x---------------x------> t | | | | | TAT ARRIVED_AT(今) NEW_TAT | | | | | DELAY_VARIATION_TOLERANCE(PERIOD) | |<--------------------------------->| ALLOWED_AT 5. ARRIVED_ATはウィンドウの中に入っているのでOK。NEW_TATをTATとして保存して終了。 2回目のセル到着(TATよりARRIVED_ATが前) 次にTATより前にセルが到着したときを見ていきます。よりたくさんリクエストが来ているときのイメージです。 6. セルが到着 -----------------x----------------------> t | ARRIVED_AT(今) 7. TATを取得する。 -----------------x----------------x-----> t | | ARRIVED_AT(今) TAT 8. NEW_TATを決める。TATとARRIVED_ATを比較して、より後ろの時間にEMISSION_INTERVALを加算し、その点をNEW_TATとする。この場合はTATに加算。 |<- EMISSION -->| | INTERVAL | ----------x----------------x---------------x-------t | | NEW_TAT ARRIVED_AT(今) TAT 9. 以降は初回セルと同じようにNEW_TATからDELAY_VARIATION_TOLERANCE(PERIOD)を引いてALLOWED_ATを決める。 |<- EMISSION -->| | INTERVAL | ---x---------x----------------x---------------x--> t | | | | | ARRIVED_AT(今) TAT NEW_TAT | | | | | DELAY_VARIATION_TOLERANCE(PERIOD) | |<---------------------------------------->| ALLOWED_AT 10. ARRIVED_ATはウィンドウの中に入っているのでOK。NEW_TATをTATとして保存して終了。 ARRIVED_ATに対して、初回到着よりALLOWED_AT~NEW_TAT間の時間ウィンドウが前にスライドしていることがわかると思います。 さらにTATより短い間隔でセルが到着するとどうなるか見てみます。 N回目のセル到着(TATよりARRIVED_ATが結構前) 1. セルが到着 ---x--------------------> t | ARRIVED_AT(今) 2. TATを取得してNEW_TATを決める。TATがだいぶ先の時間なので、NEW_TATもだいぶ先となる。 |<- EMISSION -->| | INTERVAL | ----x-----------------------------------x---------------x--> t | | | ARRIVED_AT(今) TAT NEW_TAT 3. 以降は初回セルと同じように決めていくと次のようになる。 |<- EMISSION -->| | INTERVAL | ---x--------x--------------------------x---------------x--> t | | | | ARRIVED_AT(今) TAT NEW_TAT | | | DELAY_VARIATION_TOLERANCE(PERIOD) | |<---------------------------------------->| ALLOWED_AT ARRIVED_ATがALLOWED_ATより前にあります。 つまり時間ウィンドウの外に出ているということなので、ここで初めてそのセルは拒否(またはキューイング)されます。 セルが拒否された場合は、NEW_TATは更新しません。 ARRIVED_ATがALLOWED_ATより後の時間になるまでは、セルは拒否され続けます。 WebでいうとALLOED_ATとARRIVED_ATの差がRetry-Afterになります。 以上でGCRAアルゴリズムの説明は終わりです。 記事中にはKEYやQUANTITYなどもありますが、時間ウィンドウのスライドが読み取るという趣旨であるため, 説明から省きました。 少しだけ説明すると、QUANTITYというのは到着したセルの数で、その分NEW_TATが後ろに伸びます(ウィンドウが前に進みます) Webの世界だとレート制限はリクエスト数でやることが一般的かと思いますが、その場合QUANTITYは1となります。 KEYは制限の対象を識別するための任意値です。 例えば、クライアントIPアドレスやエンドポイントのパスなどです。 GCRAの利用 さて、概念的なところは前述の通りです。実装していきましょう、と言いたいところですが、既にライブラリがあります。 ここでは throttled を使ったサンプルを紹介します。 手元で動かせるように簡単に書いてみました。 package main import ( "flag" "fmt" "log" "time" "github.com/throttled/throttled/v2" "github.com/throttled/throttled/v2/store/memstore" ) func main() { var ( maxRate int burst int interval time.Duration ) flag.IntVar(&maxRate, "r" , 1 , "" ) flag.IntVar(&burst, "b" , 1 , "" ) flag.DurationVar(&interval, "i" , time.Second, "" ) flag.Parse() store, err := memstore.New( 65536 ) if err != nil { log.Fatal(err) } quota := throttled.RateQuota{ MaxRate: throttled.PerSec(maxRate), MaxBurst: burst, } rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) if err != nil { log.Fatal(err) } ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { ng, res, err := rateLimiter.RateLimit( "sample" , 1 ) if err != nil { log.Fatal(err) } if ng { fmt.Printf( "NG res = %+v \n " , res) continue } fmt.Printf( "OK res = %+v \n " , res) } } MaxRate -> 最大レートです。秒間Nセルまでに制限します。 MaxBurst-> バースト数です。内部ではDELAY_VARIATION_TORELANCEの幅に使われています。1秒のウィンドウで1セルなのか100秒のウィンドウで100セルなのか、レートは同じでもバーストはかけられます。 go run で動くようになっているので、試してみるとわかりやすいと思います。 throttled はHTTPのミドルウェアもライブラリとして提供しています。 この使い方は同リポジトリのREADMEに書いてあるとおりなので割愛します。 ミドルウェアの実装を見ると難しいことはしていないので、自分で実装してしまっても良いかもしれません。 終わりに ライブラリを使うだけなら簡単ですが、いざ使おうとしたときにどういった原理で動いているか理解していないと困ることってありますよね。 今回はGCRAについて困ったときの助けになれれば幸いです。 aptpodではサーバーサイドエンジニアを絶賛募集中です。Goをやりたい!という方、まずはカジュアル面談から会話してみませんか? 以上です。 ありがとうございました。 参考 GCRA(Generic cell rate algorithm)でAPIリクエストをレート制限する話 Wikipedia. リーキーバケット Wikipedia. Leaky_bucket weblio. leaky Understanding Generic Cell Rate Limiting Rate Limiting, Cells, and GCRA throttled
はじめに SRE チームの川又です。 Volterra はグローバルで優れたEdge-as-a-Service プラットフォームサービスを提供する事で注目を集めています。 先日、 F5, Inc. に買収された 事でも話題になりました。 一方、弊社 intdash の一部を構成する intdash Server は基本的にクラウド上で動作させます。 ですが、お客様の要件によってはEdge 環境で動作させる事もあります。 SRE チームの検討課題の1つとして、 Edge 環境における効率的なサーバアプリケーションの管理・提供 があります。 以上の背景から、今回はVolterraのサービスプラットフォーム上で intdash Server の一部であるintdash のbackend service を簡易に動作させてみましたのでご紹介します。 はじめに Volterra について Volterra のサービス VoltStack VoltMesh 実際に使ってみた Volterra Node のインストールと設定 Virtual Site, Virtual K8s の設定 intdash backend service のデプロイ Ingress Gatewayの設定 動作確認 まとめ Volterra について 前述の通り、Volterra はグローバルスケールで優れたEdge-as-a-Service プラットフォームを提供しています。 クラウド・Edge 環境を統一的に扱いセキュアでスケーラブルなアプリケーションセントリックインフラストラクチャを構成可能なSaaS 製品です。 特に以下の点に特徴を感じています。 dev, devops, security エンジニアが統一されたコンソールで一元的な管理が可能 豊富な機能を備えたグローバルバックボーンネットワークを保有している Volterra のCo-founder & CEO である Ankur Singla 氏は Juniper Networks, Inc. に買収されたContrail Systems の創設者かつCEO を務めていた方ですので、 ネットワーク技術がこの優れたコンセプトを構成する重要な要素の1つとなっていると考えられます。 正式・詳細な情報に関しては 公式ドキュメント をご覧ください。 Volterra のサービス Volterra のサービスは主に、以下2 つのサービスで構成されます。 分散されたインフラストラクチャ上で統一されたアプリケーション管理を可能にする VoltStack と それらをグローバルスケールでセキュアに接続する VoltMesh です。 VoltStack VoltStack は Volterra Node と呼ばれるmanaged Kubernetes を Volterra Console と呼ばれる管理画面で一元的に制御可能にするSaaS です。 Volterra Node はクラウドまたはEdge 環境で動作させ、ユーザがセットアップを行います。 Volterra Console はWebUI であり、 Volterra Node はグローバルな分散コントロールプレーンで管理されます。 それぞれのmanaged Kubernetes は仮想的に1つのVirtual Kubernetes として取り扱うことが可能です。 正式・詳細な情報に関しては 公式ドキュメント をご覧ください。 VoltMesh VoltMesh はVolterra のバックボーンネットワークを介して VoltStack 間を接続します。 Volterra のバックボーンネットワーク内のRegional Edge(RE) とCustomer Edge(CE) の Volterra Node 内で動作し セキュアでisolate されたネットワーク接続を提供します。 また、ロードバランサやWeb Application Firewall(WAF) の機能も豊富でネットワーク機能を一元的に管理することが可能です。 正式・詳細な情報に関しては 公式ドキュメント をご覧ください。 実際に使ってみた 今回はこれらのVolterra サービスを用いて弊社 intdash Server のbackend service を簡易に動作させてみました。 プランはお試しのためFree プランを使ってみました。 Free プランには一部機能制限の他、各種リソースの利用制限がありますが簡易な動作確認には十分です。 プラン毎の制限事項の詳細は こちら をご覧ください。 また、本検証は非公式ではありますが以下のチュートリアルを参考にさせて頂きました。 Volterra チュートリアル 本検証構成の概要図は以下です。 Volterra Node のインストールと設定 まずは、managed Kubernetes である Volterra Node のインストールと設定を行います。 Volterra Node のイメージは こちら からダウンロード可能です。 今回はKVM 上にインストールしますので Certified Hardware & KVM Images のイメージを選択してダウンロードします。 流れとしては、このイメージでOS のインストールと初期設定を行い、 Volterra Console からSoftware のセットアップを実行します。 ダウンロードしたイメージをKVM 環境に設置したら以下のコマンドで仮想マシンを作成します。 disk のパスやイメージのパスは適宜環境に合わせてください。 また、 Volterra Node の要求スペックや動作確認環境の情報は こちら をご覧ください。 virt-install --name site4 --ram 8192 --vcpus 4 --disk path=/var/lib/libvirt/images/site4.qcow2,format=qcow2,size=20 --network bridge=virbr0,model=virtio --cdrom /home/lab/Downloads/vsb-ves-ce-certifiedhw-generic-production-centos-7.2006.9-202010131432.1602604079.iso --noreboot --autostart --cpu host-passthrough 次に、 Volterra Node の初期設定を行うために Volterra Console でToken を発行します。 以下の様に、 Volterra Console の System タブの Site Management -> Site Tokens からToken を発行できます。 Name を入力し、 Add Site Token を選択しTokenを発行します。 Tokenの発行 発行したToken の UID を確認したら、作成した Volterra Node にSSH 接続します。 初期 ID/PASS は admin/Volterra123 です。ログインしたら以下の様に初期設定を行います。 /$$ /$$ /$$ /$$ | $$ | $$ | $$ | $$ | $$ | $$ /$$$$$$ | $$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ | $$ / $$//$$__ $$| $$|_ $$_/ /$$__ $$ /$$__ $$ /$$__ $$|____ $$ \ $$ $$/| $$ \ $$| $$ | $$ | $$$$$$$$| $$ \__/| $$ \__/ /$$$$$$$ \ $$$/ | $$ | $$| $$ | $$ /$$| $$_____/| $$ | $$ /$$__ $$ \ $/ | $$$$$$/| $$ | $$$$/| $$$$$$$| $$ | $$ | $$$$$$$ \_/ \______/ |__/ \___/ \_______/|__/ |__/ \_______/ WELCOME IN VOLTERRA NODE LOGIN SHELL This allows to: - configure Volterra Node registration information - factory reset Volterra Node - collect debug information for support Use TAB to select various options. You must change password during first login: ? Please type your current password *********** ### 初期パスワードを変更します。 ? Please type your new password ********** ? Please retype your new password ********** >>> configure ### 先ほど発行したToken のUID を設定します。 ? What is your token? $TOKEN_UID ### 任意のsite 名ホスト名を設定します。 ? What is your site name? [optional] site4 ? What is your hostname? [optional] site4-0 ### site の緯度経度を設定します。Volterra Console のMap で描画される際に参照されます。 ### site4 は仮想的に大阪の緯度経度とします。 ? What is your latitude? [optional] 34.6776234 ? What is your longitude? [optional] 135.4160243 ? What is your default fleet name? [optional] trial-vk8s ? Select certified hardware: kvm-volstack-combo ? Select primary outside NIC: eth0 certifiedHardware: kvm-volstack-combo clusterName: site4 fleet: trial-vk8s hostname: site4-0 latitude: 34.677624 longitude: 135.41603 primaryOutsideNic: eth0 token: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ? Confirm configuration? Yes >>> Volterra Node がインターネットに接続可能な状態になっていればこれで Volterra Node 側での設定は完了です。 次に、以下の様に Volterra Console 側から Volterra Node の登録を行います。 System -> Manage -> Site Management -> Registrations に設定した Volterra Node がpending 状態で表示されているのでフォームに従いRegion: tokyo を選択し Accept します。 System -> Sites -> Site List に移動すると以下の様に新たに Volterra Node が追加され Provisioning が開始されます。 SW version と OS version が Successful になり、 Health Score が 100/100 になれば完了です。 なお、 System -> Sites -> Site Map や System -> Sites -> Connectivity を確認すると設定したsite の位置関係や VoltMesh RE との接続状態を確認できます。 既存のsite3 には仮想的に東京の緯度経度を設定しています。 Virtual Site, Virtual K8s の設定 Virtual Kubernetes (Virtual K8s) の設定を行います。 Virtual K8s はVolterra 独自の概念です。複数のK8s Cluster を1つの仮想的なK8s Cluster として扱えます。 こちら の概念図が非常にわかりやすいです。 まず、事前に こちら を参考に Volterra Node に任意のLabel 設定をしておきます。 今回は pref: tokyo をsite3 に設定しています。 最初に Virtual Site を作成します。 App -> Applications -> Virtual Sites で Add Virtual site を選択することで Virtual Site を作成できます。 Virtual Site を作成する際の識別子に事前に設定したLabel を利用します。 Name: pref-tokyo, Site type: CE, Site Selecter Expression: pref:tokyo Virtual Site が作成できたら Virtual K8s を作成します。 App -> Applications -> Virtual k8s で Add Virtual K8s を選択すること Virtual K8s を作成できます。 先ほど作成した Virtual Site を紐付けます。 Name: trial-vk8s, Select vsite ref: pref-tokyo Virtual K8s 作成後 Cluster status: Ready になれば完了です。 なお、 Virtual K8s は kubectl でも操作可能です。Credential は以下からダウンロード可能です。 kubeconfig intdash backend service のデプロイ intdash backend service を Virtual K8s 上にデプロイしていきます。 intdash backend service の構成は以下の通りです。 今回は RDB に PostgreSQL , TSDB に InfluxDB を利用します。 App -> Applications -> Virtual K8s -> Cluster: trial-vk8s からK8s でお馴染みのリソース群の作成が可能です。 PersitentVolume(PV) に関しては、 PVC を作成することでよしなに Volterra Node 上に作成されます。 上記、 Volterra Console の Virtual K8s UI 上から以下のManifest 用いてK8s リソースを作成します。 なお、Free プランではK8s のリソースは基本的に2つずつまでしか作成できません。 (例: Deployment は3 つ以上作れない。) Deployment kind: Deployment apiVersion: apps/v1 metadata: name: intdash-deployment spec: replicas: 1 selector: matchLabels: app: intdash template: metadata: labels: app: intdash spec: volumes: - name: influx-pvc persistentVolumeClaim: claimName: influx-pv-claim - name: intdash-pgres-pvc persistentVolumeClaim: claimName: intdash-postgres-pv-claim containers: - name: intdash image: intdash:latest ports: - containerPort: 80 protocol: TCP - name: intdash-pgres image: postgres:12-alpine ports: - name: intdash-pgres containerPort: 5432 protocol: TCP env: - name: POSTGRES_USER value: ************ - name: POSTGRES_PASSWORD value: ************ - name: POSTGRES_DB value: intdash volumeMounts: - name: intdash-pgres-pvc mountPath: /var/lib/postgresql/data - name: "influx" image: influxdb:1.8.4-alpine ports: - name: "influx" containerPort: 8086 protocol: TCP env: - name: INFLUXDB_ADMIN_USER value: ************ - name: INFLUXDB_ADMIN_PASSWORD value: ************ - name: INFLUXDB_DB value: "intdash" - name: INFLUXDB_HTTP_AUTH_ENABLED value: "true" volumeMounts: - name: influx-pvc mountPath: /var/lib/influxdb imagePullSecrets: - name: aptpod-registry-intdash PVCs kind: PersistentVolumeClaim apiVersion: v1 metadata: name: "intdash-postgres-pv-claim" spec: accessModes: - ReadWriteOnce resources: requests: storage: "20Gi" kind: PersistentVolumeClaim apiVersion: v1 metadata: name: "influx-pv-claim" spec: accessModes: - ReadWriteOnce resources: requests: storage: "20Gi" Service kind: Service metadata: name: intdash labels: app: intdash spec: ports: - port: 80 targetPort: 80 protocol: TCP selector: app: intdash ここで、intdash のコンテナイメージはプライベートレジストリから取得しているため、 kubectl を使用して以下の通り、docker-registry のsecret を事前に作成しておきます。 kubectl から作成したリソースも Volterra Console 上で確認・編集可能です。 kubectl create secret docker-registry aptpod-registry-intdash --docker-server=aptpod --docker-username=aptpod --docker-password=xxxxxxxxxxxxxxxxxxxx Ingress Gatewayの設定 作成した intdash backend service のapi を外部からアクセス可能にするためIngress Gateway の設定を行います。 流れとしては、service を Origin Pool に紐付け、作成した HTTP Load Balancer の向け先をその Origin Pool にします。 Volterra Console にて App -> Manage -> Load Balancers -> Origin Pools を選択し、 Add Origin Pool にて Origin Pool を作成します。 Name: intdash-endpoint Select Type of Origin Server: k8s Service Name of Origin Server on given Sites Service Name: intdash.default (kubernetes service名.namespaceのフォーマット) Select Site or Virtual Site: Virtual Site -> default/pref-tokyo Select Network on the Site: Vk8s Networks on Site Port: 80 次に、 HTTP Load Balancer の作成を行います。 App -> Manage -> Load Balancers -> HTTP Load Balancers を選択し、 Add HTTP load balancer にて HTTP Load Balancer を作成します。 Name: intdash-lb Domains: dummy.localhost #(作成後、DNS info にVolterra からdomain 名が払い出されます。払い出されたドメイン名を本項に上書き再設定してください。) Select Type of Load Balancer: HTTP Default Route Origin Pools: default/intdash-endpoint この設定で HTTP Load Balancer を作成するとDNS info にVolterra からdomain 名が払い出されます。 こちらのドメインを用いて外部から、作成した kubernetes service にアクセス可能になります。 (例: ves-io-56d5ae17-6da1-4fcc-bf72-bd273056f415.ac.vh.ves.io ) 動作確認 試しに Virtual K8s に対してget pods してみます。 ❯ kubectl get pods NAME READY STATUS RESTARTS AGE intdash-deployment-59f9c5c75b-sxsrx 4/4 Running 2 165m ちゃんとpod が動いているのが確認できますが、想定よりコンテナの数が多いです。 これは wingman というVolterra が提供するセキュリティサイドカーが動いているためです。 また、各コンテナにはkubectl 経由でシェル実行してログインも可能ですが、以下の様に Volterra Console からもログイン可能です。 作成した intdash backend service に対しても簡易にREST API でアクセスしてみます。 前述の、Volterra から払い出されたdomain 名に対してエッジ情報をGET するAPI を叩くと以下のレスポンスが返ってきました。 { "items": [ { "uuid": "2af10d18-6545-489c-aabd-f15ffc8a538a", "name": "intdash", "description": "", "nickname": "", "type": "user", "disabled": false, "protected": false, "internal": true, "created_at": "2021-02-24T04:45:29.230827Z", "updated_at": "2021-02-24T04:45:47.016113Z", "last_login_at": "2021-02-24T04:45:38.028793Z", "last_lived_at": "2021-02-24T04:45:47.013862Z" } ], "page": { "total_count": 1, "first": true, "next": "", "last": true, "previous": "" } } 試しにエッジを追加して、再度GET を行うと追加されたエッジの情報も返ってきました。 intdash backend service の機能の極一部ですが、問題なく動作している様です。 { "items": [ { "uuid": "c2df9231-bc99-4b7f-a742-8764c49cabd3", "name": "UPSTREAM_1614154684727910000", "description": "created by bench", "nickname": "UPSTREAM_1614154684727910000", "type": "device", "disabled": false, "protected": false, "internal": false, "created_at": "2021-02-24T08:18:04.939179Z", "updated_at": "2021-02-24T08:18:04.939179Z", "last_login_at": "1970-01-01T00:00:01Z", "last_lived_at": "1970-01-01T00:00:01Z" }, { "uuid": "2af10d18-6545-489c-aabd-f15ffc8a538a", "name": "intdash", "description": "", "nickname": "", "type": "user", "disabled": false, "protected": false, "internal": true, "created_at": "2021-02-24T04:45:29.230827Z", "updated_at": "2021-02-24T08:18:07.754192Z", "last_login_at": "2021-02-24T08:18:04.726411Z", "last_lived_at": "2021-02-24T08:18:07.753735Z" } ], "page": { "total_count": 2, "first": true, "next": "", "last": true, "previous": "" } } まとめ 今回は、Volterra のサービスを利用して弊社 intdash Server の一部であるintdash backend service をEdge 環境で簡易に動作させてみました。Edge のノードがインターネットにアクセス可能でさえあれば、簡単に高いネットワーク接続性を有するサービスを提供することができ、管理も一元的に可能で魅力を感じました。 SRE チームでは今後も引き続きVolterra サービスの検証・検討を行なっていきたいと思います。
はじめに VPoP として弊社の製品全体を統括しております、岩田です。 弊社では以前から、自社製品が使用する通信方式の下回りとして QUIC を使用することができないか 、継続的に調査や検討を行ってきました。QUIC が HTTP/3 をメインターゲットとして最低限の仕様策定を進める方向になって以降、QUIC 検討に対する社内の熱量も多少減退してはいたものの、昨年の WebTransport 周辺の動きを受けて、再度勢いを取り戻しつつあります。 QUIC DATAGRAM は、QUIC を HTTP 向けの ベターTCP としてだけではなく、UDPベース であることを生かしたユースケースで利用できるようにするための追加仕様で、UDP Like な通信を導入することで QUIC の用途を映像伝送やゲームなどのリアルタイム通信に拡張しようとするもの です。QUIC DATAGRAM 自体は、提唱されてから意外と時間の経っている仕様ではありますが、ここ最近 WebTransport や WebRTC での活用という話題が出始めて以降、動きが活発化してきているように感じています。 そんな矢先、以前から検証に使用していた Go製の QUIC ライブラリである quic-go が QUIC DATAGRAM に対応した ので、早速試して記事にしてみたいと思います! github.com 本記事の内容 はじめに QUIC DATAGRAM について そもそも QUIC とは QUIC DATAGRAM とは なぜ QUIC / QUIC DATAGRAM に着目するのか Go製 QUIC ライブラリ quic-go 早速試してみる SiDUCK サーバー SiDUCK クライアント いざ疎通 原因究明 黒魔術 いざ疎通(2回目) qviz で通信を見てみる 余談 おわりに QUIC DATAGRAM について ※ ここから説明が長く続きます。単純に quic-go や「試してみた」に興味があるだけの方は、 Go製 QUIC ライブラリ quic-go までスッと読み飛ばしてしまって構いません。 そもそも QUIC とは 現在では、QUICに関する情報は世の中に溢れてきており、わざわざ解説するまでもなく皆さんご存知かと思いますので、ざっくりとだけ説明をしておきます。 QUICは、GoogleがWebをより高速化するために考案した、UDPベースの新しいトランスポートプロトコルです。当初はHTTP/2のいくつかの課題を解決するために提案されたため、UDPベースといっても データグラムでの伝送を行うものではなく、UDPの上にTCP Like な信頼性のあるストリーム伝送を再定義したもの 、という位置づけになります。現在では、HTTP over QUIC は HTTP/3 という名称で呼ばれるようになっていますので、こちらの名前の方が馴染みがある、という方も多いかも知れません。 下の図は、ちょっと古いですがよく引用される有名な図です。これを見ると、トランスポート〜アプリケーションのレイヤでのQUICの位置づけがよく分かると思います。 QUIC のプロトコルスタック (引用: https://datatracker.ietf.org/meeting/98/materials/slides-98-edu-sessf-quic-tutorial/ ) Google がQUICを発表した のが2013年、IETFに提唱したのが2015年です。その後、2016年に IETFワーキンググループ が立ち上がり、今に至るまで標準化活動が続けられています。昨年内におおよその仕様が FIX しており、 今年いつRFC化されてもおかしくない 、というのが現在のステータスです。 QUIC DATAGRAM とは QUIC DATAGRAM は、現在検討が進んでいる QUIC の拡張仕様で、 QUIC のコネクションの中で UDP Like なデータグラム伝送を実現する仕組み です。正式なRFCドラフトの名称は「An Unreliable Datagram Extension to QUIC」で、 draft-pauly-quic-datagram-05 が現在の最新のようです。 現状、映像/音声などのメディアデータ伝送やゲームなどのリアルタイム性が求められる通信では、 業界や要件、過去の慣例等によって様々なプロトコルが使用されている状況 ですが、その多くは、セキュリティに不安があったり、必要以上に複雑な階層構造を持っていたり、どれも100点満点とはいえないものばかりです。 そのような状況の中、QUIC が UDP をベースとしており、セキュリティ機能も保有していたので、せっかくならばデータグラム通信もさせてしまいましょう、というのが QUIC DATAGRAM 策定のモチベーションとなっています。 なぜ QUIC / QUIC DATAGRAM に着目するのか 弊社の現在の製品群の主なミッションは、 大量かつ高頻度なデータストリームを、エンドツーエンドで、できる限りリアルタイムに伝送すること です。例えば、自動車の遠隔制御などのユースケースでは、人が確認するための大量の映像データには 「リアルタイム性」 、制御に使用するコマンドデータは 「確実性」 という相反する要件が求められます。 現行版の製品は、WebSocket をベースのプロトコルとして使用していますが、動画などのメディアデータの伝送に対する要望が高まり続けた結果、現在ではデータの詰まり遅延が顕著な問題となるようになってきました。これは、WebSocket が持つ TCP Like な伝送方式のリアルタイム性能の限界 を示しており、早急に UDP Like なデータグラム伝送への切り替えが必要です。一方で、遠隔モニタリングや遠隔制御といったユースケースを満たさなければいけない以上、コマンドデータのような 確実性を求める伝送もなくなるわけではありません 。 QUIC や、QUIC の特徴を最大限に生かした WebTransport といった新しいプロトコルは、TCP Like / UDP Like な2種類の伝送方式によって、我々を悩ませている 相反する2つの要件を同一のコネクションに収容する ことができ、これを採用することで通信アーキテクチャの大幅なシンプル化を見込むことができます。他にも、モビリティとコネクションマイグレーションの相性の良さなど、総じてQUICは 我々のユースケースにはピッタリのプロトコルであると考えており、提案当初から継続して技術動向を追いかけています。 Go製 QUIC ライブラリ quic-go ※ ここからやっと技術的な内容が始まります。お待たせしました。 技術的な内容はちょっと難しすぎる...という方は おわりに だけでも読んでいってやってください。 quic-go は、 100% ピュアな Go で実装された QUIC のライブラリ です。弊社はサーバーサイドを主に Go で実装していますので、QUIC を使用したプロトタイピングにはこちらのライブラリをよく利用しています。 ちなみに、Google の公式ライブラリというわけではないですが、どうやら中の人は Google のソフトウェアエンジニアのようで、IETF での標準化活動が本格化する前から現在に至るまで、かなり活発に開発が続けられています。 v0.18.0 までは、サポートしているドラフトバージョンを明示していなかったこともあり ( “It roughly implements the IETF QUIC draft, although we don't fully support any of the draft versions at the moment.“ ) 、他のライブラリとの互換性の面で少し使いにくいところもあったのですが、現在は draft-29 と draft-32 のサポートを明示するなど、段々と改善してきているようです。また、 他のライブラリから利用される例 も出てきており、Golang で QUIC を利用する上でのデファクトスタンダードとなりつつあるライブラリです。 quic-go の QUIC DATAGRAM のサポート状況としては、以前より datagram ブランチというひとつのブランチでほそぼそと開発が続けられていたのですが、 昨年末に突如として開発が再開し、あれよあれよという間に master ブランチまでマージされました 。こういった経緯もあり、quic-go における QUIC DATAGRAM は、まだタグ付けされたバージョンには入っておらず、master にのみ存在するできたてホヤホヤの機能となります。ちなみに、quic-go の GoDoc は最新のタグである v0.19.3 をベースにしているため、まだドキュメントにすら現れていません。 今回のブログネタは、待ち望んでいた QUIC DATAGRAM がついに quic-go でも使用可能になった ので、(まだタグ付けすらされていない機能ですが、少し食い気味に)とりあえず体験してみよう!というのが主な目的です。 早速試してみる だいぶ前置きが長くなりました。ここから、実際に QUIC DATAGRAM を体験してみたいと思います。 検証用に適当なエコーサーバーを書いてみるというのも選択肢としてはあるのですが、なんと QUIC DATAGRAM には、 トランスポートプロトコルを検証するためだけのアプリケーションプロトコル がRFCドラフトとして存在していますので、quic-go を使ってこちらのプロトコルを実装してみようと思います。 その検証用プロトコルは、 SiDUCK (Simple Datagram Usability and Connectivity Kata) と呼ばれるもので、 draft-pardue-quic-siduck-00 で規定されています。 ただ、プロトコルが定義されているといってもそんな大層なものではなく、ただ単に 「クワッ(quack)」 と鳴いたら 「クワックワッ(quack-ack)」 と返せ、それ以外が来たら特定のエラーコードで返せ、というだけのめちゃくちゃシンプルなものです。 ( ”QU”IC と掛けたのか、メッセージ内容がアヒルの鳴き声 “qu“ack だったり、プロトコルの名前が Si”DUCK” だったり、頭のいい人たちはこんな言葉遊びができるんですね。おしゃれです) その他の決まりごととしては、特定 のALPN (“siduck”) を使いなさい、というくらいしかありません。RFCは数分で読めます。 SiDUCK サーバー まずはサーバー用に、quic-go で SiDUCK を実装したのがこちらになります。本当に簡単です。 package main import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" "log" "math/big" "github.com/lucas-clemente/quic-go" ) func main() { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{tlsCert()}, NextProtos: [] string { "siduck" }, // ALPN は "siduck" とする } quicConfig := &quic.Config{ EnableDatagrams: true , // QUIC DATAGRAM を利用する } lis, err := quic.ListenAddr( "127.0.0.1:55555" , tlsConfig, quicConfig) if err != nil { panic (err) } for { sess, err := lis.Accept(context.TODO()) if err != nil { panic (err) } go func () { for { msg, err := sess.ReceiveMessage() if err != nil { log.Print(err) return } // quack でなければエラー (0x101=DISUCK_ONLY_QUACKS_ECHO) を返す if !bytes.Equal(msg, [] byte ( "quack" )) { sess.CloseWithError( 0x101 , "SiDUCK only quacks echo" ) return } // quack だったら quack-ack を返す if err := sess.SendMessage([] byte ( "quack-ack" )); err != nil { log.Print(err) return } } }() } } // オレオレ証明書を作る func tlsCert() tls.Certificate { key, _ := rsa.GenerateKey(rand.Reader, 1024 ) template := x509.Certificate{SerialNumber: big.NewInt( 1 )} certDER, _ := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY" , Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE" , Bytes: certDER}) tlsCert, _ := tls.X509KeyPair(certPEM, keyPEM) return tlsCert } SiDUCK クライアント SiDUCK に則るメリットのひとつとして、標準化された(される)検証方式であるため、 他のライブラリも SiDUCK クライアントを持っている 、ということが挙げられます。今回は、クライアント側はアリモノを使って楽をしてみます。 クライアントとして使用するのは、 Python 製の QUIC ライブラリである aioquic です。こちらも、 quic-go と並んでかなり活発に開発が進められているライブラリでのようです。asyncio ベースなのがすこし面倒ではありますが、Python を使ってサクッと QUIC が試せるのは便利です。 github.com このライブラリの example として、siduck_client.py というその名の通り SiDUCK のクライアントがあるので、今回はこちらを利用させていただくことにしました。 いざ疎通 先程つくったサーバーを、立ち上げて… $ go run main.go aioquic の SiDUCK クライアントを実行すると… $ python3 siduck_client.py -k 127 . 0 . 0 . 1 55555 2021-01-25 14:00:30, 093 ERROR quic [ a65ec898880237e4 ] Could not find a common protocol version !! なんと疎通ができません。なんでや。 原因究明 結論からいえば、 quic-go のバージョンの運用方針が原因で落とし穴にハマった ようです。今回、QUIC DATAGRAM が master にマージされたからと喜び勇んで master ブランチを使って試していたのですが、よくよく調べてみると、 quic-go はタグ付けされたリリース以外はサポートバージョンをあえて絞って運用している らしく、master ブランチのサポートバージョンからは draft-29 も draft-32 も消されておりました。 https://github.com/lucas-clemente/quic-go/blob/master/internal/protocol/version.go#L30 参考: 最新のタグである v0.19.3 には、たしかに draft-29 と draft-32 があるのです… https://github.com/lucas-clemente/quic-go/blob/v0.19.3/internal/protocol/version.go#L30 quic-go の README をきちんと読めば、タグを使え、と書いてあることには気づけたはずでしたが、今回すこし舞い上がってしまっていたようです。やはりエンジニアたるものドキュメントはきちんと読まなければいけません。反省。 When using quic-go as a library, please always use a tagged release. Only these releases use the official draft version numbers. しかしここまで楽しみに待っていたのですから、こんなことで引き下がるわけにはいきません。(こちらにもブログ執筆のための調査時間というサンクコストがかかっているんです) と、いうわけで、今回は 黒魔術 を使って無理やり進めていきたいと思います。 黒魔術 あえて絞られているだけなら無理やり書き換えてしまえ、ということで、絞られている値を書き換えます。(あくまで QUIC DATAGRAM を試してみたい、という欲望のままに実験を行っていますので、あまり真似はおすすめしません) まずは、GitHub から quic-go のリポジトリを clone し、さらに []VersionNumber{VersionTLS} に絞られている値を []VersionNumber{VersionDraft29} に書き換えます。(aioquic の対応バージョンが draft-29 のようでしたので、draft-29 に書き換えることにしました。quic-go の master 実装の詳細まで詳細に追ったわけではありませんので、master ブランチが draft-29 をサポートしている保証はまったくもってありません。master ブランチの README には draft-29 とdraft-32 をサポートしていると書いてあるので、動いたらラッキーということで進めてしまいます) さらに、今回使用するサーバー(main.go)が参照するリポジトリを、GitHubのリポジトリではなく、cloneして書き換えたローカルのリポジトリに無理やり置き換えます。 $ go mod edit -replace github.com/lucas-clemente/quic-go = /path/to/ local /github.com/lucas-clemente/quic-go さてここまでで、やっと準備が整いました。 いざ疎通(2回目) サーバーを立ち上げて、SiDUCK クライアントを再度実行すると… $ python3 siduck_client.py -k 127 . 0 . 0 . 1 55555 -q /tekitou/na/basho/qlog 2021-01-25 15:16:35, 900 INFO quic [ 1b6e3d98e57135bd ] Retrying with token ( 86 bytes ) 2021-01-25 15:16:35, 913 INFO quic [ 1b6e3d98e57135bd ] ALPN negotiated protocol siduck 2021-01-25 15:16:35, 916 INFO client sending quack 2021-01-25 15:16:35, 918 INFO client received quack-ack やりました!!アヒルがちゃんと鳴いてくれました!!! (しかしなんとも表示が素っ気ないです) qviz で通信を見てみる これだけでは表示があっさりしすぎていて味気がまったくないので、 qviz というツールを使ってフレームの流れを可視化 してお茶を濁しておこうと思います。先程のコマンドにしれっと追加していた -q /tekitou/na/basho/qlog は、qviz で使用するファイルを出力するためのコマンドなのでした。 qviz については、 すでに詳しく書かれている情報がある のでそちらに譲りますが、ざっくり概要としては、下の図のような形で、QUIC のフレームの流れを可視化することができるツールです。下の図は、siduck_client.py を使用して通信をした際に出力したファイルを、qviz を使って可視化してみたものです。 qviz による QUIC フレームの可視化 他の通信も混ざっていて分かりにくくはありますが、aioquic 側(クライアント、左側)より Datagram が送信され、その後 quic-go 側(サーバー、右側)より Datagram が返却されている様子が、分かると思います。(上の方と下の方にある、datagram と書かれた赤い箱がそれです) 今回は一度しか SiDUCK クライアントを実行していないので、上の図に表示されている2つの Datagram はきっと “quack“ と “quack-ack“ のはずですが、念の為中身を覗いてみます。 フレームをクリックするとフレームの中身が見える ようですので、2つのフレームをそれぞれ表示してみます。 Datagram フレームの中身(左: "quack"、右: "quack-ack") 残念ながらフレームの中身まで復号化して表示してくれるわけではないようで、詳細を表示してもデータ長までしか確認することはできませんでした。とはいえ、1フレーム目が5文字(”quack”)、2フレーム目が9文字(”quack-ack”)ですので、これで自信を持って SiDUCK に沿った通信ができた、と言えそうです。 余談 本当は、Wireshark を使用して、フレームの中身を複合して、きちんとアヒルが会話している様子をお見せしたかったのですが、どうやら 現行の Wireshark は draft-29 のフレームを正しく復号できないバグを抱えている ようで、泣く泣く断念しました。 おわりに 昨年 QUIC の仕様策定に概ね目処が立ち、 今年が QUIC 元年になることはほぼ確実 となりました。はじめは HTTP/3 がメインターゲットとされてはいますが、WebTransport やその周辺の動きをみても、HTTP/3 をターゲットとした QUIC 本体の次には DATAGRAM の標準化がくるのではないか(きてほしい) と思っているところであります。QUIC ならびに QUIC DATAGRAM は弊社にとってはキーとなりうる技術ですので、技術進化のスピードに振り落とされないよう、最新動向にキャッチアップしていく所存です。 アプトポッドでは、お客様としての案件のご依頼はもちろんですが、求人への応募やビジネスパートナーとしての提携など、ともにビジネスを推進していく仲間を随時募集しております。弊社の技術に興味をお持ちくださった皆様、ぜひ一度お声がけいただけますと幸いです。 本年もなにとぞよろしくお願いいたします。 aptpod 採用ページ aptpod プロダクト紹介ページ
はじめに 研究開発Grで機械学習関連の業務を担当している瀬戸です。前回は、 GluonCVのモデルをSageMaker Neo + Jetson tx2 + DLRで動作させてみる - aptpod Tech Blog を紹介させて頂きました。今回は、intdash SDK for PythonとGluonCVを組み合わせた深度推論のモックを紹介したいと思います。 構成イメージ 今回のシステムの構成は以下の通りになります。手元のPCから、intdash SDK for Python + OpenCV + MXNet + GluonCVを使って、DeepLearningで推論した深度画像をintdashへ送信し、同じ手元のPCでVisual M2Mで見る構成となっています。 構成イメージ図 開発ツール intdash SDK for Python このSDKは、弊社製品であるintdashをPythonでアプリケーション開発を可能にする開発ツールです。 ここ に概要があります。また、ドキュメントは、 ここ にあります。 Visual M2M Data Visualizer 弊社製品のintdashにアップロードされているデータを可視化するために利用するブラウザアプリです。 こちら に詳細があります。 GluonCV Toolkit GluonCV Toolkit (以下、GluonCV)は、MXNetの持つパッケージの一つである Gluon をComputer Visionに特化する形で発展させたパッケージです。既知の有名な論文をベースにした学習済みモデルを提供するModel Zooや、新しいデータ拡張関数などが提供されています。 推論デモ Data Visualizerで撮影した様子が以下の画像のようになります。なんとなく人のようなものが写っているのが見えるのと、後ろに物体があることがわかりそうな様子が見て取れました。 サンプル画像1 サンプル画像2 サンプル画像3 ※今回のデモでは可視化のため、グレースケールで見やすくするための、深度のスケールを変更しています。 おわりに 今回は、深度推定の実力値などは調査せず弊社のフレームワークとDeepLearningのフレームワークを組み合わせた可視化のみとしました。今後は、引き続き、深度推定の実力値の調査やエッジデバイス上でのDeepLearningの推論などのサンプルを作っていこうと思っています。
Aptpod Advent Calendar 2020 25日目=最終日の記事です。 CTOの梶田です。 今年はなんとか走りきった形で Advent Calendar最終日を迎えられました。 よかった、よかった! 昨年に引き続き、Techブログを使ってAdvent Calendarに挑戦し、今年は健全に(!?)基本的に土日を抜いて毎日投稿できました。 昨年よりさらにバラエティに富んだ形になったかなと思っています。 (みんな忙しい中、頑張った!💪) というわけで。。 早いもので2020年も終わろうとしています。 何を書こうかなーと思いつつ、時間が経ってしまったので結局 昨年と同じ話題 で2020年を振り返ろうと思います。 まぁ、年末ネタとしてはよいでしょう😅 はじめに リモートワーク AWSアドバンスドテクノロジーパートナーに認定 リリース関連 テックブログ1年継続 2021年に向けて おまけ はじめに 2020年は、昨年の振り返りで期待していたものとはまったく違う方向になってしまいました。オリンピックも延期になり、各イベントもなくなったりオンライン化したり。。。💦 世界的にも激動すぎて、今もまだ真っ只中な状態ですが 生活様式や働き方、価値観等々いろんなことがガラッと変わりました。 また、新しい時代の幕開けとも感じました。 今年はなんかもう激動すぎて何がなんだか。。 いろんな判断をしなきゃいけなかったことも多く、、大変なことも多かったですが、 準備していたことが形になってきたり、土台としてだいぶ固まってきた感はあります。 今回のAdvent Calendarのネタとして開発や連携の実例が増えたことにも、それが現れています。 intdashと自動運転シミュレータを連携させてみた :1日目 『intdash x ROS』で実現するROSメッセージの遠隔リアルタイムデータ伝送 :16日目 intdashを活用したシステム開発 : 24日目 さてさて、今回の振り返りでは、いろんなことがありすぎたので時系列というよりは、今年のトピックをいくつかピックアップして書いていきます。 リモートワーク 元々リモートワークできる環境ではあったものの基本的にはオフィスに来て仕事をするというスタイルでした。 以下の記事にも初期の段階の記載がありますが、 tech.aptpod.co.jp ↓の流れで 今年の2月あたりから リモートワークへの切り替えを推進 → 春頃:緊急事態宣言付近 ほぼ100%の社員がリモートワークに移行 → 少し落ち着いた夏あたり〜現在まで 引き続きリモートワークを中心とし、フロアごとに出社人数を制限 出社カレンダー こんな感じでスプレッドシートに出社を宣言して、出社人数を調整。 人数が多すぎると赤色、注意で黄色表示という簡単な形でわかるように (slack通知まではしてません。。) 会社としてもモノを扱うので納品前のQAや出荷業務なんかはリモートワークではやりづらく、 どうしても出社しなければならない場合があります。 (緊急事態宣言あたりはいろいろ工夫していました😫) 担当メンバーの家に開発用機材を送り、テストが終わったら会社へ送り返す、会社にある機材をリモートでテストする等々、今年はいろいろと試行錯誤の連続でした。 また、今年も何名か入社したのですが、面接のときは対面だったのにいざ入社となった時期にはリモートワーク中心でオンラインミーティングで顔をちらっと見るぐらい、対面では一度も会ったことがないという今までにないことが起こりました。 そういった中で、受け入れチームの中でのオンボーディングもあり、ほぼリモートワークのみながら活躍している姿を見ると 素晴らしいな と。 個人的には、子育て中なのでリモートで働けることにより助かる部分もありつつも、作業環境的にはまだまだ難しい面もあり、こちらもまだまだ試行錯誤中。。。 AWSアドバンスドテクノロジーパートナーに認定 今年のニュースリリースの中で 2020/4/10に出した『AWSアドバンスドテクノロジーパートナーに認定』にも触れておきます。 www.aptpod.co.jp おおよそ去年(2019年)〜2020年初にかけての活動で AWSのAPNテクノロジーパートナー 1 の資格の中でも最上位である「アドバンスド」の認定を 受けることができました。 弊社としても現在(2020/12)では、ほとんどの環境をAWSで構築しており、このようなAWSのテクノロジーに関する上位の認定を受けることができてよかったと感じています。 個人的には、去年の年末はAWS認定試験の勉強したなーと。今年は色々ありすぎて、既にもう懐かしい。。。😅 リリース関連 Advent Calendar の15日目の記事で既に振り返っていますが、 tech.aptpod.co.jp 今まで準備してきたことが実り、製品の可能性をさらに広げる土台ができてきたというところです。 まだまだ計画しているものもあり、来年以降も充実させていきます。 テックブログ1年継続 昨年のAdventCalendarに合わせて始めたTechブログですが、今年1年間目標だった週1回の投稿はほぼほぼ実施できたかなというところです👍 ローテーションしたり、指名したり。。。業務との兼ね合いでなかなか難しいこともありましたが、なんとか継続して実施できてよかったなと。 引き合いや採用面等々いろんな効果も少しづつ出てきてはいるので来年も引き続き頑張っていければなと思います。💪 地道に積み上げるのはとても重要で我々が対峙しているIoT/DXのシステムでは特に地道な積み上げが効いてきます。 『継続は力なり。』 とはよく言ったもので大切にしていきたいところです。 ちなみに Techブログの運用が難しいというのは、エンジニア組織の課題ランキングでも上位になるくらいあるあるネタだそうです。 note.com 2021年に向けて 2020年は、予定されていた案件の予算が凍結されて延期。。。💦というのもあったり、一旦落ち着いてきた秋頃には、再開された案件が集中してリソースを圧迫したり、イベントがなくなって営業活動がしづらくなったりと、弊社もビジネス面でCOVID-19の影響を受けました。 一方で、コスト削減をだいぶ意識し、成果が出たのは良い面でした。また、製品を外部の方々に使っていただくための環境やツール/ドキュメントも整ってきました。 来年2021年は、まだまだ状況は見えないですが、COVID-19を契機とした本格的なDXが推進される見込み 2 なので、現お客様やパートナーの皆様はもちろんのこと、新しいお客様やパートナー様へも広げて共創していきたいと思っています。 去年みたいな期待は書かず、来年早々からいろいろリリースネタもありますし、今年はブレーキな感もあったので来年はアクセル踏んで頑張っていきます!💪 来年もアプトポッドにご期待ください!! メリークリスマス!🎄 おまけ aptpodの採用ページ AWS Partner Network (APN) とは:AWS の世界的なパートナープログラムです。ビジネス、技術、マーケティング、および販売促進をサポートすることで、APN パートナーが AWS ベースのビジネスやソリューションの構築に成功するよう支援することに重点を置いています。(公式サイト: https://aws.amazon.com/jp/partners/ ) ↩ IDC Japanによる2021年の国内IT市場において鍵となる技術や市場トレンドなど主要10項目 ↩
こんにちは。ソリューションアーキテクトの尾澤です。 唐突ですが、いつも自分が呼吸している空気の二酸化炭素濃度を意識していますか? 温室効果ガス世界資料センターによると、2019年の世界の平均二酸化炭素濃度は410.5ppmだそうです( 出典 )。また、厚生労働省が定める 建築物環境衛生管理基準 では、室内の二酸化炭素濃度の基準を1000ppm以下としており、それを超えると倦怠感、頭痛、耳鳴り、息苦しさ等の症状がでてきて、視覚による疲労の度合いを測るフリッカー値も著しく低下すると言われています( 出典 )。 今年に入って多くの人がリモートワークや外出自粛などの影響を受けて室内で過ごす時間が増えています。気づかないうちにベストなパフォーマンスを出せない状態に陥っている可能性はないでしょうか? aptpod Advent Calendar 2020 24日目の今回は、intdashを活用したシステム開発のイメージを掴んでいただくため、室内の二酸化炭素濃度に応じて換気を促す簡単な仕組みを作ってみようと思います。 開発ツール intdash Edge Agent intdash SDK for Python データ収集 intdash Edge Agentのインストール Device Connectorの実装 Device Connectorの登録 データ可視化 データ処理 intdash SDK for Pythonのインストール SDKを使用したスクリプトの実装 まとめ 開発ツール intdashはもともと自動車の制御データの収集と可視化を目的として開発されたプロダクトであるため、CANデータの収集と可視化のための標準的な機能が Automotive Pro としてパッケージ化されています。しかし、それ以外のユースケースでintdashを活用したい場合は、目的のデータを収集することも、収集したデータを利用することも、プロダクトの開発元である我々にお任せいただくしか方法がありませんでした。 アプトポッドでは近年、ユーザ自身がintdashをツールとして活用し、自由にデータを収集して分析処理を行なったりシステム化したりすることを可能にするため、intdashをとりまく開発ツールを整備しています。 今回はその中でも2020年9月30日にリリースした、データの収集を仲介する「intdash Edge Agent」と、収集したデータにアクセスする「intdash SDK for Python」を使用します。 www.aptpod.co.jp tech.aptpod.co.jp intdash Edge Agent intdash Edge Agentは、データの自動再送や流量制御といったintdashクライアントの基本機能を提供するエッジデバイス用のソフトウェアです。ユーザは、データソースに応じたプラグインであるDevice Connectorを実装するだけで、様々なデバイスをintdashに接続することができます。 intdash SDK for Python intdash SDK for Pythonは、intdashサーバに転送した時系列データや各種リソースにアクセスするためのPythonライブラリです。来年以降もJavaScriptやSwift、Golangなどの様々なプログラム言語に対応したライブラリを順次リリース予定です。ユーザはこれらのライブラリを使って、intdashを活用したシステムを簡単に構築することができます。 今回のシステム構成 データ収集 ここから二酸化炭素濃度のデータを収集していきます。 intdash Edge Agentのインストール intdash Edge Agentデベロッパーガイド まずはapt-getコマンドでintdash Edge Agentをインストールします( 2020年12月の現時点では、パッケージリソースは弊社のプライベートリポジトリに配置しており、誰でも取得できるわけではなくBASIC認証によってアクセス制限をかけています → BASIC認証によるアクセス制限はなくし、リポジトリを公開しています)。 $ curl -s --compressed -u USERNAME:PASSWORD " https://private-repository.aptpod.jp/intdash-edge/linux/raspbian/gpg " | sudo apt-key add - $ echo " deb [arch=armhf] https://USERNAME:PASSWORD@private-repository.aptpod.jp/intdash-edge/linux/raspbian $( lsb_release -cs ) stable " | sudo tee /etc/apt/sources.list.d/intdash-edge.list $ sudo apt-get update $ sudo apt-get install intdash-edge intdashサーバへの接続情報を環境変数として与えてintdash Edge Agentを起動します。 $ sudo \ LD_LIBRARY_PATH =/opt/vm2m/lib \ INTDASH_EDGE_UUID =XXXXXXXXXXXXXXXXXXXXXXXXXXX \ INTDASH_EDGE_TOKEN =XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ INTDASH_EDGE_SERVER =dev.intdash.jp \ ... /opt/vm2m/sbin/intdash-edge-manager -C /etc/opt/intdash/manager.conf intdash Edge Agentが起動してintdashサーバに接続すると、Webコンソールで接続状況とストリーミングしているデータの概要を確認することができます。intdash Edge Agentはデフォルトでいくつかのシステムメトリクスを送信するように設定されています。 intdash Edge Agentのデフォルト接続状態 Device Connectorの実装 今回のターゲットは二酸化炭素濃度です。さまざまな環境センサがありますが、ネット上に記事が多く、使いやすいpythonのライブラリも存在するCO2センサ「MH-Z19」を使用します。 MH-Z19とRaspberry Pi $ pip3 install mh-z19 $ python3 >>> import mh_z19 >>> mh_z19.read_all() { 'co2' : 404 , 'temperature' : 14 , 'TT' : 54 , 'SS' : 0 , 'UhUl' : 35584 } pypi.org このライブラリを使ってCO2センサ用Device Connectorを実装していきます。intdash Edge Agentにデータを渡す具体的な方法は、データペイロードを所定のフォーマットでFIFOに書き込むだけです。 #!/usr/bin/env -S python3 -B # coding: utf-8 import signal import time import mh_z19 import LoggerFIFO SIGNUM = 0 def signalHandler (signum, frame): global SIGNUM SIGNUM = signum if __name__ == '__main__' : signal.signal(signal.SIGINT, signalHandler) signal.signal(signal.SIGTERM, signalHandler) ch = 1 logger_fifo = LoggerFIFO.LoggerFIFO( '/var/run/intdash/' , [ch]) while SIGNUM == 0 : data = mh_z19.read_all() now = logger_fifo.GetMonotonicTime_Nsec() logger_fifo.SendFloat64(ch, now, 'co2' , float (data[ "co2" ])) logger_fifo.SendFloat64(ch, now, 'temp' , float (data[ "temperature" ])) time.sleep( 1 ) Device Connectorの登録 用意したCO2センサ用Device Connectorを、intdash Edge Agentの設定ファイルに登録します。intdash Edge Agentは登録されたDevice Connectorを子プロセスとして実行してFIFO経由で受け取ったデータをintdashサーバに転送します。 ... "loggers" : [ { "devicetype" : "customized" , "path" : "/home/pi/intdash-edge/co2.py" , "conf" : "" , "status" : "/var/run/intdash/logger_001.stat" , "connections" : [ { "channel" : 1, "fifo_tx" : "/var/run/intdash/logger_001.tx" , "fifo_rx" : "/var/run/intdash/logger_001.rx" } ], "details" : { "plugin" : "fifo" , "plugin_with_process" : true } }, ... CO2センサ用Decive Connectorを登録してからintdash Edge Agentを再起動して、先程のWebコンソールを確認すると、新しくデータが送信されていることを確認できます。 CO2センサ用Device Connectorを登録したintdash Edge Agentの接続状態 データ可視化 intdashの標準ツールであるData Visualizerを使って、二酸化炭素濃度の推移を可視化してみました。 閉め切ったときの二酸化炭素濃度の推移 上のスクリーンショットは、閉め切った6畳くらいの部屋でひとりでデスクに向かいながら計測した結果です。換気した直後の400ppmから1時間ちょっとで1000ppmを超えました。 窓を開けたときの二酸化炭素濃度の推移 上のスクリーンショットは、1000ppmを超えた状態から窓をひとつだけ開けて換気しながら計測した結果です。二酸化炭素濃度が低下しはじめてから約10分で400ppmまで落ちました。 この結果から、 狭い部屋では1時間に1回10分程度の換気をすることで、室内の二酸化炭素濃度を1000ppm以下に保つことができる と言えるでしょう。 このように、Data Visualizerを使えばintdashに転送したすべてのデータをグラフやメーターなどの様々な視覚表現で手軽に可視化することができます。 www.aptpod.co.jp データ処理 ここからはSDKを使ってデータをリアルタイムに処理してみましょう。 intdash SDK for Pythonのインストール intdash SDKはPyPlからpipコマンドでインストールできます。 $ pip3 install intdash pypi.org SDKを使用したスクリプトの実装 intdash SDKを使って二酸化炭素濃度をリアルタイムで取得し、1000ppmを超えたら換気するようアラートし、450ppmを下回ったら換気できたことを知らせてくれる簡単なSlackのbotを作成します。 #!/usr/bin/env -S python3 -B # coding: utf-8 import intdash import json import asyncio import urllib.parse import urllib.request def post (text): try : req = urllib.request.Request( 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX' , json.dumps({ 'text' : text}).encode( 'utf-8' ), { 'Content-Type' : 'application/json' } ) with urllib.request.urlopen(req) as res: return res.read() except Exception as e: print (e) def callback (unit): try : global alart_flg # Skip basetime. if unit.data.data_type.value == intdash.DataType.basetime.value: return if unit.data.data_id != 'co2' : return if (unit.data.value > 1000 ): if not alart_flg: post( 'CO2濃度が基準値を超えました。換気をしましょう!' ) alart_flg = True elif (unit.data.value < 450 ): if alart_flg: post( 'CO2濃度が基準値をクリアしました。' ) alart_flg = False except : pass def main (): client = intdash.Client( url = "https://dev.intdash.jp" , username = "bot" , edge_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" , ) src_edge = client.edges.list(name= 'raspi' )[ 0 ] wsconn = client.connect_websocket() wsconn.open_downstreams( specs = [ intdash.DownstreamSpec(src_edge_uuid = src_edge.uuid, filters = [ intdash.DataFilter(data_type=intdash.DataType.float.value, data_id= 'co2' , channel= 1 ), ]), ], callbacks = [callback], ) loop = asyncio.get_event_loop() try : loop.run_forever() except KeyboardInterrupt : pass except Exception as e: print (e) finally : print ( "the process cancelled..." ) wsconn.close() if __name__ == '__main__' : alart_flg = False main() 実際に、締め切ったままで作業を続けて、アラートにしたがって窓を開けて換気した際のSlack通知は以下のようになりました。 Slackへの通知 このように、intdash SDKを使うことで、異常検知、他システムとの連携、リアルタイム加工など、収集したデータを使った様々なユースケースに対応することができます。 まとめ intdashを活用したデータの収集と分析処理が、比較的簡単に少ない実装で実現できるようになっていることを実感いただけたでしょうか? 今回は、intdashを使った開発のイメージを掴むためだけに、分かりやすいテーマとして二酸化炭素濃度の測定というテーマを選びましたが、これはintdashの強みを存分に活かすユースケースとは言えません。1秒あたりの二酸化炭素濃度といった低頻度で小さいデータであれば、AWS IoTのような他社のプラットフォームでも十分に収集可能であり、intdashが持つ以下の特徴のどのメリットも享受できていないためです。 モバイル網などの不安定な通信環境でのデータ完全回収 秒間数千発もの高頻度で大量なデータの低遅延データ転送 複数のデータソースの時間軸をそろえたデータ収集 読者の皆さまが、上記のようなメリットを存分に活かしてご自身のDXを実現していただけるよう、共創パートナーとしてお手伝いできることを楽しみにしております。また、本記事がintdashの開発ツールを利用するための一助になれば幸いです。
aptpod Advent Calendar 2020 の23日目を担当しますフロントエンドエンジニアの蔵下です。 弊社Advent Calendarも今年で3年目になりました。立ち上げ当初は参加メンバーも少なく、一人で4記事書くというなかなか体力気力が必要でしたが、昨年から参加メンバーも増え、文化として根付いてきたんだなとほっこりしている今日このごろです。 私事としては、今年はフロントエンドに関する社内ルールをいろいろ考えた年でした。実際に運用に乗ったものから、残念ながらうまくいかなかったことまでさまざまです。その試行錯誤の中から、 Material UI をベースに策定したデザインルールが実際の開発で運用できるレベルまで整備できたので紹介します。 なぜMaterial UIを採用したのか Material UIをベースとしたデザインルール Color Font Spacing Icon Input まとめ なぜMaterial UIを採用したのか Material UIとは、Googleが提唱する Material Design のUIコンポーネントを React で手軽に扱えるようにしたUIコンポーネントライブラリです。 Material UI 世の中で公開されているUIコンポーネントライブラリの中からMaterial UIを採用したポイントは以下の通りです。 aptpodでのフロントエンド開発で使用しているReactの実例(情報)が豊富 提供されているUIコンポーネントが豊富 aptpodで開発するアプリケーションには、日時を操作するUIが実装されることが多く、操作性の良い DatePicker が提供されていることも魅力的 APIドキュメントでコードを見ながらサンプルも触れて確認しやすい ThemeでFontやColorなどを細かく指定できるため、Material UIを使いつつもaptpodらしいデザインに近づけられる Reactと相性がよく、提供されているUIコンポーネントの使い勝手も良い。Themeもカスタマイズしやすいため、デザインもこだわるaptpodに合っていることが採用の理由です。 Material UIをベースとしたデザインルール Material UIをベースにデザインルールを策定するために、以下項目のルールを整備しました。 Color Font Spacing Icon Input ここで紹介するデザインルールは、あえて細かいところまでガチガチに決めすぎず、最低限の項目に絞りました。アプリケーションごとに仕様は異なるため、仕様に沿ったルールを後から追加しても破綻させないとうことが狙いです。 Color Material UIでは以下のColorが Palette で指定できます。必要になるColorをデザイナーと相談しながら事前に決めておくことで、組み上がったViewも統一感のあるスッキリした印象になるでしょう。 Primary Secondary Error Warning Info Success Material UIで指定できるColor 各Colorは Theme で指定でき、UIコンポーネントへ自動で適用されます。 import { createMuiTheme } from '@material-ui/core/styles' ; const theme = createMuiTheme( { palette: { primary: { light: '#4791db' , main: '#1976d2' , dark: '#115293' , } , secondary: { light: '#e33371' , main: '#dc004e' , dark: '#9a0036' , } , } , } ); Material UIには上記のColorの他に Dark Mode が用意されており、Colorと同じくThemeで背景色や文字色が指定できます。 Dark Modeで指定できるColor Font Material UIは以下のfont-familyがデフォルトで指定されています。 font-family: "Roboto" , "Helvetica" , "Arial" , sans- s ; 開発するアプリケーションによっては使用するFontが決まっていることもあるため、以下のようにThemeの typography > fontFamily で指定します。 import { createMuiTheme } from '@material-ui/core/styles' ; const theme = createMuiTheme( { typography: { fontFamily: [ '-apple-system' , 'BlinkMacSystemFont' , '"Segoe UI"' , 'Roboto' , '"Helvetica Neue"' , 'Arial' , 'sans-serif' , '"Apple Color Emoji"' , '"Segoe UI Emoji"' , '"Segoe UI Symbol"' , ] .join( ',' ), } , } ); Spacing Material UIには、UIコンポーネント間の余白の基準となる Spacing がデフォルトで 8px に指定されており、UIコンポーネントのレイアウトも Spacing値の倍数px で指定されます。Material UIを使用しない部分のレイアウトも Spacing値の倍数px に揃えることで、View全体の余白ルールが統一できます。 Spacingは以下のようにThemeで指定できるため、デザインに合わせて変更してください。 import { createMuiTheme } from '@material-ui/core/styles' ; const theme = createMuiTheme( { spacing: 4, } ); theme.spacing(2) // = 4 * 2 Icon ユーザーへ直感的に情報を伝える手段としてIconは欠かせない存在です。Material UIには、 Material Icons がsvgで提供されており、アプリケーション開発に必要なIconは一通り揃っています。Material Icons以外のIconを混ぜてしまうとアイコンの統一性が失われてしまうため、極力Material Iconsのみを使用し、どうしても用途にあったIconがない場合は、デザイナーと相談して作成を依頼しましょう。 Material Icons Input Material UIには、Input( Text Field )の種類が standard 、 filled 、 outlined の3種類用意されています。設置箇所で種類にばらつきが出ないように、事前に使用するStyleを決めておきます。aptpodではクリック領域が明確で見た目に重たさもない outlined を採用しました。 左からstandard、filled、outlined まとめ aptpodはデザインにもこだわりを持っており、ユーザーの使いやすさを日々追い求めながらデザインしています。そのため、今回のようなMaterial UIをベースにした開発は実を言うとそこまで数は多くありません。 たとえUIコンポーネントライブラリを使ったアプリケーションであっても、デザイナーが作成するデザインに少しでも近づけるようにという思いで、デザインルールを策定しました。 今後も実際の開発で運用を続け、より良いアプリケーションを世に送り出せるよう、デザインルールもブラッシュアップしていければと思います。
aptpod Advent Calendar 2020 22日目の記事です。担当は製品開発グループの上野と申します。 一昨年 、 昨年 と引き続きとなりまして今年もiOSの記事を書かせていただきます。 はじめに LiDARとは LiDARスキャナが搭載される前との精度の違い LiDARスキャナのデータに触れてみる LiDARスキャナ使って点群を検出してみた LiDARスキャナによる地形計測の為に 算出した点群データを伝送する 取得した画像データを伝送する 最適化されたメッシュデータを伝送する 最適化されたメッシュデータの取得方法 さいごに はじめに 皆さんはつい先日発売されたばかりの iPhone 12 は購入されましたか? 私個人としてはiPhone12 miniを購入したのですがiPhone SEの第1世代を彷彿とさせる角ばったデザインと小ささが良いですね、指紋認証が無いのが痛い所ですが... それはさておき、その中で発売されたiPhone 12 Proシリーズには LiDARスキャナ と呼ばれる物が搭載されました。 今回はそちらについて検証を行い分かったこと、視えてきたことについてお話ししたいと思います。 LiDARとは そもそも LiDARとは 、Light Detection and Rangingの略で光を物体へ照射し距離や性質を分析する為の技術です。 今回のiPhoneからLiDARスキャナが搭載されましたが、今年発売されたiPad Pro 2020にも搭載されていたりします。 LiDARスキャナiPadPro2020搭載 出典:Apple LiDARスキャナには ToF(Time-of-Flight) と呼ばれるセンサ(※もしくはカメラ)が搭載されています。 ToFセンサの物体との距離を求める方法にもいくつか存在し、物体に光を照射し反射して戻ってくるまでの時間で距離を計算する dToF(Direct ToF)方式 と反射した光の位相差から距離を求める iToF(Indirect ToF)方式 がありiPhone/iPadでは引用した画像にもあるように dToF方式 が採用されているようですね。 このdToF方式の場合iToF方式より屋外での計測や長距離の側位が強く、より安定した方式を採用していると考えられます。 また現状iPhone/iPadでは 最大の5m の奥行きしか計測できないとも書かれています。 iPhone/iPadのToFセンサの情報はかなりニュースとして上がっているので興味があれば調べてみてください。 LiDARスキャナが搭載される前との精度の違い iPhoneにLiDARスキャナが搭載された事によって大きく変わった点はより高速に、より正確に地形を把握する事ができる様になったことだと思います。 今までバックカメラでのワールドトラッキング(地形把握)方法はモーションセンサとカメラ画像から物体との位置関係を算出する方法でした。 具体的には計測を開始した位置から端末を動かしてその差分からどの程度の距離関係があるかと言った方法を取る必要がありました。 しかし上記の方法では凸凹としない平面の物体の検出しかできず正確な奥行きの検出はできませんでした。 iPhone 12 Pro(LiDARスキャナ搭載端末)の計測アプリキャプチャ iPhone 12 mini(LiDARスキャナ未搭載端末)の計測アプリキャプチャ 実際の長さ 実際に標準の計測アプリを使って奥行きを測定し、LiDARスキャナ搭載のiPhone 12 Proと未搭載端末のiPhone 12 miniの結果を比較すると格段に精度が上がっていることがわかります。 ※フロントカメラには TrueDepthカメラ と呼ばれる搭載された赤外線カメラや環境光センサ等をまとめて総称したカメラがFaceID導入当時から搭載されており、実はこちらを利用しても精度も高かったりします。 LiDARスキャナのデータに触れてみる ここからは実装をしながらお話ししたいと思います。 確認環境 Xcode 12.2 (12B45b) Swift version: 5.3.1 iPhone 12 Pro OS: 14.2.1 ひとまず現在(※執筆日2020/12)のARKitを利用したプロジェクトを作成してみます。 Augmented Reality Appでプロジェクト作成 Content TechnologyはRealityKit プロジェクトテンプレートは Augmented Reality App 、Content Technologyは RealityKit を選んでください。 ARAppテンプレートのViewController このプロジェクトテンプレートは開発者にとってとても優しい作りになっており、カメラを利用する為の Info.plist へのプライバシーの記述や、ARViewの自動設置、3D空間上のホームポジションへのボックスのデモ配置等を行ってくれます。 // ViewController.swift ... // Add the box anchor to the scene arView.scene.anchors.append(boxAnchor) // オクルージョンを有効化 arView.environment.sceneUnderstanding.options.insert(.occlusion) // メッシュ表示の有効化 arView.debugOptions.insert(.showSceneUnderstanding) } 最近のARKitは本当にすごい。プロジェクトテンプレートに2行コードを足しただけてこのクオリティ #iPhone12Pro #ARKit #LiDAR #Occlusion pic.twitter.com/3RkifWSVBj — aptueno (@aptueno) December 15, 2020 プロジェクトテンプレートに2行コードを追記した物を実行して動画撮影したものです。 あっという間に撮影しているオフィスのメッシュデータを収集し可視化してくれています、すごい。 LiDARスキャナでメッシュ情報スキャン 赤、緑、青と順番にメッシュの色分けをしておりiPhoneからの実際の距離が近ければ赤、遠ければ青と表現しているようですね。 オブジェクトオクルージョン 3D仮想空間上に設置したボックスに対してオクルージョン(今回の例だとMacの裏側にある仮想オブジェクトを隠す)も動作してくれています。 これらメッシュデータの可視化やオブジェクトオクルージョン機能はLiDARスキャナを搭載しているiPhone/iPadでなければ動作できません。 LiDARスキャナ使って点群を検出してみた LiDARスキャナでできることの代表的な例は 点群データ の収集です。 サンプル例は検索していただければと思いますが基本的には3次元座標データ(XYZ)と色情報(RGB)の集合で表されます。 Appleでも点群の収集、可視化の為のサンプルコードを提供してくれています。 Sample Code - Visualizing a Point Cloud Using Scene Depth このサンプルは可視化方法に MetalKit を利用しており、少しGPUを利用したレンダリングの知識が必要です。Metalの使い方が全然わからないという方は昨年のアドベントカレンダーでデモアプリを作りながら解説した 記事 があるのでご覧ください。解説している内容のかなり発展した内容がこの点群可視化サンプルには実装されていると思います。 Apple提供、点群可視化デモ 特に変更を加える事なく実行してみました。 点群が可視化されてますね、ですが少し見えづらいので中のソースコードを少し書き換えます。 // Renderer.swift ... final class Renderer { ... // Number of sample points on the grid private let numGridPoints = 10000 ... Apple点群可視化デモ修正ver かなり鮮明に点群がみえるようになりました。 上記で書き換えた内容は3D空間上のカメラの視界に対して、どれだけの点を描画するかのパラメータになります。よって取得した情報の可視化には可視化するアプリケーション、ビジュアライザ側も工夫が必要な事が分かります。 LiDARスキャナによる地形計測の為に ここまでの説明でLiDARスキャナが搭載されたiPhoneを利用すれば点群の可視化や撮影した空間の認識ができるので、今回のお題にもある地形計測がなんとなくできる気がしてくると思います。 ここからは地形計測を実用的にするための方法を考えお話していきたいと思います。 地形計測を実用的にするためにはiPhone内で完結する事なく算出した点群情報のファイルへの出力やクラウドストレージへのデータストアが必須です。 弊社では intdash という取得したデータをサーバへ伝送し保存できるプラットフォームや、伝送されたデータをリアルタイムに可視化できるアプリケーションを提供しています。 intdashを用いればクラウドストレージへのデータストアも行えるので今回のゴールとしてはiPhoneで取得したデータをサーバへ伝送し、サーバ経由で取得したデータを別アプリケーションで可視化できる事をゴールとしたいと思います。 算出した点群データを伝送する 取得できた点群データをサーバへ送信し、別PCの3D仮想空間上に表示してみた #iPhone12Pro #LiDAR #ARKit #PointCloud #Demo pic.twitter.com/ioxaAQtIfa — aptueno (@aptueno) December 16, 2020 取り敢えず点群可視化のサンプルで算出した点群データをサーバへ伝送し、可視化してみました。 ※かなりのボリュームになってしまうのでPCで可視化している方法は今回は解説しません。 ARKit で表現されている座標系は右手座標系なので一旦そこに注意とだけ言っておきます。 シンプルに点郡を伝送した場合のビットレート 動画ではサンプルアプリで少し書き換えた1フレーム当たりの点の数を1万のままXYZの座標と色情報を秒間1フレーム間隔で送っており、ビットレートは 約3.5Mbps です。 ARSession のリフレッシュレートは 60Hz ですので単純計算で 約210Mbps の全フレームをリアルタイムに伝送しきる事はかなり難しいと言えます。 また、同じ位置の点がかなり存在しているのであまり効率の良い方法とは言えないと思います。 取得した画像データを伝送する 点群データをそのままリアルタイムに送り続けるのは難しい事がわかりましたので他の方法を考えてみます。点群可視化のサンプルコードを修正した物の動画を撮影しました。 点群可視化のサンプルコードを応用したワールドトラッキングに必要なデータの確認デモ 点群だけでなくDepthMapやConfidenceMapも可視化してみた #iPhone12Pro #ARKit #LiDAR #PointCloud #DepthMap #ConfidenceMap #Demo pic.twitter.com/zKFLWOROCl — aptueno (@aptueno) December 15, 2020 サンプルコードの点群の算出にはARSessionから取得した ARFrame から取得できるカメラ情報( ARCamera )と深度マップ( depthMap )、信頼度マップ( confidenceMap )、カメラ映像( capturedImage )を用いて行っています。 動画では点群、深度マップ、信頼度マップ、カメラ映像の順番で可視化しており点群以外の画像データは CVPixelBuffer という画像の縦横のサイズやカラーフォーマット、データバッファ等を含んだクラスで構成されています。 と言う事はこれら全ては画像としての出力が可能なのでiOSで一般的に使われている画像データクラスの UIImage に変換し表示させてみました。 UIImageへの変換方法ですが調べてもあまり出てこなかったので比較的簡単な方法で変換する方法も共有します。 深度マップを可視化した例 深度マップの可視化例です。 // 深度マップのUIImage化サンプル import ARKit import UIKit extension ARFrame { var depthMapImage : UIImage ? { guard let pixelBuffer = self .sceneDepth?.depthMap else { return nil } let ciImage = CIImage(cvPixelBuffer : pixelBuffer ) let cgImage = CIContext().createCGImage(ciImage, from : ciImage.extent ) guard let image = cgImage else { return nil } return UIImage(cgImage : image ) } } ... func update (frame : ARFrame ) { imageView.image = frame.depthMapImage } 深度マップはFloat32の単色で取得でき、特に設定を変えていない状況でbytesPerRow1024バイトの幅256ピクセル、高さ192ピクセルでした。 距離が近ければ0に近い値を出力し、遠ければ4.0以上の小数も生成していました。 この値が現実世界の空間上のメートル、奥行きの値として扱われるわけですね。 信頼度マップを可視化した例 信頼度マップの可視化例です。信頼度マップは深度マップと同じピクセルサイズでUInt8の単色で取得できますが深度マップの様にそのままUIImage化しても黒い画像で表示されてしまって可視化できたとは言えません。 // 信頼度マップのUIImage化サンプル import ARKit import UIKit extension ARFrame { var confidenceMapImage : UIImage ? { guard let pixelBuffer = self .sceneDepth?.confidenceMap else { return nil } // 0 ~ 2 -> 0 ~ 255 let lockFlags : CVPixelBufferLockFlags = CVPixelBufferLockFlags(rawValue : 0 ) CVPixelBufferLockBaseAddress(pixelBuffer, lockFlags) guard let rawBuffer = CVPixelBufferGetBaseAddress(pixelBuffer) else { return nil } let height = CVPixelBufferGetHeight(pixelBuffer) let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) let len = bytesPerRow * height let stride = MemoryLayout < UInt8 > .stride var i = 0 while i < len { let data = rawBuffer.load(fromByteOffset : i , as : UInt8.self ) let v = UInt8(ceil(Float(data) / Float(ARConfidenceLevel.high.rawValue) * 255 )) rawBuffer.storeBytes(of : v , toByteOffset : i , as : UInt8.self ) i += stride } CVPixelBufferUnlockBaseAddress(pixelBuffer, lockFlags) let ciImage = CIImage(cvPixelBuffer : pixelBuffer ) let cgImage = CIContext().createCGImage(ciImage, from : ciImage.extent ) guard let image = cgImage else { return nil } return UIImage(cgImage : image ) } } ... func update (frame : ARFrame ) { imageView.image = frame.confidenceMapImage } 信頼度マップのデータは ARConfidenceLevel という列挙型で定義された範囲で出力され、最小値は ARConfidenceLevel.lowの0 、最大値が ARConfidenceLevel.highの2 です。 ですのでモノクロ画像として表示でよければ場合は0~255の範囲に変換してからUIImage化する必要があります。 その変換例が上記のサンプルとなります。 カメラ画像の可視化例 // カメラ画像のUIImage化サンプル import ARKit import UIKit import VideoToolbox extension CVPixelBuffer { var image : UIImage ? { var cgImage : CGImage ? VTCreateCGImageFromCVPixelBuffer( self , options : nil , imageOut : & cgImage) guard let image = cgImage else { return nil } return UIImage. init (cgImage : image ) } } ... func update (frame : ARFrame ) { imageView.image = frame.capturedImage.image } 最後にカメラ画像の可視化例です、特に変則的な処理はしていません。 こちらも特に設定を変えていない状況で出力されていたピクセルサイズは幅1920ピクセル、高さ1440ピクセルでした。 ここまで説明した画像データ+カメラの位置情報があれば点群の算出を行う事ができるのでこの4つのデータを伝送し、他の可視化アプリケーションで取得すれば大量の算出した点群データを送る事なく可視化できますね。 また画像として取得できるのであれば弊社でも取り扱っている H.264 等の動画圧縮形式にする事ができますので伝送量はかなり抑えることも可能でしょう。 因みに自動車の自動運転にもLiDARのデータというのは利用されています。自動運転のサポートにとても便利と言うわけではないですが、深度マップ形式でデータを伝送しておけば物体検知の様な地形計測以外の用途にも利用できるので活用範囲が広がりますね。 最適化されたメッシュデータを伝送する 先ほどまでの方法は汎用的に扱う為の伝送方法で可視化するビジュアライザで点群の算出や、不要な点を削除するフィルタ等が必要でして実用までの敷居が高いと言う事になりかねません。 LiDARスキャナでメッシュ情報スキャン なので他に取れる方法ですが、実は一番始めにプロジェクトテンプレートから触ってみて確認したARViewのデバッグオプションとして表示しているメッシュデータも参照可能です。 このメッシュデータは常に最適化された頂点情報を出力しているのでこのデータを時系列データとして伝送する事ができればかなり楽に扱えますね。 最適化された点群データをサーバへ送信し、別PCの3D仮想空間上に表示してみた #iPhone12Pro #LiDAR #ARKit #PointCloud #Demo pic.twitter.com/agf5hOypIH — aptueno (@aptueno) December 16, 2020 上記が最適化されたメッシュデータ送ってみたデモ動画です。 この場合、色情報は送信できませんがかなり軽量なデータ量でかつ、すぐに点群として表示はもちろん ポリゴンメッシュ としても表示できます。 取得した点の数 動画では撮り忘れましたが点の数は動画で移動した範囲だと63596点でした。 最適化されたメッシュデータの取得方法 ではメッシュデータの取得方法も共有します。 class ViewController : UIViewController , ARSessionDelegate { ... func setupARView () { // ARSessionDelegate arView.session.delegate = self } ... //MARK:- ARSessionDelegate func session (_ session : ARSession , didUpdate anchors : [ARAnchor] ) { NSLog( "session didUpdate anchors: \(anchors.count) - ARSessionDelegate" ) } func session (_ session : ARSession , didAdd anchors : [ARAnchor] ) { NSLog( "session didAdd anchors: \(anchors.count) - ARSessionDelegate" ) } func session (_ session : ARSession , didRemove anchors : [ARAnchor] ) { NSLog( "session didRemove anchors: \(anchors.count) - ARSessionDelegate" ) } } 基本的にはARSessionのDelegateから ARAnchor の配列情報を取得する所から始まります。 func sendPointCloud (camera : ARCamera , anchors : [ARAnchor] ) { // メッシュデータの取得 var meshAnchors = anchors.compactMap({ $0 as ? ARMeshAnchor }) for mesh in meshAnchors { // ARMeshGeometryの取得 let geometry = mesh.geometry ... // 頂点情報の取得 let vertices = geometry.vertices for i in 0 ..< vertices.count { let vertexPointer = vertices.buffer.contents().advanced(by : vertices.offset + (vertices.stride * i)) let vertex = vertexPointer.assumingMemoryBound(to : (simd_float3). self ).pointee ... } ... } } ARAnchorの配列には ARMeshAnchor というARAnchorの継承クラスが含まれておりこの中に最適化されたメッシュデータが格納されています。 ARMeshAnchorに格納されている頂点群はある一定の3次元範囲毎にまとめられているので多すぎない頂点数で扱う事ができるのでかなり扱いやすい様になっています。 取得できるデータは ARMeshGeometry をご覧ください。 今回この後の送信方法は深くは語りませんがARAnchorの identifier でARMeshAnchorを一意のデータとして扱えるので、必要なタイミングで必要なデータを送る事が望ましいと思います。 さいごに ここまでで私が調べて共有する地形計測を行うため方法の解説は以上です。 今回はiPhoneにLiDARスキャナが搭載されその能力を活用する為にはどうしたらよいかと言う面で調査を行いました。 近年ARクラウドという言葉が広がり始めており、5Gの普及やスマートフォン、PCの処理能力向上によりリアルタイムに扱う事ができるデータ量もかなり増えてきていると言えます。誰でも手軽に手に入るスマートフォンここまでできるとなるとイノベーションが劇的に進みそうですね。 個人的にはロールプレイングゲームでよくあるような個人間のリアルワールドマッピングアプリなんて出たら面白そうとか思っています。 以上、ご覧いただきありがとうございました。
はじめに こんにちは、 aptpod Advent Calendar 2020 の21日目を担当するハードウェアグループの おおひら です。 もう今年の稼働日もあと数日になりましたね。 例年、年末年始のお休みにむけて帰省や旅行を計画される時期と思いますが、今年はコロナウイルスの感染拡大もあって自宅でお過ごしになる方も多いと思います。本文に入る前のアイスブレイクとしてNetflixの最近のお勧めをひとつご紹介します。 www.netflix.com 『スタートアップ×少女漫画』とでも言えばいいのか(いや、どちらかというと優先度的に並びが逆で『少女漫画×スタートアップ』か…?)、私は韓国ドラマを観るのは初めてなんですが、コテコテな人情・恋愛要素を入れながらもスタートアップ/ベンチャーでよく聞く話がひとしきり押さえられていて面白いです。男性諸氏におかれましては夜中にお酒を飲みながらひとりで観ることをお勧めします(既婚の身で妻と一緒に観るのはすごく恥ずかしいぞ😇) 閑話休題。 さて、本記事では 日置電機株式会社 様の製品である 【非接触CANセンサー SP7001】 を、弊社の自動車計測のためのパッケージソリューション Automotive Pro と組合わせて評価した結果を紹介させていただきます。なお本記事はハードウェアグループおよびソリューションアーキテクトの複数メンバー *1 のコラボレーション記事です。 はじめに 非接触CANセンサーの説明 検証車両への機器設置 Automotive Proの紹介 計測システムへの非接触CANセンサーの追加 計測データの可視化 Visual M2M Data Visualizer CSVダウンロード おわりに 非接触CANセンサーの説明 www.hioki.co.jp 日置電機株式会社様からご提供いただいた非接触CANセンサーの詳細に関しては上記の製品HPをご確認ください。簡単に特徴を述べさせていただくと以下の通りです。 ハーネスに特別な加工や改造をしなくても被覆ごと挟むことでCAN信号を計測できるため、試作車両や市販車に対する計測の工数削減が可能 CAN/CAN-FDバスに対して電気的な影響を与えず信号品質劣化のリスクが低いため、公道でのテストと計測における安全性の確保につながる CANの極性検知は自動検知もOK プロービングした信号がそのままD-Sub 9ピンのコネクタから出力されるので、他社製の既成CANロガーを利用してデータ計測が可能 通常、車両内部の制御信号はゲートウェイECUがあるために一般ユーザーは直接アクセスできません。弊社の過去の記事でも、Raspberry Piを利用してOBDⅡポートから車両の情報を取得する試みが掲載されましたが、こういった整備端子を経由する通信はリクエスト&レスポンスのやりとりによって得られるものであり、リアルタイム性やサンプリング周期の観点で車両本体の制御情報とは隔たりがあります。 tech.aptpod.co.jp そこで非接触CANセンサーの出番というわけですね。 特に近年、CASEと呼ばれる技術革新のトレンドに伴って完成車メーカー様以外の企業様でも車両の制御信号を計測したいというニーズが高まっており、弊社としても高い関心を持って試用をさせていただきました。 以下に弊社の計測システムを利用して、非接触CANセンサーから得られた計測データのリアルタイム性や一致性を検証した結果をご紹介させていただきます。 検証車両への機器設置 Automotive Proの紹介 Automotive Pro は弊社が自動車の開発に携わるお客様向けに販売しているパッケージソリューションです。LTE通信機能とGNSS測位機能を持つ車載コンピューターに弊社のソフトウェア一式をインストールし、サーバー環境も含めて構築した状態でご提供 *2 しますので、お客様は面倒な設定作業なく即日車両計測を開始することが可能です。ハードウェアとしては前述の車載コンピューター、車室外を撮影するカメラ、およびCAN-USBインターフェースを利用して、動画とCANバスの制御信号を統合した時系列で計測することができます。さらにお客様のご要望にあわせてiPhoneアプリを利用した計測も実現可能です( intdash Motion 2.0のニュースリリース 参照)。 aptpodが提供するパッケージソリューション "Automotive Pro" 今回はこのCAN-USBインターフェースに日置電機様の非接触CANセンサーの出力を接続しました。 計測システムへの非接触CANセンサーの追加 計測システムの模式図を下に示します。 CANバス計測システムの模式図 弊社が普段の開発業務やお客様へのデモに利用している社有車は特別に車両のCANバスが取り出せるようにハーネスが改修されており、ここから得られる3系統のCANバス(図中のA,B,C)を2つのCAN-USBインターフェースで取得します。1つのCAN-USBインターフェースにつき2チャンネルのCANバスが取得可能ですので、余ったチャンネル1つに非接触CANセンサーの出力を接続し、通常のハーネス経由で計測したCANバスとのデータの差分を確認します。なお、弊社自社設計のCAN-USBインターフェースは複数の機器間で時刻同期をするためのクロック共有機能があり、各チャンネルの計測時刻を10マイクロ秒の精度 *3 で打刻することができます。 実車の写真は以下の通りです。非接触CANセンサーの電源は車両のシガーソケットから給電し、CANバスの入力極性モード設定は "FIXED"にしました。 物理的な接続が完了したら、続いてエッジ側の機器群(ターミナルシステム)の設定を行います。本稿では詳細を割愛させていただきますが、Linux OSで動作する intdash Edge というミドルウェアに対して、GUIでCANバスのチャンネル追加やサンプリングレートの設定変更が可能です。 設定変更したらエンジンを始動してひとっ走りしてきましょう。車両のイグニッション信号に連動してターミナルシステムが自動的に計測を開始してサーバーにデータを送信します。弊社のIoTプラットフォームである intdash はデータの完全回収が優れたポイントで、仮にネットワーク伝送路においてデータの欠損(LTEの通信環境悪化は分かりやすい一因)が生じても、ローカルストレージに一時保存されたデータが適切なタイミングで再送されることで 時系列が保証されたデータをサーバーに完全回収すること が可能です。 計測データの可視化 サーバーに保存されたバイナリデータの可視化にはWebブラウザで動作する Visual M2M Data Visualizer を利用します。 制御データのバイナリファイルを物理値に変換するためのDBCファイルをインポートし、可視化したい物理値を適切なビジュアルパーツに紐付けて配置することができます。 Visual M2M Data Visualizer Data Visualizerのスクリーン表示例 (ステアリング操舵角/速度/位置/映像/音声) 画面上の青色のグラフが車両のCANバスをハーネス経由で直接計測した情報で、赤色のグラフは非接触CANセンサー経由で計測した情報を時系列に沿って可視化しています。CANバスを流れる ステアリングの操舵角 および 車両の速度 の情報と、同時にGNSSから得られる車両の座標、前方視界を撮影するビデオカメラの動画、車室内のマイク音声もビジュアルパーツとして配置しています。直感的に理解しやすいですね。 さて青色と赤色の計測データはそれぞれ同じように見えますが、詳細はどうでしょうか?更に踏み込んで精緻な検証をしたい場合、計測データをCSVファイルでダウンロードすることができます。 CSVダウンロード ダウンロードしたCSVファイルにはTimestampの情報(マイクロ秒)と選択したデータが含まれます。今回は簡易的にMicrosoftのExcelを利用してCSVファイルを確認してみました *4 。 ハーネスから計測した速度データと非接触CANセンサーから計測した速度データを比較してみると、10マイクロ秒オーダーまで時刻が一致するデータが約82%存在し、時刻が一致しないデータに関しても20マイクロ秒の範囲内に同一のデータが存在することが分かりました *5 。 弊社のCAN-USBインターフェースの打刻誤差が10マイクロ秒であることを考慮すると、非接触CANセンサーの遅延は十分に小さいと言えます。 なお答え合わせ的にネタばらしすると、 こちら で謳われている性能として、 非接触方式でも信号を取りこぼさず、接触方式と同様に正確なCAN信号の取得が可能です。また、CAN信号検出における遅延が130nsと非常に小さく、リアルタイム性を失いません。 というスペックが保証されていますので、安心して利用できることが分かります。 おわりに 本記事では日置電機株式会社様からご提供いただいた非接触CANセンサーを、アプトポッドが提供する自動車計測ソリューションであるAutomotive Proと組合わせて利用し、車両の制御信号を可視化するユースケースをご紹介させていただきました。 CANバスのハーネス経由で直接得られるデータを基準に非接触CANセンサーを評価した結果、低遅延でデータの取りこぼしが発生せず、通常のCAN計測と全く遜色なく利用できることが分かりました。 自動車の研究開発に携わっている方々をはじめとして、移動体全般のデータ計測にご興味・ご関心をお持ちの方はぜひ 弊社HPの問合せフォーム からご連絡をいただけますと有難く思います。 最後になりますが、今回製品の試用と本ブログでの情報発信にご快諾いただいた日置電機株式会社の新津様、高橋様、柳澤様、誠にありがとうございました。この場を借りて厚く御礼申し上げます。 *1 : Macさん、加藤さん、富田さん、ご協力ありがとうございました *2 : 弊社ではこのエッジ側のシステムを総称して"ターミナルシステム"と呼んでいます *3 : 2020年12月時点の最新ファームウェアでは1マイクロ秒の打刻精度に性能向上しています *4 : が、当然ながら数十分~数時間の計測を行う場合はデータ量が多くてExcelでは力不足と思いますのでデータ処理に適したツールを利用しましょう^^ *5 : 厳密に評価する際には机上で任意のCANデータを生成する信号発生器や治具を利用することが必要ですが、今回はあくまで簡易的な評価ですのでご容赦を
aptpodフロントエンドエンジニアの黒川です! aptpod Advent Calender2020 の19日目を担当します。 2020年は新型コロナウイルスの世界的流行により全てが一変した年でした。 オリンピックも延期になりましたし、私達の生活様式や働き方、価値観まで変わりました。 そんな2020年にReactの状態管理を大きく変えるライブラリがリリースされました。それが Recoil です。 Recoilについては、私の 以前書いた記事 でも名前だけ触れました。 2020年の5月に行われたReact Europe2020で発表され、瞬く間に注目を浴びまして、2020年12月現在GitHubスター数1万を超えるなかなかの人気ライブラリとなっております。 とはいえ、 npm trends などを見ても、同じく状態管理ライブラリである Redux や MobX には大きく水をあけられており、まだまだ実際に使われている機会は少ない、これからのライブラリです。 今回は、そんなRecoilについて基本的な使い方と思想、そしてこういう使い方をすると嬉しいんじゃないかというお話をしたいと思います。 Recoilについて Recoilの代表的なAPI atom selector useRecoilState/useRecoilValue/useSetRecoilState useRecoilCallback まとめ Recoilについて Recoilは2020年5月にリリースされたReact用の状態管理ライブラリです。 2020年12月現在はまだexperimentalということもあり、まだまだ製品へと本格投入されるフェーズにありません。しかし、その利便性やReactの本家本元であるFacebookが開発しているという話題性から注目を集めています。 Recoilのコンセプトは非常にシンプルです。 atom と呼ばれる関数から生成された RecoilState というRecoil専用の状態をそのままコンポーネントにsubscribeさせるか、あるいは selector と呼ばれる純粋関数を通してコンポーネントにsubscribeさせるか、これだけです。Reduxのようなreducerやactionといった多くのボイラープレートをRecoilは必要としません。 これはRecoilの開発が、Reactの元々持つ状態管理(useStateやContext)のシンプルさと便利さを損なうことなく、逆にそれらを扱う上で制約となっていた不自由さを取っ払うことを目的に行われていることによるものです。以下に実際に従来のコードとRecoilを用いたコードを書いたので見比べてみましょう。 // 従来の書き方 import React , { useCallback , useState } from "react" ; export const Conventional = () => { const [ count , setCount ] = useState < number >( 0 ); const onIncrement = useCallback (() => { setCount (( prev ) => prev + 1 ); } , [] ); const onDecrement = useCallback (() => { setCount (( prev ) => prev - 1 ); } , [] ); return ( < div > < p > { count } < /p > < button onClick = { onIncrement } > インクリメント < /button > < button onClick = { onDecrement } > デクリメント < /button > < /div > ); } ; // Recoilを用いた書き方 import React , { useCallback } from "react" ; import { useRecoilState } from "recoil" ; import { counterState } from "../../atoms/state" ; // counterStateはこのような定義がされています // const counterState = atom<number>({ // key: "counterState", // default: 0 // }); export const RecoilWay = () => { const [ count , setCount ] = useRecoilState ( counterState ); const onIncrement = useCallback (() => { setCount (( prev ) => prev + 1 ); } , [] ); const onDecrement = useCallback (() => { setCount (( prev ) => prev - 1 ); } , [] ); return ( < div > < p > { count } < /p > < button onClick = { onIncrement } > インクリメント < /button > < button onClick = { onDecrement } > デクリメント < /button > < /div > ); } ; このようにほとんど変わりません。 変更点は、Recoilの方でimportしている atom 関数によって定義された counterState が useState 関数と入れ替わった useRecoilState 関数によってsubscribeされたことだけです 1 。 これだけで今やRecoilで定義されたほうの count は、他のコンポーネントからも counterState をsubscribeすることで参照可能となったのです。簡単で便利ですね! ここではRecoilが既存のReactのシンプルさを引き継ぎつつ、簡単に他のコンポーネントからも RecoilState が参照可能になることを紹介しました。続いて、Recoilの代表的なAPIと基本的な使い方を紹介したいと思います。 Recoilの代表的なAPI atom atom はRecoilで用いられる RecoilState を生成する関数で、使い方は非常にシンプルです。以下のように atom 関数で宣言するだけで完了です。 const counterState = atom < number >( { key: "counterState" , default : 0 } ); ほとんど初見でも分かりそうなくらいにシンプルですが、簡単に説明します。 atom は key と default という2つのプロパティを引数として求めます。 key はアプリケーション全体でユニークなものを渡す必要があります。これは内部的に key によってatomを判別しているからです。 default は useState やReduxにおける初期値になります。 以上を設定すれば、あとはこの atom から生成されたStateを使用するコンポーネントにおいてsubscribeすれば参照可能になります。 アプリケーションを通してこの atom は何度も使用することになるかと思います。 そのような時に、例えばユーザーネームとパスワードなど同じ input を用いたコンポーネントでありながら、別々の RecoilState を扱うコンポーネントに対して userNameState , passwordState などといちいち atom から RecoilState を生成するのは面倒ですよね。 そんな時は、 atomFamily を使えば解決です。 atomFamily は動的にatomを作成してくれる関数であり、宣言もほとんど atom と同様です。 import React from "react" ; import { useRecoilState , atomFamily } from "recoil" ; const inputTextState = atomFamily < string , string >( { key: "inputTextState" , default : "" } ); export const InputFamily = () => { // もしパラメータを同じものにすると変数名が違っても、同じ値になる const [ userName , setUserName ] = useRecoilState ( inputTextState ( "userName" )); const [ password , setPassword ] = useRecoilState ( inputTextState ( "password" )); const [ remarks , setRemarks ] = useRecoilState ( inputTextState ( "remarks" )); return ( <> < input value = { userName } onChange = { ( e ) => setUserName ( e. target .value ) } / > < input value = { password } onChange = { ( e ) => setPassword ( e. target .value ) } / > < input value = { remarks } onChange = { ( e ) => setRemarks ( e. target .value ) } / > < p > { userName } < /p > < p > { password } < /p > < p > { remarks } < /p > < / > ); } ; ほとんど atom を用いた宣言と同様な事がわかります。異なる点は、 RecoilState をsubscribeする際に任意のパラメータを atomFamily に渡すことです。ちなみに atomFamily の型引数の1つ目はStateの型で、2つ目はパラメータの型になります。 これにより内部的に atomFamily が生成した atom に key をマッピングしてくれるのでこちらで細かいことを気にする必要はありません。 今回はinputで簡単に説明しましたが、例えばユーザーが動的に追加していく項目やボタンなどにも atomFamily は用いることができますので、非常に活用の幅は広いです。 selector selector は atom で生成された RecoilState をコンポーネントにsubscribeする際、使いやすい形に加工するための関数です。 まずはサンプルコードをお見せして説明しようと思います。こちらは Recoilの公式ドキュメント から引用しました。 const tempFahrenheit = atom ( { key: 'tempFahrenheit' , default : 32 , } ); const tempCelcius = selector ( { key: 'tempCelcius' , get: ( { get } ) => (( get ( tempFahrenheit ) - 32 ) * 5 ) / 9 , set: ( { set } , newValue ) => set ( tempFahrenheit , newValue instanceof DefaultValue ? newValue : ( newValue * 9 ) / 5 + 32 ), } ); selector を使うときには、2つのプロパティを定義する必要があります。1つ目はおなじみの key ですね。ユニークなものを定める必要があります。 2つ目は、 get です。ここでコールバック関数を定めることで selector がどんな加工を行うか定義できます。また、このコールバック関数は引数に get という関数を受け取れます。このget関数に atom で定義した RecoilState を渡すことでselector内で値を読み込むことができます。ここからは自由に加工可能です。上の例では、温度の単位変換として華氏(°F)の値を読み込んで摂氏(℃)に変換しています。 さて、2つのプロパティと言いましたが、上記の例では3つ定義していますね。3つ目のプロパティ set はオプションです。定義せずに読み込み専用の selector とすることも勿論可能ですが、定義することで selector と atom の値を連動させることができます。 set の役割は、 RecoilState を新しい値に上書きすることです。 get プロパティ同様に set プロパティもset関数を引数に受け取ります。また、上書きするための新しい値も受け取ります。このset関数の第1引数に更新対象である RecoilState 、そして第2引数に set プロパティが受け取った新しい値を渡すことで、 RecoilState が更新されます。上の例では tempFahrenheit が更新の対象です。また、 tempCelcius は tempFahrenheit の値をもとに生成されているので、こちらも連動して更新されます。このように selector で RecoilState を更新させた際は、結果的にその selector の get から得られる値も更新されることとなります。 また、上記のコードを見るとset関数の新しい値を DefaultValue のインスタンスかどうかを判別しているかと思います。これは、 useResetRecoilState という RecoilState の値をdefaultに戻すHooksの使用を考慮したものです。すなわち、 useResetRecoilState で newValue が DefaultValue のインスタンス = defaultの値の時はそのままdefaultの値に更新し、それ以外の時は摂氏を華氏に変換して更新しています。文字で説明すると少々ややこしいですが、実際に使ってみるとわかりやすいAPIです。 さらに selector は非同期処理も組み合わせることが可能です。 これを利用して以下のように、例えばあるidのデータをAPIを通して取り出したいというものが簡単に実装できるようになります。 const asyncSelector = selector ( { key: "asyncSelector" , get: async ( { get } ) => { const res = await getSomething ( get ( idState )); return res.data ; } } ); ただし、非同期処理に selector を用いる時は一点だけ注意が必要です。 この asyncSelector から返ってくる値は非同期であるため、同期的にコンポーネントをマウントした場合はまだ取得されていないデータを参照する可能性があり、アプリケーションの動作に支障を来してしまうということです。これを回避するための方法は2つあります。 1つ目はコンポーネント内は同期的に取得して、親コンポーネントで回避する方法です。以下のコードをご覧ください。 // 親コンポーネントで回避するパターン const SomethingAsync = () => { const value = useRecoilValue ( asyncSelector ); return < div > { value } < /div >; } ; const ParentComponent = () => { return ( < Suspense fallback = { < div > Loading... < /div > } > < SomethingAsync / > < /Suspense > ); } ; ここでは Suspense を使っています。 Suspense について簡単に説明しますと、 SomethingAsync が読み込まれるまで待機するためのコンポーネントです。 Suspense の fallback に渡したコンポーネントを待機中に表示してくれますので、非同期中であってもアプリケーションは正常に動作できます。 もう1つは、コンポーネント内で回避する方法です。この回避をするためのHooksとしてRecoilは useRecoilValueLoadable を提供しています。 この useRecoilValueLoadable は値をそのまま返すのではなく、Loadableオブジェクトというものを返します。Loadableオブジェクトは state と contents という2つのプロパティを持ち、 contents は state の状態によって中身が変わります。 以下のコードを見て、 state と contents がどのような形になりうるか確認したいと思います。 // コンポーネント内で回避するパターン const SomethingAsync = () => { const valueLoadable = useRecoilValueLoadable ( asyncSelector ); switch ( valueLoadable.state ) { // 非同期処理が終わっていない時、stateはloadingとなり、contentsはPromiseとなる // そのままだと表示できないので、<Suspense>と同じ扱いをする case "loading" : return < div > Loading... < /div >; // 非同期処理が終わればstateはhasValueとなり、 // contentsには実際の値が入るので、コンポーネント内で使用できる case "hasValue" : return < div > { valueLoadable.contents } < /div >; // もしも非同期処理中にエラーが生じるとstateはhasErrorとなる // contentsにはErrorオブジェクトが入るので、これをthrowすることでErrorBoundaryに検知させる case "hasError" : throw valueLoadable.contents ; } } ; このような感じです。実際にどちらのパターンを取るかは好みやコンポーネントの設計によると思いますが、 selector で非同期処理を扱う時はこのような処理が必要ということだけ留意する必要があります。 useRecoilState/useRecoilValue/useSetRecoilState 今まででRecoilを扱うためのStateを作るAPIを紹介してきました。ここからは実際にコンポーネントにsubscribeするためのHooksを紹介していきます! ただ、実はRecoilでsubscribeするためのHooksはほとんど中身は一緒なんです! 違いは、 「値」と「更新するための関数」両方必要か 「値」だけ必要か 「更新する関数」だけ必要か の3つに応じて使い分けるだけです。 そして、それぞれ①のケースが useRecoilState 、②が useRecoilValue 、③が useSetRecoilState となります。 すなわち、 useRecoilValue + useSetRecoilState = useRecoilState です。簡単ですね。 useRecoilState は今までのサンプルコードで何度も用いてきましたが、以下に3つのサンプルコードを書きます。 Reactのhooksに慣れている方であれば、全く違和感なく書けると思います。 // ReactのuseStateと全く一緒です const [ count , setCount ] = useRecoilState ( counterState ); // ReactのuseStateからstateだけを取り出したものです const count = useRecoilValue ( counterState ); // ReactのuseStateからsetStateだけを取り出したものです const setCount = useSetRecoilState ( counterState ) いかがでしょうか。本当にシンプルなAPIなのでありがたいですね。 さて、ここまでで RecoilState を作り、それをコンポーネントにsubscribeさせる方法を説明してきました。これによりもう <RecoilRoot> 内のどこからでも自由に RecoilState を参照できるようになっています。ただ、まだ気になる点が1つあります。 onClick などイベント発火時のコールバック関数です。従来の useCallback は便利ですが、下手に依存関係を持たせてしまうと依存している変数の更新に伴い、 useCallback の更新も行われ、意図しない再レンダリングが生じていました。 RecoilState が更新されても不要な時はsubscribeせず、必要になった時に RecoilState を読み込む、そんなコールバック関数があれば理想的です。そのような希望を叶えてくれるのが次に紹介するAPIです。 useRecoilCallback Recoilには snapshot というRecoilの現在のStateを読み取れるオブジェクトがあります。 useRecoilCallback はこの snapshot を用いて、必要な時にだけStateを読み込み発火することができます。この必要な時に読み込むことの何が嬉しいのかは、上述の通り不要な再レンダリングを避けられることです。 この再レンダリングを説明するために、姓と名を入力して送信ボタンを押すと、フルネームが表示される簡単なアプリケーションのコードをReactのAPIのみで書きました。 gyazo.com import React , { useCallback , useState } from "react" ; type InputProps = { value: string ; onChange: ( e: React.ChangeEvent < HTMLInputElement >) => void ; } ; const Input = ( props: InputProps ) => { return < input value = { props.value } onChange = { props.onChange } / >; } ; type ButtonProps = { onClick : () => void ; } ; const Button = ( props: ButtonProps ) => { return < button onClick = { props. onClick } > 送信 < /button >; } ; export const Form = () => { const [ lastName , setLastName ] = useState < string >( "" ); const [ firstName , setFirstName ] = useState < string >( "" ); const [ fullName , setFullName ] = useState < string >( "" ); const onChangeFirstName = useCallback ( ( e: React.ChangeEvent < HTMLInputElement >) => { setFirstName ( e. target .value ); } , [] ); const onChangeLastName = useCallback ( ( e: React.ChangeEvent < HTMLInputElement >) => { setLastName ( e. target .value ); } , [] ); const onClick = useCallback (() => { setFullName ( `${firstName} ${lastName}` ); } , [ firstName , lastName ] ); return ( < div > < Input value = { firstName } onChange = { onChangeFirstName } / > < Input value = { lastName } onChange = { onChangeLastName } / > < p > FullName: { fullName } < /p > < Button onClick = { onClick } / > < /div > ); } ; こちらのGIFでわかりますように、Inputの文字を変更するたびにInputそのものとForm、そしてButtonに再レンダリングがかかっています。(白い明滅がレンダリングがかかっているということです) これはInputの変数をButtonに渡すために、その親コンポーネントであるFormがInputの変数も持っているため、 lastName や firstName 更新時にInputだけではなくFormまで再レンダリングがかかるためです。また、Buttonに降ろすコールバックも lastName と firstName に依存しているため、このコールバックを使用するButtonにも再レンダリングがかかります。勿論、いくつかの工夫により多少は抑制されますが、やはりレンダリングは必要なコンポーネントに留め、不要な再レンダリングは可能な限り避けたいものです。 これを解消するのがRecoil、そして useRecoilCallback になります!同じような構成をRecoilで書き直してみたのが以下になります。 gyazo.com import React , { useCallback , useState } from "react" ; import { atomFamily , useRecoilState , useRecoilCallback } from "recoil" ; const nameState = atomFamily < string , string >( { key: "nameState" , default : "" } ); type Props = { param: string ; } ; const Input = ( props: Props ) => { const [ value , setValue ] = useRecoilState ( nameState ( props.param )); const onChange = useCallback (( e: React.ChangeEvent < HTMLInputElement >) => { setValue ( e. target .value ); } , [] ); return < input value = { value } onChange = { onChange } / >; } ; type ButtonProps = { onClick : () => void ; } ; const Button = ( props: ButtonProps ) => { return < button onClick = { props. onClick } > 送信 < /button >; } ; export const RecoilForm = () => { const [ fullName , setFullName ] = useState < string >( "" ); const onClick = useRecoilCallback ( ( { snapshot } ) => async () => { const firstName = await snapshot.getPromise ( nameState ( "firstName" )); const lastName = await snapshot.getPromise ( nameState ( "lastName" )); setFullName ( `${firstName} ${lastName}` ); } , [] ); return ( < div > < Input param = { "firstName" } / > < Input param = { "lastName" } / > < p > FullName: { fullName } < /p > < Button onClick = { onClick } / > < /div > ); } ; いかがでしょうか。明滅が変更のかかっているコンポーネントのみに留まりましたね! Inputには atomFamily で nameState というものを作り、複数のInputであっても1つの RecoilState で対応できるようにしました。 そして、RecoilFormでは自身が使うローカル変数である fullName を useState で作り、Buttonに渡すコールバックは useRecoilCallback で作成しています。 useRecoilCallback は高階関数のような形をとっていまして、冒頭で説明した snapshot オブジェクトは1つ目の引数に受け取ります。この snapshot から他の atom の RecoilState を読み込むことができるのですが、ここで注意してほしいのは snapshot からはPromiseの形でしか読み込めないことです。なぜPromiseかといいますと、非同期 selector を考慮した設計だそうです。そして、Promiseを扱うので2つめの引数にはasyncを付ける必要があります。 コールバック内では snapshot オブジェクトの getPromise という関数に他のAPI同様に RecoilState を渡すとその値を読み込むことができます。これにより、 useCallback のように再レンダリングをかけることなくInputの値を取り扱うことができます! 一点注意してほしいのは、今回は useState の更新を行うために setState を useRecoilCallback 内部に置きましたが、もしRecoilの値を更新したい時は useSetRecoilState を用いてはいけないということです。したがって、 useRecoilCallback は selector 同様に set 関数を用意しています。使い方も selector と一緒で第一引数に更新したい RecoilState 、第2引数に新しい値を指定します。 useRecoilCallback を用いる時に RecoilState を更新したい場面は多いと思いますので、この点だけ気をつけてください。 まとめ 今回はRecoilのAPIを中心にどのような点が嬉しいのか、どのような点に気をつければいいのかを中心に書かせていただきました。 Recoilは、私自身も書いていてとても書き心地がいいですし、便利なのでもっと広まればいいなっと思っています。今回は書ききれませんでしたが、便利なAPIや機能はまだまだたくさんあります。 一方で、今回は伝えきれなかったのですが、 atom などは油断するとすぐに無秩序になりがちなので治安の良い書き方というのも意識する必要がありそうです。このあたりのベストプラクティスはまだまだ溜まっていないので、これからに期待ですね。 また、 Facebook Experimental ということで、まだまだRecoil自体もどうなっていくか分かりません。最初のリリースからAPIも変わったりしていますので、製品にホイホイと投入できるほど安心はできないというのも事実です。この点についても趨勢を見守っていきたいですが、2021年には何らかの進展が見られることを期待して、本タイトルをつけさせていただきました!2021年に活用していきたいです!(願望) さて、来週月曜日はハードウェアグループおおひらさんの「非接触CANセンサーで車両の制御信号を可視化してみた」になります。 自動車の非接触センサーを用いてリアルタイムにちゃんと可視化できるかを検証する弊社らしい記事になっておりますので、是非こちらもご注目ください! <参考文献> Recoil公式サイト  https://recoiljs.org/ recoiljs.org 正確にはRecoilを使用しているコンポーネント群のトップに <RecoilRoot> を置く必要があります。 ↩