TECH PLAY

アプトポッド

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

248

先進技術調査グループのリサーチエンジニアの酒井 ( @ neko_suki )です。 先進技術調査グループでは、新しいトランスポートプロトコルのQUICの製品への適用を検討しています。今回の記事では、自社が主に扱う高頻度なデータの伝送における課題のひとつをQUICを適用したらどうなるかを評価してみました。 目次は以下の通りです。 QUICとは Head of Line (HoL) Blocking とは 実験内容 実験結果 まとめ 参考文献 QUICとは QUICとは、Googleが提案・実装をし、現在IETFで標準化が行われているプロトコルです。(区別のため、前者はgQUIC、後者はiQUICと呼ばれています)。2020年の中頃にはRFCが発行される予定です。 QUICはUDPベースの信頼性のある接続を提供するトランスポートプロトコルです。QUICではコネクション上に仮想的なストリームを生成して通信を行います。 今回は高頻度なデータの伝送における課題のひとつであるHoL Blocking という課題について、QUICを適用して課題の解決が可能かどうかを確認します。 Head of Line (HoL) Blocking とは TCPは順序を保証します。そのため、以下の図のようにパケットロスが発生すると、サーバアプリケーションが後続のデータを受信するタイミングに遅延が発生するという課題がありました。 TCPにおけるHoL QUICでは異なるストリーム間の順序保証を行わずにデータを送信することができます。(ただし、同一ストリーム内では順序保証がされます)。下の図のようにUDPのパケットがロスしても、サーバアプリケーションは別のストリームで送信されているデータをHoL Blockingによる遅延の影響なく受信することが期待できます。 実験内容 実験は、高頻度なデータの塊をクライアントからサーバに伝送するケースを想定します。 送信するデータは仮に1unit (データの単位)を8byteとします。これを1000 unit/secで送信します。そのために1msec毎に1unitのデータをクライアント側で生成します。 クライアント-サーバ間の送信遅延を評価するために、データがクライアントで生成された時刻とサーバ側で受信した時刻の差分を遅延時間として定義します。 intdashでは、IPやTCPヘッダのオーバーヘッドを低減するために、flushという一定期間のユニットをバッファリングして送信する仕組みを導入しています 1 。 flush 構造 なので、10個の連続したデータを一つの塊として送信します。以降、このバッファリングしたデータ単位を flush と呼びます。 以下の図のように、10個の連続したデータを1個のflushとして、一つのQUICパケットおよびUDPパケットに格納します。 一つの塊に含まれるデータの到達遅延は、生成時刻が古いデータから順に、片道遅延+9 msec+α (αはタイミングに依存), 片道遅延 + 8msec + α, ... 片道遅延 + 0msec + αで到達することが予想されます。 今回の実験ではHoL Blockingの影響が発生しないことを確認するために、1flush毎に一つのQUICストリームを作成してデータを送信します。 これによって、特定のUDPパケットがロスしても他のパケットの受信には影響が出ないことが期待されます。 評価はネットワークのエミュレーション環境で行います。HoL Blockingの影響を受けずにデータを送信できることを確認するためにパケットロス率は2%としています。また、モバイル環境を想定して往復遅延50msec(片道25msec) を設定しました。 参考までに、QUICの実装は quic-go を用いています。 実験結果 以下の図では、送信したデータそれぞれに対して時系列に遅延時間として「ユニットの受信時刻 - ユニット生成時刻」を時系列にプロットしています。縦軸が遅延時間 (msec) 横軸は、時系列に10秒間送信した10,000個のデータを表しています。 片道の遅延が25msecかつ1flushで10msec分のデータを溜め込むため、パケットロスが発生していない場合は、25msec~35msecの範囲におおむねデータが収まります。図を見ると多くの場合にそうなっていることが確認できます。また、パケットロスが発生しているときのデータが、その範囲から飛び出ていることが見て取れます。 定量的な評価のため、パーセンタイルを確認してみます。ここではベースラインとしてロス率が0%で実験をした時の値も載せています。 パーセンタイル ロス率2%の時の遅延 (msec) ロス率0%の時の遅延 (msec) 2%の時の遅延 - 0% の時の遅延 (msec) 98% 85.81 35.83 49.98 97% 49.50 35.66 13.84 95% 43.10 35.53 7.57 90% 38.31 35.10 3.21 50% 32.63 31.11 1.52 98パーセンタイルの時には、パケットロスがない時と比較して、49.98 msecの遅延が発生しています。一方で、97パーセンタイルになると13.84 msecまで縮まります。95, 90, ... 50と値を小さくしていくと、パケットロスがない時との差分はほぼ無視できるレベルまで縮まることが確認できます。 さらに、生データを詳細に分析してみます。 1つのUDPパケットが、パケットロスせずに1flush分のデータを送った場合の遅延時間「ユニットの受信時刻 - ユニット生成時刻」は以下のようになります。 flush内に含まれるデータのインデックス ユニットの受信時刻 - ユニット生成時刻 1 35.81 2 34.87 3 33.79 4 32.79 5 31.79 6 30.86 7 29.74 8 28.76 9 27.76 10 26.95 10msec分のデータがバッファされるので、最もインデックスが古いデータが片道遅延 25msec + 11msec、最も新しいデータがほぼ片道遅延25msec+2msec 程度になっていることがわかります。 データを詳細に分析した結果、パケットロスしたと思われる1flush分のデータは遅延時間が100msec近い値になることを確認しました。遅延が増加した状況は1flush=10個のデータ分続きます。そして、その次のflushのデータは正常な値に回復します。 相対的なデータのインデックス ユニットの受信時刻 - ユニット生成時刻 前のflushの最後のデータ 27.27 1 103.74 2 102.76 3 101.79 4 100.68 5 99.77 6 98.72 7 97.74 8 96.58 9 95.73 10 94.75 次のflushの最初のデータ 36.25 遅延が80msec 以上だったデータをこのパターンに含まれているとすると、10000個あるデータの中で、210個がこのパターンに該当しました。これはおおむね全体の2%でありパケットロスを設定した値と同じになります。 なので 期待通りにHoL blockingの影響を回避してデータを受信できていることが確認できた と言えそうです。 まとめ 本記事では、自社が扱う高頻度なデータの伝送における課題のひとつであるHoL Blocking という課題について、QUICを適用したらどうなるかを評価してみました。HoL Blocking の回避の観点だとQUICの適用は有効であると言えそうです。 先進技術調査グループでは今後もQUICの検討を進めていきます。 QUICなどの最先端の通信・ネットワークプロトコル技術を用いた研究開発・製品開発に興味がある方は、ぜひ以下の採用ページへのアクセスをお願いします! 採用情報: https://www.aptpod.co.jp/recruit/ 参考文献 White Paper: iSCP (intdash Stream Control Protocol) https://www.aptpod.co.jp/basetech/05.iscp.pdf ↩
アバター
あけましておめでとうございます 。 2020年最初のTech Blogはデザイン室の上野が送らせていただきます。 aptpod Advent Calendar 2019 はご覧になっていただけましたでしょうか? まだの方はぜひご一読ください。 今回ですがこのAdvent Calendarで作成した OGP画像 について書かせていただきます。 OGP画像とは 今回作るに至ったきっかけ ワークフロー 素材作成 テキスト 背景画像 調整 / レビュー 書き出し / 設定 掲載 Advent Calendarを終えて [後日談] やらかしてました OGP画像とは OGP(Open Graph Protocol)とは、Webページの内容を伝えるための情報のことで、OGP画像とはその中でも投稿した記事をSNSなどで展開する際に表示されるサムネイルのような画像を指します。 これですね。 記事の具体的なイメージを伝えることで読み手に何が書いてあるかを想像させ、クリックしてもらう動線になるためとても大切なものとなります。 今回作るに至ったきっかけ 弊社ではここ数年で社内のエンジニアも増えてきまして、2018年からAdvent Calendarに取り組むようになりました。 2018年のAdvent CalendarはQiitaに載せるくらいの規模感だったのですが、2019年からTech BlogやTwitterアカウントを立ち上げ、より積極的に活動を知ってもらえるよう動き始めました。 blogやTwitterでの展開は初の試みといった部分もあり投稿初期の記事ではOGP画像を設定しておらず、SNS上で表示を確認してみると記事内で最初に使われている画像やテキストが画像化され自動的にOGP画像に設定されてしまいました。 「ナンカモッタイナイナー」 エンジニアがとてもいい記事を書いているのに、ぱっと見たときに記事の内容をイメージできるようなものにすればもっとアクセス数伸びるのでは?と思いSlackで改善を提案してみました。 結果たくさんの反応をいただきまして、そこからデザイン室でOGP画像を展開していこうということになりました。 ワークフロー ワークフローと言いましても成果物は一枚の画像なので早い人は数分で終わってしまうような作業です。また作り方に正解もありません。 今回は自分が作る上で考えたことを織り交ぜながら紹介できればと思います。 まず下準備としてデザイナーは担当する記事を読み内容を把握した上でコンテキストに沿ったOGP画像の作成に入ります。 そもそも内容と全く違うイメージを作ってしまっては元も子もありません。 内容をしっかりと把握したら、以下のようなイメージで展開していきます。 画像サイズは こちらの記事 を参考に 横1200px×縦630px で展開していきます。 素材作成 テキスト OGP画像といえば目を引くテキスト+背景画像の組み合わせをよく見ると思います。 今回のAdvent Calendarではテキストは基本的に記事のタイトルと同じ文言で作っていきました。 冗長なものは読み手を想定してどんなワードに惹かれるのかピックアップし組み直したりします。 仮にレイアウトしてから フォントの種類 サイズ 色 ウェイト(文字の太さ) 段組 などを読みやすく目を引くようにアレンジしていきます。 サイズやウェイトで抑揚をつけてみたり、色を加えたり下線を引くことでピックアップしたワードを強調したりしました。 背景画像 背景画像はAdobeStock他フリーの素材や記事内の画像を使用・加工したりと内容に沿ったイメージで展開します。 (素材を使う際は商用可・個人利用のみなど使用できる範囲がライセンスごとに細かく分かれているので気をつけましょう) コントラストが強かったりテキストを載せるスペースがないといった複雑な画像には ぼかす シェイプで座布団を敷く 塗りのオーバーレイを加える ような工夫をすることでテキストの可読性を上げることができます。 調整 / レビュー テキストと背景画像を合わせて調整していきます。 比重を考えてレイアウトや色味を調整し、全体的にバランスがよくなるような画像作りを心がけています。 aptpodデザインテイストと記事とのバランス品質を担保できるよう、デザイン室内でレビューを行います。 フィードバックを元に調整し、完成まで持っていきます。 書き出し / 設定 完成した画像を書き出し、 ImageOptim などを使いファイルサイズを軽量化します。 最後にTech Blogの管理画面からでOGP画像を設定するまでがデザイナーのワークフローになります。 掲載 OGP画像を設定し投稿された記事は掲載されるとこのように反映されます。 #はてなブログ 激動の2019年を振り返る - aptpod Tech Blog https://t.co/daS7HOFKSS — aptpod_tech (@aptpod_tech) 2019年12月25日 Advent Calendarを終えて 今回Advent Calendarで連日投稿される記事のOGP画像をつくるのはなかなか大変でした。 ですが作っていく内にグラフィック作成の知見もたまり、(途中からですが)最終日までやり遂げてとても達成感がありました! 今後も継続してOGP画像作っていきたいと思います! [後日談] やらかしてました この記事を書いているときに恥ずかしながら投稿済みのOGP画像にTypoを見つけてしまいました。 (いやもう本当にすみません) そんな僕のようにやらかしてしまって修正をしたけど、肝心のSNS側はキャッシュが残っててチェックできない! といった方のためにキャッシュのクリア方法をまとめてくださった方がいたのでリンクおいておきます! OGP情報を更新した時のTwitter・facebookのキャッシュクリア方法 | 野良人(のらんど)|大阪府堺市のWEB制作屋さん aptpod Tech Blog Twitter デザイン室がどんなことをやっているのか興味がある方はこちらの記事を是非! 高速データ可視化におけるフォントの重要性 - aptpod Tech Blog ありがとうございました!
アバター
Aptpod Advent Calendar 2019 25日目=最終日の記事です。 CTOの梶田です。 あっという間に最終日となり、小さなトラブル😷もありましたがみんなの頑張りで今年も無事に完走といったところです。 素晴らしい! 今年は新しい試みでTechブログでの挑戦になったわけですが、昨年に比べ、記事を投稿した人も増え、バリエーションも広がり来年への布石ができたかなーと個人的には思っています。 さて、本題に。 2019年も終わりということでちょうどよいので月並みですが、2019年振り返りと2020年に向けて書こうと思います。 はじめに 2019年タイムライン 1月 〜3月 〜6月 次世代への向けての整理 〜9月 組織変更 iSCP特許登録 〜12月 次世代向けの開発 さいごに おまけ 参考リンク(書籍) はじめに 昨年末に シリーズBの資金調達 を発表し、あっという間に2019年も終わりに近づいています。 あー早かった。なんかここ何年かずっと早いけど。。😅 人も増え、組織も変わり、整備することもどんどん増えていき、向き直りすることも。 技術的負債も見えてきて、このまま行くのか一旦立ち止まるのか。。。 そんな判断もありました💦 ありがたいことに売上規模は拡大の傾向にあり、来年もさらなる拡大を見込めるような状況にあります。 いろいろ試行錯誤はありましたが、結果的にはいい方向に向かっていると思っています。 さてさて、そんな試行錯誤含めた2019年の振り返りを書いていきます。 2019年タイムライン 2019年のタイムラインをざっと。 イベント出展もキーポイントなので開発のトピックと並列で記載。 ちなみにこちらは主要なイベントのみでこれ以外のイベントにも出展しています。 1月 オートモーティブワールドという比較的大きな展示会に合わせて、以下の 対外リリース を発表しました。 Visual M2M Motion for iOS Synchronized CAN Transceiver Python SDK for intdash Analytics Services このリリースの中でも Python SDK for intdash Analytics Services は、Python SDKでデータを加工したり、データ分析や機械学習環境として広く使われている Jupyter Notebook を使って処理ロジックの開発や試行錯誤したり等々、新たな利用幅拡大への礎となっています。 また、イベントも盛況でここから案件につながったものも多くありました。 〜3月 期末ということもあり、例年通り忙しく対応していたなーと。 あまり記憶がないw この中でも新しい機械学習でのユースケースのPoCを遂行できたのがポイントで今でも続いており、製品の対応の幅としても広がった出来事でした。 2日目の記事 Amazon SageMakerとintdashでお菓子の高速検出システムをサクッと構築してみた はこの派生ですね。 〜6月 Automotiveへ注力し、機能追加や周辺のドキュメント等々整備し強化を図りました。 いろいろありますが、成果物のひとつとしてこんなのがあります。 オートモーティブプロ評価ガイド 19日目の記事 エンジニアによるユーザーマニュアルの作り方 も成果物のひとつです。 また、並行してAWS Summitに向けて発表されたばっかりのAWS RoboMakerとの連携を検討し、デモの実装をしました。 この試行錯誤は 1日目 AWS RoboMakerとintdashでTurtlebot3を遠隔制御できるようにしました! の記事をご覧ください。汗と涙の結晶です! 次世代への向けての整理 時を同じくして、広がってきた製品と内部的な負債(技術的負債/組織的負債/思想的負債...)というものが見えてきました。 繁忙期を過ぎて落ち着いて見えてきたことも多く、 内部的にはスクラムの実践やプロダクトオーナーの配置、フロー策定、タスク管理の一新、インセプションデッキの作成...等々いろんなチャレンジをして模索していた時期でした。 ここでの判断は、 このまま突っ走るのではなく、先を見据えて継続的に開発できるよう整える ことを優先し、技術的もそうですが、組織的にも思想的にも整理してから前を進むこととしました。 いろんな課題も見えてきて、まだまだ今でも発展途上ではあるにせよここで立ち止まることが重要でした。 〜9月 イベントとしては、初出展となる 5G/IoT展 や 次世代自動運転・コネクテッドカーカンファレンス に出ましたが、やはり 時流としてHOTなこともあり、新しい様々な引き合いがありました。 中でも5Gは注目されているなと感じ、来年はさらに盛り上がるのでは!という期待もあります。 組織変更 8月には、 次世代への向けての整理 の整理の延長でもあるのですが、将来(スケール)を意識した体制へ向かうために組織変更を行いました。 大きなポイントとしては、 ミドルウェア/基幹技術開発に完全専任するチームを編成 CTO、VPoP、VPoEによるチーム体制(CTO室) となります。 自分としても組織が成長する中で、なかなか追いつかない部分もあり、いろいろ溢れていく中で 20日目の記事 に投稿している高橋を新しくVPoEとして迎え入れ、内部からVPoPを抜擢し、CTO室というチームとして開発組織の課題解決に取り組むことにしました。 整理していたGoogle スプレッドシートのキャプチャ(一部) その体制の準備のために多岐にわたる自分の業務を整理(キャプチャの下も続く。。。)してどう役割を分けるか等々検討しました。 まだまだできてないこともありますが、良くなってきていると感じています! iSCP特許登録 また、8月にはもうひとつ、2016年から進めていて、2017年に出願した 独自に開発したプロトコルである iSCP(intdash Stream Control Protocol) のベース特許がついに登録されました! こちらについては、別の機会のTechブログにて。 〜12月 11月には4回目となりますが、社内イベントとして千葉県の茂原のサーキットを貸し切って 自社製品に触れる(新しく入った人向け) デモ用データ取得 プロダクトの検証(新しい製品も含む) といった目的で走行しに行きました。 準備も大変ですが、製品に実際に触れて体験できるいい機会で 大人の遠足 といった感じです♪ ここでは、 モノへつながる感動 もあるのでこれを忘れずに製品開発に活かしていきたいという想いがあります。 次世代向けの開発 次世代向けの開発も進行し、継続的に開発できるような土壌固めを中心にアドベントカレンダーの記事内でもあった 従来のモノリス型のサービスから、徐々にマイクロサービスへ移行 : 8日目 スケールに向けたインフラアーキテクチャの実現 : 22日目 映像関連のハードウェア製品開発 : 12日目 、 13日目 、 23日目 の開発もありますが、そのほかにも様々な製品プロジェクトが進行中です。 12月に出展した AWS re:Inventについては、こちらの 15日目の記事 AWS re:Invent 2019 で AWS RoboMakerとintdash によるTurtlebot3の遠隔制御の展示を行いました! を見てください。 さいごに 駆け足になってしまいましたが、主な出来事を中心に2019年をざっと振り返りました。 そのほかにもいろんなことがありすぎて、書ききれないですが2019年 も 激動であったなと。 <そのほかのネタ> 通常のQAや市街地走行では問題ないけど、箱根を走行すると問題が出る → 魔物がいる!とか。。。 大変なこともありましたが、2019年も様々な知見や経験を得ることができました。 知見/経験の蓄積はテクノロジー企業の源泉であり、IoTの事業をやる上で重要だと考えています。 これは来年以降につなげていきたいと思っています。 2020年はオリンピックもあり、5GのPoCや自動運転の実証等々で様々なチャンスがありそうな期待感もあります。 来年も 激動 かなー。。。 泥臭いことも多いですが、 モノからモノへつながる感動 を忘れずに来年もさらなる進化を遂げて飛躍できればと思っています。 長くなりましたが、メリークリスマス!🎄 来年のアプトポッドにご期待ください‼ おまけ aptpodの採用ページ 参考リンク(書籍) プロダクトが進捗していないと感じた時の戦い方 カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで CTO・VPoE・VPoPの分立とCTO Delegation Pokerで権限移譲について学ぶ
アバター
TL;DR Raspberry PiでCAN通信を動かして、車両の診断データ(OBD-II)を見てみた話です。 SocketCANのISO-TPの機能が便利だったので、その紹介がメインになります。 はじめに この記事は aptpod Advent Calendar 2019 の24日目の記事です。 お送りするのは、組み込みソフトチームの 松下 です。 TL;DR はじめに 背景と目的 車両診断とは? 準備 確認環境 Raspberry Pi CAN通信用モジュール CAN通信の環境構築 下準備 can-utilsをインストール CAN Interfaceの有効化 SocketCAN interfaceの設定 車両に接続する OBD-II通信の環境構築 OBD-II通信とは? ISO 15765-2 ISO-TP 実際に使ってみると… can-isotpのカーネルモジュールをビルド&インストールする OBD-II通信してみる リクエスト時のCAN-IDについて リクエスト時のSID,PIDについて ISO-TPを使った、コマンドの送受信方法 VINの取得を試してみる 拡張CAN-IDで再トライ 注意と自己責任(免責事項) あとがき 弊社ソリューションのご紹介 参考リンク(書籍) 背景と目的 弊社の製品である intdash Automotive Pro は、車両CAN(Controller Area Network)データ等のデータロギング、可視化・解析などのワークフローをクラウドシステムで実現するソリューションです。このAutomotive Proをベースに、車両診断(ダイアグ通信)のワークフローをクラウド化できないか?という要望があり、車両診断に関する機能も開発中です。 この開発の過程で、実際にRaspberry Piを使って車両診断(OBD-II)通信を試したので、そのやり方を紹介します。 車両診断とは? 診断機能の概略については、 Vector社のはじめての診断 をご一読ください。 本記事では、CANを使ったOBD-IIによるダイアグ通信を紹介します。 準備 車両とCAN通信するために、Raspberry Piを用いた方法を簡単に説明します。 Raspberry PiでCAN通信する方法は、MCP2515を接続する方法が数多く紹介されています。 本記事でも、このMCP2515を使用します。 (詳細は、参考リンクをご覧ください) 確認環境 以下、筆者の動作確認環境です。 Raspberry Pi 3B+ Aptpod社製 ラズパイ拡張ボード 車両は弊社所有のHonda FIT Raspberry Pi Raspberry Pi 3B+ OS: Raspbian buster $ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster CAN通信用モジュール 今回は弊社で開発したラズパイ用の拡張ボードを使用します。 Aptpod社製 ラズパイ拡張ボード といっても、拡張ボード上はMCP2515をラズパイのSPIとGPIO25に接続する、一般的な方法で実装しています 1 。 データシートや、「 MCP2515 回路 」等で検索すると出てくる回路図を参考にしてください( こちら の記事がわかりやすいです)。 もしくは、 Amazonで売ってるMCP2515モジュール でも大丈夫だと思います。 宣伝 紹介ですが、弊社のラズパイ向け拡張ボードは、CAN通信以外にも、以下の機能を提供しています。 RTC(ハードウェアクロック) 電源スイッチ機能 電源管理機能(5V〜19Vの入力が可能) 状態通知用のLED(アプリケーションから制御可能) また、弊社ではCANバスに接続しUSBによってデータを取り出すCAN-USBインターフェイスもハードウェア製品として販売しております。 もしご興味がございましたら、 弊社のコーポレートサイトからお気軽にお問い合わせください 。 www.aptpod.co.jp CAN通信の環境構築 下準備 can-utilsをビルドするので、git等をインストールしておきます。 $ sudo apt update $ sudo apt install git build-essential can-utilsをインストール SocketCAN通信を使うには can-utils が便利なので、 ソースコードからbuild & installしておきます。 $ git clone https://github.com/linux-can/can-utils.git $ cd can-utils $ make $ sudo make install 以下のコマンドがインストールされます。 candump cansend isotpsend isotprecv ... CAN Interfaceの有効化 /boot/config.txt に、以下の設定を追記します 2 。 # Enable MCP2515 dtoverlay=spi-bcm2835 dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25 dtoverlay=spi-dma 再起動後、以下のようにSocketCAN interfaceの can0 が見えるようになりました。 $ ip link 4: can0: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10 link/can SocketCAN interfaceの設定 CANのビットレートは500kbpsに設定します。 $ ip link set can0 type can bitrate 500000 $ ip link set can0 up can0 のNICをUp後に、 ifconfig で can0 が認識できている事を確認できました。 $ ifconfig can0: flags=193<UP,RUNNING,NOARP> mtu 16 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC) RX packets 1992 bytes 15936 (15.5 KiB) RX errors 0 dropped 1992 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 車両に接続する 車両のDLC(データリンクコネクタ) 3 と接続して、 candump コマンドでCAN通信をダンプしてみます。 $ candump can0 can0 140 [8] 1F 39 00 00 00 1F 15 00 can0 141 [8] 89 2A 3F A8 00 C0 38 08 can0 144 [8] 00 01 00 00 00 A8 38 00 can0 140 [8] 1F 3A 00 00 00 1F 15 00 can0 141 [8] 89 2A 3F A8 00 C0 38 08 can0 360 [8] 00 00 28 FF 49 40 20 00 can0 361 [8] 00 1A 00 DC 20 00 00 00 can0 362 [8] 12 7A 82 96 00 00 00 01 can0 140 [8] 1F 3D 00 00 00 1F 15 80 ... CAN通信を確認できました! 4 OBD-II通信の環境構築 CAN通信の環境準備ができたので、OBD-IIのダイアグ通信を試してみます。 OBD-II通信とは? ものすごく簡単に説明すると、OBD-IIは多くの車両に搭載されている車両診断を行うための規格です。 5 OBD-IIでは以下の機能が定義されています 6 。 故障コード(DTC:Diagnostic Trouble Code)の取得要求 フリーズ・フレームデータの取得 エンジン系のECUのパラメーター取得 アクティブテスト機能 車両情報等 etc.. ここで、CAN通信上でOBD-IIのデータを送受信するには、ISO 15765-2の規格に対応する必要があります。 ISO 15765-2 一般的なCAN通信 7 の場合、1つのCANメッセージに載せられるデータは最大で8byteです。 しかし診断データをやりとりする上で8byte以上のデータをやり取りしたいケースも出てきます。 そこで、長い診断メッセージを分割し、複数のCANメッセージを使って伝送する(以降、マルチフレーム)方式としてISO 15765-2の規格が採用されています。 こちらの記事 ではデータの1byte目にデータ長さを設定していますが、実はこれもISO 15765-2の通信規格で定められたフォーマットだったりします。 ISO-TP ISO-TPはlinuxのsoketcan上でISO 15765-2に準拠した通信機能を提供するプロトコルドライバーです。 SocketCAN ISOTP このISO-TPの機能を使う事で、上述の面倒なCANの分割/結合/フロー制御の処理を実装する事無く、ダイアグ通信が可能となります。 ISO-TPの機能は、can-utilの isotpsend 、 isotprecv 等のコマンドで簡単に使用できます。 また、下記のように、 CAN_ISOTP のプロトコルでsocketを作成して通信もできます 8 。 int s; struct sockaddr_can addr; static struct can_isotp_options opts; addr.can_addr.tp.tx_id = 0x07e0 ; addr.can_addr.tp.rx_id = 0x07e8 ; s = socket (PF_CAN, SOCK_DGRAM, CAN_ISOTP); setsockopt (s, SOL_CAN_ISOTP, CAN_ISOTP_OPTS, &opts, sizeof (opts)); addr.can_family = AF_CAN; addr.can_ifindex = if_nametoindex ( "can0" ); bind (s, ( struct sockaddr *)&addr, sizeof (addr)): write (s, buf, buflen); 実際に使ってみると… 実際にISO-TPのコマンド使うと、 Protocol not supported と表示されてしまいました。 OTL $ echo 09 02 | isotpsend -s 7e0 -d 7e8 can0 -p 0:0 socket: Protocol not supported can-isotpのカーネルモジュールをビルド&インストールする こちら のページにしたがって、カーネルモジュール(can-isotp.ko)をビルドします。 $ git clone https://github.com/hartkopp/can-isotp.git $ cd can-isotp $ make kernel moduleをbuildするためのヘッダーがない!と怒られる場合、 make[1]: *** /lib/modules/4.19.75-v7+/build: No such file or directory. Stop. ラズパイのkernelのヘッダーファイルをダウンロードします 9 。 $ sudo apt install raspberrypi-kernel-headers 無事、ヘッダーファイルがインストールされました。使用しているkernelとヘッダーのVersionも揃っています。 $ ls /lib/modules/4.19.75-v7+/ build ... $ uname -r 4.19.75-v7+ ビルドができたら、モジュールをインストールします 10 。 $ sudo make modules_install $ sudo insmod ./net/can/can-isotp.ko can-isotp モジュールがインストールされました 11 。 $ lsmod | grep can can_raw 20480 0 can_isotp 24576 0 can 28672 2 can_isotp,can_raw can_dev 28672 1 mcp251x OBD-II通信してみる リクエスト時のCAN-IDについて 送信先のCAN-IDの指定方法ですが、11bit(通常)と29bit(拡張)の2種類があります 12 。 どちらの種別を受け付けるかは、筆者が確認する限りでは車種(メーカー)で異なる模様です 13 。 以下に、リクエスト時のCAN-IDの定義について抜粋しておきます。 種別 CAN-ID 説明 11bit (通常) 7DF リクエストを理解できる全てのECUへのリクエスト 29bit (拡張) 18 DB 33 F1 リクエストを理解できる全てのECUへのリクエスト リクエスト時のSID,PIDについて リクエスト時のデータセクションの最初のバイトはモードを指定します。OBD-IIではSID(Service ID)と呼ばれてます。 そしてデータセクションの2バイト目には、各モードに応じてPID(Parameter ID)を指定します。 SIDとPIDのリストは wikipedia にまとめられています。 以下、SIDの例を示します 14 。 SID 命令内容 0x01 現在のデータを表示する 0x02 フリーズ・フレームデータを表示する 0x04 DTCを消去し診断履歴を消去する 0x09 車の情報を要求する さらに、SIDとPIDの組み合わせの例を以下に示します。 SID & PID Description 01 05 冷却水の温度 01 0C RPM(エンジン回転数) 01 0D 車速 01 1F エンジン始動時からの稼働時間 09 02 VIN 09 04 キャリブレーションID 09 20 ECUの名前 たとえば、車の情報 (SID=0x09) からVIN (PID=02) をを取得したい場合は 09 02 を送信します。 また、現在のデータ (SID=0x01) からエンジン回転数 (PID=0x0C) を取得したい場合は、 01 0C という具合です。 ISO-TPを使った、コマンドの送受信方法 ISO-TPのコマンドを使うには、送信元( -s )と実際の受信相手( -d )をCAN-IDで事前に指定する必要があります。 しかし、どのECUが返答してくれるかは今はわからないので、以下の手順でCAN-IDを特定します。 すべてのECUコマンドへリクエストを投げてみる ( isotpsend ) 応答してくれるECUのCAN-IDを見る( candump ) 受信元のCAN-IDを指定して、マルチフレームのCANデータを受信( isotprecv )してデータを見る VINの取得を試してみる 実際に、弊社の所有するHonda FITのVINを取得してみます。 VIN(Vehicle Identification Number:車両識別番号)は最大17バイトの文字列なので、マルチフレーム通信しないと取得できません。 ラズパイでターミナルを(送信と受信用)2個開きます 受信側は candump can0 で待機しておきます 以下のVIN取得の要求コマンドで、応答があるか確認します $ echo "09 02" | isotpsend -s 7df -d 7e8 can0 -p 0:0 CAN-IDの 7DF は リクエストを理解できる、すべてのECUへリクエスト です どのECUが応答してくれるかわからないので、 7E8 は適当です 念の為、padding( -p 0:0 )も設定しておきます $ candump can0 むむ、応答がありません。CAN-IDが 7E* あたりから返答を期待したのですが。。。 拡張CAN-IDで再トライ ここで諦めず、CAN-IDを拡張(29bit)に変えてみます。 $ echo "09 02" | isotpsend -s 18DB33F1 -d 18DA33F1 can0 -p 0:0 CAN-IDの 18DB33F1 は リクエストを理解できる、すべてのECUへリクエスト です -d のCAN-IDは適当です 実際に送信してみると、CAN-ID= 18DAF10E から応答がありました! $ candump can0 can0 18DB33F1 [8] 02 09 02 00 00 00 00 00 can0 18DAF10E [8] 10 14 49 02 01 47 50 35 しかし、以降のマルチフレームのCANデータが受信できていません。 これは、ECUからの受信に対して、ラズパイ側がフロー制御の応答を返していないからです。 そこで、以下のコマンドでマルチフレームの受信処理を有効にします。 -d には、さきほど応答があったCAN-IDの 18DAF10E を指定します。 $ isotprecv -s 18DB33F1 -d 18DAF10E -l can0 この状態で、送信用のターミナルから要求を投げると... $ echo "09 02" | isotpsend -s 18DB33F1 -d 18DAF10E can0 -p 0:0 無事、応答が来ました!↓ ^15 $ isotprecv -s 18DB33F1 -d 18DAF10E can0 49 02 01 47 50 35 2D 31 32 30 31 ** ** ** 00 00 00 00 00 00 実際のcandumpの結果は以下の通り。 $ candump can0 can0 18DB33F1 [8] 02 09 02 00 00 00 00 00 can0 18DAF10E [8] 10 14 49 02 01 47 50 35 can0 18DB33F1 [8] 30 00 00 00 00 00 00 00 can0 18DAF10E [8] 21 2D 31 32 30 31 ** ** can0 18DAF10E [8] 22 ** 00 00 00 00 00 00 併せて、isotpdumpの結果も示します。マルチフレームのフロー制御が動作していますね。 $ isotpdump -s 18DB33F1 -d 18DAF10E can0 can0 18DB33F1 [8] [SF] ln: 2 data: 09 02 can0 18DAF10E [8] [FF] ln: 20 data: 49 02 01 47 50 35 can0 18DB33F1 [8] [FC] FC: 0 = CTS # BS: 0 = off # STmin: 0x00 = 0 ms can0 18DAF10E [8] [CF] sn: 1 data: 2D 31 32 30 31 ** ** can0 18DAF10E [8] [CF] sn: 2 data: ** 00 00 00 00 00 00 応答のデータフォーマットの詳細は割愛しますが、最後の17バイトのASCIIコードが、そのままVINの情報です。 47 50 35 2D 31 32 30 31 ** ** ** 00 00 00 00 00 00 変換すると GP5-1201*** となります。 実際に、市販されている故障診断機の JDiag JD101 でも取得してみましたが、結果は一致していました。 注意と自己責任(免責事項) 本記事を参考にして、機器や車両の故障、事故等の被害が起きたとしても、弊社・個人としては責任は負いかねますので、ご了承願います。 あとがき 本当はデータモニターや故障コードの解説もしたかったのですが、分量が多くなってしまいましたので今回はここまでです。 最後も駆け足になってしまいました。orz 記事を書き終わった後に気がついたのですが、書籍の カーハッカーズ・ハンドブック に今回紹介した内容が記載されていました!詳しく知りたい方・興味を持った方は、この本を読んでください! それでは、よいクリスマスをお過ごしください。 弊社ソリューションのご紹介 弊社では、自動車からのCANデータの収集や、遠隔適合のためのソリューションとして、以下のプロダクトをご提供しております。 自動車向け遠隔計測ソリューション(CAN/CAN-FD対応) www.aptpod.co.jp 自動車ECU向け遠隔適合ソリューション www.aptpod.co.jp 今回の記事では、Raspberry Piで簡易的に実現する方法をご紹介しましたが、実際にシステムとして長期に安定稼働させるにはその他にも様々な検討・開発が必要になります。 上記のような弊社のソリューションを採用いただければ、そういった個別開発の工数を省略しオールインワンパッケージとして遠隔計測・遠隔適合システムを導入いただけます 。 個別の要件に応じたカスタマイズについても可能な範囲で対応させていただきます。 まずはお気軽に、 弊社サイトのお問い合わせフォームよりご相談ください 。 www.aptpod.co.jp 参考リンク(書籍) Raspberry PiでOBD-II (CAN)の情報を取得するための基板を自作する (Qiita) Raspberry Pi で CAN通信(準備) MCP2515を使った自作基板とRaspberry Piで自動車のECUにOBDリクエストを送る (Qiita) Raspberry Pi と MCP2515 で CAN 通信 はじめての診断 by Vector (pdf) OBD (wikipedia) OBD-II PIDs (wikipedia) SocketCan (wikipedia) SocketCAN ISOTP (GitHub) カーハッカーズ・ハンドブック 弊社ボードは、更にESD保護やコモンモードチョークが入っています ↩ 最近のラズパイは -overlay をつけないので、参考リンク先の古い記事を読む方は注意してください! ↩ OBD2コネクタという表現が一般的かも ↩ 最近の車両はセキュリティの関係でCANバスの通信が見えない場合が多いです。ただし、ダイアグ通信に対してはゲートウェイを通してDLCから通信が見える模様です ↩ サポートしていない車両もあるはず ↩ https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%B3%E3%83%BB%E3%83%9C%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%8E%E3%83%BC%E3%82%B7%E3%82%B9 ↩ データサイズを8バイト以上に拡張するCAN-FDの仕様があります。ぜひ、当ブログの CAN FDことはじめ もご覧ください。 ↩ 実際のsocketの作成方法は、isotpsend.c isotprecv.c を参考にするといいです。ソースの リンク ↩ https://www.raspberrypi.org/documentation/linux/kernel/headers.md ↩ 事前にCANのモジュールがloadされている必要があります。ロードできない時は、 modprobe can も試してください。 When the PF_CAN core module is loaded ('modprobe can') the ISO-TP module can be loaded into the kernel with ↩ modprobe等によるモジュールの自動ロードの設定は割愛します。 ↩ 詳細はISO15765-4の規格を参照。 この記事 がわかりやすいです。 ↩ 弊社の所有するホンダFitは29bit、スバルXVは11bitでリクエストを受け付けました。 ↩ カーハッカーズ・ハンドブック より抜粋 ↩
アバター
aptpod Advent Calendar 2019 の23日目を担当するハードウェアGpの おおひら です。 いきなりアレな感じのタイトルでなんだか申し訳ないのですが、株式会社アプトポッド(以下aptpod)に入社してちょうど1年になることもあり、このタイミングでしか書けないだろうなぁということで書きました。 (本当は イケてるFPGA高位合成の記事 へのRTLおじさんからのアンサーとして TclスクリプトではじめるFPGAのバージョン管理 なる記事を投下しようと思っていたんですが、これはまたの機会に…) 昨日に引き続き弊社若手エースの記事です。RTLおじさんの思い上がった鼻をへし折りにくる高位合成の破壊力☺️ご笑納下さい。 高位合成でFPGA開発!最短 1日で映像リサイズ機能を実装する - aptpod Tech Blog https://t.co/hPORcfEpW8 — Ryuichiro Ohira (@ryu_ohira) 2019年12月13日 はじめに 自己紹介 転職に至る経緯 aptpodに入社して 組織のこと 意思決定の速さ 質実剛健さ 異なる経験を持つメンバーがお互いを尊敬し合う雰囲気 メンバーの能力の高さ 働く環境のこと Slackコミュニケーションで快適 G-SuiteやOffice 365やSmartHRやMFクラウドが Gitが普通に使えて最高 家族を大切にし、それを公言できる雰囲気 真の裁量労働 バックオフィスの手厚いサポート おまけ めっちゃ褒めてるけどホンマかいなと思ったあなたへ ハードウェア(電気)設計者として、移籍して思ったこと 協力会社・商社の皆様が優しい件 調達コストはやっぱり大変 設計だけが仕事じゃないんやで問題 ハードウェア製品の性質の宣伝 おわりに はじめに 本稿を読むのにかかる時間は約8分です あくまで一個人としてのポエムです Japanese Traditional Big Company (JTBC) などと言って日本の大企業を揶揄する風潮がありますが、本記事はそのような類のものではありません 令和元年12月23日(月)は平日😇ということで、ちょっと気落ちした心をリフレッシュできるよう前向きな文体を心がけます 自己紹介 2011年4月、電気設計エンジニアとして東日本大震災でてんやわんやしていた電機メーカーに入社 B2Bをやりたい(むしろB2Cは興味ない)、という奇特さを買われて業務用映像機器の設計部門に配属 画質評価やちょっとした小基板・FPGA・組込ソフト設計を3年ほど 映像処理基板・中規模のFPGA設計を2年ほど Windowsアプリのプロマネを1年ほど 商品設計 / 電気設計 / FPGA設計の各署リーダーを2年ほど 2019年1月から現職 転職に至る経緯 リーマンショック後のリストラ期に電機メーカーに入社したため 痛みを伴う改革 を多く目の当たりにした 切るほうも切られるほうも、どっちもツライ、けど儲かってないからどうしようもない 利益を出せないことの惨めさや、企業というものの効率追求システムの原則が身にしみた そんなこんなで醸成された価値観に加えて、 業務用機器という、安定したB2B市場の中で技術者としてチャレンジする機会が減っていった(とくに自分がマネジメントする立場に近づくにつれて、 自分も当然のごとく安全策をとるようになった と自覚していた) 技術的におもしろい部品や新しい仕組み・ツールの情報を得ても、なかなか活用する機会が作れない なんとか根回しして新しいことを始めようとしても、本流の既存事業の重要な案件が飛び込んでくる(しゃあない) 「あの計画してくれた案の試作費、やっぱりバジェットに入れられなかったわ…すまん!」 「そうですか😇まぁしょうがないですよね😇またどこか良きタイミングで提案します😇」 ということもあったりで、 やっぱり技術者は成長市場で勝負せねばいかんのか… とおぼろげながら感じていました。 また、勤めていたメーカーは比較的若手への権限移譲が進んだ組織でしたが、それでも私が所属していた部署は 課長は早くて35歳ぐらいから。しかし優秀な30,40代の人がゴロゴロいて椅子取りゲームの様相 部長は45歳ぐらいから。そもそも事業が儲かってないとポジションがあるかどうか…(黒字部門からの落下傘もある 部門長までいけるか、なんて完全に運 (個人が優秀かどうかなんて瑣末なこと という現実もあり、 仮に運良くマネジメント職につけたとして、自分のやりたいことと組織の方向性をすり合わせる影響力を行使できる(かもしれない)のは50歳代…20年先か🤔… ということで大きな組織に勤めるリスクをひしひしと感じていたのでした。 こんな背景から、2017年ごろから転職を見据えていくつかの会社に話を伺いに行っていたりしたのですが、2018年の夏に現職の人事の方に声を掛けて頂いてaptpodを知る機会があり、B2Bをやりたい私の志向と会社の向いている方向が合っていると感じたうえに、ハードウェア設計を含めて垂直統合でソリューションを提供するという、なかなか類を見ない(=難易度の高い)事業に挑戦する姿勢に魅力を感じて入社を決めた次第です。 aptpodに入社して 組織のこと 意思決定の速さ 経営陣までの階層が2つぐらいしかないですから当然のごとく速いです。メーカー換算すると、ちょっとデスクに行って部課長をつかまえて議論して、製品の方向性を決めて試作Go、ぐらいの体感速度。大変にストレスフリーです。 質実剛健さ 本稿のタイトルに こんにちはスタートアップ とありますが、 ごめんなさい、盛りました。 aptpodは2006年創業で、スタートアップというよりはベンチャーと呼んだほうがしっくりくる組織だと感じます。 スタートアップ キラキラ などと検索すると下記みたいな記事が出てきますが… thestartup.jp 弊社はそれよりも、目の前のお客さまに実体のある価値を届けることを重要とする空気を感じます。これはメーカーも似ているかも。ユーザーファーストの質実剛健さはとても大事で好きなところです。 異なる経験を持つメンバーがお互いを尊敬し合う雰囲気 メーカーにおられる方は分かって頂けると思いますが「XX屋を説得してこい!」「これだからXX屋さんは話が通じねぇ!」みたいな部署間の折衝、ありますよね。あるいは「君さ、これやっといて」みたいな、年齢や立場を使った上から下への謎のタスクぶん投げ。That's ディスコミュニケーション。 現職は垂直統合型の組織でそれぞれのメンバーのバックグラウンドや技術領域が多種多様なこともあり、お互いの違いを尊重して丁寧なコミュニケーションを取ってくれる方が多いと感じています (ちなみに私が入社して一番初めに書いた社内ブログは半導体の解説記事でした) お互いの持つ技術領域が大きく異なるがために、理解し合おうとする空気感は本当にありがたいものだと感じます。 メンバーの能力の高さ NETFLIXの最強人事戦略 自由と責任の文化を築く 作者: パティ・マッコード 発売日: 2018/08/17 メディア: 単行本(ソフトカバー) 仕事の満足度は、グルメサラダや寝袋やテーブルサッカーの台とは何の関係もない。仕事に対する真のゆるぎない満足感は、優れた同僚たちと真剣に問題解決にとりくむときや、懸命に生み出した製品・サービスを顧客が気に入ってくれたときにこそ得られる。 書籍の内容は賛否あれど、1万回ぐらい噛み締めたい言葉ですね。転職してよかったと思いますし、チームとして結果を出していきたいと思います。 働く環境のこと Slackコミュニケーションで快適 言わずもがなのため割愛。 G-SuiteやOffice 365やSmartHRやMFクラウドが 同上。割愛。 Gitが普通に使えて最高 メーカーで古参のHWエンジニアだとSubversionでも手取り足取り教えてもできない人は多いんですよね。そういうリテラシーが普通にあるということに感動しました (基準がおかしいとか言わないで… 家族を大切にし、それを公言できる雰囲気 前職だと40代、50代ですでに子育てを一段落した男性がマジョリティということもあり、どうしても仕事優先という空気感が出ることが多いのです。社内の食堂で晩御飯を食べて、さぁもうひと頑張りするぞ!みたいな。 aptpodに入社してからは小さいお子さんがいるメンバーが多く、家族を優先して当然という空気感が大変ありがたいです。Slackで「子どもが熱を出したので早退します」に対して【お大事に】のスタンプが付く速さよ😂 入社して数ヶ月経った頃、当初反対(そりゃそうだ)していた妻が 転職してよかったね と言ってくれたことが印象に残っています。 真の裁量労働 メンバーを信頼して任せてくれる大人の雰囲気。前職でも最後の数年は裁量労働していたはずなんですが…なぜなんでしょうかね…。 私はとくに仕事とプライベートの区切りが曖昧で ワークアズライフ を地で行く人間のため、早くに帰宅して子どもを寝かしつけてから仕事を再開したり、お風呂に入りながらスマホで新製品の部品データシートを読んだり、自分の隙間時間を効率よく活用できて自由になったと感じます。 バックオフィスの手厚いサポート いつもありがとうございます!!! おまけ コーヒー飲み放題 お菓子も食べ放題 (太る 社内自動販売機は一律30円 関東IT健保ええやん 社内サークルたくさん オフィスは四谷三丁目、気分転換に新宿御苑へGo めっちゃ褒めてるけどホンマかいなと思ったあなたへ 当然ながら上に書いたことはすべて、捉えようによってはネガティブにもなり得ます。 自由と責任は表裏一体 ということ 。 ハードウェア(電気)設計者として、移籍して思ったこと ここからは少し業務に関連して、実際にハードウェア設計をする立場として感じたことを。 協力会社・商社の皆様が優しい件 「量産の数が出ないんだから大変だろうな…メールしても返事がないとか、『ごめんなさい売れません』とかは覚悟しておこう」と思っていたんですが、ありがたいことに意外となんとかなるものだなというのが正直なところです。打合せのたびに 年間xxx個なんですが…大変申し訳ありません… という気持ちで臨むのですが、皆様とても優しくサポートしてくださり大変感謝しております🙇‍♂️ でもたまにはDigi-Keyで買います 調達コストはやっぱり大変 メーカー時代には10円で調達できた部品が簡単に100円以上に化けます。そりゃ当然だわという感じなんですが、いざ部品表から材料費をはじき出してみるとゾッとします。メーカーとガチンコ勝負したら絶対勝てない。あと市場で逼迫している部品は当然ながら手に入りません (特定スペックのMLCCとか) 設計だけが仕事じゃないんやで問題 メーカー時代には当然のごとく社内のプロの方々に協力してもらえた業務が自力になります。これも覚悟していましたが、やはり大きな組織は強い。長年蓄えられた圧倒的な資産が輝いてみえます。小規模組織 (現状、弊社ハードウェアGp 4名) に於いては製造に近い部分で協力会社の皆様にお力をお借りするのですが、それでもなかなか下記すべてに手が回らないのが正直なところで、昔は恵まれた環境にいたのだなと思い返すことも多くあります。 製品の市場調査・企画・投資回収計画の立案 信頼性試験 安全規格・EMC認証などの各種法規対応 部材調達 (見積依頼・発注・在庫管理・EMSへの部品支給・ディスコン対応) 量産工程の改善・QC ハードウェア製品の性質の宣伝 ソフトウェアのバックグラウンドを持つメンバーに説明する必要と責任があります。 いまから試作に取り掛かって、デモは半年後、出荷は1年後です と勇気を持って宣言しましょう。このあたりはメーカーで鍛えられた政治手腕が( おわりに 弊社ハードウェアGpは2020年1月15日から東京ビッグサイトで開催される オートモーティブワールド にむけて、試作品のデモ展示のために鋭意設計中です。ここ半年ほど取り組んできた映像関連のハードウェア製品開発の一端をお見せできるかと思いますので、車載計測にお悩みの方をはじめ、移動体全般のデータ計測・可視化・活用にご興味がある方はぜひブースにお立ち寄りください。 またハードウェアエンジニアに限らず全組織で絶賛メンバー募集中ですので、 aptpodの採用ページ に足を運んで頂けるとありがたく思います。 明日12/24(火)の記事は、弊社エンベデッドチームリードの @ffmatsu さんによるラズパイとCAN計測の質実剛健な(本人いわくチャラい)記事です!
アバター
アドベントカレンダー 22日目を担当します。ソリューションアーキテクトの sataro です。 前職ではホスティング専門のインフラエンジニアとして、穴蔵に籠って運用保守をやっていました。 それはそれで楽しかったのですが、自社プロダクトへの興味が膨らみ転職、心機一転aptpodではSAとして働いています💪 さて、本稿ではスクラムについてちょっとお話ししてみたいと思います。 aptpodでは自社プロダクト開発の一部にスクラムを取り入れています。私はその中でも スケーラビリティ をテーマに掲げたチームで、プロダクトオーナー(PO)として活動しています。 とはいっても、タイトルの通りスクラムに取り組んでようやく半年になろうかというところです。何か驚きのテクニックをご紹介!というわけにもいきませんが、これからスクラム始めてみようという方や、POってなんぞ?という方の参考になれば幸いです。 はじめに まずは予習してみた で、POってなに? 実践してみたこと Who Why What When Where How まとめ はじめに スケーラビリティ と書きましたが、まずは簡単にプロダクトの背景説明を。 チームのミッションは、自社プラットフォームのスケール向けたインフラアーキテクチャの実現です。 弊社の基幹プロダクトの1つである intdash ですが、大変有り難いことに研究開発分野において多方面で活用頂いています。 そして当然、研究開発の先には、どこかのタイミングで 製品化 & 大規模運用 というフェーズが待っています。 クライアントが大規模運用に打って出ようとした時に、弊社のプラットフォームが足を引っ張っては元も子もありません。 そこで、 intdashのスケーラビリティは急務である 。というわけです。 まずは予習してみた 半年前、スクラムを実施するにあたり、いくつか書籍をあたりました。 SCRUM BOOT CAMP THE BOOK(西村 直人 永瀬 美穂 吉羽 龍太郎)|翔泳社の本 カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで(市谷 聡啓 新井 剛)|翔泳社の本 どちらも言わずと知れた名著です。 基礎知識から実践のイメージまで、はじめるために必要なことは全て書いてあります。 迷うなら読んだ方が良いです。 スクラムの基礎については こちら のブログなどでも学べます。 ある程度概要が掴めたら、とにかくやってみるのが良いと思いました。 説明を読んでもなかなか頭に残らないので・・・ で、POってなに? 一度原点である スクラムガイド に立ち返りたいと思います。 スクラムガイドによると、 プロダクトオーナーは開発チームから生み出されるプロダクトの価値の最大化に責任を持つ 。とされています。 実務的な動きとしては以下のようなものです。 プロダクトバックログアイテムを明確に表現する。 ゴールとミッションを達成できるようにプロダクトバックログアイテムを並び替える。 開発チームが行う作業の価値を最適化する。 プロダクトバックログを全員に見える化・透明化・明確化し、スクラムチームが次に行う作業を示す。 必要とされるレベルでプロダクトバックログアイテムを開発チームに理解してもらう。 プロダクトオーナーがこれらを実践できればスクラムは回ります。ですが、実際に取り組んでみると、 開発チームが行う作業の価値を最適化する がものすごく深い。一筋縄ではいかないので、あれこれ考えてみました。 実践してみたこと 価値を最適化する について考え始めると沼にはまりそうだったので、少々乱暴ではありますが一度自分の中で単純化してみました。 5,6個くらいの指針なら意識して行動できるのでは?と思い、いわゆる5W1Hを意識すると良いんじゃないかと考えています。 ビジネスや受験勉強などでもよく聞くワードですし、スクラムにおいてはユーザーストーリーを考える際にも登場します。 価値の最適化を見失わないために、ここでも活躍してもらいましょう。 Who ここでの「 誰 」はチーム外にいるステークホルダーの明確化です。 チーム内で開発アイテムに対するステークホルダーは基本的にPOになると思います。 受け入れ条件を定義して各アイテムを検査する、というのはチーム内で行われます。 一方で、プロダクトの大枠においてのステークホルダーというのも別にいるかと思います。 今回の場合、「○○という案件が来年度以降で同時接続デバイス数XX台になる見込みがある」という具体的な数値目標がありましたので、機能要件のステークホルダーはその案件を担当するソリューションアーキテクトとなりました。 プラットフォームがスケールしていく中で満たすべきセキュリティやコストバランスなどの非機能要件は、経営層や管理部門に別のステークホルダーが存在するはずです。 各側面で「 誰 」と要件を握っていけば良いのか意識すると、無駄なものを生み出す危険が減ると感じています。 Why ここでの「 何故 」はゴール設定における目的意識です。 本プロダクトの場合、スケールするアーキテクチャの実現という大テーマはゆらぎませんが、一方で具体的な数値目標はふわふわしているとも言えます。 前述の通り「同時接続デバイス数XX台」という目標があり、まずはそこをゴールとして走り始めました。 ただ、案件の状況も会社の状況も随時変化しますので、ゴールが状況と合わなくなることもあります。 スクラムはゴールが変化することを前提としたフレームワークですので、ゴール設定を再確認する向き直りの機会さえ持てれば、プロダクトを継続して開発することはできるはずです。私たちのチームも一度スプリントを止めて向き直るタイミングがありました。 そのタイミングを見誤らないために、自分たちは「 何故 」それを作っているのか、という視点は常に持っている方が良いと感じています。 What ここでの「 何 」は開発アイテムへの理解です。 次々にチームから生み出される開発アイテムについて、POはチーム外へ正しく説明できる必要があると思います。 チーム内では開発アイテムの優先度について合意をとった上で進めていきます。しかし、チーム外からは、優先度の高いアイテムの意義が見えにくいこともあるかもしれません。 何となく停滞しているんじゃないか、など要らぬ心配や横槍が発生しないようにしたいところです。 外部のステークホルダーにはどういった目的で「 何 」を開発しているのか、情報共有していくことが必要だと感じています。 When ここでの「 何時 」は期限です。 最終的に開発アイテムは求められるリリース時期にアジャストしていく必要があります。 ここでは案件との兼ね合い以外にも、製品のリリースサイクルも絡んできます。せっかく作ったものを世に出すタイミングを逃しては大変です。 「 何時 」リリースに乗せるか、というのもプロダクトの大事な要素です。 Where ここでの「 何処 」は・・・5W1Hとか言ってしまいましたが、あまり関係ありませんでした😅 強いて言えば、チームとの会話の機会は「 何処 」でもつか。 face to face でもった方が良い。ということです。 極端な話POはスプリントの最初と最後、プランニングと振り返りさえ顔を出していればスクラムは回ります。 あとはタスクボードで会話すればいいじゃん。となってしまってはもったいないと思っています。 POは自分で実装はしません。だからこそ、開発チームがどんな考えで、どんなこだわりをもって開発を進めているか。肌で感じて進めていくのが、その熱量をチーム外に伝える助けになると感じています。 How 最後に「 どうやって 」です。スクラムにおいて、実現方法はPOではなく開発チームに委ねられます。 もちろん私たちのプロダクトも同様です。 弊社にはアプリケーションからハードウェアまで、イカしたエンジニアが揃っていますので、ここでのPOの役目は信じて任せるだけです。 常時各レイヤーで仲間を求めていますので、興味があれば是非 こちら をご覧ください。ダイレクトマーケティング🤣 まとめ PO視点で、チームがうまく機能するためにはどうしたら良いか・・・この半年考えていたことを語ってみました。もちろんこれで正解というものではないので、常に カイゼン を目指していきたいと思います。 何か参考になれば嬉しいです。ありがとうございました。
アバター
Aptpod Advent Calendar 2019 21日目担当のハードウェアグループの織江です。 この記事では電子工作を趣味とする界隈でも人気のM5stackの小技を紹介したいと思います。 M5Stackとは M5Stackは中国深セン発の液晶付き汎用開発プラットフォームです。 世界的に有名なArduinoと互換性が高く人気を博しています。 一般的に電子部品の開発ボードは基板剥きだしで、製品としてのケース が付いていなかったりするため製品化には大きなハードルがあります。 この製品は液晶画面ボタン、スピーカー、WiFi、BT、バッテリー等の汎用 に使える機能が(評価機としては)しっかりとしたケースに入っていて エンジニア以外の方が触っても自然に使えるようになっているのが 特徴だと言えると思います。 日本ではスイッチサイエンスから購入することが可能です。 M5Stack Basic - スイッチサイエンス 弊社では液晶等の表示装置の無い組み込みPCをよく使うのですが、簡易的な表示器が欲しい際にこうした製品を利用します。 M5Stackのリセット回路の問題点 大変便利なM5Stackなのですが、不安定な動作をするときがたまにあります。 弊社で直面した大きな課題の一つは 電源投入時に正しくリセットされないことがある というものです。(画面が真っ暗なまま何も起きない) 弊社のユースケースは下記の通りで、自動車のエンジンがかかるのに連動して起動できる 車載PC(Linux)のUSBポートの電源に連動してM5Stackの電源を入れたいという条件でした。 車載PCの状態を定期的にM5Stackに送信して液晶画面に表示する機能を実装しています。 M5Stack内臓のLipoバッテリーは自動車の中で使うことを想定しているため、 取り除いた状態で使用しています。 使用例 調査の結果M5Stack内のESP32というメインのマイコンのリセット信号が電源電圧が立ち上がる前にぐねぐねと立ち上がっており、正しくリセットできていないため後段の周辺回路も 正しく動作していないということが分かりました。 ただ、電源・充電管理とUSBシリアル変換IC(CP2104)だけは正しく 動作していることが分かりました。 M5Stack簡易ブロック図 リセットの配線を公式の回路図で確認すると下記の赤い丸の部分になります。 M5Stack回路図抜粋 この回路図でENと書かれたリセット信号はDTR・RTS信号を経由して、さらにUSBシリアル変換IC(CP2104)経由でリセットできることが分かります。 Pythonを使ったM5Stackのリセット方法 では実際にこのUSBシリアル変換IC経由のリセットを試してみたいと思います。 まず適当なLinuxマシンを用意します。 今回はVirtualbox上のUbuntu16.04LTSを用いました。 続いてドライバのインストールを行います。詳しくは説明しませんが こちらのサイトが参考になります。 usbserial とcp210xのドライバをインストールします。 drivers - Ubuntu 16.04.1 usbserial missing - Ask Ubuntu 特に何も設定しなければM5Stackが/dev/ttyUSB0として認識されるようになるかと思います。 後はシリアル通信を開始する際にDTRとRTSピンを下記のように操作するとリセットをかけることが出来ます。 ピンを操作した後rtsctsをFalseにしないとその後の通信ができなくなるのでその点が注意点です。 import serial import time ser = serial.Serial('/dev/ttyUSB0',115200) ser.setDTR(False) time.sleep(0.1) ser.setRTS(False) ser.rtscts = False time.sleep(1) # wait esp32 wakeup #your code here ser.close() 起動画面 リセット失敗した状態(画面真っ暗)からのリセット あとがき 通常リセット回路はどのような電源が繋がれても確実にリセットされるようになっていると嬉しいのですが、必ずしもそうなっているとは限らないです。 M5Stackの場合はたまたまUSBシリアル変換ICだけは 正しくリセットされるようでしたのでそこを起点にメインのマイコンを復帰させることができました。 参考URL M5Stack Basic - スイッチサイエンス pySerial API — pySerial 3.4 documentation Arduino UNOでpyserialを使ったら再起動してしまった - Qiita
アバター
aptpod Advent Calendar 2019 の20日目担当をします。高橋です。 CTO室でVPoEとして 組織マネジメント を担当させてもらっています。 弊社はハードウェアや組込みソフトの低レイヤから、ネットワークを介しクラウドに至るまでの広範囲な技術を一貫して内製で開発し提供するため、多種多様なエンジニアがおり ダイバーシティ(多様性)が尊重される会社 です。 私からは、普段の業務で心がけていることについてお話します。 人と向き合う 会話のオンとオフ Goodの発掘 弱みは隠すより見せる 任せるのではなく支援する 見過ぎるのではなく間違えずに見る 組織と向き合う 目標はたすき掛けで設計する チームの価値は面で捉える 挑戦にはリミットとリカバリープランを おっさんが輝くから若手が輝く おわりに 人と向き合う 会話のオンとオフ 私が担っている業務として、2週間に1度、エンジニア全員との 1on1ミーティング を実施させてもらっています。 オープンな場で言いにくいこと・言葉を選びにくい内容も伝えてくれるので 組織改善のヒント になっています。 ポイントとしては、オフィシャルな場での オンな会話 (言葉を選び悪目立ちしない発言)だけではなく オフな会話 (心の奥底で感じている率直な発言)をしてもOKな場にしています。 プライバシーは守り、かつ評価の参考などには絶対用いないと約束 するからこそ成立しています。 Goodの発掘 1on1では、 Good と Bad を教えてもらっており Badは一緒に解決しGoodを共に喜ぼう と伝えており 本人が当たり前だと思っていることも改めてGoodに数える ようにしています。重要なのは 自分自身で状況が発展/進展していると気づくこと です。ときとして周囲からの感謝がこのキッカケになったりもしますので、slackのスタンプでポジティブな発言に👍することを意識しています。ジワジワと効果が出ます。 弱みは隠すより見せる 失敗が許されない、パーフェクトであることを求められる職場って息が詰まりませんか?また、失敗や弱みを必死に隠そうとする相手と、さらけ出してくれる相手ってどっちが見ていて気分がイイですか? 私は自分の苦手なことやダメな部分をオープンに開示するようにしています。例えば、ダイエットするとか言っておきながら ワザと その日のうちに ライス大盛 を頼んだり、そそっかしくて会議室に忘れ物を結構したり(これはワザとではない)、許容範囲はありますが。 相手も人間なんだな、と思ってもらうこと から許し合いや協調が増えるイメージです。 任せるのではなく支援する 日本語は似た単語でもニュアンスが変わるので使うのが難しい言語だと思います。マネジメントしていて最近よく思うのですが 任せる と言う言葉が古くなってきていると感じます。 任せる という言葉には、上意下達な前提で「(俺がやりたいことをお前に)任せる」というニュアンスが入る気がします。それよりも「(あなたがやりたいことを私は)支援する」というニュアンスの方がしっくりきます。 成長する組織の多くは 、この サーバント型リーダーシップ が存在し 個々が自己組織化して動くみたいなのを ニュアンスで理解 している組織ではないかなと思います。 見過ぎるのではなく間違えずに見る マネジメントや育成というテーマにおいて、メンバー/プロジェクトのことを”ちゃんと見れてるのか”みたいな話はよく出る会話かと思います。( うん、聞き飽きてる ) 頻度高く細かくみること(マイクロマネジメント)も大事だとは思いますし、近視眼的には早く決断がなされて良いかもしれません。ただ、長く時間が経てば経つほどに私情や私見による判断のブレを招いたり 承認文化による組織スピードの劣化を招く恐れ があります。 実際にそれ(承認文化)が染みつき、意思決定に超絶根回しの時間が掛かりメンバーが疲弊する組織や、無意識のうちにそこに加担してしまった経験もあります。 そのため、間違えた見方をしないこと(エッセンシャルマネジメント?)が重要と考えています。 自身で突破する成功体験が多いほうが成長する ので、できるだけ 我慢してタッチしすぎないことも大切 ですね。 組織と向き合う 目標はたすき掛けで設計する 目標を立てるときに気をつけていることとして 個人に組織目標を依存させすぎない ことがあります。 当社でも 同じテーマや課題を持つ同士が組むと単独で目指すよりも強固な実行力を持つ ケースが多いです。 私は、目標設定において たすき掛け というキーワードを意識するようにしており、共通の課題感を持った人同士で 協力して達成する目標を設計 し提案・促すなどの工夫をしています。 チームの価値は面で捉える 近年のIT業界は、少ない専門性のみで成果を積み上げることが困難な時代にあるとも言えます。 ナンバー1品質は クラウド や サブスクリプション で 外から安価に買えば済む時代 になってきたためです。 タレントや強みを一個に限定しない 面で価値貢献ができるチーム 、言い換えると 強みのコンボが多く決まるチーム が良いチームなのではないかと考えています。 挑戦にはリミットとリカバリープランを 挑戦には期限を設けることが大切です。これはコスパを意識しろということではなく 他人から時間切れを言い渡されるより 自分でハラオチをすることが大切 だという意味です。 一方で、組織としては唯一の挑戦に賭けること/依存する判断はリスクになり得ます。うまくいかない場合は 失敗も想定し準備する ことで被害を最小にし、結果的な成功確度も上がります。バスケやサッカーで味方がシュート打った後も他の選手がゴール前に詰めてる みたいなことと同じです。 おっさんが輝くから若手が輝く 変化=成長という前提で雑に書きますが、おっさんが変化することは組織の成長にとても重要です。現時点でのキーマンが優秀でも、変化対応できず硬直しきった組織では継続成長は望めません。そのため、 おっさんから先陣をきって変化を奨励する組織作り をしないとならないと自戒を繰り返しています。もちろん吸収力/柔軟性で若い人材にアドバンテージがあるのは間違いないです。ただ、おっさんができる事って 自分の能力や活躍に固執するだけではない はずです。多くの機会を周囲に回し 組織の中で大きなパスをつないでいくこと も務めですよね。 と、まぁこんなことを考えつつ少しでも多くのエンジニアが活躍する会社にしようと思って日々仕事をしています。 今回はこの辺で! おわりに アプトポッドは エンジニアを絶賛募集中 です! 応募意思がなく興味本意でのオフィス見学も歓迎! ぜひお気軽にご連絡ください!
アバター
aptpod Advent Calendar 2019 の19日目担当、Webチームの蔵下です。普段は、自社プロダクトのUIをReactでゴリゴリ書きつつ、社内のお酒好きを集めて不定期で飲み会を開いています🍶(飲みのお誘いお待ちしています) みなさんの会社では誰がユーザーマニュアルを作成していますか? 数百人規模の大きな会社であれば専属のマニュアル作成チームがあるかもしれません。aptpodでも専属のチームがいてくれたら心強いのですが、絶賛成長中のスタートアップということもありまだ専属チームはありません。 そのような中でも、手塩にかけて開発したプロダクトを多くのユーザーに使っていただくためには、ユーザーマニュアルは欠かせません。 我らaptpodのエンジニアが、エンジニアならではのアプローチでユーザーマニュアルを作った方法を紹介します。 自動でできるところは自動で! バージョン管理でデグレなんてしない! をモットーに💪 前段: aptpodでのプロダクト開発の進め方 エンジニアによるユーザーマニュアル作成の流れ 1. 目次作成 2. 担当者をアサイン 3. 校正ツールの導入 4. 文章作成 5. GitLab上で相互レビュー 6. レビュー後の文章をWord化 7. PDF生成 おわりに 前段: aptpodでのプロダクト開発の進め方 ユーザーマニュアル作成の話に入る前に、aptpodでプロダクト開発をするときの進め方を紹介します。aptpodでは、ハードウェア・サーバー・インフラ・UI・デザインと、一つのプロダクトにさまざまなスキルを持ったメンバーがアサインされます。プロダクトの提供する技術的範囲が広いため、ユーザーマニュアルもそれぞれの領域の知識が必要になります。 そのような背景もあり、プロダクトの開発に関わった複数人が同時に、デグレが無く安全に、楽して、クオリティの高いユーザーマニュアルを作成することが求められました(aptpodではユーザーマニュアルも一つの製品として力を抜かない😎)。 エンジニアによるユーザーマニュアル作成の流れ いよいよ本題に入ります。大まかには、仕様を把握しているエンジニアが文章やざっくりとした図版のイメージ(UIのキャプチャやパワポでちょっとお絵かきしたり)を作成し、そのデータを元にデザイナーがページレイアウトや図版を起こしていく流れになります。 下記が実際の作成フローです。 目次作成 担当者をアサイン 校正ツールの導入 文章作成 GitLab上で相互レビュー レビュー後の文章をWord化 PDF生成 各Stepでどのように進めたのか解説していきます。 1. 目次作成 全体の文章構成、ボリュームをつかむべく目次を作成します。最初から完璧な目次は作れないので、書いてる中でのアップデートする前提で大丈夫です。ここでは共同編集できるようにGoogle スプレッドシートで管理しました。 実際に使用していたGoogle スプレッドシートのキャプチャです。モザイク多め... 2. 担当者をアサイン 目次をまとめたGoogle スプレッドシートに 担当者 という項目を設け、章・節ごとにメインで実装していた開発者を担当者としてアサインしました。仕様に詳しい人が書いたほうが確実で手戻りも少なくなります。中には担当者がいない(開発とは直接関係のない)章も出てくるので、そこは私が担当しました。 3. 校正ツールの導入 人によって文章の書き方にばらつきが出てくる可能性があるため、下記の 校正ツール を導入します。校正ツールとは、文章を一定ルールに沿って自動で校正してくれるツールです。 Visual Studio Code テキスト校正くん テキスト校正くんはVisual Studio CodeにインストールするPluginです。 JTF日本語標準スタイルガイド のルールに沿って校正してくれます。 自動でできるところは自動で! 人で担保が難しいところはツールに任せちゃいましょう。 4. 文章作成 いよいよ文章を書き始めます! 文章はエンジニアライクな Markdown で書き進めます。文章だけ書くのであれば通常のMarkdown記法のみでいいですが、文章の性質によっては強調して目を引くデザインにしたい部分もあり(取り扱いでの注意など)別途ルールを設けました。そのルールに沿って、後ほど説明する「6. レビュー後の文章をWord化」で自動変換します。 追加したルールの一部。誰でも読めるようにQiitaTeamで管理しました。 文章が書き上がったら、バージョン管理のためにGitLabへPushします。 Gitで管理することによってデグレという最悪の事故を未然に防げることはもちろん、レビューの内容や編集の履歴を残すことによって、文章の修正意図を知れたり、その後の文章作成に役立てることができます。 GitLab上でのディレクトリ構成は下記のような構成にしました。Markdownファイルを節ごとに分けることで、各担当者間で作業が干渉しないようにしました。 GitLabでのディレクトリ構成。章ごとにディレクトリを分け、節ごとにMarkdownファイルを分けました。 バージョン管理でデグレなんてしない! マニュアル作成でバージョン管理しないなんて... もうそんな世界では生きられない... 5. GitLab上で相互レビュー 文章をPushできたら、レビュー担当者にレビューを依頼するために Merge Request を作成します。Pushされた文章をレビュー担当者が確認し、指摘ポイントはMerge Requestのコメントに記載していきます。文章作成者はその指摘を対応して再度Push、レビュー担当者が再度確認して問題なければ Approval(承認) とし、developブランチへマージできるようになります。 レビューの様子。指摘ポイントごとに管理できるのはよかった。 マージされたら、この節は作成完了です!!👏 6. レビュー後の文章をWord化 文章が一通り揃ったら、デザイナーへ文章とざっくりした図版を渡します。デザイナーの力は絶大ですね。ざっくり図版が、かっこよく、さらにわかりやすい状態に仕上げていただけました。 文章と図版が揃ったら、いよいよPDFへの書き出しのフローに入ります。今回は、出力後のページデザインを担保するために Markdownファイル → Wordファイル → PDFファイル という段階で書き出しました。 Markdownファイル → Wordファイル の変換は、 Pandoc で自動化しました。章ごとに分かれていたMarkdownファイルを一つに結合し、「4. 文章作成」で紹介したルールに沿ってデザインを反映させながらWordファイルが生成されます。 サンプルMarkdownファイルをWordファイルへ変換した様子。デザインが反映されていいかんじ! 7. PDF生成 いよいよ最後の工程、 Wordファイル → PDFファイル です。生成されたWordファイルをWordで開き、PDF出力をポチ! と、ここでアクシデント発生... 文章内で指定していたページ内リンクが切れちゃっていました...😱 macOSのWord特有の不具合のようで、macOSではなくWindowsのWordで開き、AcrobatからPDF出力することでページ内リンクも効いた状態のPDFが出力されました!!! これにてユーザーマニュアル完成!!🎉 完成したユーザーマニュアル おわりに 最後にちょっとしたハプニングがありましたが、無事にエンジニアとデザイナーでユーザーマニュアルを作成することができました! 自動化とGitを導入できたことで、デグレもなく、レビューもスムーズにでき、ユーザーマニュアルの中身に時間を割くことができました。 今回は私が代表で記事にしましたが、他にも関わったメンバーはたくさんいます。メンバー全員で試行錯誤しながら進めてきました。みなさん本当にお疲れさまでした!!! 打ち上げしたい!!! ウェーイ🍻
アバター
aptpod Advent Calendar 2019 18日目を担当させていただきます 上野 と申します。 昨年も ARKit2.0が凄い。あなたの見ている方向を記録、可視化するデモ という記事で参加させていただきまして、 今年もiOS系で記事を書かせていただこうと思います。iOSアプリエンジニアのみなさんよろしくお願いします。 さて、今回のフォーカスする内容ですが、、、 皆さん、、Metalって使ってる、、、、? 昨年の記事では ARKit 、 SceneKit といったフレームワークを使用していますが、あれももちろん 、 UIKit などに含まれるビューコンテンツやアニメーション、イメージなどのほとんどは Metal をコアに作られています。 Metalは、 Apple製品に搭載されたGPUへアクセスを提供するAPI で基本的にUIに関わる部分ほとんどに使われているようです。 今回はそのMetalにフォーカスし、Metalの実装コストを下げた MetalKit を利用してデモアプリを作りたいと思います。 ※Metalの事前知識が欲しいと言う方はよくまとめられた記事がありましたので こちら をご覧ください。 今回作る物 実際「 よし、Metal(だけ)で何か作ってみるか、、、 」となった場合に皆さんはパッと何か思いつくでしょうか? 試しにAppleで サンプル を見てみましたが、 Creating and Sampling Textures Using a Render Pipeline to Render Primitives Reflections with Layer Selection 単純に画像を MetalView 上に描画する物だったり、 グラフィクスAPI開発の入門ではありがちですが単純に三角形の描画のサンプル、 何か 凄い3Dのモデルを描画する方法だったり等ありますが、イマイチ何を作ろうか、、、と私はなりました。 いろいろ調べていく中で、MetalKit では実装方法さえ知っていれば Texture上にピクセル単位で簡単に色を塗ることができると分かりましたので、 タイトルにあるように簡易的な ペイントツール を作ろうと思います!今回は MTKView( MetalKitに含まれるView ) が Canvas となります。 描画までの流れ MTKViewのフレームサイズに応じて解像度(縦横のピクセル数)を決める。 ピクセル数に応じた配列(テクスチャへ渡す用のバッファ)を作成。 画面がタッチされたらその座標と同等の配列のインデックスに選択されている色情報を挿入。 定期的に MetalKit から draw のリクエスト( MTKViewDelegate のコールバック)が呼ばれるので、そのタイミングで用意したバッファと書き込む対象のテクスチャを渡す。 シェーダにて描画する色情報を適切にテクスチャに渡す。 といった流れとなります。 実装開始 Metalをセットアップ ではいつも通りの感じでプロジェクトを作ります。 MetalKit Viewを設置しましょう。 ViewController.swift import UIKit import MetalKit class ViewController : UIViewController , MTKViewDelegate { override func viewDidLoad () { super .viewDidLoad() // Do any additional setup after loading the view. self .setupMetalView() } // MARK:- Metal View @IBOutlet weak var metalView : MTKView ! var mDevice = MTLCreateSystemDefaultDevice() var mCommandQueue : MTLCommandQueue ! var mComputePiplineState : MTLComputePipelineState ! var metalViewDrawableSize : CGSize ? = nil var targetMetalTextureSize : CGSize = CGSize.zero var bufferWidth : Int = - 1 var mTextureBuffer : MTLBuffer ? func setupMetalView () { guard let library = self .mDevice?.makeDefaultLibrary() else { return } // Register Texture Shader guard let kernel = library.makeFunction(name : "computeTexture2d" ) else { return } guard let computePipeline = try? self .mDevice?.makeComputePipelineState(function : kernel ) else { return } self .mComputePiplineState = computePipeline self .metalView.device = self .mDevice self .metalView.delegate = self self .metalView.framebufferOnly = false // ← これがないとXcode11以降では落ちます self .mCommandQueue = self .mDevice?.makeCommandQueue() } //MARK:- MTKViewDelegate func mtkView (_ view : MTKView , drawableSizeWillChange size : CGSize ) {} func draw ( in view : MTKView ) {} } 一旦こんな感じでViewControllerを書いてみました。 IBOutlet で定義している metalView はViewControllerに設置したもので、この辺りで出てきている mDevice だったり、 libary 、 computePipeline あたりはお作法のようなものなので 先ほどのリンク だったり こちら をご参照ください。 ※かなり初歩的な所は深くは語りません。 ここで設定したシェーダの設定 computeTexture2d は後ほど解説します。 基本的には MTKViewDelegateに含まれる draw のコールバックにて GPUへ必要なテクスチャやバッファ情報を渡す事になります。 それでは、ここからは先ほど示した描画までの流れにそって進めていきます。 1.MTKViewのフレームサイズに応じて解像度(縦横のピクセル数)を決める ViewController.swift // MARK:- viewDidAppear override func viewDidAppear (_ animated : Bool ) { self .updateMetalViewDrawableSize() } // MARK:- viewWillTransition override func viewWillTransition (to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) { coordinator.animate(alongsideTransition : nil ) { (_) in self .updateMetalViewDrawableSize() } } オートレイアウトで指定したViewのフレームサイズが決まる(変わる)タイミングって大体この2つですよね、 このタイミングで設置した MTKView の実際のフレームサイズからピクセル数を決めましょう。 ViewController.swift let kMetalTextureHeightDotSize : Int = 512 ... func updateMetalViewDrawableSize () { // Viewの実際のサイズから縦横比を参考に高さのドットサイズから幅のドットサイズを求める var width = Int(ceil(( self .metalView.frame.width /self .metalView.frame.height) * CGFloat(kMetalTextureHeightDotSize))) // // ここには後ほど書き換えがあります(1) // self .targetMetalTextureSize = CGSize(width : width , height : kMetalTextureHeightDotSize ) self .log( "drawableSize: \(self.metalView.drawableSize) , frame: \(self.metalView.frame) => targetMetalTextureSize: \(self.targetMetalTextureSize) - MetalView" ) self .metalView.drawableSize = self .targetMetalTextureSize } 今回は高さのピクセル数を 512 に固定にし、そこから縦横比で求める事にしました。 ピクセル横 = フレーム幅 / フレーム高さ * ピクセル縦 そして、求めた解像度を MTKView の drawableSize に 描画するサイズとして指定してあげましょう。 ですがこのままでは不十分で、後ほど (1) の内容を説明します。 2. ピクセル数に応じた配列(テクスチャへ渡す用のバッファ)を作成 ViewController.swift //MARK:- MTKViewDelegate func mtkView (_ view : MTKView , drawableSizeWillChange size : CGSize ) { self .log( "drawableSizeWillChange \(view.drawableSize) => size: \(size) , frame: \(self.metalView.frame) , targetSize: \(self.targetMetalTextureSize) - MTKViewDelegate" ) guard !self .targetMetalTextureSize.equalTo(CGSize.zero) else { return } if self .metalViewDrawableSize != nil { guard !self .metalViewDrawableSize ! .equalTo( self .targetMetalTextureSize) else { // 前回と同じ値だった場合は更新しない return } } self .metalViewDrawableSize = self .targetMetalTextureSize self .bufferWidth = Int( self .metalViewDrawableSize ! .width) self .setupMetalBuffer() } 先ほど MTKView に解像度を指定しましたがあちらをセットすると MTKViewDelegate 内の drawableSizeWillChange がコールされます。 このタイミングで、指定した解像度が MTKView に反映されますので、バッファの作成を行いましょう。 ViewController.swift let kMetalTextureClearColor : simd_float4 = [ 255 / 255.0 , 255 / 255.0 , 255 / 255.0 , 1.0 ] ... func setupMetalBuffer () { guard let device = self .mDevice else { return } let colors = [simd_float4]. init (repeating : kMetalTextureClearColor , count : self.bufferWidth * kMetalTextureHeightDotSize) let bufferLength = colors.count * MemoryLayout < simd_float4 > .stride // <- ここ要チェック self .mTextureBuffer = device.makeBuffer(bytes : UnsafeRawPointer (colors), length : bufferLength , options : .cpuCacheModeWriteCombined) } ここ要チェック とコメントしている箇所に注目してください。配列の数でバッファサイズを求めている処理で、今回はあまり関係ないですが、Metalのバッファ管理では かなり重要 です。 こちら で詳しく解説されていますがMelta側のバッファサイズとCPU側のバッファサイズが違ってしまう要因になりますので理解は必須です。 初期値(何も書かれていない色情報(白))と縦横の長さで配列を宣言、 MTLDevice でバッファを作成します。 3. 画面がタッチされたらその座標と同等の配列のインデックスに選択されている色情報を挿入 ViewController.swift // タッチイベントが開始された override func touchesBegan (_ touches : Set <UITouch> , with event : UIEvent ?) { guard let touch = event?.touches( for : self.metalView )?.first else { return } let point = touch.location( in : self.metalView ) self .lastPoint = nil self .drawCanvas(point : point ) } // タッチ位置が移動した override func touchesMoved (_ touches : Set <UITouch> , with event : UIEvent ?) { guard let touch = event?.touches( for : self.metalView )?.first else { return } let point = touch.location( in : self.metalView ) self .drawCanvas(point : point ) } // タッチが終了した override func touchesEnded (_ touches : Set <UITouch> , with event : UIEvent ?) { guard let touch = event?.touches( for : self.metalView )?.first else { return } let point = touch.location( in : self.metalView ) self .drawCanvas(point : point ) } // タッチがキャンセルされた override func touchesCancelled (_ touches : Set <UITouch> , with event : UIEvent ?) {} タッチイベントを取得します。 UIViewController は標準で override でタッチイベントを取得できて簡単ですね。 タッチされた位置が MetalView に対するタッチイベントのみ処理しています。 ViewController.swift func drawCanvas (point : CGPoint ) { let x : Int = Int(ceil((point.x /self .metalView.frame.width) * CGFloat( self .bufferWidth))) let y : Int = Int(ceil((point.y /self .metalView.frame.height) * CGFloat(kMetalTextureHeightDotSize))) var color : simd_float4 = [ self .isRed ? 1.0 : 0.0 , self .isGreen ? 1.0 : 0.0 , self .isBlue ? 1.0 : 0.0 , 1.0 ] let dataSize = MemoryLayout < simd_float4 > .stride if let ptr = self .mTextureBuffer?.contents() { if 0 <= x, x < self .bufferWidth, 0 <= y, y < kMetalTextureHeightDotSize { let index = (x * kMetalTextureHeightDotSize) + y memcpy(ptr.advanced(by : index * dataSize), & color, dataSize) } } } タッチされた座標を元に、用意したバッファに色情報を入れていきます。 isRed 、 isGreen 、 isBlue という項目が出てきましたが色情報を変更できる様にフラグを宣言しています。 先ほど作成したMetal用のバッファの contents() と言うファンクションでバッファの先頭のポインタ、アドレス情報を取得します。この処理はC言語っぽいですが memcpy で色情報をアドレスを指定してバッファにコピーしましょう。 これでバッファの準備は完了です。 4. MetalKit から定期的に呼ばれるdrawのリクエスト(MTKViewDelegateのコールバック)のタイミングで、用意したバッファと書き込む対象のテクスチャを渡す ViewController.swift let kMetalThreadGroupCount : Int = 16 ... func draw ( in view : MTKView ) { guard let drawable = view.currentDrawable else { return } guard let commandBuffer = self .mCommandQueue.makeCommandBuffer() else { return } guard let textureBuffer = self .mTextureBuffer else { return } let computeEncoder = commandBuffer.makeComputeCommandEncoder() computeEncoder?.setComputePipelineState( self .mComputePiplineState) let texture = drawable.texture // 書き込む対象のテクスチャをセット computeEncoder?.setTexture(texture, index : 0 ) // GPUに渡すテクスチャ用バッファをセット computeEncoder?.setBuffer(textureBuffer, offset : 0 , index : 1 ) let threadGroupCount = MTLSizeMake(kMetalThreadGroupCount, kMetalThreadGroupCount, 1 ) let threadGroups = MTLSizeMake(Int( self .targetMetalTextureSize.width) / threadGroupCount.width, Int( self .targetMetalTextureSize.height) / threadGroupCount.height, 1 ) computeEncoder?.dispatchThreadgroups(threadGroups, threadsPerThreadgroup : threadGroupCount ) computeEncoder?.endEncoding() commandBuffer.present(drawable) commandBuffer.commit() commandBuffer.waitUntilCompleted() } 今回の肝となる処理ですね、先ほど用意したバッファをMetal側に渡します。 Metalに限らず GPUプログラミング では一般的な処理フローを1つ説明します。 引用元(※NVIDIAブログ) GPU は CPU とは違い基本的には 並列 で処理が実行されます。 上の図の例で言うと、処理したい関数 saxpy を再起的に呼び出し、同じスレッド内で連続して処理しているのに対し Cuda の saxpy は非同期に処理され cuda 内のメモリに計算結果を反映しています。 今回 kMetalThreadGroupCount で指定している数値が並列処理が行われるスレッドの数になります。 commandBuffer( MTLCommandBuffer ) と computeEncoder( MTLComputeCommandEncoder ) を使って、 テクスチャとバッファをMetal( GPU側 )に渡してあげましょう。 5. シェーダにて描画する色情報を適切にテクスチャに渡す シェーダのセットアップ シェーダは 別ファイル となります。拡張子は .metal で言語は C++ ですね。 PaintShader.metal #include <metal_stdlib> using namespace metal; /* * |--|--|--|--| * | 0| 4| 8|12| * | 1| 5| 9|13| * | 2| 6|10|14| * | 3| 7|11|15| * |--|--|--|--| */ kernel void computeTexture2d(texture2d<half, access::write> output [[texture( 0 )]], device float4 *color_buffer [[buffer( 1 )]], uint2 gid [[thread_position_in_grid]]) { int h = output.get_height(); int index = (gid.x * h) + gid.y; half r = color_buffer[index].x; half g = color_buffer[index].y; half b = color_buffer[index].z; half a = color_buffer[index].w; output.write(half4(r, g, b, a), gid); } はい、ここでMetalのセットアップで出てきた computeTexture2d が出てきましたね。 引数の [[texture(0)]] や [[buffer(1)]] の数値は先ほど computeEncoder でセットした際に指定した インデックス です。 gid [[thread_position_in_grid]] は指定したスレッドの数によって並列化した際の現在の インデックス(配列のアドレスのような物)情報 が設定されています。 この gid の位置が書き込むキャンバスのピクセル位置情報となります。 X座標とY座標情報でインデックス情報を算出し、渡したバッファから色情報を抽出します。 そして、先ほどのテクスチャに色情報を渡してあげれば MTKView上に絵が描画されます!(パチパチ) ん? 謎の隙間がある?と思った方鋭いですね。 このままではキャンバス全体に対して色を塗りつぶすことができない場合があります。(※できる場合もある) ViewController.swift func updateMetalViewDrawableSize () { // Viewの実際のフレームサイズから縦横比を参考に高さのドットサイズから幅のドットサイズを求める var width = Int(ceil(( self .metalView.frame.width /self .metalView.frame.height) * CGFloat(kMetalTextureHeightDotSize))) // 書き換えた内容(1) ////// // 指定するThreadGroupCountで割り切れなければならない為調整をする let v : Int = width % kMetalThreadGroupCount if v > 0 { width -= v } // 書き換えた内容(1) /////// self .targetMetalTextureSize = CGSize(width : width , height : kMetalTextureHeightDotSize ) self .log( "drawableSize: \(self.metalView.drawableSize) , frame: \(self.metalView.frame) => targetMetalTextureSize: \(self.targetMetalTextureSize) - MetalView" ) self .metalView.drawableSize = self .targetMetalTextureSize そこで先ほど 1. で 後ほど書き換えがあります(1) と記述した内容になります。 kMetalThreadGroupCount で割った場合の余りを幅から引くことで改善できます。 つまり、 GPU側のスレッド数で割り切れない物は描画しきれない という注意すべきポイントがあります。 Appleのドキュメント にて ThreadGroup と GirdSize についての記述はありますが、今回はなるべくバッファは余分に取らずあまりを出さないようにした方が管理が楽だったのでこの方法を取っています。 +αその1、これでは線が引けないので引ける様にする このままではタッチイベントが発生したタイミングでキャンバスに色を1点塗るだけ似合ってしまうので線を引けるように対応します。 ViewController.swift // 2点間の線を引く為のポイント一覧を取得する // 参考(プレゼンハムのアルゴリズム): https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%AC%E3%82%BC%E3%83%B3%E3%83%8F%E3%83%A0%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0 func getLinePoints (p0 : CGPoint , p1 : CGPoint ) -> [CGPoint] { var points = [CGPoint]() var x0 : Int = Int(p0.x) var y0 : Int = Int(p0.y) let x1 : Int = Int(p1.x) let y1 : Int = Int(p1.y) let dx : Int = Int(abs(p1.x - p0.x)) // DeltaX let dy : Int = Int(abs(p1.y - p0.y)) // DeltaY let sx : Int = (p1.x > p0.x) ? 1 : - 1 // StepX let sy : Int = (p1.y > p0.y) ? 1 : - 1 // StepT var err = dx - dy while true { if x0 >= 0 , y0 >= 0 { points.append(CGPoint(x : x0 , y : y0 )) } if x0 == x1, y0 == y1 { break } let e2 = 2 * err if e2 > - dy { err -= dy x0 += sx } if e2 < dx { err += dx y0 += sy } } return points } 2点間の線を引くアルゴリズムは プレゼンハム のアルゴリズム が有名ですね、 ウィキペディア にあった内容を利用させていだだきました。 ブレゼンハムのアルゴリズム(Bresenham's line algorithm)は、 与えられた始点と終点の間に連続した点を置き、近似的な直線を引くためのアルゴリズム。 ブレゼンハムの線分描画アルゴリズム、ブレゼンハムアルゴリズムとも。 コンピュータのディスプレイに直線を描画するのによく使われ、 整数の加減算とビットシフトのみで実装できるので多くのコンピュータで使用可能である。 コンピュータグラフィックスの分野の最初期のアルゴリズムの1つである。 これを若干拡張すると、円を描くことができる これで簡単なメモ書きぐらいには使えそうですね。 +αその2、描ける線の太さを変えられるようにする ペイントツールといえば線の太さを変えれますよね、なのでそちらを再現したいと思います。 先ほどのウィキペディアの記述にあったように プレゼンハムのアルゴリズム を利用すれば円を描くこともできます。 こちら にあったプログラムを参考にさせていただきました。 参考にしたものから円の中を塗りつぶせるように改良しましたが特にパフォーマンスは意識していないのでそちらはご容赦を。 ViewController.swift // 中心点と半径から縁を描く為のポイント一覧を取得する // 参考(ブレゼンハム円描画のアルゴリズム): http://dencha.ojaru.jp/programs_07/pg_graphic_09a1.html func getCircleFillPoints (center : CGPoint , radius : Int ) -> [CGPoint] { var points = [CGPoint]() let centerX : Int = Int(center.x) let centerY : Int = Int(center.y) var cx : Int = 0 var cy : Int = radius var d : Int = 2 - 2 * radius // Left Top var ltx : Int = 0 var lty : Int = 0 // Right Top var rtx : Int = 0 var rty : Int = 0 // Left Bottom var lbx : Int = 0 var lby : Int = 0 // Right Bottom var rbx : Int = 0 var rby : Int = 0 // Top(0, R) var vx : Int = cx + centerX var vy : Int = cy + centerY if vx >= 0 , vy >= 0 { points.append(CGPoint(x : vx , y : vy )) } // Bottom(0, -R) vx = cx + centerX vy = - cy + centerY if vx >= 0 , vy >= 0 { points.append(CGPoint(x : vx , y : vy )) } // Right(R, 0) vx = cy + centerX vy = cx + centerY if vx >= 0 , vy >= 0 { points.append(CGPoint(x : vx , y : vy )) } // Left(-R, 0) vx = - cy + centerX vy = cx + centerY if vx >= 0 , vy >= 0 { points.append(CGPoint(x : vx , y : vy )) } while true { if d > - cy { cy -= 1 d += 1 - 2 * cy } if d <= cx { cx += 1 d += 1 + 2 * cx } guard cy > 0 else { break } // Right Bottom (Bottom To Right) rbx = cx + centerX rby = cy + centerY if rbx >= 0 , rby >= 0 { points.append(CGPoint(x : rbx , y : rby )) // 0 ~ 90 } // Left Bottom (Bottom To Left) lbx = - cx + centerX lby = cy + centerY if lbx >= 0 , lby >= 0 { points.append(CGPoint(x : lbx , y : lby )) // 90 ~ 180 } // Left Top (Top To Left) ltx = - cx + centerX lty = - cy + centerY if ltx >= 0 , lty >= 0 { points.append(CGPoint(x : ltx , y : lty )) // 180 ~ 270 } // Right Top (Top To Right) rtx = cx + centerX rty = - cy + centerY if rtx >= 0 , rty >= 0 { points.append(CGPoint(x : rtx , y : rty )) // 270 ~ 360 } // 上半分は上部分から左右に、下半分はした部分から左右に伸びている //print("[\(ltx), \(lty)], [\(rtx), \(rty)], [\(lbx), \(lby)], [\(rbx), \(rby)]") // Y軸は左右同じ地点を指している事から上版分、下半分でX軸の左端から右端にポイントを追加する事で円を塗り潰します。 for i in lbx ... rbx { if i >= 0 , rby >= 0 { points.append(CGPoint(x : i , y : rby )) } } for i in ltx ... rtx { if i >= 0 , lty >= 0 { points.append(CGPoint(x : i , y : lty )) } } } // 中心線 for i in centerX - radius ... centerX + radius { if i >= 0 , centerY >= 0 { points.append(CGPoint(x : i , y : centerY )) } } return points } 円の描画は左右の端から上もしくは下方向にループで描画していくので、左端から右端にループを伸ばして中の色情報を挿入しています。 バッファに色情報を入れる関数の全容 ViewController.swift func drawCanvas (point : CGPoint ) { let x : Int = Int(ceil((point.x /self .metalView.frame.width) * CGFloat( self .bufferWidth))) let y : Int = Int(ceil((point.y /self .metalView.frame.height) * CGFloat(kMetalTextureHeightDotSize))) var color : simd_float4 = [ self .isRed ? 1.0 : 0.0 , self .isGreen ? 1.0 : 0.0 , self .isBlue ? 1.0 : 0.0 , 1.0 ] let dataSize = MemoryLayout < simd_float4 > .stride if let ptr = self .mTextureBuffer?.contents() { if 0 <= x, x < self .bufferWidth, 0 <= y, y < kMetalTextureHeightDotSize { let index = (x * kMetalTextureHeightDotSize) + y self .log( "draw color[[ \(x) , \(y) ] => \(index) ]: \(color) " ) memcpy(ptr.advanced(by : index * dataSize), & color, dataSize) } } // 現在地点と前回地点の間に線を入れます var linePoints : [CGPoint] = [CGPoint]() let newPoint = CGPoint. init (x : x , y : y ) defer { self .lastPoint = newPoint } // 前回値を保持しておく if let last = self .lastPoint, ! last.equalTo(newPoint) { // 2点間の線を引く為のポイント一覧を取得し追加する linePoints.append(contentsOf : self.getLinePoints (p0 : last , p1 : newPoint )) } else { // ※同じ点を描画する事になるのであまり処理効率は良く無いですが説明上描画した値を入れます。 linePoints.append(newPoint) } if self .pointSize > 1 { // 円描画 var cirlePoints : [CGPoint] = [CGPoint]() for p in linePoints { cirlePoints.append(contentsOf : self.getCircleFillPoints (center : p , radius : self.pointSize / 2 )) } linePoints.append(contentsOf : cirlePoints ) } if linePoints.count > 0 , let ptr = self .mTextureBuffer?.contents() { for p in linePoints { let x = Int(p.x) let y = Int(p.y) guard x < self .bufferWidth, y < kMetalTextureHeightDotSize else { continue } let index = (x * kMetalTextureHeightDotSize) + y memcpy(ptr.advanced(by : index * dataSize), & color, dataSize) } } } できたもの Metalで簡単なペイントツールを作ってみました #iOS #Metal #MetalKit #Texture #2D #Demo pic.twitter.com/yGmzw647Up — aptueno (@aptueno) December 16, 2019 まとめ MetalKitをふんだんに使ったアプリケーションって結構実装コストと学習コストも相まってなかなか使えないことが多い気がします。 この記事をみてMetalKitを使ったアプリケーション、機能の案が浮かんだ方、ラッキーでしたね。 正直私はハイパフォーマンスで絵を描ければなんでもできる気がしているので少しでも参考になったら嬉しいです。 今回のデモは前回同様githubで公開しますのでよかったら動かしたり、ご覧ください。 少しボリューミーな記事になったかと思いますがご覧いただきありがとうございました。 ・iOS-MetalPaintDemo
アバター
デザイン室の仕事 データ表示の際のフォントの選択肢 フォント制作 7セグメントフォント apt7seg(自作7セグフォント) aptQ(既存フォントの改造) ライセンス周り 所感 デザイン室の求人 aptpod Advent Calendar 2019 17日目は、デザイン室 @tetsuがお送りいたします。 今回フォントについてのお話ですが、 apt7seg(自作7セグフォント) に自作している7セグフォントの公開先をリンクしておりますので、もし使っていただけたら Twitter などにフィードバックいただけると嬉しいです! デザイン室の仕事 まずデザイン室ではどのような業務を行っているか、簡単に紹介させていただきます。 製品及びカスタム案件のWeb/モバイル/ネイティブのアプリケーション開発におけるUIデザインを主としています。 その他にコーポレートのブランディング、マーケティングデザイン、カタログ等の印刷物制作、マニュアル制作、Webサイトのデザイン及び運用、製品/案件/プロモーションに伴うPV動画制作などもデザイン室にて、試行錯誤を重ねながら内製しています。 このTech Blogも運用し始めて、記事を書くエンジニアにOGP設定してもらうのも負担だと思い、途中からデザイン室でまとめて作り出しました。最初からやっとけよ…ということも多々ありますが、UI開発もマーケ施策も、やってみてスグ改善できるのが内製の強みだと思っています。 この辺の話も、また別の機会に記事にできればと思います! 今回は、その中でもUIデザインの業務について、特に 高速なデータを表示する際のフォントについてどのように取組んでいるか を紹介させていただきます。 データ表示の際のフォントの選択肢 UIデザインにおいてフォントの重要性は言わずもがなのことですが、数字データを表示する上での選択肢には条件がいくつかあります。 リアルタイムに高速に変化する数字を可読性をもって美しく表示するには、プロポーショナルフォント 1 は適していません。 数字が変化する度にカクカクと揺れてしまうと、読み取りづらく、美しいものではありません。 高速に変化する数字箇所には等幅フォントを使用します。文字通り、文字の横幅が等幅なので、数字が変化しても、間が詰まったり開いたりしません。 メーターパーツなどの場合、等幅であっても桁数の変化にも考慮が必要で、桁数が固定でない場合は右揃えにします。 中央揃えだと、桁数が増えた時に左右に広がり、カクつきます。 しかしUIの全てを等幅フォントで統一するのは現実的ではありません。 ラベルや、テキストなどはプロポーショナルフォントで組みます。文章において等幅では無駄にスペースを使いますし、なにより美しくデザインされたプロポーショナルフォントで表示されることにより、ストレスなく文章が頭に入ってきます。 世にあるフォントの殆どはプロポーショナルフォントです。 コーディングされる方は等幅フォントに馴染みがあるかと思いますが、等幅フォントの選択肢はそう多くはありません。 UIの世界観で見ると、デザイン的に他の要素と馴染む等幅フォントは少ないというのが現実です。 フォント制作 7セグメントフォント 等幅フォントに近いもので、セグメントフォントがあります。あらかじめ予約されたセグメントの表示非表示を切り替える形なので、高速に変化する数字がカクついて表示されることはありません。 一般的には 7セグメントディスプレイ がよく知られています。電光掲示板とか古いタイプのデジタル目覚まし時計に使われているアレですね。 こちらのフォントは等幅フォント以上に選択肢が少なく、既存のものでUI全体の世界観に合うものは見つけれらませんでした。 そこで、素人仕事ですが、7セグフォントを作ってみることにしました。 apt7seg(自作7セグフォント) Visual M2M Data Visualizer(以後VM2M) をデザインするにあたり、未来感のあるUIを実現しつつ可読性を保つメインのフォントとして AXIS を採用しています。このAXISとの親和性を考慮して、スクエアでありつつ若干丸みを保つようセグメントを切ったフォントを作りました。Illustratorでデザインして、Glyphsでフォントパッケージして書き出し。アプリ開発チームに用途ごとに適した形式で共有しています。 素人仕事で大変お恥ずかしいのですが、使ってみようという奇特な方がいらっしゃれば、以下にアップしていますので、どうぞご利用ください。それぞれOTFとWOFF2形式です。フィードバックなどいただけたら大変ありがたいです! ライセンスは SIL OFL(SIL Open Font License) とします。 ダウンロード:apt7seg_sq.zip aptQ(既存フォントの改造) 前項で7セグフォントについて紹介しましたが、数字以外の文字がうまく表現できない(可読性が低くなってしまう)というデメリットがあります。 そのためCAN 2 データなど16進数で表示するものには適していません。 VM2MのUIではこの役割を果たすフォントととして、 Quantico をベースに作成したaptQを使用し、数字、記号、アルファベットを併記する箇所に使用しています。 ライセンス周り フォントの改変時はライセンスに注意が必要です。 上述のQuanticoは SIL OFL(SIL Open Font License) ライセンスです。単独での配布はNGなので、こちらのフォントファイルは公開しませんが、改変してアプリケーションに組み込むことで配布が認められています。 所感 フォントを作ろうとしてみて、あらためて文字を作る職人の皆様の仕事の凄さの一端に触れ、この道を追求しすぎるのはやめようと、打ちひしがれました。。 今回はWEBアプリケーション向けの文脈でフォントについて書きましたが、iOSやAndroid、また他のデバイスネイティブ向けとなると他にも考慮すべき点があります。 組み込みでの有料フォント利用のライセンス料は桁が1つ2つ変わってくるので、なかなか現実的ではありません。 自作してしまえば、その問題もクリアできると考えたこともありましたが、本格的に自作に取り組むのは相当難しいと学びました。 そのようなライセンス事情も相まってか、昨今海外でもデザインシステムと同時にコーポレートフォントを外部依頼で制作する企業が増えていますね。 正直弊社のようなtoB向け事業では実現性はなさそうですが、 メルカリさんの独自フォントのニュース などは本当に羨ましいなぁと眺めております。 デザイン室の求人 最後に宣伝を。 冒頭の デザイン室の仕事 で触れた業務に興味のあるデザイナー募集中です! 一時期は世間の流れに押されて「UI/ UX デザイナー」募集としておりましたが、UIデザイナーもしくはグラフィックデザイナーを募集しております。 UX とは職業ではなくて、みんなが意識するものだと思っています。 弊社における最大のUXポイントとは、データが確実に取得できて(エッジ→サーバ)速く処理して届く(サーバ→制御/UI)こと。 UXの中でタッチポイントのひとつとしてのUIなので、もちろんどこも大事ですが、全体見ようね、その上でUIデザイン/グラフィックデザインで垣根越えたい!そんなデザイナーを募集しております。 産業IoTという分野の中で、デザインに対して意識高くやっていける環境です。僕がジョインするまで、デザインワークは代表の坂元がやっておりましたので、デザインに理解のある環境です! ぜひ、以下からご応募ください。 aptpod Recruit 文字毎に文字幅が異なるフォントのこと。日本語では可変幅フォントとも。 ↩ Controller Area Network 自動車での使用を前提に開発されたシリアル通信プロトコル。 ↩
アバター
aptpod Advent Calendar 2019 16日目担当の榮枝です。 aptpod で ソリューションアーキテクト という職種でSEのような仕事をしています。 システムの全体設計をしたり、見積書を書いたり、プロジェクト管理?みたいなことをしたり、 出張経費精算で領収書をなくして怒られたりしてます。。 さて、そんな中で社内イベントでモバイル通信環境を計測する機会がありましたのでその話をします。 0.前置き 弊社ではテレメトリという領域を軸に様々なプロジェクトを行っています。 モバイル回線を通して 車両のCANデータ・各種センサーデータ・動画データ等を遠隔から高いリアルタイム性を保ちつつ監視・計測したり、 車両の操作といった遠隔制御したり、 といったプロジェクトが多く、その中で、周回コース※などの特定の環境下を車を走らせながらデータを上げたい、といった要望があがってくることがあります。 ※そういったコースは、通信環境が悪い街外れにあることが多いです。 その際、 「(そのプロジェクトで入手/利用できる範囲で)最適構成の通信機器やキャリア」を見つけつつ、 「その環境下で一定時間におおよそどれだけのデータをアップロードできるのか」という調査データがシステム設計のために必要になって来ます。 ただ、たまにかつ急に調査需要が出てくることもあり、調査の際にあたふたしていました。。。 そんな折、先日千葉県茂原のサーキットを貸し切って自社製品を触ってみよう、という社内イベントがありちょうどよい機会だったので、このモバイル通信環境計測の作業手順を整理する意味で、準備~計測~評価までを行ってみました。 また、せっかくの機会だったので、 「通信にどれだけのレイテンシが発生しているのか」 の準備〜評価も並行して行いました。 1.用意するもの プロジェクトによって比較対象とする通信機器は異なってくるのですが、今回は計測手法自体が目的のため特別な通信機器は用意せず、弊社の標準構成 + 3キャリアで計測してみました 車載コンピュータ(通信モジュール内蔵) 写真のもの、通信計測のためだけには大仰な装置ですが apdpodでよく使う装置のためこれを利用しています。 Anker PowerHouse 車載コンピュータ用電力源。今回は6台の車載コンピュータを並行して利用するために用意しました。構成によっては車のシガーソケット供給でも良いです。 計測時間x消費電力x台数を満たせる容量と出力があることは要確認。(今回のケースでは問題ないことを事前に確認してもらっています。) 車載器6台に電力供給できるように、シガーソケットからの電力分岐ケーブルをHWチームに突貫で作成してもらいました。 sim x 3社 x 各2枚(通信帯域幅測定用 & レイテンシ測定用) 通信帯域幅測定用simはデータ容量に注意、計測時間にもよりますが、数十GBは欲しいところです。 実際計測した際は写真を撮ってなかったので、この記事用に改めて録りました。当時の状況と若干異なります。 2.事前設定 おおまかな構成 「帯域幅測定」 iperfコマンドを利用して計測します。 iperfでは、対応するiperfサーバが必要になります。 公開サーバ もありますが、どこかの誰かが同時に使っていたりする可能性もあり、 安定的に利用するには不向きなため、自前で用意する必要があります。 iperfコマンド: iperf3 -t 5 -i 2 -c <自社で用意したiperfサーバドメイン> -p <ポート> の結果をパースして、GPSデータと合わせて(厳密にはGPSとは別のパケットで送るのですが、詳細は省略・・)弊社のintdashサーバに送ります。 「レイテンシ測定」 pingコマンドを使うこと以外は、帯域幅測定とほぼ同じです。 ping受け側サーバは、iperfと違ってサービスを用意したりは不要です。 pingコマンド: ping -c 1 <サーバ側IPアドレス> 2-補足 intdashについて intdashとはaptpod社製の 「⾼速・⼤容量かつ安定的にストリーミングするための双⽅向データ伝送プラットフォーム」 で、高いリアルタイム性を維持しつつ、データの完全回収性(リアルタイムで送れなかったデータを後回収する)システムです。 今回のような計測を行う場合、 「計測完了してみたらデータが取れていなかった」 といったことが起きがちなのですが、intdashを使うことで、データの計測状況をモニタリングすることができます。 また、通信回線が弱くリアルタイムに送れなかった分の計測データも、後から自動でサーバ側に回収されるため、データの取り扱いも非常に楽に行えます。 更に、データを見比べる際、取得したCSVデータをエクセルでグラフ化して、、、といった作業をよくやりますが、intdashを使うとVM2M( Visual M2M Data Visualizer )という可視化ツールも揃っているため、グラフ化作業や、データ比較も非常に簡単に行えます。 intdash便利です! (以上、intdashの宣伝でした) 3.計測 前述の車載器に電源を入れると自動的に計測開始されるように設定しているため、 計測時は電源を入れる以外に特にやることはありません。 (可視化ツールのVM2Mで動作状況を確認するぐらい) 弊社オフィスそばの駐車場から茂原サーキットへ向かう道中と、茂原サーキットで数周走行して計測しました。 弊社オフィスからアクアライン出口まで (アクアライン出口の料金所で、一旦車を止めて計測に区切り) アクアライン出口から茂原サーキット 茂原サーキット ※茂原サーキットでは、あるキャリアは全く通信ができなかったため、グラフ上にもデータが表示されていません 4.評価 計測結果をどういった軸で評価するか(評価指標)は、プロジェクトによって変わるのですが、 よくある/今後欲しくなりそうな指標として以下2種を考え、それぞれについて計算してみました。 (なお、VM2MからはAPIやCSVとしてダウンロードするなどしてデータを取り出すことができ、今回はCSV出力したデータを元に計算しました) 「帯域幅」 1.平均送受信量 この評価指標が求められるケース: 計測機器から発生するデータをおおよそリアルタイムに上げきることができるか?を知りたい時。 通信帯域の計測結果と、計測機器が発生させる秒間データ量の見積もりなどを比較を、設計の参考にします。 計算方法: 帯域幅の平均値 (一部-1と出ているところは0として計算) 注意:千葉県茂原サーキットやその周辺は、非常に電波状況が悪いため、この結果も一般的な通信帯域幅よりだいぶ低い値になっています。 キャリアA UP平均: 2.3Mbit/sec Down平均: 2.1Mbit/sec キャリアB UP平均: 11.8Mbit/sec Down平均: 10.7Mbit/sec キャリアC UP平均: 10.4Mbit/sec Down平均: 9.2Mbit/sec 2.帯域安定度 この評価指標が求められるケース: 恒常的にリアルタイム通信ができるか?を求められるケース 平均送受信量では、通信が悪い時にデータがエッジに溜まり、良い時に回収され、後からデータが流れてくる、といった形になりますが、 リアルタイムにデータ送受信できる必要性が高い場合はこの安定度が必要になります。 計算方法: 標準偏差(ケースによっては最小値) キャリアA UP偏差: 4.8Mbit/sec Down偏差: 4.4Mbit/sec キャリアB UP偏差: 10.1Mbit/sec Down偏差: 9.3Mbit/sec キャリアC UP偏差: 10.7Mbit/sec Down偏差: 9.7Mbit/sec →正直平均値に対して、標準偏差が大きすぎて、、、通信状況の良い東京と、アクアラインの地下や、ほとんど通信できなかった茂原付近等をまとめて計算するとだめですね。。。。 茂原サーキットのみのデータで、安定して測定できたキャリアCだと UP平均: 6.2Mbit/sec Down平均: 5.4Mbit/sec UP偏差: 1.4Mbit/sec Down偏差:1.2Mbit/sec だったので、特定の範囲無いだけを評価するのは良さそうです。 「レイテンシ」 レイテンシも平均と標準偏差を出してみました。 - 計測できなかった時に丸め値として出る0値を除いて計算 - 10sec近く掛かることもあり、平均値を大きく引っ張るため、100msec以上のものを除外した平均も計算しています キャリアA 全平均:73.1msec 除外平均:48.8msec 標準偏差:263.4msec キャリアB 全平均:45.3msec 除外平均:41.2msec 標準偏差:101.1msec キャリアC 全平均:57.3msec 除外平均:46.8msec 標準偏差:178.3msec 異常値を除いた平均レイテンシは各キャリアあまり変わらなそうですね。 若干キャリアBがよいかな?ぐらい。 異常値が少ない、という点でも今回の計測の範囲内ではキャリアBが良さそうです。 まとめと感想 分析はもっと時間をかけて、じっくりやれば色々見えてくるものはありそうです。 せっかくとったデータを活かしきれてない感覚が残り、データアナリティクス的な知識をもうちょっとつけておけば、、と反省です。 ただ、計測手法や、大まかな分析方法は見えたので、今後同様の事例があった時は慌てずに済みそうです! 今回、aptpodのHWチーム、サーバチーム、エンべ(組み込みソフト)チーム各所にご協力頂いて計測実験ができました。 部署横断的に、急遽お願いして協力して頂いたのですが、即座に対応してくれて大変助かりました。 中の人間の自画自賛ですが、この様に広い分野の課題に対して部署横断的にすぐ動けるのもaptpodの強いところです!!
アバター
この記事は、 Aptpod Advent Calendar 2019 の15日目の記事です。 先進技術調査グループの酒井です。 つい先日の12/2〜6にラスベガスで開催されたAWS re:Invent2019でブース展示をしてきました! ブースでは、 Amazon SageMakerとintdashでお菓子の高速検出システムをサクッと構築してみた と 、 AWS RoboMakerとintdashでTurtlebot3を遠隔制御 をパワーアップさせたものを展示しました。 今回の記事は、このパワーアップさせたRoboMakerの展示について紹介します。 前回 は Turtlebot3 に装着した OpenManipulator (アーム) の操縦やセンサー情報の吸い上げができませんでした。しかし、今回はアームの制御とLiDARデータの吸い上げが可能になりました! また、今回はアメリカから日本のTurtlebot3の遠隔制御と、現地に持ち込んだTurtlebot3 のクラウド経由での遠隔制御の2つを展示しました。 遠隔制御では、aptpodの本社がある四谷三丁目にあるオフィスにあるTurtlebot3 with OpenManipulator を アメリカ ラスベガスから遠隔制御しています。 操作感を撮影した動画をYouTubeにアップロードしたのでご覧ください。 www.youtube.com 大きな操作遅延を感じることなく操作できているのが見て取れると思います。 本記事では、デモの構成について少し掘り下げて説明します。 デモの構成 日米間の遠隔制御の構成 まずは日米間の遠隔制御デモについて説明します。日本側ではaptpodのオフィスに、Turtlebot3 を設置しています。 US側では、制御データを送るラズパイと接続したPS3コントローラー、LiDARデータをモニタリングするためのWebアプリケーションを用意します。 US側にあるコントローラーはAWS 東京リージョンに設置されているintdashサーバーを経由して、aptpodのオフィスにあるTurtlebot3に制御コマンドを送信します。 aptpodのオフィスにあるTurtlebot3は、LiDARのデータをAWS 東京リージョンのintdashサーバーを経由して、US側にあるモニタリングアプリケーションにデータを配信します。 図では省略されていますが、四谷三丁目のオフィスの様子を映すために、 C920 というWebカメラとRaspberry Piを接続したものと、Turtlebot3のRaspberry Pi Camera Module に本体の操作とは別のRaspberry Piを取り付けたものも使用しています。 これらのRaspberry Piにもintdash Edge Moduleを搭載しています。これによって、C920からはH.264の動画データ、Raspberry Pi Camera ModuleからはMotion JPEGの画像データの配信を実現しています。 AWS RoboMakerのシミュレーション (Gazebo) と実機を同期させて動かすデモの構成 次に、AWS RoboMakerのシミュレーション (Gazebo)と実行を同期させて動かすデモの構成について説明します。 こちらのデモでは、Turtlebot3、コントローラー、LiDARデータをモニタリングするためのWebアプリケーションはすべてUS側で動かしました。 intdash サーバー、AWS RoboMakerはAWS 東京リージョンのものを使っています。 こちらのデモでは、コントローラーから送信した制御データを、実機とシミュレーション環境上のTurtlebot3がそれぞれ受信し同じ動作をします。 感想 USからの遠隔制御はぶっつけ本番で行ったので遅延がどうなるかが不安でした。しかし、実際に触っていただいたお客様からは、「操作の遅延がほとんど感じられなくてすごい」というコメントを頂けたのでよかったです。 OpenManipulatorを使って、ノベルティーとして配っていたガムを積み上げるデモを行っていたので、ブースを通りかかった人から「Cool !」、「Awesome! 」という声も頂きました。 取材記事のご紹介 クラスメソッド様にブースの様子をご紹介いただきました! dev.classmethod.jp gihyo.jp 様にも取材記事を載せていただきました gihyo.jp おまけ aptpod ブース
アバター
先端技術調査グループの南波です。 aptpod Advent Calendar 2019 の14日目は、チームビルディングのために半年間続けた1on1の振り返りです。 背景 大学で信号処理/機械学習系の研究をしていた経験を活かしたく2018年にアプトポッドに入社し、技術調査を中心に製品開発や機械学習を利用したいお客さまとの共同プロジェクトなどにも参加していました。(今年は AWS Certified Machine Learning - Specialty も取得することができました 🎉 ) 2019年に入ってからは先端技術調査グループのチームメンバーが順調に増え、8月には元々のチームリードがVPoPに就任するために自分がチームリードとなり、現在は5名(うち4名がそれぞれ2019年の4,5,8,10月入社)の新規メンバーが多数を占めるチームと変化してきました。 新規メンバーのオンボーディングは生産性に直結します。そのため、会社・チームごとにいろいろな工夫をされていると思います。 弊グループでは、データサイエンス、ネットワークプロトコル、ロボティクスなどの広い技術領域からメンバーのバックグラウンドに沿ったテーマを任されます。 結果として、 チーム内に同じ領域を専門・担当としている人が少ない という特徴があります。 そのため、他の人の型をなぞることが難しく、 参加初期の仕事の進め方に不安を覚えやすい という課題があるのかなと思っていました。 これに対応するための弊グループのオンボーディング施策として、「 一刻も早く自立して動けるメンバーになってもらう(=自信を持って仕事をしている状態になってもらう) 」ことを目指し、 週1〜月2回の1on1 に取り組んでみました。 勉強方法 施策を始める前からも、実際に始めてからも、ひたすら参考となりそうな資料を漁りました。 以下は覚えている限りの例です。 ヤフーの1on1 1on1マネジメント エンジニアのためのマネジメントキャリアパス エンジニアリング組織論への招待 EM.FM ソフトウェア・ファースト HIGH OUTPUT MANAGEMENT まず、ルールを破れ やってみて 実施してみた所感として やり方の正解がわからない難しさ 成果があったのかどうか明確にはわからない難しさ がずっと付いてまわる感覚がありました。 やり方の正解がわからない難しさ 実はタイトルの「65回」は、 60回:自分が主に聴く側(チームメンバーとの1on1) 5回:自分が主に話す側(VPoEとの1on1) でした。 1on1の目的がそれぞれ異なるために 話題 ログの残し方 お互いの話す量 など、細かい方法が異なってくるのも当然ありうることとは理解しているものの、「自分のやり方は本当に適しているのか?」という疑問は頻繁に浮かびました。 特に、お互いの話す量の観点で、勉強した資料の中では 聴く側は話しすぎない ティーチングよりコーチング という内容が頻出していたため、 「自分が話し過ぎてしまった」 「自分が解決策について考え過ぎてしまった」 という反省をすることがよくありました。 一方で、1on1の主目的をオンボーディングの円滑化においているので、社内技術のキャッチアップ補助などのティーチングの割合が多くなってしまうのも、ある程度は仕方ないのかなと思ったりもしていました。 難しいです 🙃 成果があったのかどうか明確にはわからない難しさ データサイエンスやエンジニアリングの世界で過ごす時間が長かったので、「自分の仮説・施策は正しかったか・効果があったか」を計測できる(ようにしなければならない)という考えが自分の中にある一方、今回の取り組みはA/Bテストもできず、この取り組み以外の環境要因も大きいため、「 本当に月に5時間以上使ってる意義があるのか 」という不安はありました。 幸いチームの活動は順調で、過去に行なった振り返りでも好意的な感想をもらえたので「すぐにやめた方がいい」という判断をするほどではないかな?と思えています。 またチームメンバーのほうから「今までは週1だったけど、今後は月2くらいでいいと思う」といった進め方の意見をもらえていることも、取り組みの価値が浸透していっている感覚としていいなと思っています。 一方で、取り組みが形骸化しないようにする・形骸化してきたらやめられるようにする努力・判断は自分のほうで持つ必要があるなというプレッシャーも感じています。 難しいです 🙃 まとめ 半年間続けた1on1を振り返りました。一言でまとめると「 難しい 」です。 同時に、思い込み・運の良さなだけかもしれませんが、 チームがうまくまわってる感じ は少なからずあります。 「もし月に5時間でこの効果を得られてるのだとしたら レバレッジ効いてるな 」という印象は、エンジニアリングの中で感じられる「少しの最適化で何倍にもコードが速くなったとき」と同じ楽しさ・やってやった感なのかなと思えています。 会社として、チームとして、個人として、おそらく来年も2019年と同じくらい変化のある1年になると思いますが、引き続き 難しさの中に楽しさを見つけていきたい と思います 💪 一緒に楽しさを見つけていける人を探しています ので、ピンッときた方はお気軽にコンタクトお願いします 🙏
アバター
aptpodでは複数のカメラをフレーム単位で同期させて映像を取得できるカメラデバイスの開発を行なっています。 前日の記事 では、このカメラデバイスのエンコードを担当するSoCの話でしたが、 aptpod Advent Calendar 2019 13日目の今回は映像のフロントエンドに使用しているFPGAについての話題です。 カメラデバイスを開発する上で、FPGAでイメージセンサから取得した画像データをリサイズする機能を実装する必要が出てきたのですが、RTL設計経験のない私でも流行りの高位合成でサクッと実装できた話をまとめます。 前日に続き塩出が担当します。 話の流れ まずは高位合成の説明 高位合成での実装手順 アルゴリズムのC++ソース記述方法 C++でのテストベンチ記述方法 シミュレーション結果の確認 まとめ 高位合成とは? 高位合成の詳しい話は色々記事が出ておりますので、そちらを参照してください。 論理回路の高位合成について 高位合成おぼえがき ざっくり言えば、C/C++ベースのアルゴリズムをRTL(レジスタ転送レベル)に変換してくれるツールになります。HDL(ハードウェア記述言語)であるverilogやVHDLなどで記述するよりも複雑な処理を記述しやすく、うまく使えば実装時間が短縮できると思います。ただし、動作はRTLなのでRTLも少し知っていた方が思い通りの動作をさせやすくなるのかなと思います。 今回使用した環境 FPGA :Xilinx社製のFPGA,Artix-7 speed grade 1 ツール:Xilinx社製 Vivado HLS 2018.3,Vivado 2018.3,SDK 2018.3 これらのツールはXilinxの ダウンロードページ からダウンロードできます。 インストールの方法や基本的なツールの使用方法は省略します。 高位合成で実現したいこと 今回のシステムでは、YUV422の画像のデータが Axi Stream として流れています(xilinxでは画像データは基本的にこのAxi Streamで扱うようです)。このストリームデータを受け取って、フルサイズから1/4サイズにリサイズして、同じくAxi Streamで流したいという要望がありました。また、リサイズするかしないかはAxi Liteのアクセスで変えられるようにしたい,画像のサイズも同じく変えられるようにしたいという要望がありました。 また、BRAMがカツカツだったのでラインバッファを使わない、単純な間引きで実装したいという思いがありました。 まとめますと、以下のような要望になります。 YUV422のデータを1/4にリサイズできること リサイズにはラインバッファを使わず,単純な間引きをすること Axi Stream入力,出力できること 画像のサイズ,リサイズ適用制御はAxi Liteから設定可能であること Axi Streamの入出力、Axi Liteでの制御と考えると、RTLでどうかけばいいのか全く検討がつきませんでしたが、色々調べると高位合成だったらサクッと実装できそうだったので挑戦してみました。 高位合成での実装手順 実装の前にアルゴリズムの説明 実装に入る前に,YUV422について触れて間引きのアルゴリズムを説明します。 YUV422とは? YUV422の説明は この記事 を参考にしてください。Y(輝度成分)は毎ピクセルごと、u,v(色成分)は毎ピクセル交互に格納されています。2つで1つなので、隣り合うY2つに対してuvが1セットです。その仕組み上、幅サイズは偶数になります。 ピクセル数 1 2 ・・・ n-1 n データ Y1U1 Y2V1 Yn-1Un/2 YnVn/2 YUV422の間引き方法 2ピクセルで1セットなので、以下の図のように幅方向を間引く時は4ピクセルから2セットを抽出します。なので、4の倍数ピクセル目から1つ、その1つ前のピクセルから1つを採用しあとは間引きます。 高さ方向は単純に偶数行を丸っと間引くようにします。 本当なら2ライン分バッファに入れて、上下左右のピクセルから補間するようにするのが一般的だと思いますが、ラインバッファ(BRAM)削減のためこのような単純間引きをしています。 余談ですが、実際にXilinx が提供しているライブラリに Risizer があり、こちらはRGBのみ対応ですがlinerの補間をしてくれるものになっております。 アルゴリズムのC++ソース記述方法 上記のアルゴリズムをC++で記述します。いきなりですが、コードを公開します。 メインの関数は F2Q() です。 #pragma がいっぱい入っていて見にくいですが、高位合成では重要な文言ですので我慢して見てください。 各関数をざっくり説明しますと F2Q() いわゆるメイン関数.関数の引数をpragma宣言によってAxi Streamだったり、Axi Liteだったり、関数そのものの実行制御をAxi-Liteから可能にしています。 また、メインの処理順序を記述しています。 処理の流れはAxi Streamからデータを読んでフィルタを通してAxi Streamで出すといった感じです。 YUV2Full2Quarter_local() この関数では、flagによってリサイズするかしないかを条件分岐させています。 YUV2Full2Quarter() この関数が先ほど説明したリサイズのアルゴリズムです。 mat_copy() これはリサイズしない時に入力をそのまま出力する関数です。 自分で型宣言していますので、その説明です。 typedef hls::stream<ap_axiu<24,1,1,1> >AXI_STREAM これがAxi Streamの変数型で今回は将来的にRGBも通せるようにデータ幅24bitにしています。 typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC3> RGB_IMAGE これは画像を扱う変数型です。テンプレートでMAXサイズを指定した方がメモリのサイズを抑えられるようです。1pixelあたり8bit、3要素で作成しています。 F2Q.cpp #include "F2Q.h" #include <stdint.h> template < int SRC_T, int ROWS, int COLS, int DROWS, int DCOLS> void mat_copy(hls::Mat<ROWS, COLS, SRC_T> & src, hls::Mat<DROWS, DCOLS, SRC_T>& dst) { HLS_SIZE_T scols = src.cols; HLS_SIZE_T srows = src.rows; loop_height : for (HLS_SIZE_T h = 0 ; h < srows; h++){ #pragma HLS LOOP_TRIPCOUNT min= 1 max= 1 loop_width : for (HLS_SIZE_T w = 0 ; w < scols; w++){ #pragma HLS loop_flatten off #pragma HLS pipeline II= 1 #pragma HLS LOOP_TRIPCOUNT min= 1 max= 1 hls::Scalar<HLS_MAT_CN(SRC_T), HLS_TNAME(SRC_T)> s; src >> s; dst << s; } } } template < int SRC_T, int ROWS, int COLS, int DROWS, int DCOLS> void YUV2Full2Quarter(hls::Mat<ROWS, COLS, SRC_T>& src, hls::Mat<DROWS, DCOLS, SRC_T>& dst) { HLS_SIZE_T scols = src.cols; HLS_SIZE_T srows = src.rows; HLS_SIZE_T dcols = dst.cols; HLS_SIZE_T drows = dst.rows; loop_height : for (HLS_SIZE_T h = 0 ; h < srows; h++){ #pragma HLS LOOP_TRIPCOUNT min= 1 max= 1 loop_width : for (HLS_SIZE_T w = 0 ; w < scols; w++){ #pragma HLS LOOP_FLATTEN off #pragma HLS PIPELINE II= 1 #pragma HLS LOOP_TRIPCOUNT min= 1 max= 1 hls::Scalar<HLS_MAT_CN(SRC_T), HLS_TNAME(SRC_T)> s; src >> s; if ( (h% 2 == 0 ) && ( (w% 4 == 0 ) || ((w+ 1 )% 4 == 0 ) ) ){ dst << s; } } } } template < int SRC_T, int ROWS, int COLS, int DROWS, int DCOLS> void YUV2Full2Quarter_local(hls::Mat<ROWS, COLS, SRC_T>& src, hls::Mat<DROWS, DCOLS, SRC_T>& dst, bool flag) { if (flag){ YUV2Full2Quarter(src, dst); } else { mat_copy(src, dst); } } void F2Q(AXI_STREAM& input, AXI_STREAM& output, uint16_t height, uint16_t width, uint16_t rheight, uint16_t rwidth, bool flag) { #pragma HLS_INTERFACE axis register both port=input #pragma HLS_INTERFACE axis register both port=output #pragma HLS_INTERFACE s_axilite port=height #pragma HLS_INTERFACE s_axilite port=width #pragma HLS_INTERFACE s_axilite port=rheight #pragma HLS_INTERFACE s_axilite port=rwidth #pragma HLS_INTERFACE s_axilite port=flag #pragma HLS_INTERFACE s_axilite port= return RGB_IMAGE in_image(height, width); RGB_IMAGE out_image(rheight, rwidth); #pragma HLS dataflow hls::AXIvideo2Mat(input, in_image); YUV2Full2Quarter_local(in_image, out_image, flag); hls::Mat2AXIvideo(out_image, output); } F2Q.h #ifndef F2Q_H #define F2Q_H #define MAX_WIDTH 1920 #define MAX_HEIGHT 1080 #include "hls_stream.h" #include "ap_int.h" #include "hls_video.h" typedef hls::stream<ap_axiu< 24 , 1 , 1 , 1 > >AXI_STREAM; typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC3> RGB_IMAGE; typedef hls::Scalar< 3 , unsigned char > RGB_PIXEL; void F2Q(AXI_STREAM& input, AXI_STREAM& output, uint16_t height, uint16_t width, uint16_t rheight, uint16_t rwidth, bool flag); #endif /* F2Q_H */ 関数のポイント Axi Streamについて 通常、Axi Streamはバッファを噛ませない限り要素をさかのぼってアクセスすることはできません。なので処理を書くときも要素をさかのぼるような記述をしてはいけません。 さかのぼるとは、例えばライン1とライン2の比較とかがそれに当たります。したければバッファ( LineBuffer や Window )にその要素を入れてやる必要があります。ここら辺は実際のハードを意識した書き方をしなければならないようです。 #pragma HLS dataflow この宣言によって、関数の終了を待たずに次の関数の処理に移れるようになります。イメージは下図を参照してください。 これはAxiStreamのようなデータフローの場合に最適な処理になります。自分のプログラムでは、streamの入力が来たらすぐにそのデータをフィルタに通し、通し終わったものはすぐにstream出力したいので、このpragmaを宣言しています。 この宣言には1つ注意が必要で、図から明らかなのですが処理は関数ごとに書かないと構文エラーになります。なので、 YUV2Full2Quarter_local() ではflagによって実行する関数を分けるだけの処理をさせています。例えばmainの関数の中に条件分岐は書いてはいけません。(#pragma dataflowの前での関数振り分けもできませんでした。) 詳しい説明はXilixの 解説ページ を見てください。 #pragma HLS LOOP_FLATTEN off これはonにすると、入れ子になっているループを平滑化して、内側のループ<->外側のループ間の移動に1クロックかかるのをかからなくしてくれるものなのですが、videoの処理ではoffにするようです。おそらくtlastをアサートするからだと思いますが、詳しくはわかっておりません。 詳しい説明は Xilinxのページ を見てください。 #pragma HLS pipeline これはループ内処理の終了を待たずに次の処理を行うようにさせ、レイテンシを改善するための宣言です。 詳しい説明は Xilinxのページ を見てください。 テストベンチ 先ほどの関数が思った通りの動作をしているかを確認するためのテストベンチになります。 画像サイズ8x6のデータを4x3にリサイズさせています。 元データのpixelデータは要素1に幅成分、要素2に高さ成分、要素3は要素1と要素2を足した値を格納しています。 return 0 で成功したとみなされるため、本当は期待値と比較して正しければ0を返すといったように書く方が望ましいですが、期待値を書くのが面倒だったため、printして期待値と同じかをチェックしています。 F2Q_tb.cpp #include <stdio.h> #include "../passthru.h" #define HEIGHT 6 #define WIDTH 8 #define RESIZE #ifdef RESIZE #define RHEIGHT HEIGHT/ 2 #define RWIDTH WIDTH/ 2 #define FLAG 1 #else #define RHEIGHT HEIGHT #define RWIDTH WIDTH #define FLAG 0 #endif int main( int argc, char ** argv) { AXI_STREAM input; AXI_STREAM output; RGB_IMAGE in_image(HEIGHT, WIDTH); RGB_IMAGE out_image(RHEIGHT, RWIDTH); for ( int h = 0 ; h < HEIGHT; h++) { for ( int w = 0 ; w < WIDTH; w++){ RGB_PIXEL pixel; pixel.val[ 0 ] = w; pixel.val[ 1 ] = h; pixel.val[ 2 ] = w+h; in_image << pixel; printf( "input: %d , %d , %d\n " , pixel.val[ 0 ], pixel.val[ 1 ], pixel.val[ 2 ]); } } hls::Mat2AXIvideo(in_image, input); F2Q(input, output, HEIGHT, WIDTH, RHEIGHT, RWIDTH, FLAG); hls::AXIvideo2Mat(output, out_image); for ( int h = 0 ; h < RHEIGHT; h++) { for ( int w = 0 ; w < RWIDTH; w++){ RGB_PIXEL pixel; out_image >> pixel; printf( "output: %d , %d , %d\n " , pixel.val[ 0 ], pixel.val[ 1 ], pixel.val[ 2 ]); } } return 0 ; } こちらを実行した結果になります。幅、高さ共に狙った間引きになっているのが確認できます。 input:0, 0, 0 input:1, 0, 1 input:2, 0, 2 input:3, 0, 3 input:4, 0, 4 input:5, 0, 5 input:6, 0, 6 input:7, 0, 7 input:0, 1, 1 input:1, 1, 2 input:2, 1, 3 input:3, 1, 4 input:4, 1, 5 input:5, 1, 6 input:6, 1, 7 input:7, 1, 8 input:0, 2, 2 input:1, 2, 3 input:2, 2, 4 input:3, 2, 5 input:4, 2, 6 input:5, 2, 7 input:6, 2, 8 input:7, 2, 9 input:0, 3, 3 input:1, 3, 4 input:2, 3, 5 input:3, 3, 6 input:4, 3, 7 input:5, 3, 8 input:6, 3, 9 input:7, 3, 10 input:0, 4, 4 input:1, 4, 5 input:2, 4, 6 input:3, 4, 7 input:4, 4, 8 input:5, 4, 9 input:6, 4, 10 input:7, 4, 11 input:0, 5, 5 input:1, 5, 6 input:2, 5, 7 input:3, 5, 8 input:4, 5, 9 input:5, 5, 10 input:6, 5, 11 input:7, 5, 12 output:0, 0, 0 output:3, 0, 3 output:4, 0, 4 output:7, 0, 7 output:0, 2, 2 output:3, 2, 5 output:4, 2, 6 output:7, 2, 9 output:0, 4, 4 output:3, 4, 7 output:4, 4, 8 output:7, 4, 11 こちらの結果は波形でも確認できます。stream のoutができていることが確認できました。 まとめ Axi Streamで流れてくる画像のサイズを変更する機能を高位合成で作成した話をまとめました。今回は省いてしまいましたが、高位合成で作成したIPをMicroblazeから制御して画像サイズの変更やresizeするしないなども制御できます。 RTL設計経験がない私でも、Microblazeから制御するのを含めて実働1日程度で実装することができましたので、RTLで書くよりかは早く実装できたのではないかな?と思います。 今回は単純な間引きだけでしたが、バッファを使ってもう少し高度なこともできるので時間があればそのこともまとめてみたいと思います。
アバター
aptpodでは、複数のCANバスのデータを時刻同期して取得できる Synchronized CAN Transceiver に続き、このデバイスと時刻同期し、かつ複数のカメラ映像をフレーム単位で同期させて録画できるカメラデバイスの開発を行なっています。 このデバイスには、TI社製のAM5728というSoC(System on a Chip)の採用を検討しています。このSoCには様々なコアが搭載されており、その中でもマルチメディア処理を担当するIVA(ハードウェアアクセラレータ)は1080p60の映像を処理できる優れものです。 aptpod Advent Calendar 2019 の12日目は、このSoCを使って1920x1080 30fpsのH.264エンコードをしてみた話をハードウェアエンジニアの塩出がお届けします。 AM5728について AM5728はTI社が提供している Sitara™ プロセッサシリーズ の1つです。Sitara™ プロセッサは産業用途向けに開発されたチップで、AM5728はマルチメディア処理用のハードウェアアクセラレータが乗ったシリーズになります。 AM5728の搭載機能を把握するには、以下のブロック図を見るのが一番早いと思います。 特徴として以下のようにかなり機能モリモリなSoCです。 Cortex-A15 Cortex-M4が2つ DSP 産業通信用にRPUも使える GPU搭載 ペリフェラルも充実 映像出力にも強い IVAで1080p60の映像処理性能 ビデオ信号を受けるVIP(Video Input Port)が複数あり,複数の映像入力に対応 VPE(Video Processing Engine)と呼ばれる色空間変換 などなど AM5728商品ページより引用 今回使用したボード 機能評価を行うにあたり、TI社が提供している AM572x 評価モジュール とこれに接続できる カメラモジュール を使用しました。合わせて8万円程度でした。このパッケージにlinuxイメージが入ったSDカードが同封されており、開封後すぐに機能評価を始められます。 カメラモジュールはリビジョンによって使用されているイメージセンサが異なり、リビジョンA1は MT9T11 、リビジョンC1は OV10635CSP となっています。今回手元に届いたのは MT9T11 でした。 このカメラモジュールでは解像度によって30fpsまでの出力が可能です。データシートは こちら から参照ください。 TI社AM572x 評価モジュールの商品ページより どうやってIVAを使うのか? TIの wikiページ に説明があります。ここではかいつまんで説明したいと思います。 前提 SMPモード動作しているA15コアでlinuxが動いています。また、2つあるM4の片方は IPUMM と呼ばれるTI社が提供しているマルチメディア処理用のファームウェアが動いています。このファームウェアはTI社製のRTOSをベースとしています。linuxとRTOS間の通信は remotoproc と RPMsg を使用しています。 IVAにはTI社製のコーデックが乗っているようですが、ソースコードは公開されていないので詳細はわかりません。 IVAとM4間の通信は Codec Engine というTI社が提供しているRTOSパッケージを介して行なっています。 Codec Engine はXDAIS準拠のアルゴリズムを実行するためのAPI群で、おそらくIVAに乗っているコーデックがXDAIS準拠のアルゴリズムになっていると思われます。そのコーデックアルゴリズムを Codec Engine を通じて実行しているものと考えられます。XDAISに関しては詳しく調査しておりませんので詳細は触れません。 下の図は先ほどの wikiページから抜粋 した、マルチメディア処理のソフトウェアスタック説明図です。青色がTI社が提供しているソフトウェアですが、色分けする意味がほとんどないくらいほぼ青色です。マルチメディア処理のフレームワークには,Gstreamerが採用されています。 流れ まずGstreamerのプラグインである、 GST-Ducati Plugin で上流から画像データを受け取る この画像を処理するためのエンコード指示が発行される この時に、inputする画像のメモリとエンコード後のデータ格納先メモリも一緒に指示する この指示は libdce を使用してRPMsg経由で,M4へ指示を送る M4はlinuxから送られてきたエンコード指示を受け取り、 Codec Engine に登録されているIVAのエンコードアルゴリズムを呼ぶ IVAがエンコードを開始する エンコードが終了したらlinuxの処理へ戻り、あらかじめ指示したエンコード後のデータ格納先を参照する Gstreamerなので、エンコードされたデータを下流のエレメントにpushする 1へ戻る Gstreamerのパイプラインの例 Gstreamerのプラグインを使えばIVAを使えるということがわかりましたので,1920x1080 30fpsのH.264エンコードをとりあえず動かしてみます。 GST_DEBUG =ducati*:LOG gst-launch-1. 0 -e \ v4l2src device =/dev/video1 num-buffers = 300 io-mode = 4 ! \ ' video/x-raw,format=(string)YUY2,width=(int)1280,height=(int)720,framerate=(fraction)30/1 ' ! \ queue ! vpe num-input-buffers = 8 ! ' video/x-raw,format=(string)NV12,width=(int)1920,height=(int)1080 ' ! queue! \ ducatih264enc level =level-51 profile =high intra-interval = 10 inter-interval = 5 ! queue ! h264parse ! filesink location = test .h264 エンコードされたデータは ffplay で再生できますが、am5728には入っていなかったのでffplayが使えるPCにコピーして再生します。 $ ffplay test .h264 実行結果は以下のような感じで、とりあえずのエンコードはできました。 VPEで1280x720 -> 1920x1080に変換しているので引き伸ばされている感があります。 コマンドの説明 v4l2src まずはカメラモジュールからデータを取得するために、v4l2srcを使っています。 /dev/video1 はこのカメラモジュールのことを直接意味してる訳ではなく、 VIP と呼ばれるパラレルなビデオ信号を受け取るためのペリフェラルのディスクリプタです。このVIPは結構優れもので、色空間変換やスケーリングなども行えます。 video/x-rawで指定しているフォーマット、サイズがカメラモジュール側で対応していなければ、それに合うようにカメラモジュールからの画像を変換した状態で受け取ってくれます。(もちろん限界がありますが) 重要なのは io-mode です。ここでは4を指定しています。この4はdmabufを意味しています。これにより、VIPで受け取った画像データをユーザ空間のメモリにコピーすることなく下流にpushするようにしてくれます。 vpe vpe はM2Mデバイスで、メモリにある画像データを色空間変換、スケーリングしてくれるペリフェラルです。ここでは、YUY2からNV12へのフォーマット変換とサイズの変換を行なっています。 なお、フォーマット変換をしているのは、IVAのエンコーダがNV12のみに対応しているからです。 ducatih264enc これがH.264エンコードするプラグインです。profileやlevel、IDRフレームの間隔などを指示しています。1920x1080のフルHD画質をエンコードするために、profileはhigh、levelは51を指定しています。 ここら辺はH.264のフォーマットの話なので、詳しいことは割愛します。 下流は filesink でH.264データをそのまま保存するという形です。 本当に1920x1080 60fpsを処理できるのか? 上記のコードを実行すると以下のようなログが得られ、エンコード時間がわかります。 60fpsだとすると16.6msec以内にエンコードが終わっていないといけないことになりますが、1920x1080のエンコード時間はだいたい14~15msec程度で,1フレーム以内に処理が終わっており、1920x1080 60fpsの処理を達成できると思われます。 0:00:00. 646824120 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 661454910 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14541811ns ( 14 ms ) : 13 bytes 0:00:00. 661699724 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0ba10 0:00:00. 669780062 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 684446964 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14629488ns ( 14 ms ) : 13 bytes 0:00:00. 684697797 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb5b1bef8 0:00:00. 702904178 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 718422318 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 15482353ns ( 15 ms ) : 13 bytes 0:00:00. 718666319 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0b830 0:00:00. 723180013 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 738440976 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 15225990ns ( 15 ms ) : 13 bytes 0:00:00. 738673591 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0b8d0 0:00:00. 746872188 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 761427988 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14521315ns ( 14 ms ) : 13 bytes 0:00:00. 761634738 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0b970 0:00:00. 783937083 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 798444083 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14469098ns ( 14 ms ) : 41589 bytes 0:00:00. 799538671 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0b8d0 0:00:00. 807385094 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 821465092 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14044862ns ( 14 ms ) : 21440 bytes 0:00:00. 822195631 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb5b1be58 0:00:00. 830828876 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 845438681 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14575971ns ( 14 ms ) : 13287 bytes 0:00:00. 845915134 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0ba10 0:00:00. 864899390 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 879473246 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14534166ns ( 14 ms ) : 27112 bytes 0:00:00. 880307242 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb5b1bef8 0:00:00. 888348052 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 902456842 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14073979ns ( 14 ms ) : 27080 bytes 0:00:00. 903251797 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb4f0b830 0:00:00. 921819950 1532 0x168690 DEBUG ducati gstducatividenc.c:928:gst_ducati_videnc_handle_frame: Calling VIDENC2_process 0:00:00. 936442281 1532 0x168690 DEBUG ducati gstducatividenc.c:934:gst_ducati_videnc_handle_frame: < ducatih264enc 0> VIDENC2_process took 14588171ns ( 14 ms ) : 14010 bytes 0:00:00. 936930121 1532 0x168690 LOG ducati gstducatividenc.c:1006:gst_ducati_videnc_handle_frame: < ducatih264enc 0> free buffer: 0xb5b1bef8 終わりに TI社のAM5728というSoCを使って1920x1080 30fpsの映像を H.264エンコードをしてみました。使用したカメラモジュールが30fpsしか出ないものでしたが、エンコード時間から60fpsでも問題なくエンコードできそうなことがわかりました。 今後はこのSoCを使って取得したデータを弊社フレームワークの intdash で扱えるようにしたり、並行して製作しているFPGAを搭載したカメラモジュール側と結合したりしていきます。 今回はエンコードしてみたというゆるい内容になってしまいましたが、メモリの動きやTI製のRTOSの使い方、H.264エンコードにメタデータを含める方法、M4とA15の通信など書けそうなことは多々あるので、時間を見つけて書いていきたいと思います。 読んでいただきありがとうございました。 aptpod Advent Calendar 2019 の12日目担当の塩出でした。
アバター
aptpod Advent Calendar 2019 11日目 先日 お菓子のデモの記事 を投稿したキシダがまたお送りします。みなさま、ここ最近『機械学習』とか『AI』とか耳にすることが多くなってきていると思いますが、現実はどれくらいの導入率かご存知ですか? なんと、 14〜15% (※1)らしいです。 意外に導入まで成功しているプロジェクトはまだ増えてきていません。 そこで、『機械学習プロジェクトって具体的にどういうふうにすすめるの?』とか『普通のシステム開発と違って何が難しいの?』という疑問の声に勝手にお答えして、完全な独断と偏見ですが、一般的な機械学習案件に対してよくある困った事例のご紹介とそれに対して私個人が意識していることをこちらにまとめてみようかと思います。 技術的なところではなく、案件における考え方や進め方的なところを中心に掘り下げてます。 ※この記事で出てくる事例は架空のものです。 ※1 総務省・ICR・JCER「AI・IoTの取組みに関する調査 より引用 そもそも機械学習プロジェクトってどんなもの? 『AI的なやつが自動判定していろいろ運用が自動化するやつでしょ??』 うーん、はい、まあそんな感じです(雑) wikipediaさんにも聞いて見ましょう。 機械学習(きかいがくしゅう、(英: Machine learning、略称: ML)は、明示的な指示を用いることなく、その代わりにパターンと推論に依存して、特定の課題を効率的に実行するためにコンピュータシステムが使用するアルゴリズムおよび統計モデルの科学研究である。 うーん、さすがwiki先生、頭がよすぎて凡人には全然わからないです。 私の理解では、機械学習はAI(人間並みの知能をコンピューターで実現するもの)のレベルではなく、あくまで「大量のデータを繰り返しコンピューターが計算し、パターンを見つけること」が機械学習という理解です。 このパターンの算出をアルゴリズムとして定義し、あるデータを入力値として、分類などの算出結果を出力するツールとなります。これを使用してお客様の課題を解決することが、いわゆる『機械学習プロジェクト』を指すと思っています。 もう少し掘り下げると、機械学習は以下の3種類があるそうです。 教師あり学習 教師なし学習 強化学習 私は『教師あり学習』の案件のみ経験があるので、本記事では『教師あり学習』にフォーカスした考察を記載します。   ちなみに、機械学習自体の説明については以下のQiitaがとてもわかりやすかったので、機械学習をザクッと知りたい方はこちらをご参考にどうぞ。 qiita.com そもそも機械学習案件ってどういう流れですすめるの? 機械学習のシステム開発は主に、以下の流れですすめることが多いです。 機械学習のシステム開発の一般的なフロー 上記の流れをぱっとみると、データの前処理とモデルの開発を除けば普通のシステム開発と一緒だな、って感じがしますよね。ただ、機械学習案件では人間がすべて明確なアルゴリズムを仕様として決めているわけではないので、品質の不確実性が必ずつきまといます。それにより案件をすすめることがどれだけ大変になるか、それぞれにフォーカスしてみましょう。今回はすべて触れると長くなってしまうので、前半の3つをメインにフォーカスしてすすめたいと思います。 CASE 1:企画フェーズにありがちなこと 企画フェーズでは、実際にお客さん側に何かしら課題があって、AIを使ってこの課題を解決しよう!という流れが発端で企画されることがよくあります。その際は、以下のことを考えてすすめるケースがよくありますね。 AIを導入することでビジネス的にどんなインパクトがあるか AIが何%くらいの正答率だったらビジネス的にうまくいくか 上記をみるだけだと、なんとなくすすめられそうです。 しかし、 お客様の上司『予算余ったし、ためしに今有り余ってるデータをAI使っていろいろ利用できるんじゃない?』 この上司の一言をそのままにしてすすめてしまうと、悪夢を呼び寄せてしまいます。 AI屋さん『データをつかって試しに簡単なモデルを作ったら、85%くらいの精度がでました!』 お客様『お、いいね!! だったら90%くらいの精度を目指して、モデル作ってみてよ!(これで上司になにか報告できそうだ!)』 AI屋さん『はい!!』 その後、AIベンダーは特定のデータに対して、ひたすら精度の高い90%のモデルを作り出し、早速そのモデルを使って運用することになりました。 お客様『あれ、90%の精度でも、実際に運用に乗せると意外に間違いが目立つよ?』 お客様『こんなに間違いが出ると修正が大変だよ。』 お客様『これ人間が手動でやってたほうが楽だったんじゃ。。。』 お客様『このモデル全然つかえないじゃん。。。どうゆうことだよ。。。』 AI屋さん『・・・・・(え、90%でいいって合意をとったはずなのに・・・。)』 お互い不幸になってしまいました。こういう例も少なからずあり、『なんとなく精度のいいモデルをつくってみてよ、データはあるからさ』から始まるプロジェクトでも、そのままなんとくですすめると『あれ、我々ってなにやりたいんだっけ』、『本当にモデルっているんだっけ』、となるわけです。 どうすればよいのでしょうか・・・。 考察:お客様のビジネスインパクトを評価できるKPIは事前に整合しておく 機械学習案件において、ここを握らなければお客さんとベンダー側のコミュケーションが成り立たないことがほとんどです。そもそもお客様の現状の運用における問題から、そこが改善できたと言えるKPIを予め共通認識としてもっておかなければ、AIつかってみたけどそこまで効果ないね、と残念なシステムができてしまいます。 私の場合は、以下の項目を明確化し、KPIを決めていくように心がけています。 お客様の問題点を明確化する (Why) どの指標を改善すれば問題点が収束するかを明確化する (What) その指標の改善方法に、機械学習を使用する以外に方法はないか検討する (How) 指標がきまってもその時点で終わりではなく、お客様を含めたプロジェクトの参画者はプロジェクトの間は延々を意識し、そのKPIにこだわり続ける責務があります。   また重要なのは、『整合したから大丈夫だよね!』ではなく、お客様周りの環境も必ず変わることを意識することです。そのためには、お互いの間で指標は常に可視化し、本当にその指標で正しいか定期的に議論することがベストです。指標がコロコロ変わることに戸惑うこともありますが、『変わること』より 『変わることが可視化できていないこと』 が問題だったりするので、開発側としてはアジャイル的な思想で柔軟に対応することも必要かもしれないですね。 CASE 2:データの前処理・精査にありがちなこと ここでは、学習を行うために必要な『教師データ』の作成を行うため、お客様が持っているデータについて精査していきます。 精査した後は、実際にお客様のデータに対してタグ付けを行い(これをアノテーションといいます)、教師データを作成していくフェーズとなります。 このフェーズでは主に以下のようなことを考えます。 学習できる量に相当するデータが揃っているか データが構造化されているかどうか セキュリティ的に問題があるかどうか データの加工にコストがどれだけかかるか アノテーションコストがどれだけかかるか やはり、壁だらけですね。 上記の詳細は割愛しますが、仮に上記をクリアしても以下のような落とし穴があるのです。 あるプロジェクトで、提供している商品に対するお客様からいただいたコメントに対して、分類を自動化するツールを開発している会社さんがありました。 お客さんから受け取ったメッセージを『もう一度使いたい!最高!などのプラス評価のメッセージ』、『クレーム、いらつきなどのマイナス評価のメッセージ』、『どちらでもない無評価メッセージ』に分けようと試みています。メッセージの分類をタグ付けとして実際に行うのは、実際にメッセージに対して対応するオペレーターです。 リーダー『データも構造化してデータ収集も自動的にできて、かつオペレーターが正解データを付与してくれる仕組みをつくったぞ!!』 リーダー『よし、ある程度の教師データも揃ったし、モデル作って精度試してみよう!』 しかし、モデルを作った結果、ある出来事が起きます。 リーダー『あれ、、、これはクレームと判定してほしいのに、プラス評価で判定されてしまっている!』 リーダー『あれ、教師データを見てみると、明らかにクレームなのにどちらでもない無評価になってるし、カテゴリ分けが正しくできてないじゃないか!どういうことだ!』 実際にアノテーションしたオペレーターに聞いてみると オペレーター A『僕はこれは褒めコメントだと思ってました』 オペレーター B『私はむしろ何がいいたいかよく分からなかったんでとりあえず無評価にしちゃいました』 リーダー『全然カテゴリ分けの判断基準が統一されていないじゃないか・・・!これじゃあモデルもちゃらんぽらんな判定になっちゃうよ。。。』 このケースは、データを分類する判断基準が曖昧で、正解データのタグ付けの基準がずれてしまい正しい教師データが作れないケースです。どんなにデータ収集の仕組みをシステム的に整えても、こちらを明確にしないと質の悪い教師データが作られてしまいます。 どうすればよいのでしょうか・・・。 考察:データの正解の条件を具体化し、事前に合意をとる このフェーズでは、お客様との期待値調整の一貫として、AIがどのレベルまでのデータを検知すればよいか明確にする必要があります。 そのためには、以下のようなことを意識すると良いかもしれません。 現状の運用方法を深く分析し、データのパターン化を行う パターンを決めるときは、できる限りその境界線を明確にする この境界線は一般的な事実を基準として考えるのではなく、プロジェクトのKPIからどう定義すべきか検討する データのパターンに対して、AIが担保するべきレベルを決める この際、企画フェーズで決めたKPIを元に、AIが提供すべきビジネスインパクトに基づき判断する AIが検出するべき 正解データ のパターンを、関係者全員が理解できるようにする ただこれは実際難易度が高く、データに詳しいクライアントがいないと実施が厳しいことが多いです。クライアント側でデータを一番良く見ている人、関わっている人は必ずプロジェクトに巻き込み、上記の議論を挟むようにすると良いかもしれません。 CASE 3:モデル開発・評価でありがちなこと 実際に企画内容も決まり、使用する教師データの作成も終了したら、いよいよモデルの作成です。 実際にモデルをトレーニングし、実際にできたモデルにたいして結果の正答率などを見てみます。 ここでよく陥りがちな事例とはなんでしょうか。 プロジェクトリーダー『絶対精度を90%にしたいぞ!』 AI屋さん『作成したモデルを評価したところ、今回用意したテストデータでは90%の正答率をクリアしました!』 プロジェクトリーダー『さすがじゃ!!これで大成功ですな!システム化しよう!』 実際システム化して新たなデータで検証したところ プロジェクトリーダー『あれ、全然90%の精度になってないじゃないか!どういうことだ!』 AI屋さん『90%の結果はあくまで評価時に使用したデータの話であって、新たに投入したデータが90%を常に超えるとは限らないです』 プロジェクトリーダー『そんな...!それじゃあ本番では全然つかえないじゃないか!』 AI屋さん『・・・(うーん、データも生き物だから、常に評価時と同じような精度がでるとは限らないんだけどな)』 モデルを作って評価したときはすごいうまく行っているようにみえたけど、本番で動かしてみると全然精度が出ないお話、これもよくあります。 モデルの評価時では、評価向けのデータセットを作る際に精度がでるようにデータセットをコントロールしたりすることは可能で、それ自体も1サンプルのデータなので参考値としては有効な数字です。そのため、1サンプルのデータがうまくいったからすべてのデータがうまくいく!ということはありません。モデルは必ず目標精度を担保できるものではなく、それを前提としてプロジェクトをすすめる必要があります。さて、困りました。 どうすればよいのでしょうか・・・。 考察1:モデルの精度検証は事前に整合したKPIで評価する モデルの評価では、基本お客様と事前に合意をとったKPIで評価し、お客様のビジネスインパクトがどの程度改善されるか計測した後、お客様に提示する必要があります。 最初にモデルの一般的な評価数値である 正答率 などをあげても、最終的にビジネスインパクトを満たす数値かどうか判定できないと意味がありません。 考察2:「がんばれば精度あがるでしょ!」から「精度が上がらないときどうしましょ」にシフトする 私自身、機械学習案件においては「予測誤りをなくすことはほぼ不可能」ということをプロジェクトメンバー全員が共通認識としてもっておく必要があるなと毎度強く感じます。 なぜかというと 精度はデータセットによって変化するため、特定のデータセットの評価値はあくまで参考値にしかならず、その評価結果をモデルが常に担保する数値ではないこと 追加学習を行えば、精度は必ずあがることはないということ 「いずれがんばりつづければ精度はあがるだろう」という空気感は、機械学習案件を泥沼化する考え方、と参考にしてた本やあらゆる有識者の話題にあがっていました。もしお客様にこの空気感があれば払拭しておく必要があり、以下のような精度が上がらなかったときの対応を事前に考え、共通認識として持っておく必要があります。   【精度が上がらない時の対応例】 ある程度人手を使いミスを補填する システムで事前に不正データを削減する プロジェクトを撤退する 3点目については、『え、諦めちゃうの・・・?』という方も多いと思いますが、KPIに対して撤退ラインは必ず設けておく必要があるそうです。機械学習案件は基本ギャンブルという人もいるほど、確実性を担保することが難しい案件でもあります。 まとめ いかがでしたでしょうか。今回は時間の都合上一部のフェーズしかご紹介できませんでしたが、上記以外にも機械学習案件にて難しい点は存在します。通常のシステムとは違い、テストにおける不確実性がどうしてもついてくるので、そのあたりをクライアントとどう握るかが鍵のようです。この点に関しては、弊社でも機械学習案件に関して議論をすすめ、模索しながらすすめているところです。もしこのような取り組みに興味がある方がいらっしゃいましたら、ぜひ以下の採用ページにアクセスをお願いします! 採用情報: https://www.aptpod.co.jp/recruit/ 一部以下の書籍・資料参考: - 5Dヒアリングシート(https://note.com/yukimimu/n/n90b997c6deef) - 仕事で始める機械学習 - O'Reilly Japan (https://www.oreilly.co.jp/books/9784873118215/)
アバター
この記事は Aptpod Advent Calendar 2019 の10日目の記事です。 先端技術調査グループの大久保です。 前回の記事 では、WebSocketのechoサーバにアクセスするwasmをRustとGoで作成しました。今回は、echoだけでは物足りないので、意味のあるバイナリデータをサーバから流して、クライアント側、すなわちWebブラウザ上に表示するまでやってみます。あまり大きくないデータならJSONにして文字列を流せば良いのですが、JSONだとサイズが問題になるようなケースを想定して、JSONよりコンパクトな CBOR を使ってみます。 ちなみに今回はRust作ったところで力尽きたので、対応するGoのコードは残念ながらありません。 実装 3つのクレートに実装を分けます。送るデータの定義、エンコード、デコードを担当するmydataクレート、サーバを担当するws-serverクレート、そしてクライアントを担当し、wasmにコンパイルされるws-clientクレートから成ります。前回同様、wasmにコンパイルされたRustからJavaScriptのAPIを使用するには wasm-bindgen を用います。データが全体でどのように流れるかは少し複雑ですが、以下の図のようになります。 mydataクレート Rustの構造体に格納したデータをシリアライズ/デシリアライズするために今回はcbor_serdeを用います。CBORはバイナリ版JSONみたいなフォーマットで、ちょうどいいRustの実装がありましたので使わせていただきます。シリアライザ/デシリアライザが用意できるならCBOR以外のどんなフォーマットでも同じように書くことができます。 serdeとcbor_serdeに依存するので、以下の依存関係をCargo.tomlに追記します。 [dependencies] serde = "1" serde_derive = "1" serde_cbor = "0.10" このクレートは他クレートから参照されるため、lib.rsにコードを書いていきます。 use serde :: Serialize; use serde_cbor :: de :: from_slice; use serde_cbor :: ser :: {IoWrite, Serializer}; use serde_derive :: {Deserialize, Serialize}; // 送りたいデータを定義する構造体 #[derive(Serialize, Deserialize, Debug )] pub struct MyData { pub time: u64 , pub data: Vec < u64 > , } impl MyData { // バイナリデータへエンコードし、Writeへ書き出す pub fn encode < W: std :: io :: Write > ( & self , w: W) -> Result < (), serde_cbor :: error :: Error > { let mut serializer = Serializer :: new ( IoWrite :: new (w)). packed_format (); self . serialize ( &mut serializer)?; Ok (()) } // スライスからMyDataへデコード pub fn decode (slice: & [ u8 ]) -> Result < MyData, serde_cbor :: error :: Error > { from_slice (slice) } } MyData構造体が実際に送りたいデータの本体で、UNIX時間とu64の配列とします。 #[derive(Serialize, Deserialize)] と指定することで、serde_cborのシリアライザ/デシリアライザでこの型が使えるようになります。これを利用してencode/decode関数を記述します。serdeの力によりエンコーダ/デコーダはかなり短く書くことができます。 ws-serverクレート 先ほど定義したMyDataを送信するサーバを作成します。以下の依存関係をCargo.tomlに追記します。 [dependencies] websocket = "0.23" rand = "0.7" mydata = { path="../mydata" } WebSocketを使うためにwebsocketクレートを、適当なデータ生成のためにrandクレートを、そして先ほど作成したmydataクレートを追加します。 main.rsは以下のようになります。 use mydata :: MyData; use std :: thread; use websocket :: sync :: Server; use websocket :: {Message, OwnedMessage}; // 送るデータのサイズを指定 const DATA_SIZE: usize = 512 ; fn main () { // サーバを立てる let server = Server :: bind ( "localhost:50000" ). unwrap (); for request in server. filter_map ( Result :: ok) { thread :: spawn ( || { let mut client = request. accept (). unwrap (); let ip = client. peer_addr (). unwrap (); println! ( "Connection from {}" , ip); let mut buf = Vec :: new (); for _ in 0 .. 100000 { let data = gen_random_data (DATA_SIZE); // MyDataを生成 data. encode ( &mut buf). expect ( "encode error" ); // バッファへエンコード let message = Message :: binary (buf. as_slice ()); client. send_message ( & message). unwrap (); // エンコードした結果を送る std :: thread :: sleep ( std :: time :: Duration :: from_millis ( 50 )); // 少し待つ buf. clear (); } let message = OwnedMessage :: Close ( None ); client. send_message ( & message). unwrap (); }); } } // 乱数を使ってMyDataを生成する fn gen_random_data (size: usize ) -> MyData { let mut data = Vec :: with_capacity (size); let time = std :: time :: SystemTime :: now () // timeにはUNIX時間を格納する . duration_since ( std :: time :: UNIX_EPOCH) . unwrap () . as_secs (); for _ in 0 ..size { data. push ( rand :: random ()); } MyData { time, data } } MyDataのtimeにはUNIX時間を、dataには適当な長さの乱数列を格納し、50msごとに送信します。 ws-clientクレート ws-clientでは、JavaScriptのAPIを使ってWebSocketのメッセージを受け取り、MyDataへデコードします。そして画面に表示する文字列を作成し、それをJavaScript側に渡します。ブラウザ上への反映はJavaScript側が行うものとします。JavaScriptへ渡す文字列は、サーバから送られてきたMyDataに格納されるUNIX時間と、dataの先頭にある乱数を表示するためのものです。 lib.rsは以下のようになります。 extern crate alloc ; use mydata :: MyData; use std :: cell :: RefCell; use wasm_bindgen :: prelude :: * ; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] pub fn log (s: &str ); } macro_rules! console_log { ( $($t :tt )* ) => ( log ( & format_args! ( $($t)* ). to_string ())) } #[global_allocator] static ALLOC: wee_alloc :: WeeAlloc = wee_alloc :: WeeAlloc :: INIT; use wasm_bindgen :: JsCast; use web_sys :: {ErrorEvent, MessageEvent, WebSocket}; #[wasm_bindgen(start)] pub fn start_websocket () -> Result < (), JsValue > { // WebSocketサーバに接続 let ws = WebSocket :: new ( "ws://localhost:50000" )?; // コールバックの登録 let onmessage_callback = Closure :: wrap ( Box :: new ( move | e: MessageEvent | { on_message (e); }) as Box < dyn FnMut (MessageEvent) > ); ws. set_onmessage ( Some (onmessage_callback. as_ref (). unchecked_ref ())); onmessage_callback. forget (); let onerror_callback = Closure :: wrap ( Box :: new ( move | e: ErrorEvent | { console_log! ( "error event: {:?}" , e); }) as Box < dyn FnMut (ErrorEvent) > ); ws. set_onerror ( Some (onerror_callback. as_ref (). unchecked_ref ())); onerror_callback. forget (); let onopen_callback = Closure :: wrap ( Box :: new ( move | _ | { console_log! ( "socket opened" ); }) as Box < dyn FnMut (JsValue) > ); ws. set_onopen ( Some (onopen_callback. as_ref (). unchecked_ref ())); onopen_callback. forget (); Ok (()) } // WebSocketのメッセージを受け取ったら呼ばれる関数 fn on_message (e: MessageEvent) { // WebSocketからBlobオブジェクトを受け取る let blob: web_sys :: Blob = e. data (). unchecked_into (); // Blobから&[u8]を取り出すために、データのロード後に呼ばれるon_data_loadedを登録する let promise = web_sys :: Response :: new_with_opt_blob ( Some ( & blob)) . unwrap () . array_buffer () . unwrap (); let callback = Closure :: wrap ( Box :: new ( move | array: JsValue | { on_data_loaded (array); }) as Box < dyn FnMut (JsValue) > ); promise. then ( & callback); callback. forget (); // 現状メモリリークしているが、もっといい書き方があるはず } thread_local! ( static CALLBACK: RefCell < Option < js_sys :: Function >> = RefCell :: new ( None )); // コールバックを設定するための関数 // この関数はJavaScriptから呼ばれる #[wasm_bindgen] pub fn set_callback (f: js_sys :: Function) { CALLBACK. with ( | callback | { * callback. borrow_mut () = Some (f); }); } // WebSocketから受け取ったデータを処理する関数 fn on_data_loaded (array: JsValue) { let array = js_sys :: Uint8Array :: new ( & array); let len = array. byte_length () as usize ; let mut buf: Vec < u8 > = vec! [ 0 ; len]; array. copy_to ( &mut buf); let data = MyData :: decode ( & buf). unwrap (); // CBORのデータをMyData型にする CALLBACK. with ( | callback | { if let Some ( ref callback) = * callback. borrow () { let data = format! ( "time : {} value : {}" , data.time, data.data[ 0 ]); // JavaScriptに渡す文字列 let data = JsValue :: from (data); // 文字列をJavaScriptに渡せるように変換する callback. call1 ( & JsValue :: NULL, & data). unwrap (); // 登録されたJavaScriptの関数を呼び出す } }); } 次のようなindex.htmlを用意します。 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" > < title > Wasm + WebSocket Test </ title > </ head > < body > < noscript > This page contains webassembly and javascript content, please enable javascript in your browser. </ noscript > < script src = "./bootstrap.js" ></ script > < p id = "label" ></ p > </ body > </ html > おなじディレクトリにあるindex.jsの内容は次のようにします。 import * as ws_client from "ws_client" ; // 文字列を受け取って、HTMLの内容を更新する function on_data_loaded_callback(data) { var label = document .getElementById( "label" ); label.textContent = data; } // set_callbackはRust側で定義した関数 // ここにon_data_loaded_callbackを渡す ws_client.set_callback(on_data_loaded_callback); 実行結果 ws-server以下でサーバを立ち上げます。 cargo run ws-clientでは、wasmにビルドした後にnpmでWebサーバを立ち上げます。 wasm-pack build cd www npm start ブラウザで http://localhost:8080/ にアクセスすると、次の画像のようにUNIX時間となにかの乱数が表示されるはずです。サーバで生成された値を受け取っていることを確認できました。 ちなみに、今回のwasmファイルのサイズは187KB、最適化後は139KBでした。 thread_local を使うため #![no_std] を指定していないとはいえ、それなりのサイズになりました。 考察 WebAssemblyを使用する利点としてよくあげられるのは速度面です。そして、今回のように検討では受け取ったバイナリのエンコード部分をwasmにしています。これで高速になったり、CPU負荷が小さくなるのでしょうか。それはJavaScriptで同等の実装を作成して比較しなければわかりませんが、あまり今回の構成が優位ではない可能性があります。RustとJavaScriptをどうやってリンクさせているかは wasm-bindgenのマニュアル に書いてありますが、RustとJavaScript間で関数を呼び出しあったりオブジェクトを変換するのはそれなりにコストがかかります。今回のバイナリをデコードする程度であれば、JavaScriptで完結させた方が良い可能性もあります。 ただ、ブラウザ以外のネイティブアプリに載せるクライアントやサーバ側もRustで書くのであれば、送受信データのデコード、エンコード、データ定義等を共通化できる利点もあります。実際今回のコードでは、 MyData 構造体をサーバ/クライアント側両方で扱っています。wasmで実行する場合でも、もっと重い処理を挟むようならRustで書けば最適化できます。さらに将来的にはWASIを使うことで、JavaScriptのAPIに依存することなくwasmでアプリケーションが書ける可能性もあります。 今後の展望 今回の検討でWebAssembly周りのエコシステムについて色々調べましたが、Rustのサポートは妙に充実しています。今回使用したwasm-bindgenにより、かなりシームレスにRustとJavaScriptのやりとりができるようになっています。また、WASIを実装するwasmtimeはRustで書かれているようです。WebAssemblyに力を入れているMozillaがRustの元祖開発元であることもあり、今後WebAssembly周りでRustの利用が増えていくのではないかと個人的には予想しています。 まとめ Rustを使ってWebSocketによるサーバ→クライアント(Webブラウザ)の通信ができた JSONよりコンパクトなCBORを送ることができた。エンコード、デコードも簡単 WebAssembly関連でRustの利用が増えてくはず
アバター
「データが上がって来るの遅いけど、電波悪いからしょうがないな〜」 なんてアッサリ諦めてないでしょうか? そんな方に 「BBRを有効にすればスループットが上がるかも!」 という話を、 aptpod Advent Calendar 2019 の9日目ではお送りします。担当のサーバーサイドエンジニアに憧れるエンベデッドエンジニア ochiai です。 はじめに この記事では、BBRを有効にする方法と、スループットの実測にフォーカスしています。 なので、「BBRのアルゴリズムが知りたいんだよ!」とか、「輻輳制御アルゴリズムって何なのさ?」って方は、他の資料を参考にしてください。オススメを記載しておきます。 キーワード オススメ資料 BBRとは 本家Googleの資料 TCPの輻輳制御アルゴリズムの歴史 n月刊ラムダノート BBRって とはいっても、「いきなりBBRと言われても」と感じるかもしれませんので、簡単な紹介だけしたいと思います。 そもそも、電波が悪くてデータが届かない場合にはどうなっているのでしょうか? もちろん、届かなければ、もう一度送って届くようにして欲しいですよね。とは言っても闇雲に何度も送ったのでは、それはそれでネットワークに無駄な負荷がかかってしまいます。 このあたりを賢く制御してくれているのが、TCPの輻輳制御です。 そして輻輳制御アルゴリズムで今注目をあびているのがBBR(Bottleneck Bandwidth and Round-trip propagation time)になります。 BBRを有効にする では、BBRを動かすための準備をしていきましょう。 LinuxでBBRを使用するためには、カーネル4.9以降である必要があります。 ここでは、弊社で使用している Yocto での設定方法を紹介させて頂きます。 YoctoでBBRを有効にする カーネルコンフィグを変更する必要があるので、menuconfigで変更を行います。 Yoctoでmenuconfig をするには、下記のコマンドを実行します。 $ cd <Yoctoのpokyディレクトリ> $ source oe-init-build-env $ bitbake linux-yocto -c kernel_configme -f $ bitbake linux-yocto -c menuconfig 下図のようにGUIっぽい表示がされるので、方向キーと、スペースキーを使用して操作します。 BBRと、BBRで必要なFQを設定するためには、下記の箇所を変更します。 - Networking support - Networking options <=== Spaceキーで有効化 - TCP: Advanced Congestion Control - BBR TCP <=== * にする - Default TCP congestion control <=== BBRにする - QoS and/or fair queueing - Fair Queue <=== * にする 完了したらsaveしてmenuconfigを終了します。 出来上がった.configを defconfigとして登録 したら、いつも通りにYcotoのイメージを焼き上げて完了です。 BBRが使用されていることを確認する 作成したイメージをデバイスにインストールしたら、BBRが動作しているかを確認します。 確認は、sysctl、ss、tcコマンドで行えます。 変更前 # sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = cubic reno # sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_congestion_control = cubic # sysctl net.core.default_qdisc net.core.default_qdisc = pfifo_fast # ss -tni State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 280 [::ffff:10.0.1.11]:22 [::ffff:10.0.2.127]:61981 cubic wscale:5,7 rto:204 rtt:3.117/0.808 ato:64 mss:1448 rcvmss:1392 advmss:1448 cwnd:10 bytes_acked:4441 bytes_received:4249 segs_out:52 segs_in:87 data_segs_out:47 data_segs_in:40 send 37.2Mbps lastrcv:8 lastack:4 pacing_rate 74.3Mbps delivery_rate 10.8Mbps app_limited unacked:2 rcv_rtt:7 rcv_space:28960 minrtt:1.282 # tc qdisc show qdisc noqueue 0: dev lo root refcnt 2 qdisc mq 0: dev enp1s0 root qdisc pfifo_fast 0: dev enp1s0 parent :1 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1 変更後 # sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = cubic bbr reno # sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_congestion_control = bbr # sysctl net.core.default_qdisc net.core.default_qdisc = fq # ss -tni State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 280 [::ffff:10.0.1.11]:22 [::ffff:10.0.2.127]:61390 bbr wscale:5,7 rto:203 rtt:2.364/0.44 ato:40 mss:1448 rcvmss:1392 advmss:1448 cwnd:11 bytes_acked:4633 bytes_received:4205 segs_out:54 segs_in:88 data_segs_out:49 data_segs_in:39 bbr:(bw:14.0Mbps,mrtt:1.317,pacing_gain:2.88672,cwnd_gain:2.88672) send 53.9Mbps lastrcv:6 lastack:2 pacing_rate 41.7Mbps delivery_rate 14.0Mbps app_limited unacked:2 rcv_rtt:5 rcv_space:28960 minrtt:1.317 # tc qdisc show qdisc noqueue 0: dev lo root refcnt 2 qdisc mq 0: dev enp1s0 root qdisc fq 0: dev enp1s0 parent :1 limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 low_rate_threshold 550Kbit refill_delay 40.0ms 測定 測定方法 測定は下記のパターンで行いました。 ネットワーク環境 LTE LTE(アッテネータ 10db 1 ) LTE(アッテネータ 20db) LAN LAN(パケットロス 1% 2 ) LAN(パケットロス 2%) LAN(パケットロス 4%) LAN(パケットロス 8%) 輻輳制御アルゴリズム BBR CUBIC(元々使用していたアルゴリズム) 送信データは、IoTデバイス内で動作する弊社の intdash Edge Module から、秒間一定量のデータを送信して計測を行いました。 測定結果 Network CUBIC [KByte/sec] BBR [KByte/sec] LTE (attenuation 0db) 674 631 LTE (attenuation 10db) 572 616 LTE (attenuation 20db) 261 559 LAN (loss 0%) 1126 1105 LAN (loss 1%) 1112 1138 LAN (loss 2%) 1078 1102 LAN (loss 4%) 779 1063 LAN (loss 8%) 381 1051 測定の結果、CUBICは、パケットロスする環境と、しない環境で、あきらかな差が出ました。 しかし、BBRは、パケットロスする環境と、しない環境で、あきらかな差が出ませんでした。 まとめ BBRとCUBIC(元々使用していたアルゴリズム)で、データのスループットを比較した結果、下記グラフのようになりました。 一般的に言われている通り、パケットロスが多い時ほど効果を発揮する 事がわかりました。 このことから、 通信環境が悪くパケットロスするような環境で、今までより早く送信できる ことが期待できます。 おわりに 最後まで読んでいただき、ありがとうございます。 はじめに書いた「BBRを有効にすればスループットが上がるかも!」が記事の趣旨の8割といっても過言ではないのですが、Yoctoで有効にする方法や、動作確認方法も参考になったようでしたら幸いです。 弊社のシステムでも、ここで紹介させて頂いた方法でBBRを有効にしたデバイスを順次出荷していく予定です! 以上、 aptpod Advent Calendar 2019 9日目担当の ochiai でした。 明日は、先端技術調査グループ期待の新星、 okubo さんです! アッテネータはLTEのアンテナに接続して信号を減衰させる装置 ↩ パケットロスはtcコマンドにより擬似的に発生させた ↩
アバター
はじめに こんにちは。 アドベントカレンダー 8日目担当、 サーバーサイドエンジニアの miyauchi です。 昨年は「 Goとクリーンアーキテクチャとトランザクションと 」と「 Vim初心者から中級者の入り口くらいまで 」を書きました。 早いものでもう一年経つのですね。 さて、弊社ではバックエンドシステムを従来のモノリス型のサービスから、徐々にマイクロサービスへ移行中です。 そんな中で分散トレーシング(とりわけOpenTracing)について調査する機会がありました。 よって、今年はGoのアプリケーションで、OpenTracingを使うときのコード集を、解説ありで書いていきたいと思います。 「OpenTracingとは!」や、「分散トレーシングとは!」のような概念部分は書きません。 また、OpenTracingのTracer実装である jaeger についても詳細は書きません。 OpenTracingを使ったサンプルコード中心で、 チュートリアルに毛が生えたくらいの入門記事となります。 OpenTracingの具体的な使い方については、案外まとまった記事がないのかなと思ったので書こうと思ったわけですが、 一方で、少し今更感もあります、、、が、張り切っていきましょう! 分散トレーシングについて 「概念部分は書かない!」とは言ったものの、いきなりコードはやっぱりしんどいかもしれません。 なので、分散トレーシングとOpenTracingについて少しだけ説明します。 サービスを細かく分割していくと、 一回のリクエスト(HTTPリクエストなど)で複数のサービスやミドルウェアをコールすることになります。 そうすると困ってくることの一つが、一つの処理を追跡することが難しくなることが挙げられます。 例えば 「このリクエストすごく重いんだけど、いっぱいサービスコールしていてどこがボトルネックなのかわからない!」 といったものですね。 分散トレーシングは分散システムにおいてのサービス間呼び出しの追跡を助けます。 そして、OpenTracingは分散トレーシングのための、仕様、ライブラリ、ドキュメントなどから構成される開発キットのようなものです。 詳細は公式Webサイトより。 https://opentracing.io/ それでは、早速ですがコードを見ていきます。 今回紹介するソースコードは全て aptpodのGithubリポジトリ にあります。 手元でコードを見たい方はクローンをしておきましょう。 以降、コマンドなどの実行は全て、同リポジトリルートがカレントディレクトリである前提で進めます。 ミニマムアプリケーションを実装する 実装アプリケーションについて 最初にクライアントからHTTPコールするだけの、すごく簡単なミニマムアプリケーションを実装していきます。 こんな感じです。 +--------+ | client | +---+----+ | ^ | | client | | side | +--------+--------- | server | | side | v | v +--------------+ | hoge-service | +---+----------+ はい、簡単ですね。 OpenTracingの実装の流れ 流れの説明をする前に、 OpenTracingのAPIを使っていると、 Tracer 、 Span 、 SpanContext という用語が頻出します。これらについて少しだけ補足します。 要所で簡単に解説はしますが、ちゃんと理解するために The OpenTracing Data Model 、 及び The OpenTracing API を読むことをおすすめします。 ここでは、 Span と Tracer についてだけ簡単に図と合わせて説明します。 -----------------------------------------------------> time | parentspan | +----------------------------------------------+ | child span | | child span | +------------+ +-----------------------+ | grand child span | +------------------+ 任意の時間に特定の期間である Span Span は入れ子にすることができる 一番最初に Span を開始するのが Tracer 概念だけふんわりイメージできればOKです。 繰り返しになりますが、正確な理解は本家ドキュメントを読むことをおすすめします。 では実際の実装の流れについて説明します。 OpenTracingを使うときは基本的には次のような流れになります。 Tracer の初期化(原則アプリケーション起動時に一回) Tracer の取得 Span の開始 親の Span から新しい Span の開始 親の Span から新しい Span の開始 Span の終了 ...(任意の Span の開始と終了) Span の終了 ...(任意の Span の開始と終了) Span の終了 Tracer から Span を開始して、 Span から Span を入れ子にしていくイメージです。 実装する Tracerを初期化する Span は Tracer が開始します。 まずはアプリケーションのどこからでも参照できる、 GlobalTracer に Tracer をセットしましょう。 Tracer はインターフェースとなっていて実装は様々です。 今回は、jaegerを使うので こちら のコードを参考にして、 Tracer を初期化します。 jaeger独自の設定部分の解説は割愛します。 詳細は 公式 や Githubのリポジトリ を参照してください。 下記のコードのいずれかをコールすると jaeger版の Tracer の初期化が完了し、 GlobalTracer にセットされます。 // ./lib/tracer.go package lib import ( "io" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" "github.com/uber/jaeger-client-go/config" "github.com/uber/jaeger-client-go/log" "github.com/uber/jaeger-lib/metrics" ) func InitGlobalTracer(serviceName string) (io.Closer, error) { return initGlobalTracer(config.Configuration{ Sampler: &config.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &config.ReporterConfig{ LogSpans: true, }, }, serviceName) } func InitGlobalTracerProduction(serviceName string) (io.Closer, error) { return initGlobalTracer(config.Configuration{}, serviceName) } func initGlobalTracer(cfg config.Configuration, serviceName string) (io.Closer, error) { jLogger := log.StdLogger jMetricsFactory := metrics.NullFactory return cfg.InitGlobalTracer( serviceName, config.Logger(jLogger), config.Metrics(jMetricsFactory), ) } GlobalTracer にセットするコードを紹介しましたが、そうではなく、オブジェクトとして生成し使い回すことも可能です。 // ./lib/tracer.go func CreateTracer(serviceName string) (opentracing.Tracer, io.Closer, error) { var cfg config.Configuration jLogger := log.StdLogger jMetricsFactory := metrics.NullFactory cfg.ServiceName = serviceName return cfg.NewTracer( config.Logger(jLogger), config.Metrics(jMetricsFactory), ) } クライアントを実装する 次にクライアント側の実装コードを見ていきます。 // ./minclient/main.go package main import ( "errors" "io/ioutil" "log" "net/http" "github.com/aptpod/opentracing-sandbox/lib" "github.com/opentracing/opentracing-go" ) func main() { // Tracerの初期化 closer, err := lib.InitGlobalTracer("client") if err != nil { panic(err) } defer closer.Close() // Tracerの取得 tracer := opentracing.GlobalTracer() // Spanの開始 // このSpanを引き回す span := tracer.StartSpan("get_hoge") // Spanの終了 defer span.Finish() // Hogeの呼び出し! res, err := getHoge(span) if err != nil { panic(err) } log.Println(res) } このコード断片については難しいことはないと思います。「OpenTracingの実装の流れ」 でも述べた通り、 Tracer を初期化して... Tracer を取得して... Span を開始して... 処理(Hogeサービスの呼び出し)をして... Span を終了する。 という流れが読み取れると思います。 一点、 HTTPコールは単純にHTTPを呼び出せばよいというわけではありません。 サービス間の流れを追跡するOpenTracingですから、 Span の情報を引き継ぐ必要があります。 コード中でのHTTPコールの実装箇所は getHoge() 関数内にあたるので、 掘り下げて見ていきましょう。 // ./minclient/main.go ... omitted func getHoge(span opentracing.Span) (string, error) { // リクエストの生成 // 今回はHogeのPath url := "http://localhost:18080" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return "", err } // ※ Tracerを使ってSpanの情報をInject if err := span.Tracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header), ); err != nil { return "", err } // 通常のHTTPアクセス resp, err := Do(req) if err != nil { return "", err } return string(resp), nil } サービス間で情報を引き継ぐには、HTTPリクエストに何かしらの方法で Span の情報を埋め込む必要があります。 引き継ぐ Span の情報( 情報 だと少し語弊があるかもしれませんが。。。) を SpanContext と言います。 OpenTracingでは Tracer が SpanContext をリクエストに埋め込む責務を追います。送信側でリクエストに SpanContext を入れることを Inject 、逆に受信側で SpanContext を取り出すことを Extract と言います。 また、 Inject して、 SpanContext を運ぶものを Career と言います。 今回はHTTPヘッダを Career として、 SpanContext を Inject しています。 さて、この Career ですが、HTTP以外のRPCコール時ももちろん考慮されていて、HTTPヘッダ以外にも種類があります。 気になる方はOpenTracingの実装を読んでみましょう。 // SpanContext伝播のイメージ +----------+ | service1 | `Inject` `SpanContext` to `Career` +---+------+ | | Carry using `Career` | v +----------+ | service2 | `Extract` `SpanContext` from `Career` +---+------+ さて、 Inject をしたら、あとは通常通りにHTTPコールすればOKです。 コード中では resp, err := Do(req) の部分ですが、この部分はそのままHTTPコールしているだけなので割愛します。 hogeサービスを実装する 今度は受信側の実装です。 package main import ( "log" "net/http" "time" "github.com/aptpod/opentracing-sandbox/lib" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ) func main() { // tracer の初期化 tracer, closer, err := lib.CreateTracer("hoge-service") if err != nil { panic(err) } defer closer.Close() log.Println("start hoge") http.ListenAndServe(":18080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("hoge") // SpanのExtract spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_hoge", ext.RPCServerOption(spanCtx)) defer span.Finish() // ダミー処理 <-time.After(time.Second) w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "name": "hoge!" }`)) })) } まずはTracerの初期化部分です。 サービス側の実装は GlobalTracer へセットするのではなく、新たに Tracer を生成してみます。 GlobalTracer でも良いのですが、サンプルなので。。。 1アプリケーションで複数の Tracer が必要な場合はそれぞれで生成するとよいでしょう。 次はリクエストハンドラ( http.HandlerFunc )の実装です。 リクエストハンドラ内で、まず伝播された SpanContext を Extract します。 そして、その SpanContext を利用して、新しく Span を開始しています。 ext.RPCServerOption(spanCtx) は SpanContext から適切な StartSpanOption を返す関数です。 そういうものだと思って指定します。 内容が気になる方は全然難しくない実装なのでぜひ追って見てください。 さて、ここまででミニマムコードができました。 動作確認 準備 Dockerで jaeger を起動しておきます。下記に示すコマンドで、UIも含めアプリケーションが起動します。 docker run --rm -p 6831:6831/udp -p 6832:6832/udp -p 16686:16686 jaegertracing/all-in-one:1.7 --log-level=debug ブラウザでUIを開いておきます。アドレスは http://localhost:16686/search です。 実行 hogeサービスを起動します。 go run ./minserver/*.go クライアントを実行します。 go run ./minclient/*.go ブラウザを確認すると、Spanなどが確認できるかと思います。 jaegerのUI めでたく、トレースできました。。。。。 しかしながら、 Span の情報が少なかったり、実装も少し冗長だったり、まだまだ実アプリケーションへの導入は難しそうです。 以降のセクションで少しずつ洗練していきましょう。 その他の実装リファレンス Tags/Logs/Baggage Span には Tag 、 Log 、 Baggage という付加情報をつけることができます。詳細は Tags, logs and baggage を参照してください。 このセクションではそれぞれの情報をどうやって付与するか、コード断片を紹介します。 SpanにTagをつける Span にはkey-value形式で任意の Tag をつけることが可能です。 tagValue := ... span.SetTag("tag.key", tagValue) また、 github.com/opentracing/opentracing-go/ext に Tag の変数があるので、適宜利用すると良いでしょう。 // Tagをつける例 ext.SpanKindRPCClient.Set(span) ext.SpanKindRPCServer.Set(span) ext.HTTPUrl.Set(span, "http://localhost:8080") ext.HTTPMethod.Set(span, http.MethodGet) ext.HTTPStatusCode.Set(span, http.StatusOK) SpanにLogを記録する github.com/opentracing/opentracing-go/log を使い、 Span 内で Log をつけます。 // Key-Value形式で簡易指定 span.LogKV( "hoge.log.key1", "hoge-log", "hoge.log.key2", "hoge-log2", ) // 型情報ありの指定 span.LogFields( tracelog.String("hoge.logfields.string", "hoge-log"), tracelog.Bool("hoge.logfields.bool", true), ) Log はこの関数を呼び出したタイミングで、 Span に印をつけるイメージです。 Baggageを使って情報を任意の情報を伝播する OpenTracingには(APIのI/Fを変更せずに)任意のオブジェクトを伝播する手段があります。 運ぶ情報を Baggage と呼びます。 強力な機能ですが、使い方には注意しましょう。 送信(client)側 baggage := ... span.SetBaggageItem("baggage", baggage) 受信(server)側 spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx)) defer span.Finish() // Baggageの取得 baggage := span.BaggageItem("baggage") context.Contextを利用する アプリケーションをモジュール化して実装すると Span オブジェクトを引き回すのが辛いケースがあると思います。 そこで、OpenTracingのライブラリには context.Context 内に Span を含めるユーティリティが用意されています。 利用できるのであれば、コードがシンプルになるので context.Context 使う方が良いと思います。 Contextに Span をセット span := ... ctx := ... ctx = opentracing.ContextWithSpan(ctx, span) Contextから Span を取得 ctx := ... span := opentracing.SpanFromContext(ctx) Contextにある Span を親として、新しく Span を開始 ctx := ... // もし、ctxにSpanがない場合はGlobalTracerからSpanを開始する span := opentracing.StartSpanFromContext(ctx, "new-operation-name") サンプルアプリケーションを実装する これまでは、ミニマムな実装とコード断片で解説してきました。 前提となるコード知識はついてきたので、サービスの依存関係がすこしだけ複雑なアプリケーションのソースコードも見ていきます。 ソースコードを細かく解説すると長くなるので、解説はミニマムアプリケーションとの差分だけにします。 実装アプリケーションについて まずはアプリケーションの全体像です。 矢印の番号は呼び出し順序を表しています。 +--------+ | client | +---+----+ 1| ^ | | client | | side | +--------+------------------------------------------------- | server | | side | v | v +--------------+ 3 +--------------+ | hoge-service |---->| fuga-service | +---+----------+ +---+------+---+ 2| 4| 5| | | +---------+ | | | v v v +--------------+ +-------------+ +-------------+ | foo-service | | bar-service | | baz-service | +--------------+ +-------------+ +-------------+ この呼出関係をjaegerで見ることができればOKです。 サービスの実装 各サービスのHttpハンドラに Tag を付けていきます。 Tag については説明したとおりです。 http.HandlerFunc 実装のみの抜粋です。 // ./server/baz.go http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("baz") // SpanのExtract spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_baz", ext.RPCServerOption(spanCtx)) defer span.Finish() // タグ付け ext.HTTPMethod.Set(span, r.Method) ext.HTTPUrl.Set(span, r.URL.String()) <-time.After(time.Second) w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "name" : "baz!" }`)) // タグ付け ext.HTTPStatusCode.Set(span, http.StatusOK) }) ポイントは // タグ付け ext.HTTPMethod.Set(span, r.Method) ext.HTTPUrl.Set(span, r.URL.String()) ... omitted // タグ付け ext.HTTPStatusCode.Set(span, http.StatusOK) です。全ハンドラにこのコードを実装しておけば、 UIから見たときにフィルタしやすくなります。 サービス間のAPIコールの実装 クライアントの実装と同じことをすれば良いのですが、 今回の実装は Span を context.Context に入れて伝播します。 まずは、 context.Context にセットするコードです。 // ./server/hoge.go ... omitted // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_hoge", ext.RPCServerOption(spanCtx)) defer span.Finish() ... omitted // ctxにセット ctx := r.Context() ctx = opentracing.ContextWithSpan(ctx, span) // APIコール _, _ = cli.Call(ctx, "foo") _, _ = cli.Call(ctx, "fuga") あまり難しくはないですね。 次はAPIコール部分の cli.Call の実装です。 context.Context から Span を開始する点がポイントです。 // ./server/client.go type ServiceClient struct { tracer opentracing.Tracer } func (s *ServiceClient) Call(ctx context.Context, serviceName string) (int, map[string]interface{}) { // ctxにセットされているSpanから新しく子のSpanを開始する span, _ := opentracing.StartSpanFromContextWithTracer(ctx, s.tracer, fmt.Sprintf("call_%s_%s", http.MethodGet, serviceName)) defer span.Finish() serviceURL, _ := url.Parse(fmt.Sprintf("http://localhost:%d", portMapping[serviceName])) req, _ := http.NewRequest(http.MethodGet, serviceURL.String(), nil) // Tag付け ext.SpanKindRPCClient.Set(span) ext.HTTPUrl.Set(span, serviceURL.String()) ext.HTTPMethod.Set(span, "GET") // Tracerを使ってSpanの情報をInject _ = span.Tracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header), ) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var responseJson map[string]interface{} json.NewDecoder(resp.Body).Decode(&responseJson) return resp.StatusCode, responseJson } context.Context から Span を開始するのはこの部分です。 // ctxにセットされているSpanから新しく子のSpanを開始する span, _ := opentracing.StartSpanFromContextWithTracer(ctx, s.tracer, fmt.Sprintf("call_%s_%s", http.MethodGet, serviceName)) defer span.Finish() 簡単ですね。 さて、これで準備はできました。 動作確認 Jaegerを起動した状態で サービス群を起動します。 go run ./server/*.go クライアントを実行します。 go run ./client/*.go そして、 localhost:16686 にアクセスしてみましょう。あえて、スクショははりません。ぜひ動かしてみてください! サービス間の呼び出しがこんな感じで見ることができたらシステムの運用ちょっと楽になりそうですよね。 実装をもっと楽したい サーバー側、クライアント側のコードを見ていただくと分かると思いますが、 まだ少し冗長です。 Tag をつける部分とか。。。 go-stdlib のライブラリを使うと、 もう少し処理を共通化することができます。 リポジトリを覗いてみると、サーバー側はミドルウェアの実装、クライアント側はトランスポートの実装があるので、 適宜利用すると良いでしょう。 但し、まだ実験段階のようなので、使うときには注意しましょう。 最後に 少し長い記事となってしまいました。 OpenTracing x Go の理解の一助となれば幸いです。 ここまでくれば、もっと実践的なアプリケーションサンプル Hot R.O.D. - Rides on Demand も読み解きやすいと思います。 また、導入する際は Best Practices もあるので、 このページも必読です。 話は変わりますが、弊社では サーバーサイドエンジニアを随時募集中です! 私達の取り組みに興味があればぜひ気軽に遊びに来てください。 以上、アドベントカレンダー8日目担当の miyauchi でした! 明日は弊社のエンベデッドチームの要、 ochiai さんです! 参考リンク集 記事中のコード -> https://github.com/aptpod/opentracing-go-sandbox OpenTracing オフィシャルサイト -> https://opentracing.io/ OpenTracing ユーティリティライブラリ(実験段階) -> https://github.com/opentracing-contrib/go-stdlib jaeger オフィシャルサイト -> https://www.jaegertracing.io/ jaeger Github -> https://github.com/jaegertracing/jaeger jaeger client Github -> https://github.com/jaegertracing/jaeger-client-go
アバター