TECH PLAY

アプトポッド

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

248

はじめに こんにちは、アプトポッドでハードウェア/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> を置く必要があります。 ↩
アバター
マニュアル等のドキュメント制作を担当している私は、世の中の機械翻訳エンジンの進歩を日々驚きながら観察しています。 実際に業務で使うかは別途判断するとして、「この文をこの機械翻訳エンジンに与えたら、どんな訳文が得られるのだろうか」と、試してみることもあります。 この記事では、そんなお試しの例として、Google Cloud Translation APIの「用語集」の機能を使ってみます。 aptpod Advent Calender 2020 の18日目、テクニカルライターの篠崎が担当します。 (なお、この記事で扱うのは、プログラムから利用する機械翻訳ウェブAPI Google Cloud Translation API です。ウェブブラウザーから利用する機械翻訳サービス Google Translate ではありません。) この記事について Google Cloud Translationの用語集機能とは 試してみる 準備 お試しPythonコード 得られた翻訳結果 おわりに 参考文献 この記事について この記事は、軽く「試してみる」だけの内容です。機械翻訳一般やGoogle Cloud Translation APIの性能や有用性を検証するためのものではありません。 2020年12月に試してみた結果に基づいています。翻訳エンジンは日々更新されると思われます。また、用語集機能もアップデートされるかもしれません。 Google Cloud Translation APIで翻訳を実行するには料金がかかります。また、以下に挙げるコードではGoogle Cloud Storageのバケットに用語集を保存しますが、それに対しても料金が発生します。 日本語から英語への翻訳を例とします。 あくまで1つの例としてご覧ください。 Google Cloud Translationの用語集機能とは Google Cloud Translation APIには、 用語集(Glossary)の機能 が存在します。 対訳形式の用語集をあらかじめ登録しておくことで、指定された訳語を訳文の中で使用してもらうことができます。 対訳用語集の例 Google Cloud Translation APIの公式ドキュメント では、用語集を使用する場合の例として、「商品名」、「あいまいな単語」(意味が全く違うのに同じつづりの語)、「借用語」が挙げられています。 実際に用語集を使用するとどのような翻訳結果が得られるのか、試してみました。 試してみる 以下では、必要な準備を行ったうえで、実際に用語集を使って機械翻訳をしてみます。 準備 手元のPCからGoogle Cloud Translation APIを使用するには、一般に、以下の準備が必要です(Google Cloud Platformのアカウントは取得済みとします)。 これらについては、 公式ドキュメント に詳しく説明されています。 Google Cloudコンソール(以下「GCPコンソール」)で新しいプロジェクトを作る。 GCPコンソールで、プロジェクトのCloud Translation API機能を有効にする。 GCPコンソールで、APIを使用するためのアカウントである「サービスアカウント」を作り、適切な権限を設定する。 GCPコンソールで、サービスアカウントのキー(JSONファイル)を作成し、手元のPCにダウンロードする。 サービスアカウントのキーのファイルを、環境変数 GOOGLE_APPLICATION_CREDENTIALS で指定する。 クライアントライブラリー(Python用ならば google-cloud-translate )をインストールする。 Cloud SDK をインストールする。 また、この記事で紹介する「お試しPythonコード」を実行するには、以下の準備も必要です。 使用するサービスアカウントには「Cloud Translation API 編集者」( roles/cloudtranslate.editor )と、「Storage Object管理者」( roles/storage.objectAdmin )のロールを持たせる。 サービスアカウントに設定されたロール 用語集のアップロード先としてGoogle Cloud Storage(GCS)バケットを作っておく。 GCSバケットを作っておく お試しPythonコード 上記の準備をしたうえで、実際に翻訳を試してみます。 このコードには、用語集データのアップロードから後片付けまでの手順が入っています(エラー処理は省略しています)。また、用語集とテスト用の原文もコード内に入っています。 from google.cloud import storage from google.cloud import translate_v3 as translate # 1. 各種設定 # 日英対訳用語集(公式ドキュメントにある「単一言語ペアの用語集」の形式で作成) # ここでは、簡単のためにCSV形式の1つの文字列として用意しました。 GLOSSARY = ''' \ "メールアドレス","email address" "健司","Takeshi" "国立","Kunitachi" "試験","test" "選ぶ","select" ''' # テスト翻訳対象の原文 SOURCE_TEXTS = [ 'メールアドレスを入力してください。' , 'メールアドレス設定画面' , '健司さんは国立の病院に行きます。' , '学校で試験があります。' , '3つの選択肢から選ぶことができます。' , '彼が選ぶものはいつも同じです。' , '3つの選択肢から選ばなければならなないため、Aを選んだ。' , '3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。' ] # GCPのプロジェクト情報(プロジェクトは事前に作成済みとする) PROJECT_ID = '<プロジェクトID>' # ロケーションはいまのところus-central1のみ対応とのこと LOCATION = 'us-central1' # 用語集CSVのアップロード先バケット名(バケットは事前に作成済みとする) BUCKET_NAME = '<GCSバケット名>' # バケットに用語集を保存するときのオブジェクト名 GLOSSARY_OBJECT_NAME = 'test_ja_en_glossary.csv' # 上記により、バケット内のCSVファイルオブジェクトへのURIが決まる。 GLOSSARY_URI = 'gs://' + BUCKET_NAME + '/' + GLOSSARY_OBJECT_NAME # 新しく作成する用語集のID GLOSSARY_ID = 'test_ja_en_glossary' # 2. 用語集をアップロードして、GSCバケット内にCSVファイルとして保存する # GCSクライアントを作成する storage_client = storage.Client() # 用語集のアップロード先バケットと、新たに作るオブジェクト名を指定 bucket = storage_client.bucket(BUCKET_NAME) glossary_blob = bucket.blob(GLOSSARY_OBJECT_NAME) # 用語集CSVを文字列として送信し、GCS上のオブジェクトにする glossary_blob.upload_from_string(GLOSSARY) # 3. GCSにアップロードされたCSVをもとにして、 # Google Cloud Translate用の「用語集リソース」を作る # Google Cloud Translate v3 APIのクライアントを作成 trans_client = translate.TranslationServiceClient() # 用語集リソースのパスを決める glossary_name = trans_client.glossary_path( PROJECT_ID, LOCATION, GLOSSARY_ID) # ソース言語とターゲット言語のペアを指定する language_pair = translate.types.Glossary.LanguageCodePair( source_language_code= 'ja' , target_language_code= 'en' ) # 先ほどGCSにアップロードした用語集CSVを指定する gcs_source = translate.types.GcsSource(input_uri=GLOSSARY_URI) input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) # 上記で準備した情報を組み合わせて、用語集の情報を準備する glossary = translate.types.Glossary( name=glossary_name, language_pair=language_pair, input_config=input_config) project_location_path = trans_client.location_path(PROJECT_ID, LOCATION) # 用語集リソースを作成する glossary_create_operation = trans_client.create_glossary( parent=project_location_path, glossary=glossary) # 用語集リソースの作成完了を待つ glossary_create_result = glossary_create_operation.result(timeout= 90 ) # 4. 翻訳してみる # 使用する用語集リソースを指定する glossary_config = translate.types.TranslateTextGlossaryConfig( glossary=glossary_name) # 翻訳対象、言語名、用語集リソース等を指定して翻訳する response = trans_client.translate_text( SOURCE_TEXTS, 'en' , project_location_path, source_language_code= 'ja' , glossary_config=glossary_config) # 結果を出力する for (source, trans, trans_with_glossary) in zip ( SOURCE_TEXTS, response.translations, response.glossary_translations): print ( '原文: ' + source) print ( '用語集なし: ' + trans.translated_text) print ( '用語集あり: ' + trans_with_glossary.translated_text) print () # 5. 後片付け # 用語集リソースを削除する glossary_delete_operation = trans_client.delete_glossary(name=glossary_name) # 用語集リソースの削除完了を待つ glossary_delete_result = glossary_delete_operation.result(timeout= 90 ) # GCS内のCSVファイルを削除する glossary_blob.delete() 得られた翻訳結果 以下のような結果が得られました。 原文: メールアドレスを入力してください。 用語集なし: Please enter your e-mail address. 用語集あり: Please enter your email address. ↑ 用語集で指定したとおり、ハイフンなしの"email address"となりました。 原文: メールアドレス設定画面 用語集なし: Email address setting screen 用語集あり: email address setting screen ↑ 文書内でタイトルとして使うことを想定した名詞句です。 先頭は大文字がよいのですが、用語集に入れたハイフンなしの"email address"に置換され、 小文字になってしまいました。 なお、「メールアドレス」という1つの原語に対して、"email address"と"Email address"のような2種類の訳語を登録することはできません("Can't create glossary. There are conflicting entries."というエラーになります)。 原文: 健司さんは国立の病院に行きます。 用語集なし: Kenji goes to a national hospital. 用語集あり: Takeshi goes to a Kunitachi hospital. ↑ 「たけし」さんと「くにたち」の話であることがあらかじめ分かっているときは、このような置換は効果を発揮しそうです。 ただし、"hospital in Kunitachi"のようにはならず、単純な置き換えになっているようです。 原文: 学校で試験があります。 用語集なし: I have an exam at school. 用語集あり: I have an test at school. ↑ ここでは、単純に"exam"が"test"に置換された結果、"a test"ではなく"an test"となってしまいました。 原文: 3つの選択肢から選ぶことができます。 用語集なし: You can choose from three options. 用語集あり: You can select from three options. ↑ 動詞の例です。「選ぶ」が"select"に置換されました。(このような一般的な動詞は用語集に入れないほうが良いと思いますが、試しにやってみました。) 原文: 彼が選ぶものはいつも同じです。 用語集なし: The one he chooses is always the same. 用語集あり: The one he select is always the same. ↑ 単純に"select"に置換された結果、"he selects"ではなく、"he select"となってしまいました。 原文: 3つの選択肢から選ばなければならなないため、Aを選んだ。 用語集なし: I chose A because I had to choose from three options. 用語集あり: I chose A because I had to choose from three options. ↑ 「選ぶ」を用語集に入れましたが、活用形である「選ば」、「選ん」は用語集に入れていないので置換されないようです。 原文: 3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。 用語集なし: I was instructed to choose from three options, so I decided to choose A. 用語集あり: I was instructed to select from three options, so I decided to select A. ↑ 「選ぶ」に完全に一致する文字列(連体形)の場合は置換されました。 おわりに 用語集の効果がどのようなものか、軽く試してみることができました。 用語集の項目の立て方を工夫して、他のケースも試してみたいですが、 上記のようなコードを用意しておけば、用語集のアップロードから、翻訳結果取得、用語集削除まで、いっぺんにできるので便利です。 なお、Cloud Translation APIには、対訳コーパスを使ってカスタム翻訳モデルをトレーニングし、 それを使って翻訳を行う AutoML Translation という機能もあります。 こちらも、いつか試してみたいところです。 参考文献 Google Cloud Translation公式ドキュメント 特に、 用語集の作成と使用(高度な機能) Python Client for Google Cloud Translation公式ドキュメント Improving Machine Translation with the Google Translation API Advanced (Translation APIの使用法がよく分かるチュートリアルです。) GoogleのCloud Translation API v3を触ってみる (つまづきがちな、サービスアカウントのロール(役割)の設定についても解説されています。)
アバター
Advent Calendar 2020 17 日目を担当します、 SRE チームの川又です。 SRE チームでは自社開発プロダクトである intdash のサーバサイドインフラにおいて、主に以下の職務を行なっています。 設計・構築・運用 可用性・パフォーマンスの向上のための改善 セキュリティの維持 今回は、エッジロケーションにおけるセキュリティを維持したパフォーマンス向上のために、サイト間VPN 接続を用いて弊社 intdash のトラフィックフローを効率化した話をご紹介します。 はじめに 課題 解決策 エッジ側vRouter の構成 結果 おわりに はじめに 弊社では年に1 回程度、サーキットを1日貸し切って、自社プロダクトを用いた車両走行・データ計測を行う イベント を行います。 本イベントは以下を目的としています。 自社プロダクトに触れる (ドッグフーディング) デモ用データ取得 自社新規プロダクトの検証 通算何回か行なっている本イベントですが、 現地サーキットの通信環境の悪さを原因とする課題 を抱えていました。 この記事では、ネットワーク技術でその課題の解決を試みた話をご紹介します。 ※ この実験を行ったのは昨年度です。今年度は、主に感染症対策のため本イベントは実施しておりません。 課題 例年先述のイベントを行なっているサーキットはモバイル網の通信速度が遅く、さらには、サーキット内にモバイル網がエリア外の敷地もありました。 したがって、インターネット通信を行える帯域が小さく 満足にクラウド側とデータ通信ができない課題 がありました。 弊社の intdash Server は多様なデータパイプラインを備えており、車両等の計測対象からアップストリームされるデータをリアルタイムに取得することも可能です。また、同じく弊社のWebベースダッシュボードアプリケーション Visual M2M Data Visualizer を用いることでデータの多彩な可視化が行えます。 イベント当日は、サーキットに居る弊社社員のほぼ全員が Visual M2M Data Visualizer で車両計測データの確認を行うことが予測されました。ですが、先述の通りデータ通信帯域が限られているため 何らかの対策 を行う必要がありました。 本ネットワーク環境の概要図を以下に示します。 ネットワーク環境概要図 何らかの対策を実施しなければ、下の図の様にクライアントにあたる各人の Visual M2M Data Visualizer がそれぞれインターネットを通してクラウド上の intdash Server と通信を行い、帯域を逼迫させてしまいます。 問題点のイメージ図 解決策 この課題を解決するために、エッジロケーションにあたるサーキット側にも intdash Server を設置し(以下、これを エッジサーバ と呼びます)、インターネット上ではサーバ間のみで通信を行う様に環境を構築しました。この場合、 クライアントはエッジサーバのみと通信を行う ため、インターネット側の通信量は削減されます。 問題解決構成のイメージを以下の図に示します。 解決構成のイメージ図 弊社 intdash Server はリアルタイムストリームのPub/Sub メッセージングに NATS が利用可能です。この NATS を用いてクラウド側の intdash Server とエッジ側の intdash Server で連携を行い、クライアントへのリアルタイムストリームを提供可能にしました。概念としては、CDN( Content Delivery Network) におけるキャッシュサーバに近いです。 サーバ間の通信に関しては、以下の理由からサイト間VPN接続としました。 セキュアに通信を行う サーバ側に追加設定を必要としない サイト間VPN接続には以下を用いました。 クラウド側: AWS 仮想プライベートゲートウェイ (VGW) エッジ側: vRouter VyOS 概要図を以下に示します。 解決構成概要図 エッジ側vRouter の構成 エッジ側のvRouter ではクライアントがサーバの接続先を意識しないで良い様に以下の機能を提供させました。 DHCP サーバ ( Dynamic Host Configuration Protocol ) DNS サーバ ( Domain Name System ) NAT ( Network Address Translation ) ルーティング(静的) VPN ゲートウェイ エッジ側ネットワークの機能およびリソースをvRouter の管理下に置くことで、クライアント側が接続先サーバのドメイン名を意識せずアクセス可能な構成としました。 結果 結果としては、理論通りの挙動をさせることには成功しました。ただし、他の検証施策もあった兼ね合いで、全てのクライアント側で同時にエッジサーバを利用して貰うことが叶わず、効果測定としては満足に行うことが出来ませんでした。次回、同じ構成を試す機会があれば、インターネット通信量の削減効果を測定してみたいと思います。 おわりに 今回は、インターネット通信環境が十分でないエッジロケーションでの弊社 intdash の利用に際し、エッジの intdash Server とサイト間VPN接続を用いてトラフィックフローの効率化を行なった話をご紹介しました。 インフラのエンジニアリングとしては、利用する製品の挙動を理解した上で、状況・要件に応じた設計・構築、可用性の選択が重要だと考えます。課題に対して柔軟に対応できる様に努めていきたいです。 最後までご覧くださいまして、ありがとうございました。
アバター
Advent Calendar 2020 16日目担当の ソリューションアーキテクト の岩坪です。 aptpodのソリューションアーキテクトという役割は、自社プロダクトであるIoTプラットフォーム intdash をベースに、お客様の課題解決やDX(Digital Transformation)実現に向けたソリューション提案、システム全体のアーキテクチャ設計を行い、納品までのプロジェクトリードを行っていきます。 今回は弊社の intdash と、近年ロボット開発だけでなく自動運転車両開発など様々な産業で採用されている ROS(Robot Operating System ) との連携についてご紹介します。 はじめに intdashのシステム構成 『intdash x ROS』で実現できること 『intdash x ROS』のユースケース例 おわりに はじめに ソリューションアーキテクトである我々の仕事は お客様の課題に対して、いかに自社プロダクトintdashを効果的かつ効率的に活用した解決手段をご提案できるか に尽きると考えています。 お客様のニーズ、課題、悩みは様々ですが、それぞれに対して全てをただ求められるままに対応していたのでは、費用と期間がいくらあっても足りませんし、出来上がったものもお客様にとって本当に望んでいたものになるとは限りません。 そこで提案に当たっては次のようなことを考えていく必要があります。 お客様に対してintdashが最大限に価値提供できるのはどの領域であるか 要件に対してintdashの特長が当てはまる領域はどこであるか 短期間でお客様の求めるシステムを提供する為に自社とお客様側システム/機器との境界をどこに持つべきか その産業のプロフェッショナルであるお客様に実現頂くべき領域はどこまでの範囲であるか 自社以外の技術/サービスで優れている領域があれば、その技術/サービスといかに連携を行うべきか その道のエキスパートの会社があるのであればその会社の技術に頼るべき これらを考える上で重要なのは、自社の技術/自社のプロダクトの特長を意識しつつ、 お客様 ↔︎ aptpod、世の中の技術 ↔︎ intdashの技術 との適切な境界を考え、また、その境界をいかにスムーズに繋ぐことができるか?ということです。 境界をスムーズに繋ぐ為には、自社プロダクト以外との連携を行いやすいように、プロダクトとして 世の中の標準的な技術 への対応を進めていく必要があります。それにより、新規にお客様へシステム提供するに当たってのコスト、リードタイムは大幅に削減できます。弊社のintdashもこういった進化を徐々に行っています。 前置きが長くなってしまいましたが、今回は自社プロダクト intdash と、 近年、ロボット開発だけでなく自動運転車両開発などでも採用されている ROS(Robot Operating System ) との連携について自社の取り組みをご紹介したいと思います。 intdashのシステム構成 弊社のプロダクトであるIoTプラットフォーム intdash の特長としては以下があげられます。 高いリアルタイム性でデータ伝送できること サーバを介しても低遅延でデータを届けることができる 伝送時にパケロスしたデータの完全回収ができること 不安定な通信環境化においても全てのデータを回収できる 多種多様なデータに対して時刻を同期してデータ管理が行えること エッジサイドでデータに対してタイムスタンプを付与することで種々のデータを同一の時間軸で扱うことができる サーバに保存されたデータを利活用できること リアルタイムのデータ活用だけでなく、保存された過去データも容易に分析、学習などに利活用できる intdashのシステムでは、データが実際に流れている自動車、ロボット、建機などの ターゲットデバイスに intdash Server と接続するためのゲートウェイデバイスを配置して、ゲートウェイデバイス上に自社プロダクトであるエッジ向けのミドルウェア intdash Edge を搭載することで、デバイスに流れるデータを intdash Serverまで伝送することが可能になります。 また、intdash Serverに伝送されたデータを、Webベースのダッシュボードアプリケーションである Visual M2M Data Visualizer を使用することで、多彩に可視化することができます。 intdash Edgeが扱うデータはお客様側のシステム、デバイスにより様々であり、自動車業界におけるCANなど、intdash Edgeが標準で対応しているデータ形式もあれば、お客様ごとにカスタム開発することで対応するケースもあります。 intdash Edgeでは、デバイスとデータをやり取りする機能部位を、 Device Connector と呼んでいます。上記の カスタム開発 の対応を少しでも減らして、お客様に取って 短期・低コスト でシステムに導入ができるものを提供する為には、世の中で求められるデータ形式に対してプロダクトとして標準対応を進めておく必要があります。 そして近年、ROSを取り扱うシステムが多く見られるようになってきたこともあり、 弊社のプロダクトとしてintdash Edgeにおいて、 Device ConnectorがROSに標準対応 しました。 ROS nodeとしての ROS用のDevice Connector ができたことで、お客様のROSを取り扱うシステムに、intdash Edge搭載のゲートウェイデバイスを接続し、 容易に、すぐに、低コスト でintdash ServerへのROSメッセージのリアルタイム伝送が可能となりました。 また、複数台のデバイス上にROS用のDevice Connecterを使用したintdash Edgeを搭載することで、 遠隔のデバイス間におけるintdash Serverを介したROSメッセージのやり取りも可能 になります。 これは、 遠隔制御、協調制御、遠隔監視や、遠隔におけるROSのデータ利用 などのユースケースが考えられ、ROSを取り扱うシステムの用途を大きく広げることができます。 『intdash x ROS』で実現できること これまでにご紹介の通り、 intdash EdgeのROS用のDevice Connector を使用することでROSを取り扱うシステムの様々な連携の可能性を広げることができます。 一般的にROSを取り扱うシステムのアプリケーション開発のワークフローは、下記のような流れになると思います。 特徴としては、開発しているアプリケーションを シミュレータ上で検証 を行い、ソフトウェアの修正を進めます。さらに、実機を用いて フィジカル環境における検証 を行い、またここでのフィードバックをソフトウェアに反映します。これらの作業を繰り返すことで、開発が進み、アプリケーションが成熟していきます。 このワークフローの中には下記のような要素が必要となると考えられます。 要素として、 「可視化」「シミュレーション」「テスト」「学習」「エッジ管理」 など多岐に渡りますが、これらに共通することは、エッジサイドのデバイス性能には限界があることから、 様々なクラウドサービス等と連携してこれらの要素を実現する必要がある ことです。また、クラウド上のシミュレーション環境と連携して実機を開発・検証することで、フィジカル(実機)とバーチャル(シミュレーション)のデータを統合管理できるようになり、 フィジカルとバーチャルの比較や相互フィードバックが容易になる 利点もあります。 そして、これらクラウドサービスとの連携には、 システム間のスムーズなデータ伝送なしには実現が難しい という課題があげられます。 『intdash x ROS』ではまさにこの「 スムーズなデータ伝送 」の手段を提供できます。 『intdash x ROS』で提供できる価値は具体的には下記の内容があげられます。 遠隔のROS空間を繋ぐ 同一のROS空間同様にintdash Serverを介してROSメッセージのやり取りを実現することが可能 リアルタイムデータ伝送 リアルタイムの遠隔制御、遠隔監視などの用途に耐えうる低遅延によるデータ伝送を実現することが可能 ROSのデータ利活用 intdash Serverに保存したROSメッセージを可視化・分析・学習・シミュレーションなどに活用することが可能 『intdash x ROS』のユースケース例 『intdash x ROS』の具体的なシステム構成の一例として、こちらの記事 『AWS re:Invent 2019 で AWS RoboMakerとintdash によるTurtlebot3の遠隔制御の展示を行いました!』 でも紹介している「intdash x AWS RoboMaker」による Turtlebot3の遠隔制御、デジタルツイン環境のシステム構成を紹介します。 実際にTurtlebot3とAWS RoboMakerのシミュレーションが動いている動画はこちらです。 後ろのモニタに表示されているのが、Visual M2M Data Visualizerと、AWS RoboMaker上のGazeboの画面表示です。 Visualize remote control TurtleBot3 with AWS RoboMaker+intdash RoboMakerのシミュレーション環境(Gazebo)と、実環境のController、Turtlebot3を各デバイスに搭載されたintdash Edgeがintdash Serverを介して接続することで、 実環境のTurtlebot3の遠隔制御、データ可視化だけでなく、シミュレーション環境のロボットに対する遠隔制御、データ可視化 までを行うことができます。 一例ではありますが、『intdash x ROS』が実環境とシミュレーション環境である他システムを スムーズなデータ伝送により連携 できることがお分かり頂けたのではないでしょうか。 おわりに 今回は弊社のIoTプラットフォーム intdash と ROS の連携についてご紹介しました。 繰り返しになりますが、『intdash x ROS』で解決することは下記です。 遠隔のROS空間を繋ぐことができる リアルタイムデータ伝送することができる ROSのデータ利活用ができる ROSを取り扱うシステムを開発される中で課題をお持ちで、『intdash x ROS』の世界観に興味を 持たれた方はぜひaptpodへ コンタクト 下さい。 弊社では、intdashを AWSマーケットプレイス でも順次提供していきますので、興味ある方には 容易にお試しいただける環境をご用意できます。 『アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始』 また、ロボティクスでバリバリROSを扱っています!や、ROSって興味あったんだよね。というような方やintdashの技術、価値に興味を持って頂けたエンジニアの方は是非、こちらの 弊社採用ページ もご覧になって頂けましたらと。 aptpodと一緒にリアルタイムデータ伝送が実現する明るい未来をめざしましょう。
アバター
自己紹介 はじめまして。株式会社アプトポッド ビジネスデベロップメントグループの小宮です。 aptpod Advent Calendar 2020 の15日目を担当します。 ビジネスデベロップメントグループとは、 個別の商談からイベント出展まで、対外的な情報発信の取りまとめ 製品の価格決めから売り方検討 納品物のデリバリーまでの支援 その他ビジネスサイドのプロセスの穴埋めと補強 などをしているチームです。要するに営業サイドの何でも屋さんです。 実は私、そろそろアプトポッドに入社して、ちょうど一年が経つところです。まだ「例年のアプトポッド」がどのようなものかわからないのですが、それでも今年はお客様との接点が例年より少なかった一年でした。 そんな中、今年は9本のプレスリリースを出しまして、その半分が製品のアップデートに関するものです。総括すると、2020年は 今後増えるであろう様々なスケールの案件に最適に対応するための、準備を色々と仕込んだ年 だったと言えます。 コロナ禍の折、なかなかちゃんとご説明する機会もないので、今年のプレスリリースのうち製品に関わるものが、弊社製品のどの部分をどうアップデートしたものだったのか、という観点でご説明させてください。 自己紹介 はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です intdashの構成 プレスリリース振り返り プレスリリース1:intdash 2.0リリース プレスリリース2:テラデータ様との協業 プレスリリース3:SDKの提供開始 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 おわりに はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です アプトポッドは、産業向けに強みを持つIoTプラットフォーム”intdash”をご提供している会社です。 IoTに必要な、データ伝送やデータ管理などのコア機能は、上図上側のサーバーサイドで動く ミドルウェア に含まれています。さらに、上図下側の、「 エッジ 」と呼ばれる各種産業機器に取り付けるハードウェアや、エンドユーザーが直接操作する、時系列データの可視化ツールや各種ユーティリティ類も一気通貫してご提供しています。 このように、サーバーサイド、フロントエンド、ハードウェアの機械系から組み込みソフト、さらにはスマホアプリまで、いわゆるフルスタックに対応できる点が弊社の強みであり、このテックブログで取り扱うテーマがめちゃくちゃ幅広い所以でもあります。 intdashの構成 改めて弊社の製品群を、データ活用の流れに沿って分解したのが上図です。 一番左、つまり一番エッジに近いところから行くと、エッジのセンサーやアクチュエータと通信する ペリフェラル があり、エッジとクラウドの間のゲートウェイの役割を果たす ターミナルシステム があります。これらが弊社で取り扱う ハードウェア になります。 ターミナルシステム上で動く組み込みソフトも、一連の仕組み”intdash”に対応したものである必要があります。この組み込みソフト(エッジサイドのミドルウェア)を intdash Edge 、そしてそのうち特にintdashの特徴である、データの完全回収やリアルタイム伝送など、データ伝送機能を担うソフトウェアを intdash SDK for Edge Device (intdash Edge Agent) と呼びます。 続いて真ん中、サーバーサイドで動くミドルウェアの部分を、 intdash Server と呼びます。またこのミドルウェアには外部連携するためのAPIをご用意しており、 intdash SDK for Python を用いてカスタムの後処理やデータ分析などを手軽に行うことができます。 最後、一番右側の、データ活用の手段として、データ可視化ツールである Visual M2M Data Visualizer をご用意しております。こちらはブラウザ上で手軽に時系列データを可視化する、デザイン性に優れたツールです。 Visual M2M Data Visualizer DEMO 今年の夏に、弊社のコーポレートサイトのデザインが一新されたのをご存知でしょうか。この「PRODUCTS」以下には、弊社製品がカテゴリごとにならんでいます。各製品の詳細に興味がある方は、ぜひ コーポレートサイト も覗いていただけると幸いです。 プレスリリース振り返り プレスリリース1:intdash 2.0リリース 今年の製品関連のプレスリリース第1弾は、7月28日の「 DX プラットフォームintdash 2.0 をリリース 」でした。これは、intdash Serverのメジャーアップデートであり、本運用などにも使えるスケーラブルな環境を提供できるようになったことを指します。まず真ん中のコアの部分を、様々な規模の案件に対応できるようにした、ということです。 プレスリリース2:テラデータ様との協業 続いて第2弾は、8月4日の「 アプトポッドとテラデータ、自動車開発向けDXソリューション提供で協業 」です。これは、intdashで取得したデータの活用の手段を広げるため、日本テラデータ様のもつ次世代分析基盤「 Teradata Vantage 」と連携できるようになったことを指します。Visual M2M Data Visualizerが扱えるのは時系列データの分析のみであり、統計処理をやろうと思うと何かしらのカスタムコードをPythonで書く必要があったのですが、Vantageとの連携により、データ取得から分析まで、エンドユーザー側はコーディングすることなく、本来やるべき作業に集中できるようになりました。 プレスリリース3:SDKの提供開始 第3弾は、9月30日の「 高速IoTプラットフォーム「intdash」 アプリケーション開発キット「intdash SDK」の提供を開始 」です。上のintdashの構成で、しれっと2つのSDK: intdash SDK for Edge Device と intdash SDK for Python 「があります」とご紹介しましたが、これらは今年新規にリリースされたものです。 intdash SDK for Pythonについては以下のウェブサイトをご参照ください。 https://pypi.org/project/intdash/ また以下の情報もご参照ください。 ドキュメント サンプルコード(GitHub) intdash SDK for Edge Deviceについては以下のデベロッパーガイドをご参照ください。こちらは誰でも入手可能というわけではなく、ご利用される方に都度アクセス情報を払い出す形でご提供しています。 intdash Edge Agentデベロッパーガイド これまでのintdashの標準構成では、弊社の指定するターミナルシステム(エッジとクラウドのゲートウェイとなるコンピュータのこと)と、その下にぶら下がる弊社製のペリフェラルでしかシステムを構築できませんでした。しかしintdash SDK for Edge Deviceにより、ターミナルシステムとして扱えるコンピュータの種類が広がり *1 、システム構築の自由度が一つ上がりました。またこの開発環境を公開したことにより、弊社のエンジニアでなくとも、エッジサイドの組み込みソフトや、データ分析用のツールなどを作ることができるようになりました。 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 続いて第4弾は、同じく9月30日の「 アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始 」です。これにより、お客様の持つAWS環境内に簡単にintdashを構築することが可能になりました *2 。先にご説明した各種SDKと組み合わせると、弊社の直接のご支援なくとも、弊社が構築するのと同機能のintdash環境をお客様の手で構築することができるようになります。 おわりに 以上、各プレスリリースの位置づけをざっと説明してきました。さらにざっくりまとめると、 データ活用の流れの上流から下流まで、弊社自身のマンパワーが制約にならないように、要所要所を強化、または汎用化してきた 、ということになります。 intdashの正式リリースは2018年5月 で、まだリリースから2年半の比較的若い製品です。実はこの後も、年始にかけていくつか製品関連のプレスリリースを予定しております。本投稿が「このプレスリリースはどの部分をどう強化することを意図しているのか」の理解につながれば幸いです。 *1 : 現時点で対応しているプラットフォームは、AMD64 アーキテクチャー上の Linux、Raspberry Pi 上の Raspbian、NVIDIA Jetson 上の NVIDIA L4Tのみになります。 *2 : 2020/12時点ではAWS Marketplaceは一般公開ではなく、Private Offerのみ
アバター