TECH PLAY

アプトポッド

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

248

aptpod Advent Calendar 2021 の14日目を担当するHW/OTグループの矢部です。前日の塩出さんの記事でエディタの話がありましたが、私が使用しているエディタはEmacsです。社内では多分2、3人しかいない少数派。何年か前は Spacemacs で使っていましたが、ここ数年は Doom Emacs を Vim キーバインドで利用しています。もはやデフォルトのEmacsのキーバインドはほぼ忘れているレベルです。たまに間違ってキーバインドが変わってしまうととても焦る。 この勢いでエディタの話をしてもよいですが、一応ネタとしては別のものを事前に用意していたので、そちらの話をします。 今回私が紹介するのは、aptpod 製エッジコンピュータ EDGEPLANT T1 で動画の処理を行い、かつそのデータをサーバにリアルタイムに上げてみるというお話です。 磯光雄監督の最新作「地球外少年少女」が2022年1月にNetflixで公開されることを記念して、電脳コイル的なAR表現をできたら面白いなと思ってはいましたが、残念ながら力及ばす、今回は比較的簡単なもののご紹介です。 どういうことをやるのか VPIとは デモ 2D Image Convolution Harris Corners Detector 2D Image Convolution & Harris Corners Detector 走行デモ まとめ どういうことをやるのか aptpod でよくあるソリューションとして、 intdash Edge を利用してカメラ画像をサーバに上げ、Web UIで動画として見る、というものがあります。以下の図は HPで Device Connector *1 の説明に使われているものですが、ここで書いているところの「データデバイス(カメラ) → Device Connector → Edge Agent → intdash API」という経路でデータが流れていきます。 今回は、この Device Connector に手を加えて、取得した画像に処理を行ってサーバに上げるようにしてみます。エッジコンピューティング的なアレですね。 比較できないとよくわからないと思うので、元画像も同時にサーバに上げるようにします。こちらは Edge Agent にプラグインという形でデータをクローンする機能を追加します。 イメージとしては大体こんな感じです。 VPIとは 記事のタイトルにも入れた、NVIDIA の Vision Programming Interface (VPI) について軽く説明します。 VPIは、CPUやGPUなどJetson内の複数の計算リソースを利用して画像処理を行うことができるAPIです。CUDAのようにゴリゴリにGPUを使い倒す、というようなものではなく、比較的実装しやすい方法で効率的に処理が行えます。バックエンドにCPUやGPUなどの複数のリソースを選択できるようになっており、実装する際にはバックエンドが何かを意識せず共通のコードが書けるため、同じアルゴリズムを容易に様々な環境で動かすことができるようになります。GPUプログラミングとかやらなくていいのは楽ですね。 docs.nvidia.com www.macnica.co.jp デモ では、実際に動かしてみましょう。今回は、VPI のページにある サンプルコード の中から、以下の2つを動かしていきます。 2D Image Convolution Harris Corners Detector Device Connector や Edge Agent のプラグイン部分のコードはちょっと量があるため、今回ここでは記載しません。画像処理部分については、処理負荷の都合上変更したところが一部ありますが、NVIDIAのサンプルコードをほぼそのまま使っています。 2D Image Convolution イメージのエッジを検出する処理になります。 左が元のカメラ画像、右が処理を行った後の画像です。静止画なのでわかりづらいですが、ほぼ遅延なく処理できていました。ちなみに右で見切れているのは aptpod の製品 ANALOG-USB Interface です。 2D Image Convolution Demo Harris Corners Detector コーナー検出のアルゴリズムの一つです。Harrisは人の名前。 en.wikipedia.org こちらも同様に、左が元のカメラ画像、右が処理を行った後の画像です。 サンプルにあった写真を印刷してみたんですが、品質の問題か全然検出されてません。その代わり、EDGEPLANTのロゴはがっつり検出されています。あとパンダの鼻もしっかり色ついてますね。 Harris Corners Detector Demo 2D Image Convolution & Harris Corners Detector せっかくなので両方同時に動かして負荷を見てみます。負荷情報などのデータは Edge Agent が常時サーバに上げているため、このようにエッジの状態を容易にチェックできます。 負荷チェック CPUはほとんど上がらず、GPU(グラフの紫ライン)が10%~30%あたりを行ったり来たりしています。まだまだ余裕がある印象です。 走行デモ 最後に、実際に車に EDGEPLANT T1 を乗せて、今回の処理を動かした際のデモ動画。道路の白線や橋などがエッジとしてきれいに検出されていて、結構気に入っています。 youtu.be この動画は画像処理の精度が上がるように HD(1280x720) 画質で撮っているため、GPU使用率が +10% 程度上がっていますが、問題ないレベルです。 まとめ EDGEPLANT T1 での VPI 実装をお試しで作ってみましたが、VPI の使い方などまだまだ改善の余地がありそうです。今回紹介した VPI でのエッジ処理の事例は aptpod ではまだありませんが、画像処理を行いたいというニーズは多いですので、ブラッシュアップされたものがどこかで日の目を見るかも?しれません。 aptpod のエッジ側のチームでは、こんな感じでいろいろやっています。がっつり組込みから Linux アプリケーションまで、エンジニア随時募集してますのでぜひぜひ ご連絡 ください。Emacs使い、待ってるよ! *1 : デバイスからデータ送受信を行うためのモジュール
アバター
aptpod Advent Calendar 2021 の13日目を担当する製品開発グループの塩出です。 動画関連のハードウェア開発を行なっていくうちにサーバサイドの開発に惹かれ、昨年の10月ごろにハードウェアの開発からサーバーサイドの開発へジョブチェンジしました。 Advent Calender1日目 の記事で紹介のあった、ハードウェアエンジニア→サーバサイドエンジニアの事例の人です。 サーバサイドは未経験だったのでキャッチアップは大変ですが、知らない技術にたくさん触れることができとても充実しています。 今回は復習も兼ねてサーバサイド開発で使用している技術を紹介したいと思います。 使っている技術一覧 Golangでの開発 おまけ 開発の進め方 サーバサイドに異動してから関わった案件 さいごに 使っている技術一覧 分類 項目 言語 RDB PostgreSQL 時系列データベース ストレージ AWS S3 ミドルウェア NATS Streaming         仮想化技術 ソースコード管理 CI GitLab CI Document作成 プロトコル iSCP on QUIC, iSCP on Websocket アーキテクチャ マイクロサービス 実行基盤 AWS, GCP, Azure, オンプレ 認可制御 OAuth2.0 製品のサーバーアプリケーションは100%Go製です。また、マイクロサービスを採用しており各アプリケーションへのリバースプロキシには Traefik を採用しています。認可制御はOAuth2.0を採用しています。 各アプリケーションの開発は基本的にDocker環境で行い、CIでのテストで問題なければ AWS上にある開発環境にデプロイする流れで開発を進めています。ただ、アプリケーション自体は実行基盤に縛りがないようにしています。 RDBは PostgreSQL と MySQL に対応していましたが、両方対応するメンテコストの削減のため現在は PostgreSQL のみになりました。 エッジから送信される計測のデータ点を時系列で格納するために、 InfluxDB を採用しています。 Golangでの開発 分類 項目 実装方針 クリーンアーキテクチャ タスクランナー Makefile O/Rマッパー GORM Webフレームワーク gorilla, chi, Gin テストツール Dredd ソースコード管理 GitLab CI GitLab CI エディタ VS Code, Vim サーバアプリケーションはクリーンアーキテクチャを取り入れて実装しています。クリーンアーキテクチャ自体は色々ネットに良い記事たくさん出ているので、ここではその詳細は省略します。クリーンアーキテクチャを採用すると複雑になる DI(Dependency Injection) の解決には wire を使用しています。 webフレームワークは gorilla , chi , Gin などを採用しています。サービスごとで差がありますが、なるべく標準準拠なものに寄せていきたい想いがありゆくゆくはGinの採用をやめるかもしれません。 テストはユニットテスト、結合テストの他に API Documentとの整合性チェック用で Dredd のテストを実施しています。 使用するエディターに縛りはありませんが、 社内のサーバサイドエンジニアはVS Code派とVim派しかいないようです。なお、VS Codeの方が人口多めで、筆者もVS Code派です。 ソースコードの管理はGitLabで行っており、MR (マージリクエスト)のレビューは基本的にそのリポジトリオーナーがやっています。案件によってはリポジトリオーナー以外もレビューを行います。 おまけ 開発の進め方 分類 項目 開発フレームワーク スクラム開発 タスク管理 JIRA, GitLab issue 設計ドキュメント作成 Confluence コミュニケーションツール Slack, Google Meet リリースサイクル 四半期に一度 弊社は四半期に一度製品リリースを行なっているので、期の終わりごろまでに設計を行なって期の頭から中頃までに実装、QA中に次の期の設計を開始するという感じで開発を進めています。 実装のフェーズでは、スクラム開発を採用して進めています。といっても、ガチガチのスクラムではありません。1スプリントは1週間で、スプリントの最初にプランニング(そのスプリントで行うタスクの内容共有)を行い、実装を進めていきます。そして毎日15分程度の会議の中で進捗確認や、困りごとの共有などを行なっています。コロナの影響で基本的に在宅勤務なので、会議はGoogle Meetを使用して基本的にオンラインで行っています。また、会議以外にもSlackで困りごとの相談を随時行っています。 レビューは毎スプリントごとではなく、ある程度の見せられる成果物が出来上がるころにプロダクトオーナーを交えて行っています。 サーバサイドに異動してから関わった案件 未経験でしたが色々な案件に携わることができ、良い経験をさせてもらいました。以下に約1年間で関わった案件を列挙します。 メディアサービスのスケールアウト設計、実装 メディアサービスはH.264をHLS形式で配信する機能や、H.264をブラウザアプリケーションにリアルタイムに転送する機能を有しています。 認証認可サービスのモデル変更の実装 文字で書くと簡単そうですが、かなり大幅な改修でした 次期iSCPのClient, Serverの実装 マルチテナント化の実装 さいごに 最後まで読んでいただきありがとうございました。技術紹介を通してどのような感じで開発をしているのか少しでも伝わったら幸いです。 この記事を読んで弊社での開発に興味を持った方、もっと詳しく話を聞きたいと思った方はぜひ 採用ページ からエントリーをお願いします。
アバター
aptpod Advent Calendar 2021 の 10日目を担当する、プロジェクト開発グループの尾澤です。 現在、自動車のOBD-IIから車両データを取得するiOSアプリを開発しています。 簡易的なデータ収集で事足りるケースであれば、OBD-IIアダプターと呼ばれる2000〜4000円程度のデバイスとスマートフォンだけでお手軽に環境を揃えることができます。 今回は簡単なiOSアプリを作りながら、 LELink というOBD-IIアダプターを利用した車両データ取得の過程を辿りたいと思います。細かい話はなるべく省き、全体像がざっくりと掴めるようにしたつもりです。どうぞ最後までお付き合いください。 接続する データを取得する コマンド ATコマンド OBDコマンド BLE通信 実装 定義 初期化 CBCentralManagerDelegate CBPeripheralDelegate コマンド送信 データを解析する まとめ 接続する OBD(On Board Diagnosis)とは、自動車の排気ガス低減を目的とし、排ガスの異常を検知するための仕組みです。当初、自動車メーカーごとに異なっていた規格等を共通化し、OBD-IIとなりました。異常の検知だけではなく、車速やエンジンの回転数など、様々なデータをリアルタイムで取得することもできます。 OBD-IIと外部機器を繋ぐためのデバイスがOBD-IIアダプターです。 ELM327 というチップを搭載したものが多いようです。接続方法としてBLE(Bluetooth Low Energy)タイプとWiFiタイプが存在しますが、本記事ではBLE接続タイプの LELink という製品を利用します。 www.outdoor-apps.com 全体の構成は以下のようになります。 データを取得する 車両データの取得は、アプリからOBD-IIアダプターに対しコマンドを送信してそのレスポンスを受信する、言わば対話形式で進みます。 コマンド コマンドはたとえば以下のようなものです。 AT Z AT FE AT E0 AT H1 AT SP 0 010C AT から始まるのが ATコマンド 、16進数で表されるのが一般的に OBDコマンド と呼ばれます。 ATコマンド ELM327 に対するコマンドです。主に通信周りやレスポンスの形式などの設定を行います。車種によって必要なコマンドが異なる場合があります。詳細は こちら をご参照ください。 OBDコマンド ECU(Electronic Control Unit)に対するコマンドで、各種車両データを取得します。先頭の2桁をSID(Service ID)、次の2桁をPID(Parameter ID)と呼びます。詳細はWikipediaに良くまとめられています。 en.wikipedia.org これによると、たとえば 010C はエンジン回転数(Engine speed)を取得するコマンドであることが分かります。 なお、 Each manufacturer may define additional services above #9 (e.g.: service 22 as defined by SAE J2190 for Ford/GM, service 21 for Toyota) for other information という記述は注意が必要で、トヨタ車の場合は車種によってSID= 01 の代わりに 21 が使われます。 ATコマンドで初期設定を行った後にOBDコマンドでデータを取得する、というのがよくあるパターンだと思います。 BLE通信 LightBlue というアプリを利用すると、そのデバイスに用意されているServiceとCharacteristicの一覧を確認することができます(BLE通信自体の解説は割愛します)。 LightBlue® Punch Through ユーティリティ 無料 apps.apple.com このアプリと LELink を繋いでみると、幸い用意されているServiceは1つ(UUID=0xFFE0)、その中のCharacteristicも1つ(UUID=0xFFE1)だけで、かつそのCharacteristicがWriteとNotify属性を併せ持つことが分かるので、これがコマンド送信(Write)とレスポンス受信(Notify)を兼ねたものであると推測できます。 実装 ではここまでを踏まえて、いったんサンプルコードを示します。おおよそ以下のような処理です。 BLE上で対象のOBD-IIアダプター(ペリフェラル)を探す OBD-IIアダプターと接続 コマンドを送信 レスポンスを受信 定義 LELink はWriteもNotifyも同じCharacteristicですが、異なる場合でも対応できるように定義は敢えて分けています。 /// ServiceのUUID private let serviceUUID = CBUUID(string : "FFE0" ) /// 通知用CharacteristicのUUID private let notifyCharacteristicUUID = CBUUID(string : "FFE1" ) /// 書き込み用CharacteristicのUUID private let writeCharacteristicUUID = CBUUID(string : "FFE1" ) private var centralManager : CBCentralManager? private var discoveredPeripheral : CBPeripheral? 初期化 CBCentralManager を生成します。 centralManager = CBCentralManager(delegate : self , queue : nil ) CBCentralManagerDelegate 次に CBCentralManagerDelegate を実装します。まずは CBCentralManager の状態を監視し、利用可能であればスキャンを開始し、指定のServiceを持つペリフェラルを探索します。 public func centralManagerDidUpdateState (_ central : CBCentralManager ) { switch central.state { case .poweredOn : centralManager? .scanForPeripherals(withServices : [ serviceUUID ] , options : nil ) default : break } } ペリフェラルが見つかったら接続開始します。 public func centralManager (_ central : CBCentralManager , didDiscover peripheral : CBPeripheral , advertisementData : [ String : Any ] , rssi RSSI : NSNumber ) { discoveredPeripheral = peripheral centralManager?.stopScan() centralManager?.connect(peripheral, options : nil ) } 接続に成功したら、当該ペリフェラルが持つServiceを探します。以降は CBPeripheralDelegate に処理が移ります。 public func centralManager (_ central : CBCentralManager , didConnect peripheral : CBPeripheral ) { peripheral.delegate = self peripheral.discoverServices([serviceUUID]) } CBPeripheralDelegate CBPeripheralDelegate を実装します。Serviceが見つかったら、そのService内のCharacteristicを探します。 public func peripheral (_ peripheral : CBPeripheral , didDiscoverServices error : Error? ) { guard let service = peripheral.services?.first( where : { $0 .uuid == serviceUUID }) else { return } peripheral.discoverCharacteristics( nil , for : service ) } Characteristicが見つかったら、その中から指定のUUIDのものを探します。通知用に対しては更新を受け取るように設定します。 public func peripheral (_ peripheral : CBPeripheral , didDiscoverCharacteristicsFor service : CBService , error : Error? ) { // 通知用characteristic if let characteristic = service.characteristics?.first( where : { $0 .uuid == notifyCharacteristicUUID }) { self .notifyCharacteristic = characteristic discoveredPeripheral?.setNotifyValue( true , for : characteristic ) } // 書き込み用characteristic if let characteristic = service.characteristics?.first( where : { $0 .uuid == writeCharacteristicUUID }) { self .writeCharacteristic = characteristic } } 前後しますが、後述のコマンド送信のレスポンスをここで受信します。受け取ったData型をString型に変換します。 public func peripheral (_ peripheral : CBPeripheral , didUpdateValueFor characteristic : CBCharacteristic , error : Error? ) { guard let value = characteristic.value, let string = String(data : value , encoding : .utf8), ! string.isEmpty else { return } print(string) } コマンド送信 エンジン回転数を取得する 010C を送信します。終端文字として \r を付加してData型に変換し、書き込み用Characteristicに対してこれを書き込みます。 guard let data = "010C\r" .data(using : .utf8) else { return } guard let characteristic = writeCharacteristic else { return } discoveredPeripheral?.writeValue(data, for : characteristic , type : .withResponse) データを解析する さてレスポンスを受け取ることができたら、いよいよその解析です。 010C を送信した結果、たとえば 7E8 04 41 0C 10 A6 のようなレスポンスが返却されたとします。この意味については下記のサイトに分かりやすく整理された図がありますのでそちらを参照ください。 www.csselectronics.com 結論だけ書くと最後の 10 A6 が値の部分に相当しますが、これではまだ意味を成しません。まるで謎解きのようですが、これをさらに物理値変換式にかける必要があります。先述のWikipediaによるとPID= 0C の変換式は、 また単位は rpm であることが分かります。ここで 10 A6 をそれぞれ10進数に変換した 16 166 を上記のAとBに代入します。 ということで、めでたくエンジン回転数1065rpmというデータを得ることができました! まとめ iOSアプリでOBD-IIから車両データを取得する流れを駆け足で説明しました。今回のサンプルコードは1つのコマンドを投げ、そのレスポンスを受け取るところまでですが、実際にはこれをループさせ、コマンドを順番に投げる仕組みがあると便利です。 少しでもご興味頂けたら幸いです。 以上、お読み頂きましてありがとうございました。
アバター
aptpod Advent Calendar 2021 9日目担当のハードウェア/OT製品Grの野本です。 ANALOG-USB Interface が Industrial I/O に対応し、アナログデータを簡単に取り扱えるようになりましたのでご紹介します *1 。 はじめに Industrial I/Oとは Industrial I/O インターフェイスの操作方法 サンプリング周波数を設定する 入力電圧範囲を設定する 入力チャンネルを設定する タイムスタンプ出力を設定する バッファサイズを設定する アナログ入力を開始する アナログ入力データを取得する アナログ入力を停止する 設定内容を確認する IIO Oscilloscopeの操作方法 アナログ入力波形を表示・解析する ネットワーク経由で利用する まとめ はじめに 弊社のインターフェイス周辺機器 EDGEPLANT Peripherals は、これまで intdash Appliances に付属する形としてのみ提供していました。 より幅広いお客様にお使いいただけるよう、 USB-CAN Interface ではLinuxのオープンソース CANドライバであるSocket CANに対応 *2 しましたが、ANALOG-USB InterfaceにおいてもLinux標準インターフェイスである Industrial I/O に対応しました。 この対応で、より簡単にアナログデータを扱うことができるようになるほか、オープンソースのオシロスコープアプリケーションであるIIO Oscilloscopeを利用した表示&解析処理が可能になりました。 *3 Industrial I/Oとは Industrial I/O とは、ADC(アナログ-デジタルコンバーター)やDAC(デジタル-アナログコンバーター)といったデバイスをサポートするためのLinux kernelのサブシステムです。以下のようなセンサーデバイスを操作するアプリケーションの標準インターフェイスを提供しています。 ADC/DAC 加速度センサー ジャイロセンサー 色/光センサー 磁界センサー 圧力センサー 近接センサー 温度センサー Industrial I/O インターフェイスの操作方法 Industrial I/Oには、2種類のインターフェイスがあります。 インターフェイス 用途 /sys/bus/iio/iio:deviceX/ 設定変更および確認のために利用 /dev/iio:deviceX/ 入力データの読み込みに利用 ※ deviceX の X には項番が入ります 以降、ANALOG-USB Interfaceでの利用例を説明します。 サンプリング周波数を設定する 10000[Hz]に設定する場合、以下のコマンドを実行します。 $ echo 10000.0 > /sys/bus/iio/devices/iio\:deviceX/in_voltage_sampling_frequency 設定可能なサンプリング周波数は以下のコマンドで確認できます。 $ cat /sys/bus/iio/devices/iio\:deviceX/in_voltage_sampling_frequency_available 10000 . 0 5000 . 0 2500 . 0 1250 . 0 625 . 0 312 . 5 156 . 25 10 . 0 1 . 0 0 . 1 0 . 01 入力電圧範囲を設定する チャンネル0の入力電圧範囲を「max: 10V、min: -10V」(+-10V)に設定する場合、以下のコマンドを実行します。 $ echo pm10v > /sys/bus/iio/devices/iio\:deviceX/in_voltage0_range 設定可能な入力電圧範囲は以下のコマンドで確認できます。 *4 $ cat /sys/bus/iio/devices/iio\:deviceX/in_voltage_range_available pm10v pm5v 入力チャンネルを設定する チャンネル0~7を有効化する場合、以下のコマンドを実行します。 $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage0_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage1_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage2_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage3_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage4_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage5_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage6_en $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage7_en タイムスタンプ出力を設定する タイムスタンプ出力を有効化する場合、以下のコマンドを実行します。 *5 $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_timestamp_en バッファサイズを設定する バッファに保存するサンプル数を100個に設定する場合、以下のコマンドを実行します。 *6 $ echo 100 > /sys/bus/iio/devices/iio\:deviceX/buffer/length アナログ入力を開始する enableに1を書き込むと、 /dev/iio:deviceX のバッファへのアナログデータの入力が開始されます。 $ echo 1 > /sys/bus/iio/devices/iio\:deviceX/buffer/ enable アナログ入力データを取得する アナログ入力データ(CH0-7有効、タイムスタンプ有効)を取得する例です。以下のコマンドを実行すると、1サンプルとして16bitのデータx8個が取得されます。 $ cat /dev/iio\:deviceX | xxd - 00000000: 1111 2222 3333 4444 5555 6666 7777 8888 00000010: TTTT TTTT TTTT TTTT 1111 2222 3333 4444 00000020: 5555 6666 7777 8888 TTTT TTTT TTTT TTTT ... # 1111: 0chのデータ(16bit) # 2222: 1chのデータ(16bit)、以下他チャンネルも同様 # TTTT: タイムスタンプ(64bit) アナログ入力を停止する enableに0を書き込むと、 /dev/iio:deviceX のバッファへのアナログデータの入力が停止されます。 $ echo 0 > /sys/bus/iio/devices/iio\:deviceX/buffer/ enable 設定内容を確認する 上記の設定内容は、 cat コマンドで現在の設定値を確認することができます。 以下に、デフォルト設定の出力を示します。 # サンプリング周波数 $ cat /sys/bus/iio/devices/iio\:deviceX/in_voltage_sampling_frequency 10 . 0 # 入力電圧範囲 $ cat /sys/bus/iio/devices/iio\:deviceX/in_voltage*_range pm10v pm10v pm10v pm10v pm10v pm10v pm10v pm10v # 入力チャンネルの有効化設定 $ cat /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_voltage*_en 0 0 0 0 0 0 0 0 # タイムスタンプ出力 $ cat /sys/bus/iio/devices/iio\:deviceX/scan_elements/in_timestamp_en 0 # バッファサイズ $ sudo cat /sys/bus/iio/devices/iio\:deviceX/buffer/length 2 以上のように、sysfsによる操作で簡単にアナログ入力の設定やデータ取得が可能とりました。 IIO Oscilloscopeの操作方法 IIO Oscilloscope は、Analog devicesによるIndustrial I/Oに対応したオープンソースの波形測定・解析ツールです。 クロスプラットフォームに対応しており、Windows/Linux/Macをサポートしています。インストール方法は こちら をご参照ください。 アナログ入力波形を表示・解析する EDGEPLANT T1 にANALOG-USB Interfaceを接続した環境での操作例を説明します。 IIO Oscilloscopeを起動します。 IIO Oscilloscope起動画面 Device SelectionでANALOG-USB Interface(EP1-AG08A)を選択します。 デバイスを選択すると、各種設定項目が表示されます。 上記コマンドラインでの操作に相当する項目がメニューに表示されており、各種設定操作がGUI上で可能です。 Plot画面で取得したいチャンネルにチェックを付けて、キャプチャボタンを押すと波形が表示されます。 例:サイン波(5[V]、100[Hz]) Plot TypeをFrequency Domainに変更すると、周波数解析が可能です。 例:サイン波(100[Hz])の周波数解析 ネットワーク経由で利用する IIO Oscilloscopeはバックエンドに libiio を利用しており、ローカルバックエンドだけでなく、ネットワークバックエンドでも利用することができます。 *7 ローカルバックエンドは単一のアプリケーションしかデバイスにアクセスできませんが、ネットワークバックエンドは複数のアプリケーションからアクセスが可能です。 画像: https://wiki.analog.com/resources/tools-software/linux-software/libiio ネットワークバックエンドを利用する場合、ANALOG-USB Interfaceが接続されているLinuxマシンでiiod *8 サーバーを起動する必要があります。 $ sudo iiod 次に、閲覧したいPC(今回はWindows10)でIIO Oscilloscopeを起動します。 Settings>接続メニューを選択してネットワーク上のIIOデバイスをスキャンし、接続ボタンを押下してサーバーに接続します。 以降は、ローカルバックエンドと同様の操作が可能です。 まとめ ANALOG-USB InterfaceがLinux標準インターフェイスであるIndustrial I/Oに対応したことをご紹介しました。 弊社のインターフェイス周辺機器 EDGEPLANT Peripherals は、より幅広いお客様にお使いいただけるよう様々な対応を予定しておりますので、今後の続報にご期待ください。 弊社製品にご興味がありましたら、お気軽に こちら よりお問い合わせください。 以上、お読みいただきありがとうございました。 *1 : Industrial I/O対応のドライバは GitHub にて順次公開予定です。 *2 : https://tech.aptpod.co.jp/entry/2021/06/18/100000 *3 : IIO Oscilloscopeだけでなく、 libiio などのIndustrial I/Oのインターフェイスに対応したライブラリ/アプリケーションから利用可能になります。 *4 : Industrial I/Oによるインターフェイスでは、入力電圧範囲「max: 5V、min: 0V」(+5V)の設定をサポートしていません。+5Vの設定を利用する場合はシステムコールによるインターフェイスを利用してください。 *5 : タイムスタンプ出力を有効化した場合、サンプル毎のアナログ入力データの末尾にタイムスタンプ(単位はナノ秒)がs64_t型で格納されます。 *6 : バッファに保存するサンプル数です、バイト数ではありません。 バッファ内のサンプル数が設定値を超えた場合、バッファへの新しいデータの書き込みは行われず、バッファ内の古いデータが保持されます。取りこぼし無くデータを取得し続けたい場合は、余裕を持った値を設定し、バッファが一杯になる前にバッファからデータを読み出す必要があります。 *7 : ネットワークバックエンドを利用する場合は、iiodサーバーとしてLinuxマシンが必要です。 *8 : iiodはlibiioのインストールで同時にインストールされます。
アバター
aptpod Advent Calendar 2021 の8日目を担当します、コーポレート・マーケティング室、デザインチームの高森です。今回はAdobe XD(以降XD)を使ってフリートマップの中でアクションしているようなプロトタイプの作り方を紹介します。 【サンプル動画】 プロトタイプでフリートマップを制作するメリット これまでの作り方 XDを使った作り方 STEP1:マップ上をカーソルが動いている背景用動画を準備する STEP2:デザインに背景用動画を配置する STEP3:車両一覧クリック時に反応するカーソルを配置する STEP4:クリック時に遷移する画面を作成する STEP5:アートボード間のリンクを作成する 終わりに プロトタイプでフリートマップを制作するメリット aptpodでは、計測中の複数車両をフリートマップに表示して俯瞰できるビューをお客様に紹介する機会が多く、実際に Visual M2M Data Visualizer でも確認いただくことができます。既にあるサンプルを見てもらうことでも機能は伝わりますが、各用途に合わせたフリートマップを提示することでアプリケーションをよりリアルに感じてもらうことができ、仕様のブラッシュアップや要望のヒアリングに繋げられるのではないかと考えています。実際にフリートマップを作成するとなると開発コストがかかるため、デザイン側で作成するプロトタイプの段階で再現度を高めたフリートマップを作成できれば、早い段階でデザインや仕様のブラッシュアップが可能になります。 これまでの作り方 フリートマップをプロトタイプ内で再現する場合、これまではAdobe After Effectsなどのタイムラインによって動きを作り込むか、Sketchなどのプロトタイプ機能で静止したイメージ画としてマップを表示する方法をとっておりました。しかしAdobe After Effectsではインタラクティブにボタン操作をさせることができず、プロトタイプ機能ではマップ上を常に動いているようなカーソルの動きは表現できませんでした。 本格的に作り込めるプロトタイプツールを導入することも可能ですが、日常的に使っているAdobe系のツールやSketchの範囲で、 必要な時だけ、学習コストをかけずに 作成できる良い方法はないかと模索しておりました。 今回、2021年10月にリリースされたXDの新機能に動画を埋め込める機能がついた *1 ということで、フリートマップを再現した動画を背景にし、ボタンでマップにアクションしているような動きが再現できないか試作してみました。 XDのプロトタイプ機能は、今回のフリートマップのように、常に動いているエリアに何かしらのアクションをさせることをメインに設計されているわけではないため、XDの機能内で試行錯誤が必要です。その点についても記載しておきます。(XDの機能については今後アップデートされていくかもしれません) XDを使った作り方 以下の仕様とシナリオを想定してサンプルを作成しました。 【仕様】 計測中の複数のEV車両を一覧で表示 車両一覧に表示されている車両位置をマップ上に表示 車両一覧から選択した車両の詳細をマップ上の対象車両横に表示 詳細表示からドライバーにアクションを送付 【シナリオ】 管理者が10台の車両の充電状態(SoC)とドライバーの連続運転時間を遠隔で監視 充電状態の残りが少ない車両が赤くアラート表示 管理者はアラートの内容を確認し、ドライバーに「充電してください」のメッセージを送信 ざっくりと作り方を説明すると、 マップ上を車両カーソルが動いている動画の上に、クリック可能なカーソルを紛れ込ませ、ボタンでマップにアクションしているように見せる 内容となっております。 詳細の作り方をSTEP1〜STEP5にかけて以下に記載していきます。 STEP1:マップ上をカーソルが動いている背景用動画を準備する マップの上にカーソルを配置し、進行方向にカーソルが移動するアニメーションを作成します。 プロトタイプモードでアートボード1からアートボード2への遷移をつけ、自動アニメーションにセットします。 自動アニメーションのデュレーションについては最長5秒となっているため、必要な長さに合わせて動画を繋げます。今回は30秒の動画を作成するため自動アニメーションを6回繋げて作成しました。 再生すると以下のように表示されます。 カーソルの向きを細かく変えたり、途中で止まってまた動き出すカーソルを配置していくと、リアルな動きを再現できます。 こちらを録画して背景動画として書き出しておきます。 【注意点】 使用できる動画のサイズは15M以下となるため、書き出した動画サイズが大きい場合はサイズ圧縮が必要です STEP2:デザインに背景用動画を配置する STEP1で書き出した動画をアプリケーションのデザインに配置します。 ヘッダーと左サイドのメニューはイメージ。画面右の車両一覧のリストがクリック可能 動画を貼り付けた際の設定は 自動再生/ループ再生 にしておきます。 これでプロトタイプを起動するとマップ上をカーソルが動いている画面のベースが出来上がります。 STEP3:車両一覧クリック時に反応するカーソルを配置する マップ上を動いているカーソルは背景動画として埋め込んでいるため、クリック時に状態を変化させることができません。そのため別途状態が変化するカーソルを地図上に配置します。 【注意点】 ここで配置するカーソル自体に移動などのアニメーションをつけることはできません STEP4:クリック時に遷移する画面を作成する 必要な画面を作成しておきます。 車両一覧クリック後、詳細表示画面 詳細表示から「充電してください」ボタンクリック後 STEP5:アートボード間のリンクを作成する プロトタイプモードに切り替え、車両一覧のリストから遷移先のアートボード、詳細表示内のボタンから遷移先のアートボード…といった具合にリンクをセットします。この時、 自動アニメーション としておくことで、背景動画はシームレスに再生されます。 【注意点】 ボタンコンポーネントのステートにアニメーションが別途設定されていると、ページ遷移のタイミングで背景動画の再生開始位置がリセットされてしまうため、コンポーネントのステートは削除しておきます 背景動画のレイヤー名が違っていると、ページ遷移のタイミングで背景動画の再生開始位置がリセットされてしまうため、繋げて再生したい動画の名称は同じにしておきます 以上で完成です。 終わりに これまではアプリケーションを第三者に説明する目的でプロトタイプを作成することが多かったのですが、デザイナー自身も、動きのあるプロトタイプで確認することで操作の改善点に気づくことがありました。このように日常的に使っているデザインツールで簡単に実装に近い動きを再現することで、確認⇆改良のプロセスを繰り返し、デザインの精度を高めていければと考えています。 最後まで読んでいただきありがとうございました。 *1 : 新機能の概要 | Adobe XD の最新リリース
アバター
aptpod Advent Calendar 2021 の 7 日目を担当する、ハードウェア/OT製品Gr.の夏井です。 資材調達・製造マネジメントを担当しています。今回は、同じハードウェアGr.の加藤さんの 「アプトポッドに入社してみた」 の記事を参考にしながら、入社6ヶ月目に突入した夏井ver.をご紹介いたします。 自己紹介 銀行員時代 結婚・育児専念後、少しずつ社会復帰 本格的に社会復帰 資材調達・製造マネジメントの仕事 資材調達・製造マネジメントの魅力 アプトポッドで仕事して思ったこと、感じたこと 大変だった! 自由だった! 凄い! 広がる! 驚いた! お母さん大助かり! アプトポッドに入社してみた結果 最後に 自己紹介 銀行員時代 時は不景気真っ只中の1997年。安定した仕事に就こうと銀行に入行。 総合職ではなく、一般職として入行したはずが、事務回りを3ヵ月のローテションで全てまわり、 後輩に教えられるレベルまで覚えてから営業の外回りに行って欲しいと当時の支店長からお達し。 支店長が変わった2年目にも方針は変わらず、そろそろ外回りにと打診があったため、 丸3年で退職するため営業は出来ないと拒否し、丸3年で結婚を理由に退職しました。 ただ、3年目になると全体の事務量と各ポジションの状況を見ながら動けるようになっており、 この経験は後々の仕事で大いに生かすことが出来ました。 結婚・育児専念後、少しずつ社会復帰 金融系の派遣/大学の研究室秘書パートなどをしていました。 本格的に社会復帰 プローブと外観検査装置を製造する会社に営業事務として入社。 しかし気づけば内勤営業や海外調達を主に担当し、2年目になる頃には社長のお供で取引先に商品提案もしていました。 またこの頃は顧客管理システムの導入も担当していました。 建築資材を製造するメーカーに営業事務として入社。 ここで、組織を拡大するにあたり、仕入予算を組み製造マネジメントをする人が必要になり、生産管理担当(営業事務兼務)となりました。 複数の量産品を生産していたため、ほぼ毎日どこかの製造委託先や倉庫会社、運送会社と日程調整や様々な問題解決をしていましたが、このとき初めて自分が凄く楽しんで仕事をしていることに気が付きました。 もちろん、生産管理の仕事ですので、諸々の精神的なキツさというのはありましたが、そのキツさよりも楽しさの方が上回っていました。 それまで営業が合っていると会社側に舵を切られがちな私でしたが、生産管理の仕事に携わり、委託先とお互いの状況を共有しながら、試行錯誤を重ね、納期という一つの目標に向かって走るのは私に合ってる!と実感した出来事でした。 そして今年の6月に、品質レベルが高く、今まで経験したことがない試作品⇒量産品を生産していく過程に携わりたいと思いアプトポッドに入社しました。 資材調達・製造マネジメントの仕事 平たく言えば、生産管理の仕事になります。 生産管理の仕事は、会社ごとに多少の違いはあるかと思いますが、大体は下記のような内容かと思います。 需要予測 生産計画 調達 生産実施 品質管理 在庫管理 この内、ハードウェア/OT製品Gr.が担っているのは、需要予測以外の業務です。 そして私が主に担っているのは、調達、生産実施、在庫管理です。 その他、契約書や支払関係、生産管理システムを使用しての原価関係やM-BOM *1 管理(※工程設計はエンジニア)も担当しています。 品質管理についてはエンジニアのほんの少しのサポートです。 資材調達・生産管理の仕事は精神的にキツイとよく言われますが、 この辺りの内容はネット記事でも紹介されているのでバッサリと割愛し、今回はこの仕事の魅力をピックアップしていきたいと思います。 資材調達・製造マネジメントの魅力 パズルを解くようで楽しい(問題が複数個所で同時発生することもあり、都度問題を解決しながら工程を組み直す必要があります) 他社の同職種の人と、苦労を分かち合える 海外調達をしていると、仕事の合間に「まだ仕事しているの?ご飯食べた?」等色々と英語で話しかけられることが多いので単純に楽しい 製造委託先とWin-Winの関係を築きながら、様々な問題をクリアし、一緒に目標を達成することは楽しい 製造委託先の工場視察は新しい発見が多く楽しい 在庫管理や原価計算辺りは根を詰めることが多いけど、月末に数字が合ったときはスッキリサッパリする アプトポッドでは特に!ルーティンワークではない構築する楽しさがある!笑 ゲームで言えば、 シミュレーションゲーム のカテゴリーでしょうか。ハマると楽しいですよ。 アプトポッドで仕事して思ったこと、感じたこと 大変だった! 生産管理システムの導入作業が大変だった 量産品の生産が始まり、今年の7月に生産管理システムを本格導入しましたが、そこにM-BOMを落としこむ作業が大変でした。 今までM-BOMがあった上での生産管理・購買経験しかしてこなかったのと、私自身が電子部品や設計そのものに精通していないことで、 アプトポッドの状況を理解し、製品構成を理解するのには時間が掛かりました。今後もずっと勉強です。 しかもこの時期の調達は大変だった ニュースにもなってるくらいですから、言わずもがなですね。 自由だった! 自分で仕事を構築することが出来る これは新しく出来た職種ならではなのかもしれませんが、特に細かいルールは決まっていないため、 法令や認証等の情報を踏まえながら、自分の仕事に何が必要なのか、どういったフローであればスムーズに流れるのか等を 考えながら、日々の仕事を構築することが出来ます。 凄い! エンジニアのレベルが高い 右を見ても左を見ても、凄いな~と日々感じています。 しかも自然と不足しているところをフォローしてくれる優しさも兼ね備えている方ばかりで素直に尊敬します。 輸送時の振動も考慮し、量産品の梱包箱が設計されている 広がる! 様々な職種の社員がいるため、知識が広がる 私はまだまだ分からないことが多いのですが、オンライン懇親会等で他の職種の仕事内容を教えてもらったり、 Slackで他の方のやり取りを眺めてみたり、出社時に教えてもらったりして、少しずつ点と点が繋がっていくのを感じます。 皆さん何でも答えてくれます。教えながら溜息をつかれたり、ってこともないです笑 驚いた! お菓子も食べ放題 社内自動販売機は一律30円 社内サークルたくさん お母さん大助かり! 働き方が自由なため私生活を大切にすることができる スーパーフレックス制度を導入しているため、月間総労働時間を満たせば、出退勤時間を自由に設定することが出来ます。 場所も、在宅・オフィス出社どちらでも自由です。私は在宅勤務のときは、夕方の時間に2、3時間程家事のため離席し、家事が終わるとまた仕事を再開するというパターンが多いです。 朝の開始時間も早い時は7時半、遅い時は10時からとその日によって様々です。 入出荷があるときは短時間オフィスに出社し、終わったら在宅勤務に切り替えています。小さなお子さんをお持ちの方も多く、皆さんご自分のペースで仕事をされています。 ※一部社員は裁量労働制 アプトポッドに入社してみた結果 試作品⇒量産品になるまでの過程を学ぶことが出来る材料が揃っていた 日々凄いな~と思いながら仕事ができるようになった 家事や子供の行事など、私生活を大切にしながら仕事をすることが出来るようになった その分、学校のPTAの仕事が増えた 最後に 会社が大きくなっている最中のため、まだまだ各部署の業務フローが確立しているとは言えない状況ですが、 その分、自分の提案や構築が生きる素地はたくさんありますので、新しいことにチャレンジしたい! 自分のポジション以外に、色んな専門職の人と関わりながら仕事をしたい!視野を広げたい!という方は 是非飛び込んで頂ければと思います。 もちろん、アプトポッドの〇〇さんと働きたい!という動機でも個人的にはいいんじゃないかと思っています(個人的には笑) 詳しくは、 採用情報 をご覧頂けると幸いです。 また、 私たちと一緒にモノ作りをしたい!手を貸してやるぜっ!という会社様も随時募集しております ので、 夏井までお声掛けを頂けると幸いです。製造メーカー様や商社様、よろしくお願いします! www.aptpod.co.jp *1 : Manufacturing BOMと呼ばれています。エンジニアが使用するE-BOM(Engineering BOM)とは異なり、工程の進捗管理がしやすいようストラクチャー型に加工したBOMとなります。在庫、原価もここに紐づいています。当社では標準BOMと製番BOMを分けて管理出来るよう整備を進めています。
アバター
aptpod Advent Calendar 2021 の 6 日目を担当する、SRE チームの柏崎です。 弊社では、intdash を組み合わせたプロジェクトが多くあります。 とあるプロジェクトでは、車両に設置するエッジコンピュータが Amazon API Gateway を利用した API と通信する、というカスタマイズ部分があります。 先日このプロジェクトで、エッジコンピュータと Amazon API Gateway の通信に、セキュリティ強化のため相互 TLS 認証を導入することになりました。 今回は、Amazon API Gateway の相互 TLS 認証での課題を解決し、より厳格に導入する方法をご紹介します。 「相互 TLS 認証」とは 例えば、ブラウザで一般的なウェブサイトを閲覧するとき、HTTPS (HTTP+TLS) が利用されます。 TLS のレイヤーでは、サーバから提示された証明書をクライアントが検証することで、クライアントがサーバを認証しています。 この認証によって、接続先のサーバが正当なものであるかどうかの確認ができ、安心して通信を行うことができます。 TLS のデフォルトでは、 クライアントがサーバを 認証するのみですが、オプションで サーバがクライアントを 認証することもできます。 クライアントとサーバが相互に相手を認証することから、「相互 TLS 認証」「Mutual-TLS」「mTLS」などと呼ばれています。 また、オプションであるクライアントの認証を有効にしているので、単に「クライアント認証」と呼ばれることもあります。 Amazon API Gateway での相互 TLS 認証と課題 Amazon API Gateway での相互 TLS 認証は、カスタムドメイン名の設定にて、簡単に有効にすることができます。 トラストストア 1 を S3 にアップロードし、カスタムドメイン名の設定にてその S3 URI を指定するだけです。 カスタムドメイン名の設定 ただし、ここで課題となるのは、(現時点で) Amazon API Gateway の相互 TLS 認証は、証明書が失効したかどうかの検証を行わない という点です。 2 ドキュメントの一部 証明書は、「対応する秘密鍵が漏洩した可能性がある」「誤って発行したので取り消したい」等の理由で失効される事があります。 特に今回のようなクライアントに設定する証明書は、プライベート認証局から 10 年等の長めの有効期間で発行されることも多く、 失効させてもその間は接続できてしまう という問題がおきてしまいます。 Lambda オーソライザーでの解決 Amazon API Gateway には、Lambda オーソライザーという、Lambda 関数を利用してアクセス制御を行う機能があります。 この Lambda 関数に入力される内容には、クライアントから提示された証明書が含まれています。 3 ここでは、Lambda オーソライザーを利用して証明書の失効状態を検証する方法を示します。 1. Lambda オーソライザーを実装する Python 3.8 にて、下記の Lambda 関数を実装します。(長ったらしいですがご勘弁を…!) 環境変数 TRUSTSTORE_URI には、Amazon API Gateway のカスタムドメイン名のトラストストアと同一の S3 URI を指定してください。 また、Lambda 関数の実行ロールには、この S3 URI を読める権限を付与してください。 CertificateValidator() で allow_fetching=True を与えると、CRL または OCSP 4 を利用した証明書の失効状態が検証されるようになります。 ここでのポイントは、「lambda_handler の外で、S3 からトラストストアを読み込んでいる」という点です。 これにより、S3 との通信が Lambda 関数のコールドスタート時のみ実行されるようにしています。 import os from urllib.parse import urlparse import boto3 from asn1crypto import pem import json from certvalidator import ValidationContext, CertificateValidator trust_roots = [] truststore_uri = os.environ.get('TRUSTSTORE_URI') if truststore_uri is not None: u = urlparse(truststore_uri) bucket = u.netloc key = u.path.lstrip('/') truststore = boto3.client('s3').get_object(Bucket=bucket, Key=key)['Body'].read() for _, _, der in pem.unarmor(truststore, multiple=True): trust_roots.append(der) def lambda_handler(event, context): print("Event: " + json.dumps(event)) principalId = event['requestContext']['identity']['clientCert']['serialNumber'] cert = event['requestContext']['identity']['clientCert']['clientCertPem'].encode() context = ValidationContext(trust_roots=trust_roots, allow_fetching=True, revocation_mode='hard-fail') validator = CertificateValidator(end_entity_cert=cert, validation_context=context) try: validator.validate_usage(key_usage=None) except Exception as e: print("The certificate could not be validated: " + str(e)) return generate_policy(principalId, EFFECT_DENY) else: print("The certificate has been validated") return generate_policy(principalId, EFFECT_ALLOW) EFFECT_ALLOW = 'Allow' EFFECT_DENY = 'Deny' def generate_policy(principalId, effect): return { 'principalId': principalId, 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Effect': effect, 'Resource': '*' } ] } } 2. Amazon API Gateway に Lambda オーソライザーを設定する API に Lambda オーソライザーを設定します。 ここでは、認可のキャッシュを有効にしておくと良いでしょう。 クライアントからのリクエストの都度、Lambda オーソライザーが実行されてしまうことを回避できます。 API のオーソライザー設定 続いて、メソッドリクエストに Lambda オーソライザーを設定します。 メソッドリクエストのオーソライザー設定 これで、設定は完了です。 確認 実際に、失効済みの証明書を利用して、Lambda オーソライザー設定前後の Amazon API Gateway からのレスポンスを確認してみました。 Lambda オーソライザーの設定前は、失効済みの証明書でも下記のように 204 が返ってきていましたが、 $ curl -i --cert cert-revoked.pem --key privkey.pem https://my-great-api.example.com/ HTTP/2 204 x-amzn-requestid: cba741e3-e2eb-4ced-8332-efae0a6f4910 x-amz-apigw-id: JpzGqG_rtjMF1jg= content-type: application/json date: Wed, 01 Dec 2021 04:49:58 GMT 設定後は、下記のように 403 が返るようになりました。 $ curl -i --cert cert-revoked.pem --key privkey.pem https://my-great-api.example.com/ HTTP/2 403 x-amzn-requestid: 0c4e363e-25f3-4206-8d67-665641787512 x-amzn-errortype: AccessDeniedException x-amz-apigw-id: JpzipH4CNjMFiWw= content-type: application/json content-length: 82 date: Wed, 01 Dec 2021 04:52:58 GMT {"Message":"User is not authorized to access this resource with an explicit deny"} まとめ 今回は、Amazon API Gateway の相互 TLS 認証での、失効状態が検証されない、という課題を解決する方法を紹介しました。 実際のところ、失効はそこまで頻繁に行われるものでは無いですが、いざというときにはしっかりと検証されていなければなりません。 とても地味ですが、こういった所を突き詰めるのは楽しいですね。 認証局の証明書を連結したもの。証明書を検証する際、その証明書がトラストストアに含まれる認証局から発行されているかどうか、が検証される。 ↩ HTTP API の相互 TLS 認証の設定 - Amazon API Gateway ↩ Amazon API Gateway Lambda オーソライザーへの入力 - Amazon API Gateway ↩ CRL は失効された証明書が記載されているリスト。OCSP は失効状態をオンラインで検証するプロトコル。双方、証明書内に接続先等の情報が記述されている。 ↩
アバター
aptpod Advent Calendar 2021 の3日目を担当しますOTチームの大久保です。 今年はRustのエッジ製品への適用がはじまり、RustでLinuxのシステムコールを呼ぶような処理を実装するような場面が増えました。今回はその一例として、Linux上でキー入力カスタマイズをするコードをRustで実装してみます。ついでに、debパッケージにしてUbuntuにインストール、systemdのサービスとして立ち上げるまで行います。 uinputとは bindgenでCのヘッダファイルを読み込む ioctlを使用可能にする 本体を実装する deb/systemd service化する まとめ uinputとは uinputは仮想的な入力デバイスを作成するためにLinuxが提供する機能です。 /dev/uinput にイベントを書き出せば、あたかも実体のあるデバイスからのイベントが起きたように振る舞います。キーボードのデバイスファイルからイベントを読み出し、それを変換して /dev/uinput に書き出せば、キー入力のカスタマイズが可能になります。本来ならC言語で書くようなものなのですが、これをRustで書くのが今回の趣旨になります。 1.7. uinput module — The Linux Kernel documentation 筆者は以前Shiftキーの押しすぎで小指を痛めたことがあり、それ以来変換キーをShiftに割り当てる設定をX Window Systemの仕組みを使って行っていました。しかしこの方法はキーボードの抜き差しがあると機能しなくなったり、また今後XからWaylandに移行すると使えなくなることが予想されました。そのため、今回は変換キーの入力をShiftに変換するよう実装します。そのためこのRustのプロジェクト名は henkan-shift とします。 bindgenでCのヘッダファイルを読み込む uinputはLinuxのカーネルモジュールなので、その定義は linux/uinput.h ヘッダファイルに書かれています。これをRustから利用できるようにするためには、 bindgen を使います。 bindgenは、CやC++のライブラリを、Rustから呼び出すためのコードを生成してくれるものです。 次のような wrapper.h を作成します。 #include <linux/uinput.h> build.rs を用意します。 use std :: env; use std :: path :: PathBuf; fn main () { println! ( "cargo:rerun-if-changed=wrapper.h" ); let bindings = bindgen :: Builder :: default () . header ( "wrapper.h" ) . ctypes_prefix ( "libc" ) . parse_callbacks ( Box :: new ( bindgen :: CargoCallbacks)) . generate () . expect ( "Unable to generate bindings" ); let out_path = PathBuf :: from ( env :: var ( "OUT_DIR" ). unwrap ()); bindings . write_to_file (out_path. join ( "bindings.rs" )) . expect ( "Couldn't write bindings!" ); } ここで生成された bindings.rs を読み込むため src/wrapper.rs を用意します。 #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] include! ( concat! ( env! ( "OUT_DIR" ), "/bindings.rs" )); ioctlを使用可能にする bindgenを使えば大方の定義はインポートできますが、現状は関数形式マクロをうまく取り扱ってくれません。そして、マクロを多用するioctlに関する定義をインポートすることもできません。 Rustからioctlシステムコールを利用するには、 nixクレート が提供する マクロ を使用します。マクロを使用すると、Rustから普通に呼び出せる関数を定義することになります。 今回は以下のように使います。コメントは対応するヘッダファイル内の定義になります。 // #define UI_DEV_CREATE _IO(UINPUT_IOCTL_BASE, 1) nix :: ioctl_none! (ui_dev_create, UINPUT_IOCTL_BASE, 1 ); // #define UI_DEV_DESTROY _IO(UINPUT_IOCTL_BASE, 2) nix :: ioctl_none! (ui_dev_destroy, UINPUT_IOCTL_BASE, 2 ); // #define UI_DEV_SETUP _IOW(UINPUT_IOCTL_BASE, 3, struct uinput_setup) nix :: ioctl_write_ptr! (ui_dev_setup, UINPUT_IOCTL_BASE, 3 , uinput_setup); // #define UI_SET_EVBIT _IOW(UINPUT_IOCTL_BASE, 100, int) nix :: ioctl_write_int! (ui_set_evbit, UINPUT_IOCTL_BASE, 100 ); // #define UI_SET_KEYBIT _IOW(UINPUT_IOCTL_BASE, 101, int) nix :: ioctl_write_int! (ui_set_keybit, UINPUT_IOCTL_BASE, 101 ); // #define EVIOCGRAB _IOW('E', 0x90, int) nix :: ioctl_write_int! (eviocgrab, 'E' , 0x90 ); Cの定義からの変換は比較的単純なので、 build.rs に自動生成させることもできます。その場合は以下のように書くことになるでしょうか。 use once_cell :: sync :: Lazy; use regex :: Regex; use std :: env; use std :: fs :: OpenOptions; use std :: io :: prelude :: * ; use std :: path :: {Path, PathBuf}; fn main () { println! ( "cargo:rerun-if-changed=wrapper.h" ); let out_path = PathBuf :: from ( env :: var ( "OUT_DIR" ). unwrap ()); let callback = Callbacks :: new (out_path. join ( "ioctl_generated.rs" )). expect ( "couldn't create callback" ); let bindings = bindgen :: Builder :: default () . header ( "wrapper.h" ) . use_core () . ctypes_prefix ( "libc" ) . parse_callbacks ( Box :: new (callback)); // Set header include paths let bindings = bindings. generate (). expect ( "unable to generate bindings" ); bindings . write_to_file (out_path. join ( "bindings.rs" )) . expect ( "couldn't write bindings" ); } static RE_IOWR: Lazy < Regex > = Lazy :: new ( || { Regex :: new ( r"#define\s+(\w+)\s+(_IO[WR]*)\(([^,)]+),\s*([^,)]+),?\s*([^,)]*)\)" ). unwrap () }); #[derive( Debug )] struct Callbacks (PathBuf); impl Callbacks { fn new < P: AsRef < Path >> (path: P) -> std :: io :: Result < Self > { let _ = std :: fs :: File :: create ( & path) ? ; Ok ( Callbacks (path. as_ref (). to_owned ())) } fn generate ( & self , filename: &str ) -> std :: io :: Result < () > { let input_file = std :: fs :: read_to_string (filename) ? ; let mut output_file = OpenOptions :: new (). create ( true ). append ( true ). open ( & self . 0 ) ? ; for caps in RE_IOWR. captures_iter ( & input_file) { let define_name = caps[ 1 ]. to_ascii_lowercase (); let iorw = & caps[ 2 ]; let io_type = & caps[ 3 ]; let number = & caps[ 4 ]; let (macro_name, ty) = match iorw { "_IO" => ( "ioctl_none" , None ), "_IOR" => ( "ioctl_read" , Some ( & caps[ 5 ])), "_IOW" => { let ty = & caps[ 5 ]; let macro_name = if ty == "long" { "ioctl_write_int" } else { "ioctl_write_ptr" }; (macro_name, Some (ty)) } "_IOWR" => ( "ioctl_readwrite" , Some ( & caps[ 5 ])), _ => continue , }; writeln! (output_file, " \n // {}" , & caps[ 0 ]) ? ; match ty { Some (ty) if macro_name != "ioctl_write_int" => { let ty = ty. trim_start_matches ( "struct " ); let ty = if ty == "int" { "libc::c_int" } else { ty }; writeln! ( output_file, "nix::{}!({}, {}, {}, {});" , macro_name, define_name, io_type, number, ty ) ? ; } _ => { writeln! ( output_file, "nix::{}!({}, {}, {});" , macro_name, define_name, io_type, number ) ? ; } } } Ok (()) } } impl bindgen :: callbacks :: ParseCallbacks for Callbacks { fn include_file ( & self , filename: &str ) { println! ( "cargo:rerun-if-changed={}" , filename); self . generate (filename). expect ( "failed" ); } } bindgenで ParseCallbacks というトレイトを実装してやると、ヘッダファイル中に現れた記述に応じて呼び出されるコールバック関数を実装できるのでそれを使用します。関数形式マクロが用いられる箇所に対するコールバックは現状無いので、ioctlに関する定義を正規表現で見つけてnixのマクロに変換して書き出します。Cの型からRustの型への変換はこの例では雑にやっています。自動生成するためのコード自体割と長くなるので、今回のuinputの例では手書きで実装します。 本体を実装する src/main.rs は以下のように記述します。 pub mod wrapper ; use anyhow :: Result ; use libc :: {c_char, c_ushort}; use nix :: fcntl :: {open, OFlag}; use nix :: unistd :: {close, read, write}; use notify :: {watcher, DebouncedEvent, RecursiveMode, Watcher}; use std :: os :: unix :: io :: RawFd; use std :: path :: PathBuf; use std :: thread :: sleep; use std :: time :: Duration; // bindgenで生成した定義を使用する use wrapper :: { input_event, uinput_setup, BUS_USB, EV_KEY, KEY_ESC, KEY_HENKAN, KEY_INSERT, KEY_LEFTSHIFT, KEY_MICMUTE, UINPUT_IOCTL_BASE, }; const DEVICE_NAME: & [ u8 ] = b"henkan-shift-kbd" ; const EVENT_FILE_DIR: &str = "/dev/input/by-id" ; const INPUT_EVENT_SIZE: usize = std :: mem :: size_of :: < input_event > (); // #define UI_DEV_CREATE _IO(UINPUT_IOCTL_BASE, 1) nix :: ioctl_none! (ui_dev_create, UINPUT_IOCTL_BASE, 1 ); // #define UI_DEV_DESTROY _IO(UINPUT_IOCTL_BASE, 2) nix :: ioctl_none! (ui_dev_destroy, UINPUT_IOCTL_BASE, 2 ); // #define UI_DEV_SETUP _IOW(UINPUT_IOCTL_BASE, 3, struct uinput_setup) nix :: ioctl_write_ptr! (ui_dev_setup, UINPUT_IOCTL_BASE, 3 , uinput_setup); // #define UI_SET_EVBIT _IOW(UINPUT_IOCTL_BASE, 100, int) nix :: ioctl_write_int! (ui_set_evbit, UINPUT_IOCTL_BASE, 100 ); // #define UI_SET_KEYBIT _IOW(UINPUT_IOCTL_BASE, 101, int) nix :: ioctl_write_int! (ui_set_keybit, UINPUT_IOCTL_BASE, 101 ); // #define EVIOCGRAB _IOW('E', 0x90, int) nix :: ioctl_write_int! (eviocgrab, 'E' , 0x90 ); // /dev/input/by-id/*Keyboard-event-kbd となるデバイスファイルを検知する pub fn detect_device (postfix: &str ) -> Result < PathBuf > { for entry in std :: fs :: read_dir (EVENT_FILE_DIR) ? { let entry = entry ? ; let path = entry. path (); if path. to_string_lossy (). ends_with (postfix) { return Ok (path); } } let (tx, rx) = std :: sync :: mpsc :: channel (); let mut watcher = watcher (tx, Duration :: from_millis ( 100 )) ? ; watcher. watch (EVENT_FILE_DIR, RecursiveMode :: NonRecursive) ? ; loop { match rx. recv () { Ok ( DebouncedEvent :: Create (path)) if path. to_string_lossy (). ends_with (postfix) => { return Ok (path); } Err (e) => { return Err (e. into ()); } _ => (), } } } // /dev/uinput に出力するためのルーチン pub struct Emitter { fd: RawFd, } impl Emitter { fn new () -> Result < Self > { // /dev/uinputを開いていろいろセットアップ let fd = open ( "/dev/uinput" , OFlag :: O_WRONLY | OFlag :: O_NONBLOCK, nix :: sys :: stat :: Mode :: empty (), ) ? ; unsafe { // /dev/uinputを利用可能にするためのioctlの呼び出し ui_set_evbit (fd, EV_KEY. into ()) ? ; for key in KEY_ESC.. = KEY_MICMUTE { ui_set_keybit (fd, key. into ()) ? ; } sleep ( Duration :: from_secs ( 1 )); let mut usetup: uinput_setup = std :: mem :: zeroed (); usetup.id.bustype = BUS_USB as c_ushort; usetup.id.vendor = 0x1234 ; usetup.id.product = 0x5678 ; for (i, c) in DEVICE_NAME. iter (). enumerate () { usetup.name[i] = * c as c_char; } ui_dev_setup (fd, & usetup) ? ; ui_dev_create (fd) ? ; } Ok (Emitter { fd }) } fn emit ( & self , event: input_event) -> Result < () > { let event: [ u8 ; INPUT_EVENT_SIZE] = unsafe { std :: mem :: transmute (event) }; write ( self .fd, & event) ? ; Ok (()) } } impl Drop for Emitter { fn drop ( &mut self ) { unsafe { let _ = ui_dev_destroy ( self .fd); } let _ = close ( self .fd); } } fn main () -> Result < () > { env_logger :: builder () . filter_level ( log :: LevelFilter :: Info) . init (); let emitter = Emitter :: new () ? ; 'detect_loop: loop { let kbd_event_file = loop { match detect_device ( "Keyboard-event-kbd" ) { Ok (kbd_event_file) => { break kbd_event_file; } Err (e) => { log :: warn! ( "Keyboard detection failed. \n {} \n retry.." , e); sleep ( Duration :: from_secs ( 1 )); } } }; log :: info! ( "detect keyboard {}" , kbd_event_file. display ()); let fd = open ( & kbd_event_file, OFlag :: O_RDONLY, nix :: sys :: stat :: Mode :: empty (), ) ? ; unsafe { eviocgrab (fd, 1 ) ? ; } loop { // デバイスファイルからイベント読み込み let mut buf = [ 0u8 ; INPUT_EVENT_SIZE]; match read (fd, &mut buf) { Ok (INPUT_EVENT_SIZE) => (), Err ( nix :: Error :: ENODEV) => { log :: info! ( "No device file {}" , kbd_event_file. display ()); sleep ( Duration :: from_secs ( 1 )); continue 'detect_loop ; } Err (e) => { return Err (e. into ()); } Ok (len) => { log :: warn! ( "invalid read len {}" , len); continue ; } } let mut event: input_event = unsafe { std :: mem :: transmute (buf) }; // 変換キーについてのイベントをShiftキーのものに変換する if event.type_ == EV_KEY as c_ushort && event.code == KEY_HENKAN as c_ushort { event.code = KEY_LEFTSHIFT as c_ushort; } // ついでによく押し間違えるInsertキーを無効化しておく if event.type_ == EV_KEY as c_ushort && event.code == KEY_INSERT as c_ushort { event.value = 0 ; } // /dev/uinputへの出力 emitter. emit (event) ? ; } } } ここではあまり細部まで解説しませんが、やっていることは /dev/input/by-id 以下に追加されたキーボードを検知して、そこから読み取ったイベントを書き換えて /dev/uinput に書き出しているだけです。このread/writeも、nixクレートが提供する関数を用いて行っています。 deb/systemd service化する 前項までのコードを動かす際には、デバイスファイルを読み書きするためにroot権限で実行しなければなりません。また普段使いのために、PCの起動のたびに自動で立ち上がるようにしておきたいです。 そこで、systemd serviceでのデーモン化と、これを含めたdebパッケージを作成します。 Rustのプロジェクトをdebパッケージにするには、 cargo-deb というツールを使います。以下のようにインストールします。 cargo install cargo-deb Cargo.toml に以下のように追記します。 [package.metadata.deb] depends = "$auto" section = "utility" priority = "optional" maintainer-scripts = "debian/" systemd-units = { enable = false } depends は生成されるdebパッケージが依存するであろう他パッケージを記述します。 $auto としておくことで勝手に判定してくれますが、依存するパッケージやバージョンを厳密に指定したい場合は自分で記述することになります。 また、 systemd-units という設定項目があることからも分かるように、systemd用の設定も cargo-deb が面倒を見てくれます。systemdのunitについて設定するには、 debian/service というファイルに記述します。 [Unit] Description=Convert henkan key to shift key. StartLimitIntervalSec=5 StartLimitBurst=1000 [Service] ExecStart=/usr/bin/henkan-shift Restart=always [Install] WantedBy=graphical.target 今回はプロジェクト名を henkan-shift にしたので、実行ファイルの名前も同様になっています。そのため、 ExecStart に /usr/bin/henkan-shift を指定します。 以上の用意ができたら以下のコマンドでビルドします。 cargo deb target/debian 以下にdebパッケージが生成されるので、後はこれをインストールするだけです。 sudo dpkg -i target/debian/henkan-shift_0.1.0_amd64.deb 後は普通のsystemdサービスとして使えます。 sudo systemctl enable henkan-shift.service sudo systemctl start henkan-shift.service sudo systemctl status henkan-shift.service あとはこのサービスが立ち上がっている状態で、キー入力が想定通りに機能するか確かめるだけです。 まとめ 今回は、uinputによるキー入力カスタマイズを行うコードを実装しました。これまでCで実装していたような、Linuxのデバイスファイルの読み書きやioctlの呼び出しもRustから行うことができます。さらに、 cargo-deb を使えば、比較的簡単な設定でdebパッケージ化、systemdサービス化することができます。 最近はRustまわりのライブラリやツールが充実してきたこともあって、Cで書いていた箇所をRustで書くことの利点が増えてきていると感じます。これからも社内で採用箇所を増やしていきたいと思っています。
アバター
aptpod Advent Calendar 2021   2日目の記事を担当する、コーポレート・マーケティング室、デザインチームの「チェン ・ルイ」と申します。普段は社内製品のアプリケーションのUIデザイン業務を行なっています。 現在、aptpodでは自社Webアプリケーションのデザインガイドラインを整理しております。 これまであったaptpodのWebアプリケーションにおける課題と、それに対処するためにデザインガイドラインの作成において意識したポイントについてご紹介します。 現状デザインの課題 デザインガイドラインとは コンポーネントライブラリとデザインガイドラインの違い 想定してる管理方法、デザインガイドラインの立ち位置 デザインガイドラインの作成プロセス 準備段階 整理手順 意識している7つのポイント 初期に目次を整理、ゴールが見えるように コンポーネントの命名を統一 画面全体の状態を揃え、対応可能なパターンを精査 コンポーネントの状態を統一 レスポンシブ対応を考慮 コンテンツの行数が違う場合を考慮する 各パターンでの使い道を追記 最後に 現状デザインの課題 これまでもアプリケーションごとに、共通のデザインコンポーネントはまとめられておりましたが、既存デザインを修正、または既存デザインを踏襲して、 新たなアプリケーションのデザインを作成するとなった際に、以下のような課題がありました。 コンポーネント単体ではまとめられているが、組み合わせ方が記載されていないため、組み合わせるときにOKとNG例を把握できない  どの製品のデザインファイルが、最新のデザイン基準か把握できない 製品ごとにデザインスタイルにばらつきがある 最終デザインまでの経緯が残っていない など 上記の課題を解決するため、Webアプリケーションのデザインガイドラインを策定しはじめました。 デザインガイドラインとは デザインガイドラインとは、色・文字・レイアウトなど様々なデザイン要素について、ルールを綿密に定義したドキュメントのことです。 使用環境や運用プラットフォームによってガイドの思考や内容が変わります。今回は主に「Web製品におけるデザインガイドライン」について書いていきたいと思います。 コンポーネントライブラリとデザインガイドラインの違い これまで社内では各Web アプリケーションのコンポーネントを共通で使えるようにライブラリにまとめてきました。 コンポーネントでは最低限共通のデザイン要素だけ入れています。デザインを作るとき、コンポーネントの組み合わせ方、余白、文字揃え、レスポンシブ対応、文字記入ルールなどの規則があり、コンポーネントのライブラリだけではカバーできません。 建築で例えるなら、コンポーネントは建築の素材、デザインガイドラインは現場で使うためのより詳細な設計図です。 以下の2つの画像ではコンポーネントライブラリとデザインガイドラインにおけるボタンの項目を比較できます。 コンポーネントライブラリ デザインを作るとき、コンポーネントライブラリを直接適用します。 デザインガイドライン デザインファイルのパターンを精査、共通点をまとめる。詳細の使い方、使う場面を細かく文章化しています。 想定してる管理方法、デザインガイドラインの立ち位置 デザインガイドラインは、現段階ではデザイナー向け、今後はエンジニアにも共有する予定です。 社内には既にコンポーネントのライブラリがあります。デザイナーがWebアプリケーションをデザインするとき、デザインファイルにこれらのライブラリから直接コンポーネントを適用しています。 デザインガイドラインは、 コンポーネントを直接適用るものではなく、デザインを展開するときに、迷う場合はルールを確認できるファイル です。 デザインガイドラインの作成プロセス 準備段階 準備や調査段階が大事です。この段階では一般的なガイドラインと、内部的な製品の要求やパターンをまとめることを分け、項目の目次を整理します。 参考にした他社のデザインガイドライン 外部のデザインガイドラインは主にデザイン要素の命名や定義の参考にしました。以下は参照したデザインガイドラインの一部です。 material.io docs.microsoft.com 参考にしているデザインガイドラインはOS、プラットフォームを提供しているサービスのもので、適用範囲が広く、膨大なシステムであります。 それぞれ参考にできる部分をピックアップし、整理するときにはすぐ参考リンクにアクセスできるようにします。 参考にした内部の既存デザイン これまで社内の製品デザインファイルを全て参考にし、どんなパターンが存在しているか精査します。 上記の参考情報から、各状態のコンポーネントの一覧以外、以下の項目を策定することにしました。 問題: 要素を使うことで解決したいユーザビリティ関連の問題 文脈/状況: この状況はA、この状況はB…(条件を考慮して使い分ける) 理由 : パターンが存在する理由と、それがユーザビリティにどのように影響するか デザイン例: パターンの成功した実際のアプリケーション事例を示します(製品名、機能名明記) サイズとレイアウト: サイズのバリエーション、画面の中での位置 関連する原則: エラー状態管理、通知の重要度分類など デザイン以外のルールも追記 製品の機能によって現時点では実装できないものや、機能追加のため新しいデザインが必要になる可能性もあるため、80%はルールを決め、20%サンプルで提示するという考えになります。 ※ 補足: 実験段階ですので、運用して行く中では修正する可能性があります。 整理手順 各コンポーネントに対して、以下の手順でガイドラインを整理します。 【Step1】 各デザインファイルのパターンを精査や羅列、考察結果を文章化 【Step2】 複数のパターンが存在する場合、統一できるものを統合し、複数のパターンが必要の場合は各自の使う場面を追記 【Step3】 該当コンポーネントにおける曖昧なルールを発見、使い方含め定義を提案 【Step4】 策定したガイドラインをチーム内で共有や相談 【Step5】 ブラッシュアップ 初期段階では目次を作成してまとめる内容の見通しをつけておくことの必要性を痛感しました。 次の項目ではデザインガイドラインを整理するときに意識しているポイントを紹介します。 意識している7つのポイント 初期に目次を整理、ゴールが見えるように 最初はどんなコンポーネントがあるか、各コンポーネントにどんな項目が必要か、書き出すことがおすすめです。途中で項目を足す手間がかからない、ゴールが見えることでモチベーションが保てます。 複数の外部デザインガイドラインを参考にし、現在各コンポーネントは基本的に以下の項目で定着しています。 Overview: 基本の情報 Definition: コンポーネントの定義 Usage: 解決したい問題、どんな時に使えるか Sizing: サイズのバリエーション Variants(Name): 単体あるいは組み合わせのパターンの名称 Variants 各パターン具体的に展開、ルール 各状態を羅列 製品におけるデザイン例 Layout : サイズのバリュエーション、画面の中での位置、余白のルール コンポーネントの命名を統一 命名はあまり重要視されることはないですが、チーム内での共通言語としてデザイン後のプロセスにも影響していく大事なことです。修正依頼やフィードバックが発生する場合、どの要素を示すか認識のずれでコミュニケーションのコストが発生する場合もあります。 各コンポーネントの定義を明確にし、名称を統一する作業を行なっています。 【例】ダイアログ と モーダル ダイアログについて、デザイナーとエンジニア間で「Dialog」「Modal」「Window」「Pop up」などたくさんの呼び方があり、名称が統一されていませんでした。 デザインガイドラインでは「Dialog」「Modal Dialog」「Dialog」「Window」の定義を明確にし、関連するコンポーネントやUIの実例も提示します。 Dialog Definitionユーザーの操作を求めるコンポーネント。Modal Dialog と NoN-modal Dialogを分けています。        Modal Dialog Definition ダイアログの一種、メインのコンテンツの上部に表示され、ユーザーとの対話を必要とする特別なモードのダイアログです。ユーザーがモーダルダイアログを明示的に操作するまで、メインコンテンツを無効にします。 Usage 重要な警告や機能、操作を行わないと次のプロセスに進めない場合を使います。情報はシンプルであるべきです。情報が複雑な場合、通常の画面かNon-modal Dialogを考慮します。             Modal (Modal Window) Definition 画面表示の形式。Modalの表示形式のなかで、ダイアログの他に複数形式もあります。モーダル(モーダルウィンドウも呼ばれます)は、他のすべてのページコンテンツの一番前に表示され、メインコンテンツは操作できなくなります。 一般的にメインコンテンツを覗き見できるように、背後に透明度のレイヤーがあります。(対義語は Modeless、Main window) Usage モーダルの目的は一言で要約することができます:フォーカス。 重要なアクションや情報にユーザーの注意を向けるためによく使用されます。 単純なことに集中する必要がある場合、最も効果的な手段の1つです。 モーダルを閉じるか、モーダル内の特定のアクションを完了する必要があります(たとえば、メッセージを読んで[OK]をクリックする、フォームに入力するなど)。       Window かなり汎用的な呼び方です。 コンピューターモニターのディスプレイの(通常は)長方形の部分であり、その内容(ディレクトリ、テキストファイル、画像など)を画面の他の部分とは独立して表示します。 Windowsでは、グラフィカルユーザーインターフェイス(GUI)を構成する要素の1つです。 現状社内の Webのアプリケーションでは、コンポーネント名では使わない方向も考えられます。アプリケーションによって、「機能名 x Window」という命名方法も検討可能です。 【例】Data Table Data Tableについても「List」「Table」など複数のの表記が混在しています。使用方法も含めて定義を明確にしました。 Data Table Definitionデータテーブルは、複数行と列にまたがるデータのセットです。行と列のグリッドのような形式で情報を表示します。 データから洞察を得ることができるように、一覧しやすい方法で情報を整理します。一般的に項目の数を把握できない、サーバーから流し込みます。ページネーションをよく使います。 項目の数が固定の設定系はFormを参照してください。 曖昧な名称を全て定義を明確にし、どんな「コンポーネント」「状態」「表示形式」を示すか、改めて整理してデザインチーム内に共有します。 チーム内の共通認識を促進し、共同作業の効率が期待できます。 画面全体の状態を揃え、対応可能なパターンを精査 Loadingの時、データが空状態の時はどうなりますか? これまでルールが曖昧で製品ごとに実装していましたが、デザインガイドラインにまとめました。主にCabon design guideを参照しています。 Error: システムエラー(実装側と集う相談) 接続エラー ユーザー操作がエラーを引き起こした場合 エラーから簡単に回復手段提示 Blank: データが空状態の場合 Success: 操作が成功する場合 Loading: データを読み込む途中、データをアップロードする途中 Filtering: フィルターをかけている場合 Modal: モーダル表示を使用する場合 【例】 Loadingの全てのパターンを精査(一部)  Skeleton ページ上の情報がまだ完全にロードされていないことを示すために、最初のページロードで使用されるコンポーネントの簡略化されたバージョンです。 Usage それらはほんの数秒間だけ表示され、コンポーネントとコンテンツがページに入力されると消えます。 Data Table 、Card、など動的データを表示する部分のみ使います。 一般的に操作できるコンポーネント(Button、Input 、Toggleなど)で使う必要はありま>せん。     Loading indicators ページ全体を処理しているときに、読み込みコンポーネントを使用する必要があります。 一般的に、データをアップロードまたは保存した後に使います。     コンポーネントの状態を統一 画面全体の状態以外、まず要素の状態を全て精査して、状態の命名や定義を統一します。特に、英語の表記では属人性になっていますので、名前と対応関係を作って共有することによって、各自のデザイン作業では命名について迷いを減らし、作業を引き継いだ時もデザインの意味を理解しやすくなります。 コンポーネント状態名称と定義の一部、このようにテーブルでまとめてみます。 状態名称 定義 関連要素例 Normal 操作できる要素を示す。通常、デフォルト、何も手を加えていない状態 操作できる要素全般 Hover ユーザーが操作できる要素の上にカーソルを置いたときの状態 Button、Pulldown menu、Side menu、Inputなど Pressed 要素が押された状態は Button、Pulldown menu、Side menuなど Disabled 無効化、操作できない要素 Button、Pulldown menu、Side menu、Inputなど Focused フォーカスを受け取った状態(フォーム入力など)。 ユーザーが要素をクリックまたはタップするか、キーボードのTabキーで要素を選択するとこの状態になる Input 、Button Typing 入力中(主に入力系) Input Entered 入力済み(主に入力系) Input 参考: Material Design State material.io 【例】 Text input Input系の名称も煩雑になっていました。特に「普通」最初のInput系の状態は混乱しました。まだ入力していない、普通の状態の表記は複数あります: 「Default」「Normal」「Inactive」は「何も手加えていない状態」という定義でNormalに統一する予定です。 キーボードで入力している状態では、「Entering」「Active」「 Inputting」複数混在しています。 他のサービスを参考し、入力済みの「Entered」や要素の名称「Input」をはっきり区別できるよう、「Typing」に統一を検討しています。 また、各状態一覧を出す以外、補足文言とエラー文言を入れることを想定して、ルールを策定します。 【例】 Side menu Side menuは閉じる(Closed)、開く(Opened)の状態があり、さらにメニューの第二階層有り無しの状態があります。デザインガイドラインでは全ての組み合わせを提示します。 (状態の一部) レスポンシブ対応を考慮 テーブルの1行に、スペースが足りない場合、手段を提供します。 画面が狭い場合表示手段一覧 ・フォントサイズを小さくする ・改行 ・省略: 省略された項目をHoverすると、Tool tipで全部のコンテンツを表示 ・画面スクロール: テーブルの最小限720px ・余白を縮む:都度調整 コンテンツの行数が違う場合を考慮する 文字が1行と複数行の場合、文字揃えや整列方法が違う可能性もある 背景は可変か固定か決める、最小縦幅や最大縦幅を考慮する 【例1】 ダイアログ コンテンツは1行の場合は中央寄せですが、複数行の場合は左寄せ 最小限の高さを設定 【例2】Input error message 各パターンでの使い道を追記 デザインガイドラインは単純にコンポーネントのパターンを羅列したものではありません。製品のUIデザインにおいてニーズに合わせて選択する可能性を提示します。ここで大事なことは「どういう場面」で「何を考慮する」をある程度で決めれば良いと考えています。 【例】通知系のダイアログ 通知系のダイアログでは、確認ボタン1つパターンや、「Cancel」と主要操作の選択肢を与えるパターンがあります。 Notification dialog (Confirmation only ) システムの異常、何かが機能しなかった場合を通知します。ユーザーからの情報確認を必要とします。提供する動作は、このメッセージを確認するだけです。通常は「OK」ボタンのみになります。    Notification dialog (Cancel + Function) モーダルを完了して閉じるには、アクションを実行する必要があります。 2つのボタン、Primary buttonとCancel buttonの両方があります。 キャンセルボタンをタップするか、ダイアログの外側の領域をタップすると、 ダイアログが閉じます。    Danger dialog 破壊的または不可逆的なアクションに使用される特定の種類のダイアログです。 誤って実行された場合に、重大なデータ損失をもたらすアクションの確認として、影響の大きい瞬間に一般的に使用されます。 3パターンがあります ・「main content only」: Notification dialog (2 buttons) と同じ、Primary buttonをCaution buttonに置​​き換えます。 ・「+ supporting message」タイプ : 次の操作を提示するサポート文言を追加する場合 ・「+ confirm」タイプ : 破壊的または不可逆的なアクション、重要なデータを失う場合は「+ confirm」タイプ、チェックボックスを使い確認が必要があります。 使う場面も追記しています。必ずではなく、こういう場合は考慮する、選択できる手段を提供します。 【例】Pulldown menu 以下の場合、最初から項目を全部出すのではなく、Pull downを考慮します。 ラベルが長い場合。 例: タイムゾーン、ファイル名 項目名は動的なデータ、数が固定ではない場合。 例: Edge名 項目数が多い場合(Windows11ガイドを参照して8件以上) 。 例:都道府県 最後に 今回はデザインガイドラインを整理する時に手順や意識している7つのポイントを紹介しました。上記のポイントを抑えて、デザインガイドラインを整理することで、以下の効果を得られます。 コンポーネント定義の名称をチーム内で共通認識を持つ デザインルールを1つのファイルに統合するため、複数のデザインファイルをまたいでデザインパターンの確認時間を減らす 曖昧なルールがなくなり、デザインに迷う時間を減らし、問題解決の思考に集中できる デザイン属人化から開放され、一貫性のあるデザインができる 新アプリケーションのデザインガイドの制作時間を減らす 紹介した通り、社内ではすでにコンポーネントのライブラリを用意しています。デザインガイドの整理は、このライブラリの使い勝手を検証する一環にもなります。 既存のライブラリは「100%正しい」ではなく、整理しながら、デザイン全体とライブラリの課題点を見つけて、ブラッシュアップしていくことが大事です。 これからもより使いやすくより良いものにしていくためにブラッシュアップしていきます! ※今回策定しているルールは、デザインの展開によって調整していく可能性があります。 最後までご覧いただき、ありがとうございました!
アバター
aptpod Advent Calendar 2021 1日目の記事です。 みなさまお久しぶりです。アプトポッドで人事をしている神前(こうさき)と申します。 前回の記事 からおよそ1年半ぶりの執筆、かつAdvent Calendarの一発目を担当することになり、またもや戦慄することになったのですが宜しくお願いいたします。 CTOからの依頼に応える、の図 さてどんなテーマで書こうかなと考えていたのですが、ちょうど3年前のAdvent Calenderに人事マネージャーの小沢が書いた 組織振り返りの記事 がありましたので、これを引き継ぐ形でいまのアプトポッドを数字で色々みてみることにしました。 組織構成 職種 職種属性 年齢 性別 居住地 まとめ 組織構成 本記事公開の12月1日時点で 71名 (社外役員含む)。 前回の3年前から比べてメンバーの入れ替わりはあるもののいまのところ組織としては 拡大中 となります。 また、今年初めて学生アルバイトを採用したりもしました。 職種 70名ぐらいの規模にしてはだいぶ細分化されている 3年前のグラフ 3年前の時点でも相当職種としてはバリエーションがあったのですが、さらに細分化が進みました。 増えた職種としては テクニカルコンテンツディレクター プロダクトマネージャー プロジェクトマネージャー 資材調達・製造マネジメント あたりがあります。 他にもインフラエンジニアがSREになっていたりとメンバーは変わらず名称が変化したケースもあります。 組織化が進む中で細分化されてできた職種もあれば、プロダクトのラインナップが増える中で専門的なポジションの必要性に応じてできた職種もあり背景は様々あります。この細分化傾向といいますか、バリエーションの拡大はまだしばらく続くと思われます。 一つの会社でソフトからハードまで幅広い職種のメンバーが在籍しているのは、この規模感の会社にしては珍しく、まだまだ事例としては少ないですが、 社内でのポジションチェンジも可能 なので幅広くいろんなレイヤーに携わりたい方にとってはよい環境ですね(ハードウェアエンジニア→サーバサイドエンジニア、アプリエンジニア→管理部、といった事例があります)。 職種属性 技術職が6割弱 職種の属性としては技術系が およそ6割 となります。 ただし、分類としてソリューションアーキテクトやプロジェクトマネージャーはビジネス職に分類をしてますので、元々開発者としてのバックグラウンドをもっているメンバーまで計算をすると 7割強 ぐらいにはなります。 年齢 30代と40代で7割 3年前の年齢構成 3年の時間の経過に伴い50代、60代のメンバーもちらほらとでてきましたが、依然30代、40代が圧倒的なボリュームゾーンを形成しています。 そのためいわゆる家族持ちのメンバーも多く、小さなお子さんがいるメンバーも多いです。 あまり大々的に宣伝をしているわけではないのでここで宣伝をさせてください! 弊社では入社のタイミングで有給休暇を付与 しています。入社をして6ヶ月経過しないと有給休暇が付与されない企業が一般的かと思います。弊社であれば入社直後に家族やお子さんの行事ごとがあっても安心して休みをとって参加することができます。 合わせて、 こちらの記事 もご覧ください。記事にもある通り現状リモートワークを基本とし、 働く時間帯や働き方についても個々人の裁量にお任せしています 。保育園への送り迎えのために一時的に抜けるメンバー、朝の5時や6時から業務を開始して、午後の3時や4時に退勤、夕方以降は家事や育児に時間を使うメンバーも実際にいます。 仕事や会社にライフスタイル、ライフサイクルを合わせるのではなく、家庭や育児に合わせて柔軟に働き方を調整できるのでだいぶ働きやすい環境ではないかと思います。 性別 男女比率は3年前からあまり変わらず 3年前の男女比 技術系のメンバーが多いのでこのあたりの男女比はあまり変わらずですね。 居住地 首都圏がやはりまだ大半 居住地については首都圏がほとんどです。ただし、昨年からリモートワークを開始したことに伴い、採用時の居住地や職種といったものを考慮して、 遠方地からでの勤務も可能 となっているのは大きな変化です。現状では大阪府在住のメンバーが1名と変化としては小さいですが、リモートワークはこれからも継続をしていく予定ですので、今後はこうした遠方地からの参画は増えるでしょうし、遠方地から参画したい!という方も歓迎です。 We are Hiring! まとめ いかがだったでしょうか。あまり社内の情報を出す機会がないのでこの機会に色々データを参照してまとめてみました。 3年前と比較して組織は大きくなってきていますし、特に昨年はコロナ禍の影響もあり社内外の環境もだいぶ変化をしました。 リモートワークによって、良くなっていることもあれば、それに伴って課題もまた生まれています。例えば居住地の制限が緩くなったことや、働き方が柔軟になったことは良いことです。反対に、リモートワークによる社内のコミュニケーションの希薄化や、拡大に伴う組織作りの大変さはこれから解決していかないと課題です。一言で言えば まだまだカオス といっても差し支えない状況ではありますが、出来上がった組織に参画するのとはまた別の楽しみがあるとも言えます。こんなカオスな状況を楽しみながら未来のアプトポッドを一緒に作っていけるメンバーを全方位で募集中です。 We are Hiring! 本記事では出していない数字面や、会社の雰囲気、カルチャーといったところについてはカジュアル面談の場でもお話できますし、 入社してみた系の記事 や、 産休育休関連の記事 もいくつかありますので、弊社に興味がある方は、是非本記事に合わせてご覧ください。 ではまた 3年後に お会いしましょう!
アバター
はじめに こんにちは。Visual M2M Data Visualizer Team の白金です。 弊社の製品の intdash では、H.264形式の動画データを収集/計測できます。計測した動画データは、Fragmented MP4 のフォーマットを使用したライブ動画をストリーミング再生したり、計測した動画を後から確認するためにHLSのフォーマットで再生する機能があります。 今回は、ライブ動画の再生機能を改善するための施策として 先日 Google Chrome の Version 94 でリリースされた WebCodecs の機能に含まれる VideoDecoder を使用して、H.264 のライブ動画をストリーミングで再生を試してみたのでご紹介します。 はじめに WebCodecs とは VideoDecoder を使用することで解決したい課題 課題 1: 欠損時の各動画フレームのタイムスタンプの判別 課題 2: Google Chrome で動作する H.264 デコーダーの遅延 VideoDecoder の使い方 H.264 の動画をデコードする Annex B AVCDecoderConfigurationRecord H.264 の動画を再生してみる Server を起動する 動画を再生する画面を表示する 動画を送信する おわりに WebCodecs とは WebCodecs は、ブラウザが内部で実装している H.264、V8 などのコーデックを用いて Video、Audio の動画ストリームをエンコード・デコードを実現するための低レベルAPIです。エンコード・デコードのみに特化したAPIとなるため、通信プロトコルに依存せず、アプリケーションが定義するタイミング、設定内容に応じて動画ストリームのエンコード・デコードが可能になります。 VideoDecoder を使用することで解決したい課題 VideoDecoder は通信プロトコルに依存しないため、弊社が提供している intdash Streaming Control Protocol との相性が良く、さらに下記2点の課題を解決することが可能になる見込みです。 課題 1: 欠損時の各動画フレームのタイムスタンプの判別 弊社から提供している Visual M2M Data Visualizer は、Fragmented MP4 フォーマットを使用した ライブ動画のストリーミング再生と、他のセンサーの値と、タイムスタンプを同時に可視化する機能を提供しています。 正確なタイムスタンプが取得できない課題 Fragmented MP4 のタイムスタンプは、計測を開始した時刻の情報と Google Chrome で参照可能な Video DOM の currentTime プロパティで判別が可能ですが、動画を計測するための各カメラは移動体に設置し、モバイル回線を通じて H.264 の計測データを送信するケースがあるため、通信環境、伝送帯域に依存して送信する H.264 フレームが欠損する場合があります。当ケースが発生すると欠損したフレームを詰めた状態でFragmented MP4 に変換されるケースがあるため、ライブ動画のタイムスタンプの表示にずれが生じることがあります。 *1 正確なタイムスタンプを参照可能にする VideoDecoder を使用すると、デコードした結果の画像とあわせてデコード時に指定したタイムスタンプを取得することができるため、上記課題を解決できるようになります。 課題 2: Google Chrome で動作する H.264 デコーダーの遅延 弊社から提供している Visual M2M Data Visualizer は、様々な H.264 エンコーダーで作成した動画を再生するケースがあります。 ハードウェアアクセラレーションを使用した時のデコード遅延の課題 動画を計測する際に使用する H.264 エンコーダーと、Google Chrome が使用している ハードウェアアクセラレーションの H.264 デコーダーの組み合わせによっては、デコードまでの十分なフレームを確保してからデコードするケースがあるため、低遅延表示の再生に影響が発生する可能性があります。当ケースはソフトウェアデコードを使用することで改善することがありますが、 Google Chrome の詳細設定で、「ハードウェア アクセラレーションが使用可能な場合は使用する」を変更する必要があり、他のコンテンツページへの影響範囲が広くカジュアルに設定変更ができない、など運用面で課題がありました。 VideoDecoder でソフトウェア・ハードウェアデコードの設定を切り替える VideoDecoder を使用することで、作成したインスタンスごとにハードウェアアクセラレーションの使用の ON / OFFを設定することが可能になるため、上記運用の課題が解決できるようになります。 VideoDecoder の使い方 ここからは、VideoDecoder を使用したサンプルコードも含めてご紹介したいと思います。 VideoDecoder は、動画をデコードするためのAPIに特化しているため、シンプルな構成になっています。 各APIの仕様については、 こちら に詳細情報が掲載されています。 実装の流れとしては、下記5つのステップになります。 output, error の Callback を実装します。 Callback を指定して VideoDecoder のインスタンスを作成します。 デコードする動画の情報を設定します。 分割された動画のキーフレーム、サブフレームをデコードします。 デコードが完了した画像から output の Callback に渡されて呼び出されます。 VideoDecoder の使い方のサンプル 下記は上図をもとに実装した VideoDecoder のサンプルコードです。 const canvasElement = document .getElementByID( 'canvas' ) /** * VideoDecoder のインスタンスを作成します。 */ const videoDecoder = new VideoDecoder( { // 動画フレームのデコードが完了すると、output のコールバックが実行されます。 output: (videoFrame) => { // デコードした videoFrame を Canvas に描画します。 canvasElement.width = videoFrame.codedWidth canvasElement.height = videoFrame.codedHeight const context = canvas.getContext( '2d' ) context.drawImage(videoFrame, 0, 0) // デコード実行時に指定したタイムスタンプも参照可能です。 console.log(videoFrame.timestamp) // 必要な処理が完了したら、VideoFrame を破棄します。 videoFrame.close() } , // 動画フレームのデコードが失敗すると、error のコールバックが実行されます。 error: (error) => { // エラー情報を出力します。 console.error(error) } } ) /** * VideoDecoder でデコードする動画の情報を設定します。 */ videoDecoder.configure( { codec 'avc1.64001E' , hardwareAcceleration: 'prefer-hardware' } ) /** * キーフレームをデコードします。 */ videoDecoder.decode( new EncodedVideoChunk( { type: 'key' , timestamp: 1234, data: new Uint8Array( [ ...... ] ).buffer, ) } /** * サブフレームをデコードします。 */ videoDecoder.decode( new EncodedVideoChunk( { type: 'delta' , timestamp: 4567, data: new Uint8Array( [ ...... ] ).buffer, ) } /** * VideoDecoder のインスタンスを解放します。 */ videoDecoder.close() H.264 の動画をデコードする VideoDecoder でデコードする情報は、 configure メソッドで指定する codec 、または任意で指定する description で決まります。 H.264 の動画フォーマットを指定する場合は、 Annex B と AVCDecoderConfigurationRecord の2種類があります。 どちらも、 codec の指定方法に違いはありませんが、description の指定方法によって EncodedVideoChunk で指定する data のフォーマットが異なります。 では、それぞれ違いを見ていきましょう。 Annex B Annex B はバイトストリーム形式です。開始コードプレフィックス 0x00、0x00、0x00、0x01 から始まる NAL Unit で構成されています。 詳細は、 T-REC-H.264 に添付されている仕様書に記載されていますので、ここでは詳細は割愛します。 videoDecoder.configure( { // 事前に判定済みのcodec、またはSPSフレームから判定します。 codec 'avc1.64001E' , hardwareAcceleration: 'prefer-hardware' } ) videoDecoder.decode( new EncodedVideoChunk( { type: 'key' , timestamp: 1234, // 開始コードプレフィックスから始まる各SPS、PPS、IDR の NAL Unit の先頭に追加したデータを指定します。 data: new Uint8Array( [ // SPS 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x1e, ..., // PPS 0x00, 0x00, 0x00, 0x01, 0x68, ..., // IDR 0x00, 0x00, 0x00, 0x01, 0x65, ..., ] ).buffer, ) } videoDecoder.decode( new EncodedVideoChunk( { type: 'delta' , timestamp: 4567, // 開始コードプレフィックスから始まる non-IDR の NAL Unit を指定します。 data: new Uint8Array( [ // non-IDR 0x00, 0x00, 0x00, 0x01, 0x41, ..., ] ).buffer, ) } AVCDecoderConfigurationRecord VideoDecoderConfig に掲載されているように、configure メソッドで description を指定すると、 SPS、 PPS の NAL Unit を使用した AVCDecoderConfigurationRecord で定義されているフォーマットでデコードするようになります。 // SPS、PPS NAL Unit から、 AVCDecoderConfigurationRecord に変換します。 // 当サンプルコードでは変換する処理は割愛します。 // 詳細は、上記添付の仕様書を参照してください。 const sps = new Uint8Array( [ 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x1E, ... ] ) const pps = new Uint8Array( [ 0x00, 0x00, 0x00, 0x01, 0x68, .... ] ) const extradata = toAVCDecoderConfigurationRecord(sps, pps) videoDecoder.configure( { codec 'avc1.64001E' , description: extradata, hardwareAcceleration: 'prefer-hardware' } ) videoDecoder.decode( new EncodedVideoChunk( { type: 'key' , timestamp: 1234, data: new Uint8Array( [ // AVCDecoderConfigurationRecord の `lengthSizeMinusOne` に応じたByteLengthと IDRの NAL Unit を指定します。 [ Length Bytes ] , 0x65, ..., ] ).buffer, ) } videoDecoder.decode( new EncodedVideoChunk( { type: 'delta' , timestamp: 1234, data: new Uint8Array( [ // AVCDecoderConfigurationRecord の `lengthSizeMinusOne` に応じたByteLengthと non-IDR の NAL Unit を指定します。 [ Length Bytes ] , 0x41, ..., ] ).buffer, ) } H.264 の動画を再生してみる MP4形式の動画ファイルを gstreamer 、Server を経由して Google Chrome で表示した VideoDecoder で再生するためのデモです。 下図構成で実装しています。 Web Codecs デモ構成図 Server を起動する Server のコードは下記の通りです。必要な処理のみ実装しています。 npm i -S ws で依存パッケージをインストールして実行してください。 H.264 の開始コードとNAL Unit 判定については、最低限のケアのみサポートしています。 *2 /* @ts-check */ const net = require( 'net' ) const EventEmitter = require( 'stream' ) const ws = require( 'ws' ) /** * 定義値 */ const WEB_SOCKET_SERVER_PORT = 18000 const TCP_SERVER_PORT = 3010 const H264_START_CODE = new Uint8Array( [ 0x00, 0x00, 0x01 ] ) /** * EventEmitter と使用するEvent Nameを定義します。 */ const EVENT_NAME = { CHUNKED_H264_PARSE: 'CHUNKED_H.264_PARSE' , H264_FRAME_SEND: 'H.264_FRAME_SEND' , } const eventEmitter = new EventEmitter() /** * ブラウザと連携する WebSocket Server を作成します。 */ const wsServer = new ws.Server( { port: WEB_SOCKET_SERVER_PORT, } ) wsServer.on( 'connection' , (conn) => { console.log( 'connect WebSocket Client' ) const handler = (data) => { conn.send(data) } eventEmitter.on(EVENT_NAME.H264_FRAME_SEND, handler) conn.on( 'close' , () => { console.log( 'close WebSocket Client' ) eventEmitter.off(EVENT_NAME.H264_FRAME_SEND, handler) } ) } ) wsServer.on( 'listening' , () => { console.log(`listening WebSocket Server on port $ { WEB_SOCKET_SERVER_PORT } `) } ) /** * 開始コードプリフィックスから始まる NAL Unit の単位に分割し、 * H264_FRAME_SEND Event に連携します。 */ let restChunkedH264Frame = new ArrayBuffer(0) eventEmitter.on(EVENT_NAME.CHUNKED_H264_PARSE, (data) => { let restBuffer = concatArrayBuffer(restChunkedH264Frame, data) while ( true ) { const restU8a = new Uint8Array(restBuffer) const startIndex = arrayIndexOfMulti(restU8a, H264_START_CODE, 0) if (startIndex < 0) { break } const endIndex = arrayIndexOfMulti(restU8a, H264_START_CODE, startIndex + 1) if (endIndex < 0) { break } const naluWithStartCode = restBuffer.slice(startIndex, endIndex) eventEmitter.emit(EVENT_NAME.H264_FRAME_SEND, naluWithStartCode) restBuffer = restBuffer.slice(endIndex) } restChunkedH264Frame = restBuffer } ) /** * gstreamer の tcpclientsink から H.264 のデータを受信する * TCP Server を作成します。 */ net.createServer((conn) => { console.log( 'connect TCP Client' ) conn.on( 'data' , (data) => { eventEmitter.emit(EVENT_NAME.CHUNKED_H264_PARSE, data) } ); conn.on( 'close' , () => { restChunkedH264Frame = new ArrayBuffer(0) console.log( 'client closed connection' ); } ); } ).listen(TCP_SERVER_PORT, () => { console.log(`listening TCP Server on port $ { TCP_SERVER_PORT } `) } ) /** * ArrayBuffer を結合します。 * @param {ArrayBuffer} buffer1 * @param {ArrayBuffer} buffer2 * @returns {ArrayBuffer} */ const concatArrayBuffer = (buffer1, buffer2) => { const dstU8a = new Uint8Array(buffer1.byteLength + buffer2.byteLength) dstU8a.set( new Uint8Array(buffer1), 0) dstU8a.set( new Uint8Array(buffer2), buffer1.byteLength) return dstU8a.buffer } /** * searchElements で与えられた内容と同じ内容を持つ * 最初の配列要素の添字を返します。 * 存在しない場合は -1 を返します。 * @param {Uint8Array} array * @param {Uint8Array} searchElements * @param {number} fromIndex * @returns {number} */ const arrayIndexOfMulti = (array, searchElements, fromIndex) => { const index = array.indexOf(searchElements [ 0 ] , fromIndex); if (searchElements.length === 1 || index === -1) { return index } let i = index for ( let j = 0; j < searchElements.length && i < array.length; i++, j++ ) { if (array [ i ] !== searchElements [ j ] ) { return arrayIndexOfMulti(array, searchElements, index + 1) } } return i === index + searchElements.length ? index : -1 } 動画を再生する画面を表示する 次に、Google Chrome で 動画を再生するデモを準備します。 下記 Javascript を含むHTMLのコードを localhost でホスティングしたサーバーで表示します。 WebSocket を 前述で起動した Server に接続し、開始コードと NAL Unit を message で取得し、WebCodecs の VideoDecoder を使用してデコードします。 また、下記サンプルコードは最低限のケア *3 のみサポートしているため、エラー発生時の処理が十分ではありません。 <!DOCTYPE html> < html lang = "ja" > < head > < meta charset = "utf8" /> < title > Web Codecs H.264 Play Demo </ title > </ head > < body > < h1 id = "timestamp" > Display Timestamp </ h1 > < canvas id = 'canvas' ></ canvas > < script > /** * 参照する HTMLElement を取得します。 */ const timestampElement = document .getElementById ( 'timestamp' ) const canvasElement = document .getElementById ( 'canvas' ) /** * H.264 NAL 関連の定義 */ const NAL_UNIT_TYPE = { IDR: 5 , NON_IDR: 1 , SPS: 7 , PPS: 8 , } let cachedSPSBuffer = new ArrayBuffer () let cachedPPSBuffer = new ArrayBuffer () /** * 複数の ArrayBuffer を結合します。 * @param {ArrayBuffer[]} arrayBuffers * @returns {ArrayBuffer} */ const concatArrayBuffers = ( arrayBuffers ) => { const sumByteLength = arrayBuffers.reduce (( acc, cur ) => { return acc + cur.byteLength } , 0) const concatenatedUint8Array = new Uint8Array ( sumByteLength ) let offset = 0 for ( const arrayBuffer of arrayBuffers ) { concatenatedUint8Array.set ( new Uint8Array ( arrayBuffer ) , offset ) ; offset += arrayBuffer.byteLength } return concatenatedUint8Array.buffer } /** * タイムスタンプの文字列を整形します。 * @param {number} unixTimeMillisecond * @returns {string} */ const formatTimestampText = ( unixTimeMillisecond ) => { const d= new Date ( unixTimeMillisecond ) const year = d.getFullYear () const month = ( d.getMonth () + 1) .toString () .padStart (2 , '0' ) const date = d.getDate () .toString () .padStart (2 , '0' ) const hour = d.getHours () .toString () .padStart (2 , '0' ) const minute = ( d.getMinutes ()) .toString () .padStart (2 , '0' ) const second = d.getSeconds () .toString () .padStart (2 , '0' ) const millisecond = d.getMilliseconds () .toString () .padStart (3 , '0' ) return `$ { year } /$ { month } /$ { date } $ { hour } :$ { minute } :$ { second } .$ { millisecond } ` } /** * VideoDecoder のインスタンスを作成します。 */ let prevDisplayedTimestamp = 0 const videoDecoder = new VideoDecoder ( { /** * @param {VideoFrame} videoFrame * @returns {void} */ output: ( videoFrame ) => { // Callback は順不同に実行される可能性があるため、 // 直前のタイムスタンプと逆転している場合は描画をスキップします。 if ( videoFrame.timestamp > prevDisplayedTimestamp ) { // デコードしたVideoFrame を Canvas に描画します。 canvasElement.width = videoFrame.codedWidth canvasElement.height = videoFrame.codedHeight const context = canvas.getContext ( '2d' ) context.drawImage ( videoFrame, 0 , 0) // デコード実行時に指定したタイムスタンプを表示します。 timestampElement.textContent = formatTimestampText ( videoFrame.timestamp ) } // 表示したタイムスタンプを保持します。 prevDisplayedTimestamp = videoFrame.timestamp // 必要な処理が完了したら、VideoFrame を破棄します。 videoFrame.close () } , /** * @param {Error} error * @returns {void} */ error: ( error ) => { console.error ( 'VideoDecoder Error' , error ) } } ) /** * WebSocket のインスタンスを作成、必要な情報を設定します。 */ const url = 'ws://localhost:18000' const ws = new WebSocket ( url ) ws.binaryType = 'arraybuffer' ws.onmessage = ( message ) => { // 開始コードプリフィックス "0x00、0x00、0x01" から始まる NAL Unit の単位で取得します。 const messageData = message.data // 当デモは、WebSocket のメッセージ受信した時刻をタイムスタンプとして扱います。 const timestamp = Date .now () const u8a = new Uint8Array ( message.data ) const nalUnitType = u8a [ 3 ] & 0x1f switch ( nalUnitType ) { case NAL_UNIT_TYPE.IDR: const spsU8a = new Uint8Array ( cachedSPSBuffer ) const hexProfile = spsU8a [ 4 ] .toString (16) .padStart (2 , '0' ) const hexCompatibility = spsU8a [ 5 ] .toString (16) .padStart (2 , '0' ) const hexLevel = spsU8a [ 6 ] .toString (16) .padStart (2 , '0' ) const codec = `avc1.$ { hexProfile } $ { hexCompatibility } $ { hexLevel } ` const hardwareAcceleration = 'prefer-hardware' videoDecoder.configure ( { codec, hardwareAcceleration, } ) videoDecoder.decode ( new EncodedVideoChunk ( { type: 'key' , timestamp, data: concatArrayBuffers ( [ cachedSPSBuffer, cachedPPSBuffer, u8a.buffer, ] ) } )) break case NAL_UNIT_TYPE.NON_IDR: videoDecoder.decode ( new EncodedVideoChunk ( { type: 'delta' , timestamp, data: u8a.buffer } )) break case NAL_UNIT_TYPE.SPS: cachedSPSBuffer = u8a.buffer break case NAL_UNIT_TYPE.PPS: cachedPPSBuffer = u8a.buffer break default : break } } </ script > </ body > </ html > 動画を送信する 最後に、gstreamer を使用して、MP4 のデータを H.264 の Annex B のフォーマットで擬似的に 動画をストリーミングします。動画データは、tcp プロトコルで、事前に起動した Server に送信します。 ${MP4_SRC_PATH} にはMP4ファイルのパスを指定します。MP4ファイルは ffmpeg を使用して libx264 のエンコーダーで作成した動画で確認しました。 gst-launch-1. 0 -q \ filesrc location = " ${MP4_SRC_PATH} " \ ! qtdemux \ ! h264parse config-interval = 1 update-timecode =true \ ! video/x-h264,stream-format = byte-stream, alignment =au \ ! tcpclientsink port = 3010 WebCodecs の VideoDecoder を使用して Google Chrome でライブ動画のストリーミング再生することができました! *4 youtu.be おわりに WebCodecs の VideoDecoder を使用して、ライブ動画ストリーミングを再生することを確認できました。 今回ご紹介できなかった、Media Source Extension API を使用した Fragmented MP4 の動画再生の比較、他の H.264 エンコーダーで作成した ライブ動画ストリーミングの確認結果については、次回以降のテックブログでご紹介したいと思います。 上記も含めて弊社製品、またはフロンドエンドに興味を持って頂けた方は是非、こちらの 弊社採用 ページもご覧ください。 製品に関するお問い合わせはこちらへ! www.aptpod.co.jp *1 : ライブ動画計測時に回収できなかった動画データは後で回収することで、HLS のフォーマットで再生する動画はずれなく再生が行われます。 *2 : tcp から受信したデータから、0x00、0x00、0x01 が見つかるまで、一つの開始コードプリフィックスから始まる NAL Unit を抽出するように実装しています。また、IDR、 non-IDR の1フレームが複数のスライスで構成されている H.264 のフレームの再生には対応していません。 *3 : 受信する message に含まれる開始コードは、0x00, 0x00, 0x01, ... のみサポートしています。また、受信する message が一定時間経過した後にVideoDecoder をが自動で closeする処理のリカバリー処理は実装していません。 *4 : 添付の動画の Web ページ画面左上のタイムスタンプは、H.264 の NAL Unit を受信した時点のタイムスタンプとしてサンプルコードを実装しました。そのため、MP4ファイルの動画をストリーミング再生している iPhone に表示している時刻と一致していません。
アバター
Protocol/Robotics Teamの酒井 ( @neko_suki ) です。 以前「 ROS2メッセージの遠隔リアルタイムデータ伝送を実現する新プロダクトのご紹介 」という記事で開発中の intdash_ros2bridge というプロダクトを紹介しました。 今回は、前回記載したとおり、技術にフォーカスして技術的な詳細を含めた進捗をご紹介します。 intdash_ros2bridgeは、ROS2上で「任意のトピック、サービス、アクションのC++実装によるブリッジ」を実現します。ブリッジとは、ROS2空間の内部のデータからインターネット経由で伝送できるメッセージ形式への橋渡しをする処理のことを指します。 intdash_ros2bridgeによって遠隔地のROS2空間をつなぐことが可能になります。その結果ROS1と同様にROS2でも遠隔制御やモニタリングなどが実現できます。 構成図 intdash_ros2bridgeではROSメッセージの伝送のために以下の2つの方法でブリッジを行います。 効率的なメッセージ伝送のための Common Data Representation (CDR) 形式という生データに近いフォーマットへのブリッジ 弊社製品のVisual M2M Data Visualizer で可視化するための、JSONへのブリッジ 今回の記事では、最初にトピック、サービス、アクションをブリッジする方法について簡単に紹介します。次に、トピックをブリッジする方法、トピックをJSONに変換して伝送する方法の2点を詳細に掘り下げて説明します。 ROSメッセージをブリッジする方法 トピックのブリッジ subscribe publish JSONへのブリッジ まとめ Appendix create_generic_subscriptionの実装 create_generic_publisherの実装 MessageMembers の取得方法 ROSメッセージをブリッジする方法 intdash_ros2bridge がブリッジするROSメッセージは主にトピック、サービス(サービスリクエスト・サービスレスポンス)、アクション(ゴールリクエスト・レスポンス、キャンセルリクエスト・レスポンス、フィードバック・リザルト)の3つです。汎用的な使用を想定しているため任意のROSメッセージを扱える必要があります。 任意のトピックのブリッジには、ROS2ユーザーにはおなじみの rosbag2 に実装されている rosbag2_transport::GenericSubscription と rosbag2_transport::GenericPublisher の技術を使用しています。 *1 。 rosbag2は任意のトピックをsubscribeして保存し、保存されたbagファイルに含まれるトピックを再生(=publish)することが可能です。intdash_ros2bridge ではこの技術を応用します。 一方で、技術調査を行った時点では、c++で任意のサービスやアクションをブリッジする実装は見つけられませんでした。 具体的な実装は見つけられませんでしたが、discourse.ros.orgの スレッド で実装方法について議論がされているのを見つけました。 スレッドでは、 GenericSubscription や GenericPublisher と同じ方針で実装できると言及されています。 そこで、intdash_ros2bridgeはスレッドで言及されている方法で自前でサービスとアクションのブリッジを実装しました。 ここからは現在オープンにアクセスできる情報で紹介が可能なトピックのブリッジとJSONへのブリッジについて、掘り下げて説明します。 トピックのブリッジ subscribe トピックをsubscribeする側は以下のような構成になります。 subscribeする側の構成 intdash_ros2bridgeは、トピックのブリッジに GenericSubscription という任意のトピックをsubscribeできる技術を使用します。 通常、ROS2でトピックをsubscribeする処理は Subscription を使って以下のように実装します。 rclcpp::Node *node = ROSノードのポインタ; const std::string topic_name = "topic" ; auto subscription_callback = []( const std_msgs::msg::String::SharedPtr msg) ) { // subscribe したトピック msg を元に処理を行う }; rclcpp::Subscription<std_msgs::msg::String>::SharedPtr> subscription = node->create_subscription<std_msgs::msg::String>(topic_name, 10 , subscription_callback); }; Subscription のインスタンスを生成するときは、 create_subscription という関数のテンプレートにsubscribeしたいデータ型を記述します。この例では6行目の std_msgs::msg::String がsubscribeしたいデータ型になります。 トピックをsubscribeしたときに呼び出されるコールバック関数(この場合は3行目の subscription_callback ) には、指定したデータ型が引数として渡されます。 ただ、 Subscription を製品に使うには2つ課題があります。 一つ目は、subscribeしたいすべてのデータ型について個別の実装が必要になる点です。ROS2では自作のデータ型を作成して使うことが可能です。ユーザーが作成する任意のデータ型に対して Subscription を使用した汎用的な製品を開発することは現実的ではありません。 二つ目は、コールバック関数で渡されるものは構造体という点です。構造体なので何らかの形式でシリアライズしないと、メッセージを受け取った側が正しく解釈できない可能性があります。 そこで、 GenericSubscription を使うとこの二つの課題を解決することができます。 rclcpp::Node *node = ROS ノードのポインタ; const std::string topic_name = "topic" ; const std::string topic_type = "std_msgs/msg/String" ; // シリアライズされたトピックへのポインタ以外の情報は渡されないので、トピック名、データ型はラムダ式でキャプチャする auto subscription_callback = [topic_type, topic_name](std::shared_ptr<rclcpp::SerializedMessage> msg) { // intdash Edge Agentにデータを渡す }; rosbag2_transport::GenericSubscription::SharedPtr subscription = create_generic_subscription(node_, topic_name, topic_type, rclcpp::QoS( 10 ), subscription_callback); GenericSubscription と Subscription には、大きな違いが2つあります。 一つ目は、 create_subscription の代わりに、 create_generic_subscription という 関数 を呼び出す点 *2 です 。 create_generic_subscription は、データ型 std_msgs/msg/String をテンプレートで指定せずに、文字列として引数で渡します。これにより、ユーザーが作成した任意のデータ型を実行時に指定することができます。 二つ目は、コールバック関数の引数です。 Subscription の場合は、 std_msgs/msg/String 型のトピックへのシェアードポインタがコールバック関数の引数でした。 GenericSubscription は rclcpp::SerializedMessage というCDR形式にシリアライズされたトピックへのシェアードポインタが渡されます。 シリアライズされた形式なのでそのまま伝送することが可能です。 intdash_ros2bridgeでは、このsubscribeしたシリアライズされたトピック、トピック名、データ型を、intdash Edge Agent に渡し、サーバーに伝送します。 publish トピックをPublishする側は以下のような構成になります。 publish側の構成 intdash_ros2bridgeは、intdash Edge Agent から受け取ったシリアライズされたトピックのpublishに、 GenericPublisher というシリアライズされたトピックをpublishできる技術を使用します。 GenericPublisher を使用すると、シリアライズされたトピックのpublishは以下のように書くことができます。 const std::string topic_name = "topic" ; const std::string topic_type = "std_msgs/msg/String" ; rosbag2_transport::GenericPublisher::SharedPtr publisher = create_generic_publisher(node, topic_name, topic_type, rclcpp::QoS( 10 )); // intdash edge Agent からシリアライズされたメッセージ「serialized_message」を取得する publisher->publish(serialized_message); GenericPublisher は create_generic_publisher という 関数 を呼び出して生成します *3 。 GenericPublisher のインスタンス生成時にデータ型 std_msgs/msg/String を文字列として引数で渡しています。これにより、ユーザーが作成した任意のデータ型を実行時に指定することが可能になります。 そして、 publish() メソッドにシリアライズされたトピックを渡すとそのトピックをpublishします。 こうすることで、intdash Edge Agentを経由して受け取ったシリアライズされたトピックをpublishすることが可能になります。 JSONへのブリッジ 次にシリアライズされたトピックをJSONに変換する方法を解説します。 以下の図のように、弊社製品のVisual M2M Data VisualizerはJSONを可視化することが出来るため、トピックをJSONに変換して送信すると遠隔モニタリングが可能になります。 JSON可視化の構成 intdash_ros2bridgeはシリアライズされたトピックをJSONに変換してintdash Edge Agentに渡します。渡されたJSONはサーバーを経由し、ブラウザ上で可視化が行えます。 JSONへのブリッジ方法を説明するために以下のような custom_msgs/Sample というデータ型を定義します。 int64 a # 整数 int64 b[] # 整数の任意長配列 sensor_msgs/Imu c # もしデータ型がわかればその構成を元に実装ができるので、JSONへのブリッジは以下のように書けます。 auto subscription_callback = []( const std_msgs::msg::String::SharedPtr msg) ) { // json にメンバa (64bit整数)を追加 // json にメンバb (64bit整数の配列)を追加 // json にメンバc (sensor_msgs/Imu) を追加 // json をintdash Edge Agentに渡す }; しかし、 GenericSubscription でsubscribeしたデータはシリアライズされています。そのままでは「データ型に含まれるメンバ構成の定義」や「それぞれのメンバの定義」がわからないため、JSONへのブリッジはできません。 rclcpp::Node *node = ROS ノードのポインタ; const std::string topic_name = "topic" ; const std::string topic_type = "std_msgs/msg/String" ; // シリアライズされたトピックへのポインタ以外の情報は渡されないので、トピック名、データ型はラムダ式でキャプチャする auto subscription_callback = [topic_type, topic_name](std::shared_ptr<rclcpp::SerializedMessage> msg) { // JSONに変換するためにはデータ型に含まれるメンバ構成の定義と、それぞれのメンバの定義が必要 }; rosbag2_transport::GenericSubscription::SharedPtr subscription = create_generic_subscription(node_, topic_name, topic_type, rclcpp::QoS( 10 ), subscription_callback); そこでROS2で提供されている「データ型に含まれるメンバ構成の定義」を含む rosidl_typesupport_introspection_cpp::MessageMembers (以下MessageMembers)と「それぞれのメンバの定義」を含む rosidl_typesupport_introspection_cpp::MessageMember (以下 MessageMember) という構造体を活用します。 データ型の定義と MessageMembers 、 MessageMember の関係は以下の図のようになっています。 MessageMembersとMessageMemberの関係 ROS2ではデータ型の定義を元に、共有ライブラリが生成されます。 MessageMembers と MessageMember は共有ライブラリに含まれます。 MessageMembers はデータ型に含まれるメンバの数、 MessageMember の配列へのポインタを含んでいます。 MessageMembers は以下のように定義されています。 typedef struct ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC MessageMembers { const char * message_namespace_; const char * message_name_; uint32_t member_count_; size_t size_of_; const MessageMember * members_; void (* init_function)( void *, rosidl_runtime_cpp::MessageInitialization); void (* fini_function)( void *); } MessageMembers; custom_msgs/msg/Sample に対して生成された MessageMembers は以下のようになります。 (staticで定義されていますが外部から取得可能です。詳細な取得方法は Appendix を参照してください。) custom_msgs/Sample の場合はメンバa, b, c の定義を含む MessageMember の配列へのポインタを持ちます(下記プログラムの6行目)。 static const ::rosidl_typesupport_introspection_cpp::MessageMembers Sample_message_members = { "custom_msgs::msg" , // message namespace "Sample" , // message name 3 , // number of fields // メンバの数 sizeof (custom_msgs::msg::Sample), Sample_message_member_array, // message members // それぞれのメンバの定義を含む MessageMember の配列 Sample_init_function, // function to initialize message memory (memory has to be allocated) Sample_fini_function // function to terminate message instance (will not free memory) }; custom_msgs/msg/Sample の場合は、 member_count_ から3つのメンバがいることがわかります。 そして、 Sample_message_member_array から custom_msgs/Sample を構成するそれぞれのメンバの定義を参照できます。 メンバの定義を記述している MessageMember は以下のように定義されています。 typedef struct ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC MessageMember { const char * name_; // メンバの名前 uint8_t type_id_; // メンバの型 size_t string_upper_bound_; const rosidl_message_type_support_t * members_; // sensor_msgs/Imu など別のデータ型の場合、ハンドラを含む bool is_array_; // 配列かどうかの判定 size_t array_size_; bool is_upper_bound_; uint32_t offset_; const void * default_value_; size_t (* size_function)( const void *); const void * (*get_const_function)( const void *, size_t index); void * (*get_function)( void *, size_t index); void (* resize_function)( void *, size_t size); } MessageMember; そして、それぞれのメンバの定義を含む Sample_message_member_array は 以下のように定義されます。 static const ::rosidl_typesupport_introspection_cpp::MessageMember Sample_message_member_array[ 3 ] = { { // メンバ a の情報 "a" , // name ::rosidl_typesupport_introspection_cpp::ROS_TYPE_INT64, // type // メンバの型 0 , // upper bound of string nullptr , // members of sub message false , // is array 0 , // array size false , // is upper bound offsetof(custom_msgs::msg::Sample, a), // bytes offset in struct nullptr , // default value nullptr , // size() function pointer nullptr , // get_const(index) function pointer nullptr , // get(index) function pointer nullptr // resize(index) function pointer }, { // メンバbの情報 "b" , // name ::rosidl_typesupport_introspection_cpp::ROS_TYPE_INT64, // type // メンバの型 0 , // upper bound of string nullptr , // members of sub message true , // is array 0 , // array size false , // is upper bound offsetof(custom_msgs::msg::Sample, b), // bytes offset in struct nullptr , // default value size_function__Sample__b, // size() function pointer get_const_function__Sample__b, // get_const(index) function pointer get_function__Sample__b, // get(index) function pointer resize_function__Sample__b // resize(index) function pointer }, { // メンバcの情報 "c" , // name ::rosidl_typesupport_introspection_cpp::ROS_TYPE_MESSAGE, // type // メンバの型 0 , // upper bound of string ::rosidl_typesupport_introspection_cpp::get_message_type_support_handle<sensor_msgs::msg::Imu>(), // members of sub message false , // is array 0 , // array size false , // is upper bound offsetof(custom_msgs::msg::Sample, c), // bytes offset in struct nullptr , // default value nullptr , // size() function pointer nullptr , // get_const(index) function pointer nullptr , // get(index) function pointer nullptr // resize(index) function pointer } }; custom_msgs/Sample の場合、 MessageMembers 経由で取得した MessageMember 配列の情報を走査すると以下のことがわかります。 メンバaは64bit整数 メンバbは64bit整数の配列 メンバcは別途定義されたデータ型である。 このように MessageMembers と MessageMember を組みわせると、実行時に動的に custom_msgs/Sample の構成を動的に取得することが可能になります。 長くなるため詳細は書きませんが、 eProsima/Fast-CDR というソフトウェアにはCDR形式にシリアライズされたメッセージから所定の型を読み取るAPIがあります。このソフトウェアを使うと、 MessageMember に書かれている型の値をシリアライズされたメッセージから読みとれます。 シリアライズされたメッセージの読み込み custom_msgs/Sample の場合は、最初に64ビット整数を読み込みます。次に、64bit整数の配列を読み込みます。最後に、sensor_msgs/Imu 型のメッセージを読み込みます。 読み取ったそれぞれのメンバの値はJSONに追加します。 // トピック名、トピック型をキャプチャしておく auto subscription_callback = [topic_type, topic_name](std::shared_ptr<rclcpp::SerializedMessage> msg) { MessageMembers members; // MessageMembers を取得 for ( size_t i = 0 ;i < members.member_count_; ++i){ // members.members_[i] 型のデータを読み込む // JSONに要素を追加する } // JSONに変換されたトピックをintdash Edge Agent に渡す }; subscription = create_generic_subscription(node_, topic_name, topic_type, rclcpp::QoS( 10 ), subscription_callback); このようにJSONにブリッジしたトピックはintdash Edge Agent、intdash Server を経由し、Visual M2M Data Visualizer で可視化することが可能になります。 Visual M2M Data Visualizer で可視化したROSトピック まとめ 今回は弊社が開発しているROS2メッセージの遠隔リアルタイムデータ伝送を実現するintdash_ros2bridgeというプロダクトを実現する方法について紹介しました。 本記事では、トピックのブリッジについて扱いましたが、サービスやアクションも同様の方法でブリッジすることが可能です。 弊社では現在のintdash_ros2bridgeの利用拡大やROSコミュニティへの貢献を視野に入れたOSS化に向けた計画も進めています。 プロダクト開発の進捗やOSS化の進捗がありましたらまた続報をお届けできればと思います。 最後までご覧いただきありがとうございました。 Appendix create_generic_subscriptionの実装 foxyでは GenericSubscription を生成する関数は、 Rosbag2Node の メンバとして実装されています が、Rosbag2Nodeを使う必要は無さそうだったので独自に生成用の関数を定義しています。 std::shared_ptr<rosbag2_transport::GenericSubscription> create_generic_subscription( rclcpp::Node * node, const std::string & topic, const std::string & type, const rclcpp::QoS & qos, std::function< void (std::shared_ptr<rclcpp::SerializedMessage>)> callback, rclcpp::CallbackGroup::SharedPtr callback_group ) { auto library_generic_subscriptor = rosbag2_cpp::get_typesupport_library(type, "rosidl_typesupport_cpp" ); auto type_support = rosbag2_cpp::get_typesupport_handle( type, "rosidl_typesupport_cpp" , library_generic_subscriptor); auto subscription = std::shared_ptr<rosbag2_transport::GenericSubscription>(); try { subscription = std::make_shared<rosbag2_transport::GenericSubscription>( node->get_node_base_interface().get(), *type_support, topic, qos, callback); node->get_node_topics_interface()->add_subscription(subscription, callback_group); } catch ( const std::runtime_error & ex) { RCLCPP_ERROR(node->get_logger(), "Error subscribing to topic %s , Error: %s " , topic.c_str(), ex.what()); } return subscription; } create_generic_publisherの実装 create_generic_publisher も同様に、独自に生成用の関数を実装しています。 std::shared_ptr<rosbag2_transport::GenericPublisher> create_generic_publisher( rclcpp::Node * node, const std::string & topic, const std::string & type, const rclcpp::QoS & qos) { auto library_generic_publisher_ = rosbag2_cpp::get_typesupport_library(type, "rosidl_typesupport_cpp" ); auto type_support = rosbag2_cpp::get_typesupport_handle(type, "rosidl_typesupport_cpp" , library_generic_publisher_); return std::make_shared<rosbag2_transport::GenericPublisher>( node->get_node_base_interface().get(), *type_support, topic, qos); } MessageMembers の取得方法 MessageMembers は build/custom_msgs/rosidl_typesupport_introspection_cpp/custom_msgs/msg/detail/sample__type_support.cpp というファイルに生成されています。 厳密には、 rosidl_typesupport_introspection_cpp__get_message_type_support_handle__custom_msgs__msg__Sample() という関数が定義されます。これを呼び出すことで、 rosidl_message_type_support_t 型のハンドラ Sample_message_type_support_handle へのポインタが取得できます。 MessageMembers へのポインタがここに含まれているため、これを使用します。 static const rosidl_message_type_support_t Sample_message_type_support_handle = { ::rosidl_typesupport_introspection_cpp::typesupport_identifier, &Sample_message_members, get_message_typesupport_handle_function, }; // ROSIDL_TYPESUPPORT_INTERFACE__MESSAGE_SYMBOL_NAME(rosidl_typesupport_introspection_cpp, custom_msgs, msg, Sample)は // rosidl_typesupport_introspection_cpp__get_message_type_support_handle__custom_msgs__msg__Sample に展開される ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC const rosidl_message_type_support_t * ROSIDL_TYPESUPPORT_INTERFACE__MESSAGE_SYMBOL_NAME(rosidl_typesupport_introspection_cpp, custom_msgs, msg, Sample)() { return &::custom_msgs::msg::rosidl_typesupport_introspection_cpp::Sample_message_type_support_handle; } ハンドラの取得方法は、 rclcppのtypesupport_helpers.cpp で実装されています。 *1 : galacticではROS2のc++クライアントであるrclcppに取り込まれています。 rclcpp::GenericSubscription 、 rclcpp::GenericPublication *2 : galacticではnode->create_generic_subscription() と呼び出せます。 *3 : galacticではnode->create_generic_publisher() と呼び出します。
アバター
こんにちは、オートモーティブグループの榮枝です。 オートモーティブグループでは、アプトポッド製品のIoTプラットフォームである「intdash」を活用して自動車産業へのIoTソリューションを提供しています。 昨今では、お客様の個別ニーズに応じてのソリューション提供に加えて、業界規格や業界共通的なニーズに対応すべくサービス開発も進めています。 今日は、開発中の、CCP(CAN Calibration Protocol)という自動車の内部状態を計測し調整するための通信をintdashを活用して遠隔で行えるようにするサービスについてご紹介させていただこうと思います。 予備知識 CCP 適合 ASAM CAN ECU A2Lファイル DAQ 開発中のCCPサービスのご紹介 既存の適合プロセス 1. 初期設定 2. 計測・適合の設定・準備 3. 計測・適合 4.分析 既存の適合プロセスの課題 本サービスが提案する適合プロセス 1.初期設定 2&3.計測の設定 4&5.(自動)CCP通信とクラウドへのデータ送信 6&7.リアルタイムモニタリングと計測データダウンロード 参考:本サービスのUIイメージ まとめ-今後の展望と要望投稿のお願い 予備知識 CCPは業界特有の規格のため、あまりご存知無い方も多いと思います。かくいう私も、CCPがC言語の派生言語ではないと知ったのはつい最近です(笑)。 そのため、本記事を読んでいただく前にそのあたりから簡単に説明しようと思います。 CCP CCP(CAN Calibration Protocol)を一言で説明すると以下のような感じです。 「主に車種開発時の適合業務に於いて、ECUのメモリ値の取得と書き換えをCANを利用して行うための通信プロトコルで、 ASAM という協会によって策定された規格です。」 といっても、これだけでは良くわからないと思います。 適合、ECU、CAN、ASAM、それと、CCPと合わせて利用されるA2LファイルやDAQ機能といった用語についても以下で簡単に説明します。 ちなみにこのCCPは、最後にリリースされたのは1999年(古い!)で、ASAM協会によると、CCPはXCPに置き換わった(=廃止になった)、と書かれていますが、車種開発の現場ではまだまだ現役です。 適合 近年の自動車では車両の制御は「車載のコンピュータ」が行っていますが、この制御パラメータを調整し、理想とする出力特性になるようにしていく、自動車開発におけるプロセスのことを適合と呼んでいます。 例えば、『アクセルをどれくらい踏み込んだときに、エンジン回転数はこれくらいになってほしい』という理想値があったとします。 この場合、エンジン回転数=f(アクセル踏み込み度, その他変数, ...)のような関係で表現できますが、この関数fの特性やパラメータを調整していくプロセス、というイメージです。 この適合プロセスについては少し先の「既存の適合プロセス」の章でもう少し詳しく説明します。 CCPの二文字目を示す「Calibration」という単語がこの「適合」を表しています。 一般に「Calibration」という単語を日本語訳する際には「較正」とか、 そのままカタカナで「キャリブレーション」ということが多いですが、 自動車開発の文脈では「適合」と訳されるようです。 ASAM Association for Standardisation of Automation and Measuring Systems(自動化システムと測定システムの国際標準化団体)の略称です。 www.asam.net 自動車の研究・開発・システム検証等におけるデータモデルや、ファイルフォーマット、モジュール間の通信(API)、通信プロトコルなどを定義(整理)することなどを主な活動としています。 BMW、ボッシュ、コンチネンタル、ダイムラー、デンソー、デルファイ、GM、ホンダ、PSA、SAIC、トヨタ、TRW、フォルクスワーゲン、ボルボなど、ドイツやUS、日本の主な自動車メーカー関連が参画しています。 ASAMではASAMが策定した各種規格書を有料で販売しているのですが、若干お高く、、 ASAMに入会すると会費は必要なのですが、これが自由にダウンロードできるようになり、規格書だけを単品購入するよりかなりリーズナブルになります。 このCCP向けサービスを開発するにあたり、aptpodも先日メンバーになりました!! https://www.asam.net/members/detail/aptpod-inc/ CAN 「Controller Area Network」の略で、主に「車載のコンピュータ」間の通信に利用されている通信プロトコルです。 CCPではこの「車載のコンピュータ」間の通信に割って入って「車載のコンピュータ」と通信し、メモリ値の取得や書き換えを行います。 ECU 「Electronic Control Unit」の略で、車載のコンピュータのことです。 近年の車両では、一つの車両に数十〜数百のECUが搭載されていて、自動車の様々な挙動の制御を行っています。 A2Lファイル 「ASAM MCD-2 MC Language」の略で、ASAMによって策定された規格であるASAM MCD-2 MCにて定義された、ECUの内部状態を記述するためのファイルフォーマットです。 XMLに似た独自のタグ形式のフォーマットで、ECUのメモリ上のどのアドレスにどういったデータが格納されているかやそのデータの解釈方法等が記載されています。このデータ数は数千から、多いものだと数万レコードほどあったりします。 だいぶ省略したものですが、以下がA2Lファイルのサンプルです。このように/beginと/endのタグで構成された構造をしています。 ASAP2_VERSION 1 71 /begin PROJECT ASAP2_Example "" /begin HEADER "ASAP2 Example File" VERSION "V1.7.1" PROJECT_NO P2016_09_AE_MCD_2MC_BS_V1_7_1_main /end HEADER :(省略) /begin MODULE Example "" /begin A2ML :(省略) /begin COMPU_METHOD CM.RAT_FUNC.DIV_10 "rational function with parameter set for impl = f(phys) = phys * 10" RAT_FUNC "%3.1" "km/h" COEFFS 0 10 0 0 0 1 /end COMPU_METHOD :(省略) /begin MEASUREMENT ASAM.M.SCALAR.UBYTE.RAT_FUNC.DIV_10 "Scalar measurement with status string" UBYTE CM.RAT_FUNC.DIV_10 0 0 0 252 /* restricted limits to exclude status string values from standard measure range */ ECU_ADDRESS 0x13A00 FORMAT "%5.0" /* Note: Overwrites the format stated in the computation method */ DISPLAY_IDENTIFIER DI.ASAM.M.SCALAR.UBYTE.RAT_FUNC.DIV_10 /* optional display identifier */ /begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 5 /end IF_DATA /end MEASUREMENT :(省略) /begin CHARACTERISTIC ASAM.C.SCALAR.SWORD.RAT_FUNC_DIV_10 "Scalar SWORD" VALUE 0x810004 RL.FNC.SWORD.ROW_DIR 0 CM.RAT_FUNC.DIV_10 -10000 20000 EXTENDED_LIMITS -32268 32267 DISPLAY_IDENTIFIER DI.ASAM.C.SCALAR.SWORD.RAT_FUNC_DIV_10 /end CHARACTERISTIC :(省略) /end MODULE /end PROJECT DAQ 「Data Acquisition」の略で、CCPで定義されている機能の一つです。 複数のデータセットを高いサンプリングレートで連続的に取得するための機能で、最初に取得したいデータのリストをECUに登録し、登録し終えたら「取得開始」というリクエストを送ることで、ECUから指定のデータが一定のサンプリングレートで流れてくる様になります。 なお、CCPでは取得したいデータを逐次ECUにリクエストして取得する機能なども定義されています。 開発中のCCPサービスのご紹介 既存の適合プロセス では、ようやくこの記事の本題の遠隔CCPサービスについてご紹介していきます。 CCPを利用する車種開発の適合プロセスでは、現状では概ね以下のような作業が行われています。 (目的や利用する機材などによって異なる部分もありますが概ねこのような流れです。) 1. 初期設定 CCPに対応した専用ソフトウェアがインストールされたPCを用意します 専用ソフトウェアに対象車両・ECUのA2Lファイルを読み込ませます 2. 計測・適合の設定・準備 専用ソフトウェア上で、A2Lに記載されているデータ一覧から計測したいデータを選択します 3. 計測・適合 PCと車両(CAN)を接続します 専用ソフトウェアで計測開始します 必要に応じて専用ソフトウェアで制御パラメータ書き換えを行います 4.分析 ソフトウェアに溜まった計測データをダウンロードします 別途用意したシステム等で、ダウンロードした計測データを分析、適合状況を確認します その後2,3,4のサイクルを繰り返しながら、徐々に車を目的の出力特性に近づけて行きます。 既存の適合プロセスの課題 前述の適合プロセスでは、4の「分析」は車両内で行えなくもないのですが、落ち着いて分析するために大抵は作業スペースへ移動して作業します。つまり、この作業サイクルを繰り返すには車両と作業スペースを行ったり来たりすることになります。 また、計測したデータはローカルファイルとして扱いますので、誰がいつどういった目的・設定で計測したかの管理や、開発チーム内での共有を行うには別途何かしらの運用ルールやシステムを用意する必要があります。 この様に、現行の適合プロセスには 落ち着いた環境で一連の業務を行えない。 計測データの管理や共有に一苦労する。 といった点で課題感があります。 本サービスが提案する適合プロセス 本サービスは以下のイメージのシステムで、前述の課題を改善・解決しつつ更にいくつかの付加価値を提供します。(なお、現在現状では計測(DAQ)機能のみに対応しており、制御パラメータ書き換え機能は将来対応となります。) このシステムでの適合プロセスの流れは以下のようになります。 1.初期設定 弊社が提供するクラウドサービスのWebアプリケーションにウェブブラウザでアクセスします A2Lファイルをクラウドにアップロードします 2&3.計測の設定 クラウド上で、A2Lファイルに記載されているデータ一覧から計測したいデータを選択し、車載器に適用します。なお、車載器は自動的にクラウドから計測設定をダウンロードします。 4&5.(自動)CCP通信とクラウドへのデータ送信 車載器は計測設定に従って自動的にCCP通信を開始し、取得したデータをクラウドに送信します。 6&7.リアルタイムモニタリングと計測データダウンロード 車載器から送られたデータは、VisualM2Mという弊社サービス(参考: https://www.aptpod.co.jp/products/data-visualization/ )を利用して、リアルタイムに確認することができます。また、もちろん計測後にダウンロードすることもできます。 この様に、各種データのクラウド管理と通信機能を持った車載器によって、「作業スペースにいながら計測設定・計測・分析」と「計測データをクラウドで一元管理」できるようになります。 また、本システムではCCPに合わせて、CANバスデータや、位置情報、動画、音声、アナログ信号(参考: ANALOG-USB Interface )なども時刻同期しながら計測することが可能です。 参考:本サービスのUIイメージ A2Lファイルから計測対象のデータの選択画面 まとめ-今後の展望と要望投稿のお願い 本サービスは2021年秋にリリースします!! ASAMのMeasurementとCalibrationの規格群への対応を目標にまずはCCPのDAQへ対応しています。 今後、CCPの適合やXCPも遠隔で行えるような機能や、計測データをMDFとして出力する機能なども計画しています。 このサービスを使ってみたい! CCPだけでなくこんな機能も欲しい! といったご要望があればぜひお問い合わせください。 お問い合わせはこちらから ↓↓↓ aptpod,Inc. Contact
アバター
Protocol/Robotics Teamの酒井 ( @neko_suki ) です。 今回は「ROS2メッセージの遠隔リアルタイムデータ伝送を実現する新プロダクトのご紹介」というタイトルでintdash_ros2bridgeという開発中の新プロダクトについてご紹介します。 弊社は、ROS1で 任意トピックをC++ノードでPublish/Subscribeする方法 を活用したROSメッセージの遠隔リアルタイムデータ伝送を行う intdash Bridge というプロダクトを提供しています。intdash Bridgeを使うことで遠隔地のROS1空間をつなぎROS1のメッセージをやり取りすることによる遠隔制御やモニタリングなどのユースケースが実現できます。 今回ご紹介するintdash_ros2bridgeは、ROS2上で「任意のROSトピック、サービス、アクションのブリッジ」を実現 *1 します。intdash_ros2bridgeによって遠隔地のROS2空間をつなぐことが可能になります。その結果ROS1と同様にROS2でも遠隔制御やモニタリングなどが実現できます。 現在、intdash_ros2bridgeは開発の最終段階を進めるとともに、将来のintdash_ros2bridgeの利用拡大やROSコミュニティへの貢献を視野に入れたOSS化に向けた計画も進めています。 本記事では実際に動作させてみたデモ動画と構成について紹介します。 デモ intdash_ros2bridge によるROS2のトピック伝送デモ intdash_ros2bridge によるROS2のサービス伝送デモ intdash_ros2bridge によるROS2のアクション伝送デモ intdash_ros2bridge によるJSONの伝送デモ デモ構成 まとめ デモ トピック・サービス・アクションの伝送、JSON伝送の様子をキャプチャしたデモ動画を用意しました。 デモの構成とデモ画面の構成は以下のようになっています。 デモ構成と画面構成 デモでは遠隔地を模倣するために2台のUbuntu上で別々にROS2ノードを動かします。図には書いていませんが実行を簡単に行うためにDockerコンテナを使用しています。Dockerコンテナはそれぞれ左右のターミナル上で実行されています。 それぞれのDockerコンテナはROS2で接続されないため直接トピック・サービス・アクションが届くことはありません。 tmuxコマンドでそれぞれのターミナルを分割し、「(トピック・アクション・サービスを実行する)ROS2ノード」、「intdash_ros2bridge」、「intdash Edge Agent (サーバーへのデータ伝送に使用する弊社プロダクト)」を実行します。 それでは動画をご覧ください。 intdash_ros2bridge によるROS2のトピック伝送デモ 左側のターミナルは文字列型のメッセージ "data: Hello world" をpublishします。右側のターミナルではブリッジされてインターネット経由で届いたメッセージをsubscribeします。 Dockerコンテナ間のROS2の接続が無いことを確認するために、左側のターミナルでpublishした文字列メッセージを右側のターミナルではsubscribeできないことも確認しています。 youtu.be intdash_ros2bridge によるROS2のサービス伝送デモ この動画では、3つの整数値を受け取り和を返すサービスを実行します。左側のターミナルで実行されるサービスクライアントは3つの整数を送信します。右側のターミナルで実行されるサービスは3つの値を加算した結果を返します。 この動画でも、Dockerコンテナ間のROS2接続が無いことを確認するために、左側のターミナルのintdash_ros2bridgeとintdash Edge Agentを停止した場合はサービスが実行されないことを確認しています。 youtu.be intdash_ros2bridge によるROS2のアクション伝送デモ この動画では、フィボナッチ数列を返すアクションを実行しています。 左側のターミナルで実行されるアクションクライアントはゴールリクエストとして整数値11を送信します。 右側のターミナルで実行されるアクションサーバーは、フィードバックとして長さを一つずつ増やしてフィボナッチ数列を返します。 ゴールリザルトは、長さ11のフィボナッチ数列です。 この動画でも、Dockerコンテナ間のROS2接続が無いことを確認するために、左側のターミナルのintdash_ros2bridgeとintdash Edge Agentを停止した場合はアクションが実行されないことを確認しています。 youtu.be intdash_ros2bridge によるJSONの伝送デモ この動画では、弊社プロダクトの「Edge Finder」を使用して、/chatter メッセージをJSONに変換して伝送させたものを表示させています。 youtu.be 記事本文では、デモの構成と技術的な実現方法を簡単に説明します。 デモ構成 デモは以下のような構成で動いています。 デモ構成図 デモでは、2台のUbuntu上で別々にROS2を動かします。この2台のUbuntuは弊社プロダクトのintdash Edge Agent というソフトウェアによって、弊社プロダクトのintdash Server経由で双方向のデータのやり取りを行います。 intdash_ros2bridgeがROS2空間とintdash Edge Agentをつなぐ役割を担います。 具体的には、intdash_ros2bridgeは、ROS2ノードからトピック、サービス(サービスリクエスト・サービスレスポンス)、アクション(ゴールリクエスト・ゴールレスポンス、フィードバック、リザルト)を取得しシリアライズしてintdash Edge Agentに渡します。 また、intdash Edge Agentから受け取ったシリアライズされたROS2のトピックのpublish、サービス、アクションのメッセージをデシリアライズしてROS2空間内に送信します。 トピックは以下のような流れで伝送されます。 左側のUbuntu上で実行しているintdash_ros2bridgeは、ROS2ノードがpublishしたトピックを CDR(Common Data Representation) という形式にシリアライズされた状態でsubscribeします。 intdash_ros2bridgeはシリアライズされたデータをintdash Edge Agentに渡します。 intdash Edge Agentに渡されたデータはintdash Serverを経由して右のUbuntuで動作しているintdash Edge Agentに渡されます。 右側のUbuntu上で実行しているintdash_ros2bridgeは、intdash Edge AgentからCDRにシリアライズされたROS2のトピックを受け取ります。 intdash_ros2bridgeは、CDRにシリアライズされているトピックをpublishします。 このような流れで直接接続されていないROS2空間の間でトピックの伝送を実現しています。 サービス・アクションは以下の流れ伝送されます。 左側のUbuntu上で実行しているintdash_ros2bridgeは遠隔先で動いているサービス・アクションと同名のサービス・アクションを提供します。これによって、左側のUbuntuのROS2ノードが実行したサービスリクエスト・ゴールリクエストを受け取ることが可能になります。 intdash ros2bridgeは受け取ったサービスリクエスト・ゴールリクエストをCDRという形式にシリアライズし、intdash Edge Agentに渡します。 intdash Edge Agentに渡されたデータはintdashサーバーを経由して右側のUbuntuで動作しているintdash Edge Agentに渡されます。 右側のUbuntu上のintdash_ros2bridgeはintdash Edge Agentから受け取ったシリアライズされたサービスリクエスト・ゴールリクエストをデシリアライズし、サービスリクエスト・ゴールリクエストを送信します。 右側のUbuntuのROS2ノードで実行されているサービス・アクションは、intdash_ros2bridgeが発行したサービスリクエスト・ゴールリクエストを受け取り、処理を行います。そして処理した結果(サービスレスポンス、ゴールレスポンス/ゴールフィードバック/ゴールリザルト)をintdash_ros2bridgeが受け取ります。intdash_ros2bridgeはここまでの流れと逆の流れで左側のUbuntuにメッセージを送信します。 このような流れで、直接接続されていないROS2空間の間でサービス・アクションの伝送を実現しています。 まとめ 今回は「ROS2メッセージの遠隔リアルタイムデータ伝送を実現する新プロダクトのご紹介」というタイトルで、intdash_ros2bridgeという「ROS2で任意のROSトピック、サービス、アクションのブリッジ」が行える開発中のプロダクトについてご紹介しました。 次回は技術にフォーカスして、9月頃に技術的な詳細を含めた進捗を紹介します。 最後までご覧いただきありがとうございました。 *1 : pythonによる実装は、 rosbridge_server がありますが、本プロダクトはc++で実装しています
アバター
はじめに Hardware/OTグループの加藤です。 私はハードウェア系のエンジニアで、これまでデータ計測にオシロスコープ・デジタルマルチメータ・データロガー等を使用したことがあります。 それぞれ一長一短なので組み合わせて使用することになるのですが、それぞれ別々の測定器である為、取得したデータは連携していませんでした。特にオシロスコープの波形には時刻の概念はなく、他の測定器のデータとタイムスタンプを合わせて検証することはできませんでした。 また、長時間連続稼働テストや遠隔地での長時間におよぶ動作テスト等ではネットワークカメラを使用しました。 自宅やオフィスで状態を確認できるようになるのは便利なのですが、 エラーや故障が起きてしまった場合にはどういう状況でそうなったかはさっぱりわからず遠い場所で途方に暮れるしかありませんでした。 製品そのものに通信機能が無い場合が最悪で、常に見張る必要がありました。 製品か自分か動作が止まってしまう(寝てしまう)のはどちらが先かみたいな状況になりがちでした。 intdash 、 EDGEPLANT T1 を使用すると、LTE回線で各種データを同じ時系列上のタイムスタンプを付加して記録することができます。 今回、ANALOG-USB Interfaceという製品を約1年かけて設計→開発→量産に至り、アナログデータも測定できるようになりました。 これら弊社の製品を組み合わせると、LTE回線経由の遠隔アナログデータ計測や、計測したデータの可視化が行えるようになり、冒頭の課題が全て解決します。 本記事では、ANALOG-USB Interfaceを使用したアナログデータ計測のメリットとこれを試した結果を共有します。 ANALOG-USB Interface はじめに aptpodの遠隔アナログデータ計測 特徴 メリット 測定の内容 測定データの確認方法 洗濯の流れ 洗い すすぎ 脱水 結果 最後に aptpodの遠隔アナログデータ計測 以下の特徴・メリットがあります。 特徴 アナログ信号をデジタル(16bit)に変換して保存します。 測定対象に合わせたサンプリングレートを設定できます。 カメラ画像とセットで記録できます。 先日のブログ でご紹介させていただいたようなCANのデータとも同じ時系列上のデータとして記録できます。 測定データ・画像は、測定中直ちにクラウドに保存されます。 LTE回線を使用できます。 トンネル内等、電波が届かないエリアを通過しても測定データは失われません。 メリット 情報共有がスムーズ 任意のタイミング、場所から確認できます。ブラウザが動作すれば、弊社が提供するソフトウェア Visual M2M Data Visualizer を使ってパソコンで確認できます。 直感的に理解できる Visual M2M Data Visualizerを使うとアナログ信号を波形で確認することができます。 解析できる カメラ画像とセットでアナログ信号を記録できます。画像から、アナログ信号波形を見たいタイミングを探すことができます。 トリガーを設定して検索することもできます。連続運転テストやデバッグに便利です。 机の上で使える エッジコンピューターも測定用デバイスもコンパクトサイズです。机の上で使用することも持ち運びすることも可能です。 移動する物の測定にも使用できる LTE回線でデータを送信することが可能な為、自動車やAGV、その他移動ロボット内部の信号を測定することも可能です。 測定の内容 測定のイメージをつかんでいただく為に実際に測定を行います。 aptpodは自動車などの車両関係のお客様の実績が多いのですが、 今回のテックブログを書くにあたってはより幅広い業種の方々に興味をもっていただきたいと考えたこと、在宅勤務が一般化したことなどから、家庭にあり身近なもので、普段の生活では可視化されていないものを測定したいと考えました。 結果、洗濯機を測定することにしました。家庭内で毎日激しく動いている物だからです。 洗い・すすぎ・脱水と3種類の動作モードがありますので、各動作モード時の振動の計測を行います。 アナログ信号が出力可能な3軸加速度センサを洗濯機の上に貼り付け、このセンサの出力信号を測定します。 センサの出力信号は、冒頭ご紹介させていただいたEDGEPLANT ANALOG-USB Interfaceに接続します。 また、洗濯機の表示器をUSBカメラで録画します。洗濯機の表示を記録することで、アナログ信号がどの動作モードの時のものなのか分かるようになります。 ANALOG-USB InterfaceとUSBカメラは、エッジコンピュータに接続します。エッジコンピュータには、EDGEPLANT T1を使用します。弊社が提供するミドルウェアintdash Edgeを組み込みます。 接続図 エッジコンピュータの電源をONにするだけで、測定は開始されます。 ただし、測定対象の設定をあらかじめ行う必要があります。 今回の測定では、アナログ信号の測定レンジは0-5V、サンプリング周波数は625Hzに、カメラ画像の解像度は640x480、フレームレートは秒間15フレームに設定しました。 測定データの確認方法 測定データは、弊社が提供するソフトウェア Visual M2M Data Visualizerで確認します。 このソフトウェアはWebブラウザを使用して測定データを確認できるツールです。信号波形、電圧値、カメラ画像など各表示はパーツ化されていて自由に配置できます。信号波形の表示パーツはANALOG-USB Interfaceの為に新規開発したものです。今回の測定では上記のような画面イメージで、アナログ信号波形、電圧、カメラ画像を確認しました。 各電圧波形の色と内容の対応は下記のとおりです。 青(ch0)=奥行き方向の加速度 赤(ch1)=左右方向の加速度 緑(ch2)=上下方向の加速度 使用したセンサーの感度は、660mV/gです。緑(ch2)は常に重力加速度が加わる為、静止時の電位が高くなっています。 洗濯の流れ 洗濯は標準コースで行いました。洗い→すすぎ→脱水の順に処理が行われました。 Visual M2M Data Visualizerは、測定全体を表示する機能もあります。 下記の様に処理が行われたことをカメラ画像を見て確認しました。また、全体の処理はおよそ33分でした。 洗い 各動作モードにおける加速度センサの出力の詳細を見ていきましょう。 「洗い」の特徴は単純な波形が長時間続くことです。 最初に洗濯する衣類の量の測定、洗剤の投入、注水がありますが、ドラムの回転が始まると規則的な挙動の繰り返しになります。 およそ1秒おきに左右方向と上下方向の振動が発生します。ドラムが回転し衣類が動く周期が現れているものと考えられます。 他の動作モードと比較して振動は小さいです。 「洗い」時の代表的な波形は以下のとおりでした。 「洗い」時の波形 すすぎ 「すすぎ」は前半と後半で動作が違います。前半は多くの水を使い、すすぎを行います。後半は、排水を行います。 すすぎは、上下方向の振動が他よりも大きいのが他の動作と違います。音が大きく出ませんし、見た目に大きく揺れているようには見えないので意外でした。 「すすぎ」時の代表的な波形は以下のとおりでした。 「すすぎ」の時の波形 脱水 「脱水」動作も2段階で構成されています。最初に小さい振動による動作を行った後、大きい振動動作になります。 時間的には、殆どが大きい振動動作になっています。脱水は3軸とも波形が大きく振れるのが特徴です。 「脱水」時の代表的な波形は以下のとおりでした。 「脱水」時の波形 結果 動作全体のフローをつかんで、洗い・すすぎ・脱水時の加速度センサ出力信号波形を確認することができました。 「ANALOG-USB Interface」と「EDGEPLANT T1」を組み合わせた遠隔のアナログデータ計測や、「Visual M2M Data Visualizer」による計測した結果の可視化が行えることが、ご理解いただけたのではないかと思います。 測定時のデータはcsv形式でダウンロード可能です。お客様がご使用になられる測定では、測定データのアナログ信号のフィルター処理や解析を行えます。 今回は洗濯機の振動を加速度センサで測定しましたが、アナログ信号であればセンサ以外についても測定可能です。 例えば、刻々と変化するカメラ画像を入力とするシステムや製品が出力する信号を測定すること等もできます。画像も信号も同じ時系列上のタイムスタンプが割り振られますので、どういった入力状態で何を出力したか検証できます。それらの開発やテスト等にもご利用いただけるのではないかと考えております。 加えて、CAN-USB Interfaceも併用すればCANの信号も測定できますので、自動車の様に複雑なもので統合的な計測を行う事も可能となっております。 最後に 次回私が担当させていただく記事では、自動車でCANとアナログ信号の時刻同期を行った測定を行いたいと思います。 今回の測定で使用した弊社の製品・サービスのリンクを以下に記載いたします。 intdash Visual M2M Data Visualizer EDGEPLANT T1 EDGEPLANT Peripherals もし、ご興味を持たれましたら お問い合わせ までご連絡ください。
アバター
こんにちは。製品開発グループにて機械学習の研究開発まわりを担当しているきしだです。 現在、研究開発の一環として、「デバイスから収集された時系列データから、発話された箇所を自動で検出するシステム」を開発しています。現在はまだ試作段階ですが、この試作品を用いて実際に利用機会があるかどうか検証しているフェーズです。そこで、今回はこの試作内容をテックブログにてご紹介します。 ※ 現在こちらの試作品にご興味がある方々を募集しております。試しに利用してみたい、という方がいらっしゃいましたらぜひ こちら までお問い合わせください。 ことの背景 走行データをフィードバックデータとして活用する フィードバックデータの価値を高める難しさ フィードバックデータの価値を"発話箇所"で高める 機能の概要 実現方法 アーキテクチャ 発話検出のロジック 実際に動かしてみる まとめ ことの背景 走行データをフィードバックデータとして活用する 弊社のプロダクトである intdash は、各デバイスから収集した高頻度の時系列データを伝送・管理するミドルウェア的要素を持っています。収集および可視化にはそれぞれアプリケーションが提供され、その間をつなぐパイプラインとして様々なデータリソースを活用できます。 利用ケースとしては、走行する車のエンジン回転率や前方方向を撮影した走行動画、走行位置などを走行データとして記録しておき、走行時におかしなところがないか後からチェックしたいケースが挙げられます。例えば、最近発表があった大阪ガス様の "AI/IoTによる工事現場自動検出システム" では、走行ルートから工事現場という特定の要素を検出し、後から工事現場の配置状況を走行時のフィードバックとして確認する運用フローをサポートしています。 このように、走行状況を後からフィードバックデータとして利用したいケースは、他案件でも需要があることが確認できています。ここでは"おかしなところ"を検出しておくことで、フィードバックデータの価値をより高めることができる点がポイントです。 フィードバックデータの価値を高める難しさ しかし検出を行うためには、検出を行うためのトリガーが必要です。 ここでいうトリガーは、"スピードが80km/hを超えた"というような単純なしきい値で定義付けできるものもあれば、 "道に穴が空いている" というような、数式化するには骨が折れるような定義付けも含まれます。現状では後者に対する需要が高まっており、機械学習を使ってソリューションを検討する場合が多いですが、機械学習では精度の担保にリスクがついてまわったり、開発リソースがコントロールしづらいため、機械学習に知見がないと手を出しにくい領域です。 一方でこのような作業を人手で行うと、一日数時間の走行データを最初から最後まで目でみて、異常がないかチェックすることになり、時間が非常にかかってしまいます。加えて複数人にスケールすると、判断基準がぶれたりしてチェック内容が正確でなくなったりと、課題はなかなか無くなりません。 フィードバックデータの価値を"発話箇所"で高める そこで少し発想を変えてみて、 トリガーの判断は人間が行いつつ 判断時の記録を運転しながらできるようにし 後から確認すれば記録を見れるようになる とすれば、フィードバックデータの価値を高められるのでは?と考え「音声を使って"おかしなところ"を記録し、後から見れるようにする」ことを思いつきました。 運転中に人が"穴があった!"といい、その発話した時間帯をシステムに記録しておけば、後から見たときにその発話箇所を参照することで、"穴があった"箇所を確認できます。トリガー自体の開発は行わなくとも、走行時の記録を簡易的にシステム化することで、ユーザーの業務改善フローを簡単に実現することができ、システム導入にあたる効果検証を素早く行うことができます。 ということで、まずはこのシステムの効果検証を行うべく、intdashに取り込まれたデータから、発話した箇所を検出して検索する機能を試作品としてつくってみました。 機能の概要 前置きが長くなりましたが…. ユーザーの利用フローは以下を想定します。 走行中に気づいたことがあったらドライバーがマイクを通して発言を行う システム上でデータ収集が完了したあと、サーバー側で音声データを解析し、発話されている時間範囲を検出する ユーザーは検出結果をダッシュボード画面で確認する ユーザーは発話された場所に移動し、確認したい時間範囲のデータの様子を観察する ユーザーの目線では、「走行中に気づいたことを話していれば、あとからその箇所を確認できるようになる」という、至ってシンプルなシナリオとなります。 実現方法 アーキテクチャ 今回実現するにあたって、全体のアーキテクチャ構築には AWS Fargate を用いたコンテナの並列処理を行える構成を採用しています。Fargateを用いることで、以下のことを期待できます。 計測の数に応じて、フレキシブルにコンテナのリソースをアサインすることができる 処理の負荷が高くなった場合、処理を分散させスケーリングする構成を容易にたてることができる もともと発話検出を行う環境をコンテナ化していたのもあり、コンテナの操作のみを意識したシステム構築を可能にするサービスとして、非常に親和性が高かったため採用にいたりました。 今回は、以下のような役割をもつ “Manager“ を実装し、AWS Lambda にデプロイしています。 intdash側の計測状態の監視 Fargateのクラスタへのタスクの投下 タスクステータスの管理 最終的に、以下のような構成になりました。 構成イメージ デバイス上で計測を開始し、データをサーバーにためます 計測が終了すると、AWS LambdaにデプロイされているManagerが計測終了を検知し、発話箇所検出が動作するコンテナを立てます コンテナのジョブの状況はDynamo DBなどのキャッシュに利用できるサービスを用いてキャッシュしておき、ジョブのステータスによりリトライなどの処理に用います コンテナ上で検出された発話の検出結果はサーバーに送付されます 検出結果は VM2M Data Visualizer などの可視化アプリケーションで確認します 非常に簡単な構成ですが、検証レベルでは充分運用が可能な性能を提供できます。構築自体も1日程度でサクッとできました。 発話検出のロジック 発話検出のロジックについては、現段階だとサービスの仮説検証段階ということもあり、発話の検出を行うモデル開発は行わず、こちらの inaSpeechSegmenter を利用しました 。 github.com モデルの詳細については こちらの論文 で確認することができます。 今回の利用ケースでは、精度・パフォーマンス共に利用可能なレベルだったので、こちらを一旦採用することにしました。 実際に動かしてみる それでは、実際に動かしてみましょう。 今回は、車の代わりにiPhoneを使って手軽に動画・音声をアップロードできる intdash Motion を用いてアップロードします。「Motionを利用して走行動画を撮影しながら、”車が止まったところ”を記録し後から確認する」という利用シーンを想定します。 Motionで計測を開始すると、アップロードできていることがVM2M Data Visualizerから確認できます。 Visual M2M Data Visualizer 画面 それでは、Motion上で計測を終了します。終了すると、AWS Lambda上にデプロイしたManagerが計測終了を検知し、Amazon ECS上でコンテナを立てます。 コンテナが新たに立てられ、タスクが開始されていることを確認します。 Amazon ECS クラスター画面 この時、発話検出の処理時間はかかりますが、計測を生成する度にコンテナが生成されるため、くりかえし計測を生成しても処理がスタックすることがありません。請求にかかるリソースも計測が生成された分発生するのみです。 並列にコンテナが立ち上がった時 本来は自分自身で設計・実装しなければいけないところなので、このあたりを考える必要がない点は非常に楽ですね。 さて、コンテナのタスクが完了しました。 それでは、発話の検出が反映されているか見てみましょう。 検出結果後 無事、検出されていることが確認できました。 早速、検出された区間を参考に、最初に"車が止まったところ"を確認します。 検出結果の様子 すぐに確認できましたね。検出した時間帯を記録しておくと、動画の中で確認した気づきがどの時間帯なのかはっきりわかり、すぐに対象のデータにたどり着くことができました。 まとめ 今回は、弊社で実施している研究開発のうち、”発話箇所を自動検出するシステム”の効果検証の取り組みについてご説明しました。 冒頭の再掲にはなりますが、現在試作品を試してくださる方々を募集しております。もしご興味がある方がいらっしゃいましたら こちら まで問い合わせください!
アバター
はじめに こんにちは、ハードウェア/OT 製品開発グループ 1 でソフトウェア開発を担当している矢部です。 アプトポッドでは以前より、 EDGEPLANT CAN-USB Interface という自社開発製品を取り扱っています。 こちらは車載機器の通信規格であるCAN 2 データの送受信に対応したもので、CANバスに接続して使用する製品です。 EDGEPLANT CAN-USB Interface EDGEPLANT CAN-USB Interface とその周辺プロダクトについては、こちらの記事でもご紹介しておりますので、よろしければご覧ください。 tech.aptpod.co.jp これまでCAN-USB Interfaceは、 弊社のアプライアンス製品 に付属する形としてのみ提供していました。 デバイスドライバーも専用のものを開発・提供しており、専用ソフトウェアからしか利用できませんでした。 今後、幅広いお客様にこちらの製品をお使いいただけるよう、オープンソースのCANドライバーであるSocket CAN 3 への対応を進めています。 Socket CANはオープンソースのため、当社の専用ソフトウェアで直接利用する場合と比較すると、インターフェイス機器側で付与されるタイムスタンプが利用できないなど機能面で劣る部分もありますが、様々なサードパーティプロダクトと連携できる点が魅力です。 当社製品をSocket CANを介して利用可能にすることで、サードパーティプロダクトを介して誰でも簡単に当社製品を利用できるようになります。 今回は、CAN-USB InterfaceをSocket CANによってLinuxマシン上で使用する方法についてご紹介します。以降の説明では、当社の車載対応のLinux搭載エッジコンピュータ EDGEPLANT T1 を前提として解説していきます。 はじめに これまでの使われ方 EDGEPLANT CAN-USB Interfaceの特長 EDGEPLANT T1 でCANデータを見る CAN-USB Interface を準備する ドライバをインストールする インターフェースを確認する CAN の設定を行う can-utils を利用する CANデータを受信する CANデータを送信する SavvyCAN を利用する 事前準備 SavvyCAN をビルドする SavvyCAN でデータを見る Python で実装する python-can をインストールする CANデータを送受信する おわりに これまでの使われ方 前述の通り、これまでは 弊社のアプライアンス製品 という形で何らかの車載ターミナルデバイスとともに提供していました。これらの車載ターミナルデバイスには Yocto 4 ベースのOSを搭載しており、CAN-USB Interface用のドライバや、クラウドサーバーへの接続用のクライアントソフトウェアなどが標準で組み込まれています。 こちらを用いたCANデータの可視化については、この記事などをご覧ください。 tech.aptpod.co.jp EDGEPLANT CAN-USB Interfaceの特長 Y分岐ケーブルによりCANバス2チャンネル分のデータを取得 TCXOを搭載、高い周波数精度とタイマー分解能 ペリフェラル側で、受信直後の正確なタイムスタンプを付与 デイジーチェーン型のクロック共有機構 優れたコストパフォーマンス(価格) CAN-USB Interfaceは、バスからCANデータを取得した際の正確な時刻情報を記録するため、インターフェイス機器側でタイムスタンプを付与します。インターフェイスにはマイコンが搭載されており、CANバスからデータを受信するとすぐにTCXO(温度補償型水晶発振器)による正確なタイマーによってタイムスタンプを付与します。 たとえばRaspberry PiのようなSPI経由のCANインターフェイスではCPUの負荷次第でCANデータの取りこぼしも起き得ますが、本製品ではマイコンで処理しているため、CANデータを取りこぼすことなく取得できます。(もちろん、本製品からUSBでホストコンピュータに転送する際のバッファなどには注意が必要です) TCXOの周波数精度は±2.5ppmで、1時間連続計測でも9ミリ秒ほどしか時刻はずれません。また、タイマーの分解能は1マイクロ秒で、タイムスタンプのぶれは10マイクロ秒程度に収まります。さらに、複数台のインターフェイス機器を併用しても打刻ずれが発生しないクロック共有機構を備えており、複数台同期したタイムスタンプを取得できます。クロック共有はデイジーチェーン型の配線でシンプルにつなげることができます。これら高いタイムスタンプ精度と複数台での同期タイムスタンプの機能により、R&Dなど取得されたデータの品質が重要なケースで広くご利用いただいております。 この記事ではSocket CANを介した利用方法をご紹介しますが、CAN-USB Interfaceが持つこれらの機能性や性能を最大限活かすには、当社がご提供する専用ソフトウェアをご利用いただく必要があります。 たとえば、Socket CANを経由してやり取りするデータにはタイムスタンプを付与できないため、インターフェイス側で付与したタイムスタンプは利用できません。 製品の詳細については、CAN-USB Interfaceの製品ページをご覧ください。本製品含め当社のソリューションにご興味をお持ちいただけましたら、当社Webサイトより是非お問い合わせください。 www.aptpod.co.jp EDGEPLANT T1 でCANデータを見る 続いて、 EDGEPLANT T1 上で CAN-USB Interface を利用する方法についてご紹介します。 EDGEPLANT T1 は Ubuntu 18.04 ベースのOSで動作しているため、その他PCなどで Ubuntu がインストールできる環境であれば同様の手順で利用可能です。 CAN-USB Interface を準備する EDGEPLANT T1 上で CAN-USB Interface を利用するための事前準備を説明します。 ドライバをインストールする CAN-USB Interface を利用するためには、専用のドライバのインストールが必要になります。 CAN-USB Interface のページ で公開されているデバイスドライバをダウンロードして、インストールしてください。 インターフェースを確認する CAN-USB Interface はインターフェースが2つあるため、接続すると can0 と can1 として見えます。 $ ip link show type can 7: can0: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10 link/can 8: can1: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10 link/can CAN の設定を行う CANのビットレートを 500kbps に設定する場合、以下のように行います。 $ sudo ip link set can0 type can bitrate 500000 $ sudo ip link set can0 up up することで ifconfig でも確認できるようになります。 $ ifconfig can0 can0: flags=193<UP,RUNNING,NOARP> mtu 16 unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 can-utils を利用する EDGEPLANT T1 にはデフォルトで can-utils がインストールされているため、これを使用してCANデータを見ることが出来ます。 CAN-USB Interface は使用していないですが、Raspberry Pi 上で can-utils を利用した記事もありますので、興味のある方はご覧ください。 tech.aptpod.co.jp CANデータを受信する candump コマンドで受信したCANデータを見ることができます。 $ candump can0 can0 000 [8] 00 00 00 00 00 00 00 01 can0 000 [8] 00 00 00 00 00 00 00 02 can0 000 [8] 00 00 00 00 00 00 00 03 can0 000 [8] 00 00 00 00 00 00 00 04 can0 000 [8] 00 00 00 00 00 00 00 05 can0 000 [8] 00 00 00 00 00 00 00 06 CANデータを送信する cansend コマンドでCANデータを送信することができます。例として、 can0 と can1 を接続して、データをループバックさせてみます。 can1 も 500kbps で設定して立ち上げます。 $ sudo ip link set can1 type can bitrate 500000 $ sudo ip link set can1 up can0 からCANパケットを送信して、 candump を実行した can1 でデータを受信することを確認できました。 $ candump can1 & $ cansend can0 001#11.22.33.44.55.66.77.88 $ can1 001 [8] 11 22 33 44 55 66 77 88 // can1 の受信データ SavvyCAN を利用する Socket CANに対応した製品は有償無償含め多数存在していますが、ここではオープンソースの Savvy CAN を利用します。EDGEPLANT T1 は ARM アーキテクチャ の Jetson TX2 モジュールで動作していますが、Savvy CANのARM用イメージは配布されていません。自前でビルド、インストールする必要があります。 事前準備 ビルドには、Qt5 と Qt Serial Bus が必要になるので、それらをまずインストールします。 Qt5 のインストール $ sudo apt install qt5-default qtdeclarative5-dev libqt5serialport5-dev qttools5-dev -y Qt Serial Bus のビルド、インストール # Overkill: get all qt-stuff $ sudo apt install qml-module-qt-labs-folderlistmodel qml-module-qtquick-extras qml-module-qtquick-controls2 qt5-default libqt5quickcontrols2-5 qtquickcontrols2-5-dev qtcreator qtcreator-doc libqt5serialport5-dev build-essential qml-module-qt3d qt3d5-dev qtdeclarative5-dev qtconnectivity5-dev qtmultimedia5-dev # To get rid of the private/qobject_p.h error $ sudo apt-get install qtbase5-private-dev # Build and install qtserialbus $ cd <work dir> $ git clone git://code.qt.io/qt/qtserialbus.git $ cd qtserialbus $ git checkout 5.9.8 $ qmake $ make -j6 $ sudo make install Use qt serialbus in ubuntu 18.04 with qt 5.9 · GitHub SavvyCAN をビルドする SavvyCAN の 1.0.20 以降は Qt5.10(QRandomGenerator) に依存していますが、L4Tのデフォルトは Qt5.9 になっており、動作できません。 そのため、1.0.20 より前のバージョンを使う必要があります。ここでは V199.1 を使ってビルドします。 $ cd <work dir> $ git clone -b V199.1 https://github.com/collin80/SavvyCAN.git $ cd SavvyCAN $ qmake $ make -j6 SavvyCAN でデータを見る ビルドした SavvyCAN を立ち上げます。 $ ./SavvyCAN ツールバーの Conecction → Open Connection Windowを選択します。 Add New Device Connection-> QT SerialBus Device-> socketcan → can* を設定します。 CANフレームを受信することが出来ました。 Python で実装する これだけではただのツール紹介なので、Pythonを用いてCANデータを扱う方法を簡単にご紹介します。 python-can をインストールする python-can というライブラリが公開されており、こちらを利用すると簡単に実装することが出来ます。 $ sudo apt install python3-pip $ pip3 install python-can CANデータを送受信する can-utils の例と同じく、 can0 と can1 を接続して、データをループバックさせてみます。 import asyncio import can def print_message (msg): print (msg) async def main (): can0 = can.interface.Bus(bustype= "socketcan" , channel= "can0" , bitrate= 500000 ) can1 = can.interface.Bus(bustype= "socketcan" , channel= "can1" , bitrate= 500000 ) listeners = [ print_message, # Callback function ] # Create Notifier with an explicit loop to use for scheduling of callbacks loop = asyncio.get_event_loop() notifier = can.Notifier(can1, listeners, loop=loop) # Send messages for id in range ( 10 ): msg = can.Message(arbitration_id= id , data=[ id , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]) await asyncio.sleep( 0.5 ) can0.send(msg) notifier.stop() can0.shutdown() can1.shutdown() loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() このコードを実行すると、以下のように can1 でデータが受信できます。 $ sudo python3 loopback_test.py Timestamp: 1623221566.564898 ID: 00000000 X DLC: 8 00 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221567.066872 ID: 00000001 X DLC: 8 01 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221567.570862 ID: 00000002 X DLC: 8 02 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221568.072851 ID: 00000003 X DLC: 8 03 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221568.574905 ID: 00000004 X DLC: 8 04 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221569.078850 ID: 00000005 X DLC: 8 05 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221569.580852 ID: 00000006 X DLC: 8 06 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221570.082847 ID: 00000007 X DLC: 8 07 00 00 00 00 00 00 00 Channel: can1 Timestamp: 1623221570.584880 ID: 00000008 X DLC: 8 08 00 00 00 00 00 00 00 Channel: can1 おわりに 今回は CAN-USB Interface の紹介になりましたが、その他にもアナログデータを扱うことができる ANALOG-USB Interface も販売しております。 また、自動車からのCANデータの収集や、遠隔適合のためのソリューションとして、以下のプロダクトをご提供しております。 自動車向け遠隔計測ソリューション(CAN/CAN-FD対応) www.aptpod.co.jp 自動車ECU向け遠隔適合ソリューション www.aptpod.co.jp また、自社以外の製品を使用したカスタマイズ事例も多数ありますので、ご興味をお持ちいただけましたら、まずはお気軽に 弊社サイトのお問い合わせフォームよりご相談ください 。 www.aptpod.co.jp OT : Operational Technology の略。ハードウェアおよび組込みソフトの製品開発をミッションとするグループ ↩ Controller Area Network の略。 https://ja.wikipedia.org/wiki/Controller_Area_Network ↩ https://en.wikipedia.org/wiki/SocketCAN ↩ Linuxをベースとして独自にカスタマイズしたシステムを構成できるもの。 The Yocto Project ↩
アバター
コーポレートマーケティング室でデザインを担当しております高森です。 アプトポッドはベンチャー企業でありながら育休を取得されるパパ社員が多く、社内の勤務スレッドでも家族のお世話のためお休みや時短勤務をしますというやりとりが頻繁に行われています。とても子育てフレンドリーな会社だなと感じておりましたが、今回私自身も、社内で初の産休(産休育休合わせて8ヶ月ほど)を取得させていただくこととなりました。4月に仕事復帰して早1ヶ月が経ちましたので、情報のアップデートが激しいIT業界のベンチャー企業で8ヶ月間お休みするとどうなったかをレポートしたいと思います。 コロナ禍と重なった妊娠期間 産休&育休中の過ごし方 便利だった育児アプリ 復帰後に変わっていた社内環境 あれ?macOSが変わってる あれ?SketchのUIも変わってる 社内のデザインライブラリがたくさんできてる みんなXD使ってる 社内ツールがなんだか変わっている 出産後に変わった働き方、取り組んでること 限られた環境で集中できるようになった 共同作業を意識した働き方に レビューは半日早めを目標に おわりに コロナ禍と重なった妊娠期間 アプトポッドでは、新型コロナウィルス感染対策として2020年2月から早々にリモート勤務が推奨され、その後出社の必要がない社員は原則リモート勤務となりました。 昨日まで対面で打ち合わせをしていたメンバーとも突然音声のみのオンラインミーティングとなり初めは違和感があったものの、一気にリモート体制が整えられていきました。すっかり普通となったオンラインミーティングですが、初めは"ソワソワする"なんて思っていた時代があったことは、将来子供に語ることができそうです。 私の妊娠&出産期間は完全にこのリモート期間と重なりました。コロナ禍での出産は大変なこともありますが、つわりがひどい時は横になって休みながら作業をすることができたり、大きいお腹で満員電車に乗る必要がなかったり、妊娠の時期が標準でリモート勤務だったことで、仕事面での不安はなかったように思います。 また、リモートでのやりとりが当然となっていたため、産休中も連絡があったら対応できるだろう(実際はそんな事態は無く完全にお休みをいただいてました)と気楽に構えられていたことはとても良かったです。出産に関する届け出も、不明な点があれば人事の方といつでもやりとりできるという安心感がありました。 産休&育休中の過ごし方 デザイナーが4人しかいない中で長期のお休みをいただいていたこともあり、復帰後のブランクがやはり心配でした。復帰後もすぐに戦力になりたい...と思っていたため、ツールの感覚が鈍らないように隙を見つけてちょっとした作業ならできるのではとも考えておりました。実際に出産直後の子供がほとんど寝ている時期は、会社の会議資料やプロジェクトのスレッドを覗いたり、子供グッズを作るのにパソコンで作業したり、意外と時間取れる?と思ったりもしていたのですが...徐々に覚醒して泣いたり笑ったりするようになっていく子供のお世話で、1日があっという間に過ぎていくようになりました。この時期は子供をあやしながら流し見できるNetflixとワイドショーがお友達、起きてる時間はとにかく育児について調べまくる、という子供100%の生活になり、パソコンは部屋の隅っこでほとんど触ることはなかったです。 便利だった育児アプリ せっかくなので子育てに使って便利だったアプリを紹介します ピヨログ 育児の記録をひたすら付けていくアプリです。社内の育児サークルでパパ社員たちが絶賛していたので産後すぐから使っていました。子供が生まれて以降ブルーライトに敏感になりスマホのナイトモードの大事さを知ったのですが、当然のごとくダークモードにも対応されていて、ウィジェットにも早々に対応、使いやすいように常に細かくアップデートされていて素晴らしいなと思いました。 睡眠時間の変化です(青いところが寝ている時間) 【生後0ヶ月】→【生後5ヶ月ごろ】→【最近】 1日中寝たり起きたりだった生活リズムが整ってきて、保育園が始まってからは平日日中は親元を離れるようになりました。成長が感じられてしみじみします(泣) こうしてみると初期は本当に頑張っていました。夜間授乳で寝不足が2週間以上続くと丸1日寝てない状態と同じなのだそうです。将来子供の反抗期が訪れた際はこのスクリーンショットを印籠のように使いたいと思います。 ステップ離乳食 食べた食材を記録していくアプリです。月齢ごとに食べられる食材がわかりやすく表示され、食べたらチェックを入れていきます。調理法も確認できシンプルで使いやすいアプリだと思いました。離乳食は色々な情報が溢れていてサイトによって書いていることもバラバラなので、色々調べるのに疲れた時にはこのアプリのみで十分かと思ってます。 Notion 情報・タスク管理ツールです。子供の食べたものや成長記録など、それまでバラバラで付けていた記録をまとめて管理したいなと思い使い始めました。Notionを育児記録に使ってるtipsもたくさんあるので参考にしながら使ってます。この頃には記録を夫と共有という点はもう諦めてw、自分が見やすく記録しやすくを目標に、自分の食事管理も兼ねて使ってみてます。業務に関するメモなどあらゆるメモ帳として使っており、入力した項目を行ごと列ごとコンポーネントごとごそっと並び替えできるので、とにかくメモしておいて整理は後でといった使い方ができる点が気に入ってます。 そんなこんなで気がつくと復帰する月となっていました。慣らし保育期間は子供がもらってきた風邪菌により私自身がダウン。3週間体調不良(子供は元気)が続き、なんの準備もできないまま仕事復帰となりました。こうなることは予想してはおりましたが見事に、本当にあっという間に産休&育休は終わってしまいました。(体感では3週間くらいしか休んでないのではという感じです) 復帰後に変わっていた社内環境 あれ?macOSが変わってる 復帰後、macOSがBigSurに変わっておりました。まずはOSをアップデートしよう...と思うもそのための容量が不足しているためファイルを整理するところから、さらにファイルの保存場所を探すのもまごつくレベル...半年以上パソコンに触らないとこうなるのか、と先が思いやられ1日8時間なんてとても脳が働かなったことを覚えています。心配していたデザインツールについてはすぐに感覚が戻りました。 あれ?SketchのUIも変わってる アプトポッドでは主にSketchを使用してデザインしておりますが、SketchもBigSurに合わせてデザインが大きく変わっておりました。全体的に丸くなり色合いも変わっている他、シンボル一覧が見やすくなっていたりライブラリのシンボルが使いやすくなっていました。 社内のデザインライブラリがたくさんできてる 産休&育休前は、デザインコンポーネントのたたき台を作成した段階で長期休暇に入ったのですが、その後デザインチーム内でコンポーネントが整理され、ライブラリにまとめる作業が進められていました。Sketchの変更と合わせてコンポーネント共有化のワークフローがイメージできるようになっていました。動画作成時のテロップなども、とにかく繰り返し使いそうなものはライブラリ化されており、久しぶりに作業する際にとても助けになりました。 みんなXD使ってる AdobeのXDはこれまであまり使っていなかったのですが、ファイルの軽さと共有しやすさなどからXDが以前よりも使われるようになっていました。私も復職後から現在まで主にXDを使っているのですが、デザインファイルの共同作業がとても便利に感じています。 以前からデザインファイルの共同作業に向けて世の中がどんどんアップデートされている気配を感じてはおりましたが、私自身は黙々と一人で作業を進めたい派で、作業中のデザイン(ファイルも)ほど見られたくないものはないというくらい、共同作業についてはネガティブな印象を持っておりました。しかしリモート環境でデザインのレビューをする際に、デザイナー同士であれば同じファイルを見ながらの方が、それぞれの画面パターンをいちいち切り替えて説明せずとも各自でファイルを見渡してさっと共有できたり、パッと出たアイディアもその場で仮イメージを作成して残しておくという使い方ができてとても便利に感じています。 社内ツールがなんだか変わっている 入社以来、社内の手続きやtipsはQiitaにまとめられているものを探して確認していたのですが、ConfluenceやJiraが使われるようになっておりました。タスクや議事録をプロジェクトごとに管理する目的とのことでしたが、産休明けでも会社で進行中のプロジェクトをざっくりと把握できるようになっておりました。 また、社内情報については管理本部の方々が更新してくれている社内向けポータルサイトが更新されており、申請手続きの入り口が設けられていたので手順を調べなくても大丈夫、といった具合に整理されておりました。(保育園に必要な書類もポチッと申請できるようになっていたのを知らず...活用できなかったことが悔やまれます) ちなみに社内ポータルサイトではたまに社員へのインタビューなども掲載されており、リモート体制になってから入社された方が紹介されていたりします。リモート体制でも社内コミュニケーションが取りやすいように工夫いただいていることが、長期休暇明けの私にとっても社内の振り返りや把握にとても助けになっており、ちょっとした息抜きになっています。みんなのリモートでの仕事状況やパパたちがどのように時間を使っているのか参考になりました。 出産後に変わった働き方、取り組んでること 出産後に変わったことはズバリ、 残業ができません! 正確にいうと残業はできるのですが、自分の都合の良いタイミングまで仕事を続けることができません。私の場合は毎日17時頃に保育園のお迎え時間がやってくるため、16時ごろから作業に集中できるようになっても、"このままキリの良いところまでやるぞー"ということができません。もう少しやりたくても、中途半な状態でも、切り上げなければならない時間が毎日決まっています。 子供が寝た後に再開、週末に集中して...なども(家の間取りの関係もあり)業務時間ほど集中することができず、家にパソコンがあるからいつでも作業できるというゆるい心構えでいたことを反省しました。 限られた環境で集中できるようになった 勤務時間外にまとまった作業時間を取ることが難しいため、何がなんでも勤務時間に終わらせたい、集中しなきゃという緊張感からか、以前よりも限られた環境で集中することができるようになりました。 以前はすぐに気が散ってしまうためホワイトノイズをかけて強制的に集中できるようにしたり、気分が上がる作業スペース作りに精を出していましたが、今はリビングの片隅の60cm四方のスペースで、オムツのストックの上に足を乗せたまま黙々と作業できるようになりました。自分の中では結構変わったなと思っています。 共同作業を意識した働き方に これまでは作業ファイルの共有はプロジェクトが落ち着いたタイミングで整理してから、と後回しにしがちだったのですが、デザイン作業と並行して早い段階からコンポーネントを整理するように心がけてます。デザイン案を展開する際にもこの方が効率が良さそうだと感じてます。 その他にも、作業プロセスも可能な限りドキュメントに残しておきたいと思っているのですが、デザイン作業を優先してついつい後回しになりがちとなってしまってます。こちらは昼食前後でデザイン作業とドキュメント作成を分けるなど、週の中に強制的にドキュメント作成の時間を設けるように試してみてます。 レビューは半日早めを目標に アプトポッドはフレックス勤務がOKなので、復帰後は基本の就業時間よりも2時間早くずらして勤務しています。そのため、自分の勤務終了後に連絡を受けてもレスポンスが遅くなる、または翌日になってしまうこともあり得ます。 これまでも作成した資料に不備があってレビューをもらうもその後対応できず、上司に修正いただくということがありました....終了時間が決まっているため修正時間も考慮して、余裕を持って共有するようにしていきたいです。 おわりに 復帰後は仕事内容に制限がかかってしまうかな...と心配しておりましたが、今のところそういった不安はありません。 復帰して1ヶ月が経ちましたが、想像していたよりも、驚くほど普通に働くことができています。 これもフレックスやリモートといった柔軟なワークスタイルを作ってくれている会社のおかげであり、足りないところをサポートしてくれる上司とチームメンバーのおかげです。今のところ病気ひとつせず休まず保育園に行ってくれている子供にも感謝です。 まだ子供が熱を出した経験をしたことがないため、保育園に呼びされたらどうしようか、そもそも保育園が休園になったら(こんな時期なので)どうしようかなど心配事は尽きないのですが、進め方を一緒に模索してくれる社風であることは確信してますので、時間の使い方を試行錯誤しながらこれからも育児と仕事の両立を頑張っていきたいです。 アプトポッドへの入社に興味を持たれた方は 採用ページ をご覧ください! (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "https://platform.wantedly.com/visit_buttons/script.js"; fjs.parentNode.insertBefore(js, fjs); }(document, "script", "wantedly-visit-buttons-wjs"));
アバター
はじめに こんにちは、 SRE チームの金澤です。 弊社は intdash というIoTプラットフォームを展開しています。そのサーバサイドである intdash Server はクラウドインフラを用いた構築が多く、その一つが Amazon Web Service(AWS) です。 パブリッククラウドを使用する上で気をつけたい点の一つとして 障害の把握 が挙げられます。サービス障害の要因確認として役立ち、その内容をもとに今後のプロアクティブな対策を検討する助けにもなります。また大規模の障害の場合はお客様が把握されている可能性も高く、いただいたご質問にスムーズな回答を差し上げる一助にもなります。素早く把握していることに越したことはありません。 そのような体制を目指すべくまずは、 障害情報を一か所に集約する 影響を受けない障害内容についても通知を受ける 上記が必要と定め検討を開始しました。この記事ではAWSサービスの障害通知方法の検討とツールを利用した通知システムの構築についてご紹介します。 はじめに 障害情報の把握 PHD利用時の課題 shd notifierの利用 結果 まとめ 障害情報の把握 障害を通知するためには、まずは障害を把握する必要があります。把握の手段はいくらかありますが、以下が代表的なものかと思います。 Service Health Dashboard(SHD) SHD AWSが提供するサービスの稼働状況を示すダッシュボードです。各リージョンおよび各サービス毎の稼働状況が示されています。それぞれにRSSフィードが配信されているため、確認したいリージョンまたはサービスのRSSフィードを購読して、サービスの状況を確認することが可能です。 Personal Health Dashboard(PHD) PHD AWSアカウントに紐づくかたちでサービス障害を記録してくれる機能です。バックエンドに AWS Health API が使用されており過去90日間の障害情報が表示されます。ダッシュボードの閲覧にはAWSアカウントへのログインが必要となります。 Twitter(非公式) AWSが公式に提供しているものではないですが障害情報をツイートされています。東京リージョンに特化した @awsstatusjp と、リージョンを全体に広げた @awsstatusjp_all が存在するようです。 DownDetector(非公式) こちらもAWS非公式ですがSNSからの投稿内容を利用し各種サービスの状況を判断するWebサービスがあります。各種サービスにはAWSも含まれており、状態を確認することができます。 PHD利用時の課題 まず、上記の情報を通知に利用し、AWSだけで構成できる仕組みを考えました。 SHDを利用する場合、購読が必要になるRSSフィード数が多くなり、RSSフィードを登録した後のメンテナンス(追加・削除・確認等)に工数が掛かることが予想されたため、ここではPHDを利用した形での検討を進めました。 PHDはすでに構築されているダッシュボードを閲覧して障害状況を把握する方法の他に、AWS Cloudwatch Eventsと連携したアクションを取ることが出来ます。AWS Cloudwatch EventsのイベントトリガーをAWS Healthイベントに設定することで、PHDの更新に併せてターゲットを呼び出すことができます。これは ドキュメントにも方法が紹介されています 。このドキュメントではAWS Cloudwatch Eventsでイベントを受信する方法や、イベントを受信した後にAWS Chatbotを利用してビジネスチャットなどに障害情報を通知する方法が紹介されています。 ドキュメントを参考に構築したあとはしばらく状況を確認していましたが、障害発生しても通知がされないことがありました。確認を進めていくとHealth APIでは障害イベントの種類は2種類存在し、通知されるイベントはアカウント固有のイベントである ACCOUNT_SPECIFIC のみであったことが分かりました。これは ドキュメント にも記載があります。 Only AWS Health events that are specific to your AWS account are delivered to CloudWatch Events. For example, this can include events such as a required update to an Amazon EC2 instance and other scheduled change events that might affect your account and resources.Currently, you can't use CloudWatch Events to return public events from the Service Health Dashboard. Events from the Service Health Dashboard provide public information about the Regional availability of a service. These events aren't specific to AWS accounts, so they aren't delivered to CloudWatch Events. 上記引用にて記載されているとおり、今回通知されなかった障害イベントの種類は PUBLIC となっているようでした。 shd notifierの利用 記事冒頭で要件とした「影響を受けない障害内容についても通知を受ける」を満たすためには、 PUBLIC となる障害イベントについても通知も受けたいところです。その場合には、 AWS Health Tools - shd notifier の利用を検討することができます。こちらはAWSが提供しているツールで、SHDの更新情報を検知してAWS SNSやSlackなどのビジネスチャットに通知できるようにするものです。 shd-notifier はAWS Step Functions、AWS LambdaやAWS Cloudwatch Eventsを利用して構築されます。障害検知・通知をまとめて一つのワークフローとして定義して処理を行います。具体的には以下のような動作を行います。 SHDへの定期的なポーリング 検知した障害に対しての定期的な状況確認 確認結果をSNS/ビジネスチャットへ投稿 デプロイは上述のGithubにCloudFormationで使用するテンプレートとLambda関数をデプロイするシェルが提供されているため、そちらを利用してデプロイする方法が簡単です。(弊社ではインフラ基盤をTerraformでコード管理しているため、一部を別途書き起こししています。) 結果 今回のデプロイの結果を、先日発生した障害内容から確認してみます。なお、投稿する先としてSlackを設定しています。 2021/05/07 03:16-04:43(JST) に AWS CloudFrontにおいてコンソール表示遅延の障害が発生しました。aws healthコマンドで確認すると以下の応答が返却されます。 eventScopeCodeの値が PUBLIC となっていることが確認できます。そのためこのAWSアカウントとしては本障害は公開イベント扱いとなります。 $ aws health describe-events --region us-east-1 | jq .events[] : { "arn": "arn:aws:health:global::event/CLOUDFRONT/AWS_CLOUDFRONT_OPERATIONAL_ISSUE/AWS_CLOUDFRONT_OPERATIONAL_ISSUE_ZQLBL_1620324983", "service": "CLOUDFRONT", "eventTypeCode": "AWS_CLOUDFRONT_OPERATIONAL_ISSUE", "eventTypeCategory": "issue", "region": "global", "startTime": "2021-05-07T03:16:23.386000+09:00", "endTime": "2021-05-07T04:43:59.487000+09:00", "lastUpdatedTime": "2021-05-07T04:44:00.233000+09:00", "statusCode": "closed", "eventScopeCode": "PUBLIC" } : PUBLIC となっている通り弊社環境では幸いにも影響はありませんでした。通知状況を確認してみます。 初報 障害発生後に初報が発報されたことが確認できました。 [SHD AUTO] という文字がありますがこれは投稿時に付与することが出来るprefixで任意で設定することができます。 続報/終報 続いて、続報および終報が発報されました。 12:43 PM(PDT) に正常に回復した旨が報告されています。 この続報/終報では、初報から時間が経過しており途中経過をまとめた報告となっています。これは投稿先への投稿間隔を比較的長めに設定したことによるものです。前述のGithubリポジトリにある Health-Event-Iterator-LambdaFn.py で投稿間隔を調整可能です。 以上で、無事障害イベントの内容が通知されていることが確認できました。 まとめ 今回はAWSのサービス障害が発生した場合における、広い範囲での障害情報の収集および集約化についての手法の確認・構築を行いました。なお、AWS Step Functionsステートで動作するLambda関数はHealth APIを使用するため、AWSアカウントが ビジネスアカウント 以上である必要があります。ご注意ください。 今後は、 投稿内容のブラッシュアップ 障害対応自動化への応用検討 など引き続き改善を進め、障害対応の体制作りに貢献出来ればと考えています。
アバター
はじめに 事業開発室の小宮です。 今日は、これまであまり語られる機会の少なかった、当社のビジネスサイドの業務についてご紹介します。 はじめに 会社・チーム紹介 会社紹介 チーム紹介 アカウントマネージャー業務内容 見込み顧客獲得 提案 受注とそれ以降 アプトポッドの営業として働くことの魅力 お客様の課題解決に自社製品で貢献できる 先進的な業務に関われる 納得感のある会社である おわりに 会社・チーム紹介 会社紹介 アプトポッドは産業用IoTプラットフォーム”intdash”などのご提供を通じて、お客様のプロジェクトの成功をお手伝いさせていただいている会社です。まずは当社について、かいつまんでご紹介させてください。 昨今は様々なIoT製品があります。intdashは、以下の特徴を全て兼ね備えている、という点で、他のサービスと差異があると考えています。 モバイル回線(不安定回線)経由でも低遅延なデータ伝送の実現 モバイル回線経由でもデータの完全回収の実現 動画、各種センサーなど、プロトコルに依存しない多様なデータの取得 複数種類のセンサーデータの正確な時刻同期 可視化、分析プラットフォームの一体提供 ただ、上記の特徴を全て同時にご要望されるお客様は少なく、ケースバイケースで必要な要素に着目いただいています。 ご活用いただいている業界としては、 自動車はじめ各種モビリティが大半 、ユースケースとしては、 高頻度データの遠隔データ収集、遠隔診断、遠隔制御 などとなっております。 お客様のお仕事の性質上、研究開発に深く関わることが多いので、公開できる事例をご用意するハードルが非常に高いのが悩みどころです。実際には ほぼ全てのお客様が一部上場企業 であり、日本の未来を支える縁の下の力持ちのような存在だと思っています。 チーム紹介 当社の人員構成は、エンジニア7割、ビジネスサイド2割、管理部(マーケも内包される)1割となっております。 ビジネスサイドは、私が所属する 事業開発 を担当する部門と、商談を技術面から支援する ソリューションアーキテクト のチーム、お客様へのソリューション提供に責任を持つ プロジェクトマネージャー のチームからなります。 事業開発チームは、この記事を書いている2021年中旬時点で4名の、まだまだ小さなチームです。このチームで営業と、いわゆる事業開発を兼ねている形になります。本記事では主に、営業(アカウントマネージャー)としての業務内容をご紹介いたします。 アカウントマネージャー業務内容 以下は有名な本”The Model”を元にした、営業プロセスの図解になります。現時点では、ここに出てくる全てのプロセスを事業開発チームが担当しております。 図は Salesforceのブログ より引用 以降では、プロセスごとにもう少し詳細な業務内容を説明いたします。 見込み顧客獲得 見込み顧客の獲得は、主として自社独自の施策によるものと、販売パートナー様との協業によるものに分かれます。 自社施策としては、従来展示会等のリアルイベントからの流入が最も多くなっていました。展示会出展の際には、社内外のマーケ・デザインのチームと協力して、企画、当日の説明、終了後の見込み顧客のフォローまで一貫して行います。ただ、直近では展示会がCOVID-19の影響をもろに受け、それ以外の施策の試行錯誤を重ねているところです。 イベント出展については以下の記事も参考になります。 tech.aptpod.co.jp 販売パートナー様は、純粋な販売代理店様から、自社でintdash構築能力を持つシステム開発会社様まで多岐にわたります。共通しているのは、intdashを最初にきちんと理解していただくプロセスに気をつかうこと。多くの販売パートナー様にとってintdashとは、「一言での説明が難しい商材」であるため、きちんとご理解いただけるよう心がけています。 提案 具体的なニーズがありそうな見込み顧客とは、商談を重ねます。COVID-19以降はほぼ100%の商談がWeb会議で完結するようになりました。 ほぼ全ての商談で、ソリューションアーキテクトと協働します。当社のお客様は、普段から技術を扱っている方が多く、初回の商談から深い仕様の相談をされることも少なくないためです。また、intdashは継続して成長していく製品であるため、例えば現状の製品のカバー範囲外のご要望について、今後の製品開発でカバーしていくのか、あくまでその場限りのカスタムでご対応するのか、など、提案内容に対し製品開発的な目線も必要になります。 そのため営業目線であっても、特に現状の自社製品の限界や、カスタム開発での提案をする際のやり方など、 製品仕様だけでなく、開発に関する理解も重要になります 。単純にお客様の声をヒアリングできるだけではなく、 社内外双方の幸福度が最大になるよう 立ち回ることが求められます。 受注とそれ以降 無事、お客様のご要望を叶えられそうな提案ができ、お客様内のIT部門からのセキュリティに対する懸念も払拭され、先方のご予算確保の目処も立つと受注となります。バンザイ、お疲れさまでした。 …で終わりにはなりません。2つの観点があります。 1つ目は、 継続して案件を育てていくこと 。 当社の取り扱う案件の大半が、何か大きな目的のための実証(POC: Proof of Concept)から始めるケースが多くなっています。POCは最終的にもう少し大きなサービスを目指していることが多く、これに対してお客様側のプロジェクト全体の成功に貢献するための進め方を、受注前後を通じて継続して支援する姿勢が求められます。この姿勢は社名の由来でもある、 『a project to prove one’s delight』 という言葉に現れています。 2つ目は、 カスタマーサクセスの視点 。 現時点で当社では、サービス導入までのデリバリーと、納入後のサポートが部門として分かれていません。そのため、納入後の問い合わせ対応なども、セールス対応しているチームが受けています。この導入後の問い合わせや要望から、新たなお仕事や製品改善の種が見つかることもあります。 アプトポッドの営業として働くことの魅力 ここまでアプトポッドの営業としての業務内容をご紹介してきました。続いて当社の営業メンバーに、当社で働く魅力をヒアリングしたのでご紹介します。 お客様の課題解決に自社製品で貢献できる IoT/DX化の大きな難題を抱えている、日本を代表する製造業の方々に対し、 当社のミドルウェアを提案することで課題解決に寄与したり、協創の中で自身や会社を成長させていただけるところ 。 自社で製品を作っているので、製品が採用されてお客様の役に立てればとても嬉しい。課題にも一緒に取り組めるし、他社製品を売るだけでは味わえないかなーと思います。 お客様に対して自社製品で貢献できることは、商社など仕入れて売る系の営業では味わえない楽しさかと思います。特に当社はエンジニアとビジネスサイドの距離が近いことと、社内のエンジニアの幅が広い(ハードからスマホアプリまで)ため、お客様からのご要望に対しても幅広い対応力があります。 同時に、現場の声を製品開発にフィードバックできるハードルも低く、一緒に製品を成長させていく手触りも感じられます。新製品は営業メンバー自らが最初のユーザーになり、時には自ら車を運転して機能や性能を確かめます。 www.youtube.com (この動画も営業メンバーが実際に商品を扱いながら撮影しました) 先進的な業務に関われる 技術革新が激しい業界のため、日進月歩で新しい知識が求められる点や、 非常に幅広いレイヤーの知識が必要となる ので、常に刺激になっている。 自動運転・遠隔制御とか未来の社会システムを作る事業に、自分が関われる機会はなかなか無い。 当社のお客様は、主として大手製造業のR&D部門の方が多く、扱う案件は先進的なものばかりです。同時に自社製品に関しても、IoT、クラウドなどの技術面の他に、サブスクリプションモデルのビジネスモデルなど、売り方面でも新しい概念の説明が求められます。かっこよく言えば、 お客様とともに将来のサービスを構築していく ことが好きな方、シンプルに言うと新しいものが好きな方に向いているお仕事と言えます。 納得感のある会社である 技術を重視する会社だからかもしれませんが、会社全体の雰囲気として、非常に合理的で、働いていて各所で納得感のある会社だと思います。 例えばそれが顕著に現れたのはCOVID-19に対する対応で、当社では2020年早期の段階で、「ワクチンが出回るまで基本フルリモートワークとする」方針を打ち出していました。COVID-19という未知の課題に対し、リーズナブルな出口を早期に定義できており、このようなことは各所で見られます。 社員も謙虚で論理的な方が多く、テキストのやりとりが多くなりがちなリモートワークにおいても、コミュニケーションで困ることは全くといっていいほどありません。 このあたりについては、過去に当社のエンジニアが中途入社を振り返った記事でも同じようなことが述べられていますので、概ね間違っていないのだろうと思います。 tech.aptpod.co.jp tech.aptpod.co.jp おわりに アプトポッドは アカウントマネージャーを募集中 です! これまでSaaSプロダクトの営業(フィールド・インサイド含む)や、製造業を中心としたエンタープライズ向け営業をやっていた方などは親和性が高いと思います。お客様との会話は最初から技術的な仕様の話になる事が多いため、技術的なバックグラウンドを持つ方も歓迎です。何より様々なジャンルの新しいことが好きな方、新規事業が好きな方、お客様と一緒にプロジェクトを進めていくのが好きな方、ご応募お待ちしております。 アカウントマネージャー以外にも、ビジネスに近い職種だと、 プロダクトマーケティングマネージャー、ソリューションアーキテクト、サポートエンジニアなども募集しております 。合わせてご確認いただけますと幸いです。
アバター