TECH PLAY

アプトポッド

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

248

こんにちは。intdash グループ フロントエンドエンジニアの佐藤です。 Next.js を使った管理画面を作成するプロジェクトを担当する機会がありました。 管理画面は「 頻繁にデータが更新されることがない 」、「 同時アクセスはあまり起きない 」という前提の元に作成することが多いと考えています。 なのでサーバー、表示ともにパフォーマンス的に無駄なリクエストを投げたくはありません。 それを解決するため、プロジェクトに React Query を導入しました。 今回はその導入事例をご紹介したいと思います。 SSRを使用してデータを表示する データを更新する まとめ SSRを使用してデータを表示する SSR については下記のリンクが参考になります。 nextjs.org まずサーバーサイドはgetServerSideProps でデータを取得します。 // index.tsx const USER_KEY = "USER_KEY" ; const fetchUser = async ( baseUrl: string ) : Promise < { name: string } > => { const res = await fetch ( ` ${ baseUrl } /api/user` ); return await res.json (); } ; export const getServerSideProps: GetServerSideProps < ServerSideProps > = async ( { req , } ) => { const queryClient = new QueryClient (); const protocol = req.headers [ "x-forwarded-proto" ] || "http" ; const baseUrl = req ? ` ${ protocol } :// ${ req.headers.host } ` : "" ; await queryClient.prefetchQuery ( [ USER_KEY ] , () => fetchUser ( baseUrl )); return { props: { baseUrl , dehydratedState: dehydrate ( queryClient ), } , } ; } ; 次に fetchUser でPrefetchしたデータを USER_KEY のQuery に紐づけます。 dehydrate でクエリキャッシュをDehydrate し、dehydratedState を介してページにProps として渡します。 queryClient.prefetchQuery で特徴的なのはその返り値です。 公式ドキュメント にある通り Promise<void> を返します。エラーもスローされません。 エラーのハンドリングや取得したデータをサーバーサイドで使用する場合は queryClient.fetchQuery を使います。 (参考: https://github.com/TanStack/query/discussions/1494#discussioncomment-226271 ) クライアントサイドでは useQuery を使って実装します。 const Page: React.FC < ServerSideProps > = () => { const { data } = useQuery ( [ USER_KEY ] , () => fetchUser (), { staleTime: Infinity , } ); return ( < div > < p > { data.name } < /p > < /div > ); } ; useQuery のQuery Key はサーバーサイドと同様に USER_KEY を指定します。 オプションには staleTime: Infinity を追加しました。 このオプションによりキャッシュは常に「新鮮 (新しい)」と見なされ、バックグラウンドでの再取得が発生しません。 ブラウザでのReact Query の動作の様子 StaleTime やCacheTime については下記のブログがわかりやすかったです。 tkdodo.eu データを更新する 「データを表示する」でオプションに staleTime: Infinity を追加しました。 これによりデータの再取得は「キャッシュを無効化 (= 古くなったとみなす)」させるか「キャッシュをリセット」する必要があります。 「キャッシュの無効化」するには queryClient.invalidateQueries を使います。 const updateName = React.useCallback (() => { mutate ( name , { onSuccess: () => { queryClient.invalidateQueries ( [ USER_KEY ] ); } , } ); } , [ mutate , name , queryClient ] ); updateName が実行される際に、 USER_KEY に紐づくキャッシュを無効化しました。 同じような形で「キャッシュをリセット」するには queryClient.resetQueries を使用します。 この2つはメソッドを実行したときのデータの残り方に違いがあります。 queryClient.invalidateQueries は無効化されたキャッシュが再取得完了まで残ります。 一方、 queryClient.resetQueries は再取得前にキャッシュが初期状態にリセットされるので「なにも取得していない状態」になってから再取得をします。 どちらが良いかはワークフローによると思いますが、管理画面は「更新しようとしているデータ」をわかりやすくするために queryClient.invalidateQueries が良さそうです。 まとめ React Query はAPI の状態を返してくれるだけではなく、強力なのはそのキャッシュ機構にあると考えます。 このキャッシュをうまく使いこなせれば、リクエストを減らすことができ、よりよいユーザー体験につながるアプリケーションにすることができます。
アバター
主にネイティブアプリケーションの開発を担当している上野です。 先日、東京ビッグサイトにて開催された第5回5G通信技術展に出展し、 リアルタイムデジタルツイン及び産業分野別ソリューションなどのいくつかのデモンストレーションを行いました。 www.aptpod.co.jp この記事では展示したデモの1つである "リアルタイム"デジタルツイン について、開発を担当していた私が解説をしたいと思います。 そもそもデジタルツイン(DigitalTwin)とは "リアルタイム"デジタルツインとは デモンストレーション構成 実際にアプリを開発してみて Unityを選択した理由 位置測位精度の課題 Unityによる業務アプリケーション開発の難しさ 今後について そもそもデジタルツイン(DigitalTwin)とは デジタルツイン(DigitalTwin)とは、現実の世界から収集した様々なデータを、まるで双子であるかのように、コンピュータ上で再現する技術のことです。コンピュータ上では、収集した膨大なデータを元に、限りなく現実に近い物理的なシミュレーションが可能となり、自社製品の製造工程やサービスの在り方をより改善するうえで有効な手段となります。例えば製造ラインの一部を変更する場合など、事前にデジタルツイン上でテスト運営することで、開発期間やコストの削減が見込めます。 引用元: デジタルツインとは?意味・定義 | ITトレンド用語 | ドコモビジネス | NTTコミュニケーションズ "リアルタイム"デジタルツインとは 前項で デジタルツインは膨大なデータを元に とある様に実際にデジタルツインで取り扱うデータは大量のデータを扱うことが多く、更新の頻度も数十秒以上かかるものが多いなど高頻度に情報を取り込むのは難しい様です。 弊社では intdash という高速でかつ大容量のデータ伝送を行えるプラットフォームと Visual M2M Data Visualizer のような伝送されたデータを数十~数百ミリ秒の遅延で可視化する製品を持つなど リアルタイム に取り扱うノウハウを持っています。 このデモは、アプトポッドの技術と最近注目されつつあるデジタルツインを組み合わせてなにか出来ないかを調査・検討した プロトタイピングプロジェクトの成果 になります。 ※ 今回展示した デジタルツインデモ は、すぐにご提供できる商品ではありません。研究開発成果をご紹介するためのものとお考えください。 デモンストレーション構成 デモンストレーション構成 今回デモアプリとして、バーチャル空間上に現実世界の倉庫を再現するデモアプリを開発しました。また現実世界としては、実際の倉庫を用意するまで準備コストをかけられなかったため、代替として物理シミュレーターである Gazebo を使用しました。 youtu.be デモアプリとGazeboシミュレーターには Turtlebot3 を設置しています。 今回のデモでは リアル環境の代替としてクラウド上に設置したGazeboシミュレーターの情報をintdashを用いて伝送し続けています。 展示会場には一般的なゲームパッド *1 が接続された Raspberry Pi を設置し、Raspberry Piに組み込まれた弊社製品である intdash Edge Agent を用いてintdashのプラットフォームを利用してコントロール情報をGazeboシミュレーターに伝送することでTurtleBotの遠隔制御が可能な状態にしています。 伝送され続けているリアル環境データは数十から数百ミリ秒ほどの遅延で開発したデモアプリに表示されています。 バーチャル空間上では、倉庫内の固定物(棚など)は3Dモデルとして描画しています。一方でダンボールなどの可動物は、事前にモデリングできないため、本来であれば3Dモデルとして描画することはできませんが、今回のデモではわかりやすさのためにワイヤフレームの形式で描画しています。 実際の利用シーンでは、ダンボールなどの可動物は Turtlebot3 に搭載した LiDAR などによって検出する想定となるため、LiDARから取得した点群をバーチャル空間にオーバーレイして表示しています。 という内容となっています。 以上、デモンストレーションの内容に関しては 動画 にて詳しく解説されておりますので是非ご覧ください。 youtu.be 実際にアプリを開発してみて ここからは開発者目線でのお話をしたいと思います。 今回展示したデモは弊社では初の試みで Unity を用いて開発しております。 近年デジタルツインと始めとする3次元データや3Dモデルの可視化ツールの需要が高まってきており、その開発の主軸としてUnityや UnrealEngine などの一般的にはゲーム用エンジンと呼ばれる3D開発プラットフォームが注目されています。 Unityを選択した理由 Unity を選択した理由は、インターネット上に存在する情報、特に日本語情報の多さです。さらにUnityの主な開発言語はC#で、C#はゲーム以外にも利用される言語であるため、今回のようなゲームではない産業アプリケーションの開発に関する情報も多いと考えました。また、C#での開発で不足があればC++を使用することも可能です。一般的なゲーム開発から少し外れる今回の開発プロジェクトでは、こういった情報量の多さや開発の自由度の高さがプラスに働くと考えました。 位置測位精度の課題 実は今回のデモでは、ロボットの位置はGazeboシミュレーターが出力する値を直接取得して使用しています。実際の現実世界でこのデモを使用する場合には、例えば SLAM(Simultaneous Localization and Mapping) のような位置測位技術を使用して、様々なセンサーから算出した推定位置を使用しなければなりません。今回のデモではTurtleBotに搭載されたLiDARによりダンボールなどの障害物を検出してバーチャル空間上にマッピングしていますが、このマッピングが極めて正確に行えるのは、完全な位置情報が推定されている状態だったためです。 実際の現実世界で今回のデモのように正確に障害物の位置をマッピングするには、相当精度のよい位置測位技術が必要となりますが、現在屋内における位置測位技術は屋外におけるGPSのようなスタンダードが確立していない状況です。もし屋内位置測位技術として、汎用的かつ精度の高い技術をお持ちの場合は、ぜひお声がけください。 また、以前私が執筆した記事 *2 ではiPhoneを用いて地形をマッピングする様なデモを行いましたがあれらはAppleが提供する高度な自己位置推定機能と位置測位技術をユーザーへ提供しているからこそ実現しています。 取得できた点群データをサーバへ送信し、別PCの3D仮想空間上に表示してみた #iPhone12Pro #LiDAR #ARKit #PointCloud #Demo pic.twitter.com/ioxaAQtIfa — aptueno (@aptueno) 2020年12月16日 デジタルツインにおいてこの 自己位置推定の課題 は大きいと言えます。 Unityによる業務アプリケーション開発の難しさ 今回のデモアプリケーションは、業務アプリケーションとして使用できるように、各種ウィンドウやマウスクリックで操作する画面要素(テキストボックスやドロップダウンリストなど)もUnityで表現することにチャレンジしてみました。iPhoneアプリやWebアプリケーション開発であれば標準コンポーネントとして簡単に導入できる画面要素でも、ゲームであまり使用されないものはUnityには存在しなかったため、スクラッチから開発する必要がありました。 また、Unityでは2Dの画面をただ描画するだけでも、裏側には必ず3Dの要素を保つ必要があり、想定以上にコンピューティングリソースが必要になるという問題もありました。現在ゲームエンジンの産業利用は進んできていますが、業務アプリケーションのような単純な画面構成を開発する用途には、まだまだ最適化されていない感じることが多くありました。 今後について 弊社のプロダクトにUnityを組み合わせることで実現される、ユースケースの広がりを想像いただけましたでしょうか。 今回のデモで得られた知見は、今後お客様からご依頼いただく開発プロジェクトにも活かしていきたいと思っております。 デジタルツインシステムを作ってみたいが、何から手を付けてよいか分からない 既存のデジタルツインシステムにもっとリアルタイム性をもたせたい AGVや自動運転車両のリアルタイム監視を行いたい 監視だけでなく遠隔操作も行いたい データ伝送の必要なアプリケーションを作りたいが、通信プロトコルに詳しくない Unity を使って業務アプリケーションを作ってみたい 大量データを扱えるハイパフォーマンスな可視化アプリケーションを開発したい などなど、アプトポッドではIoTに関する豊富な実績と高い技術力で、お客様の課題解決にご協力いたしますのでお気軽にお問合せください。 www.aptpod.co.jp *1 : ※使用したゲームパッドは こちら *2 : ここから始まるお手軽地形計測 iPhoneへLiDARスキャナ搭載【ARKit】 - aptpod Tech Blog
アバター
開発本部 SRE グループの金澤です。 今回は GitLab にアルファ版としてサポートされた OIDC(OpenID Connect) を使用して、よりセキュアな環境で GitLab CI/CD を実施するようにした内容をご紹介します。 現状と課題 CI/CD と OpenID Connect 設定 クラウドインフラでOpenIDプロバイダーを登録する 登録した OpenID プロバイダー用の IAM ロールを作成する GitLab プロジェクトへの変数定義 CI/CD を動作させて確認する 結果 注意点 まとめ 現状と課題 弊社製品の intdash のサーバサイドソフトウェアである intdash Server は、基本的にクラウドインフラを使用して構築しており、Terraform や Ansible による構成管理を行っています。コード管理には GitLab を利用しており、GitLab はセルフマネージドで運用しています。 GitLab は契約の種類(SaaS・セルフマネージド)を問わず CI/CD 機能を使用することができます。インフラのソースコードについても本機能を利用してコードの正常性を担保するようにしています。コードの内容と実装が相違ないことの確認や、必要な定期処理の実施が主な目的です。 インフラにおける CI/CD 利用時はその特性上、クラウドインフラへのリソースアクセスを行う機会が多く、これまでは各クラウドインフラのID・アクセス管理(IdAM/IAM)サービスから認証情報を払い出し使用していたのですが、払い出した認証情報のローテーション対応や万が一の漏洩に対するリスク管理が必要となり、セキュリティの管理的にはあまり望ましいオペレーションではありませんでした。 CI/CD と OpenID Connect OIDC(OpenID Connect)は、OAuth 2.0 をベースとする認証プロトコルです。 GitHub の CI/CD 機能である GitHub Action は昨年11月末に 対応アナウンス があり話題になりましたが、GitLab も今年1月にリリースされた GitLab 14.7 にて アルファ版として機能対応が発表されました 。 本機能を使用することによって、ID・アクセス管理サービスから認証情報を払い出す必要はなくなり、一時的な認証情報によってクラウドインフラのリソースを操作することが可能となりました。払い出されるのは一時的な認証情報となるので、前述したローテーション管理やリスク管理を軽減することが期待できます。GitLab では このような認証ワークフロー で動作します。 設定 それでは実際に設定していきます。今回は「あらかじめ認証情報を登録せず GitLab CI からクラウドインフラリソース(Amazon S3)を参照できること」を確認します。 設定する内容については GitLab 公式ドキュメント で紹介されています。この内容を踏まえながら、Terraform コードベースで設定していきます。アプトポッドではクラウドインフラとして主に AWS を利用していますので、登録するクラウドインフラを AWS としています。 クラウドインフラでOpenIDプロバイダーを登録する クラウドインフラに OpenID プロバイダーを登録します。 aws_iam_openid_connect_provider リソースを使用して OpenID プロバイダーを設定します。 url: GitLab インスタンス URL ex) " https://gitlab.example.net " client_id_list: GitLab インスタンス URL ex) " https://gitlab.example.net " thumbprint_list: こちら を参照して算出する。 resource " aws_iam_openid_connect_provider " " oidc-sample " { url = " https://gitlab.example.net " client_id_list = [ " https://gitlab.example.net ", ] thumbprint_list = [ " 算出した値 ", ] } 手動で登録する場合 thumbprint_list の値は AWS コンソール上に存在する「サムプリント取得」というボタンを押下することで取得することが出来ますが、コードで表現する場合は事前に取得する必要があります。事前取得ではなく自動化したい場合は、 tls_certificate というデータリソースを使用して実現することができます。(今回は実施していません。) 登録した OpenID プロバイダー用の IAM ロールを作成する 登録した OpenID プロバイダーを信頼されたエンティティに設定するかたちで、IAM ロールを作成します。 statement.condition.variable: GitLab インスタンス URL および JWT フィールド ex) "gitlab.example.com:sub" statement.condition.values: 対象とする GitLab プロジェクトおよびブランチ ex) "project_path:mygroup/myproject:ref_type:branch:ref:main" 下記の例の statement.condition.values ではワイルドカードを指定することもできます。ワイルドカードを指定することによって1つのプロジェクトにおける複数のブランチを対象とすることができます。その場合、 statement.condition.test の値を StringLike としてください。指定の方法は 公式ドキュメント にも記載がありますのでご参照ください。 resource " aws_iam_role " " oidc-sample " { name = " role_oidc_sample " assume_role_policy = data.aws_iam_policy_document.oidc - sample - principal.json } data " aws_iam_policy_document " " oidc-sample-principal " { statement { actions = [ " sts:AssumeRoleWithWebIdentity " ] principals { type = " Federated " identifiers = [ aws_iam_openid_connect_provider.oidc - sample.arn ] } effect = " Allow " condition { test = " StringEquals " variable = " gitlab.example.com:sub " values = [ " project_path:mygroup/myproject:ref_type:branch:ref:main " ] } } } resource " aws_iam_policy " " oidc-sample " { name = " for-oidc " path = " / " policy = data.aws_iam_policy_document.oidc - sample - policy.json } data " aws_iam_policy_document " " oidc-sample-policy " { statement { actions = [ " s3:* ", ] resources = [ " * " ] effect = " Allow " } } resource " aws_iam_role_policy_attachment " " oidc-sample " { role = aws_iam_role.oidc - sample.name policy_arn = aws_iam_policy.oidc - sample.arn } GitLab プロジェクトへの変数定義 作成した IAM ロールの ARN を GitLab CI の変数定義に登録します。今回登録する変数は IAM ロールの ARN のみとしています。 GitLab CI/CD 変数定義 CI/CD を動作させて確認する 実際に CI/CD を動作させて確認してみます。 .gitlab-ci.yaml は以下のように記載しています。表記中の CI_JOB_JWT_V2 はOIDCサポートと同時に導入されたJWT(JSON Web Token)です。こちらを利用してクラウドインフラとJWTトークンベースの接続を行っています。CI スクリプトの中で echo を行い存在の確認をしようとしています。 .gitlab-ci.yaml image : name : amazon/aws-cli:latest entrypoint : - '/usr/bin/env' assume role : script : - echo ${CI_JOB_JWT_V2} - > STS=($(aws sts assume-role-with-web-identity --role-arn ${ROLE_ARN} --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}" --web-identity-token $CI_JOB_JWT_V2 --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) - export AWS_ACCESS_KEY_ID="${STS[ 0 ]}" - export AWS_SECRET_ACCESS_KEY="${STS[ 1 ]}" - export AWS_SESSION_TOKEN="${STS[ 2 ]}" - aws s3 ls 結果 GitLab CI の結果を記載します。GitLab 上にはアクセスキーなどの認証情報を登録していませんでしたが、 aws s3 ls で現在の S3 バケットの一覧情報が表示されました。 CI_JOB_JWT_V2 については、 [MASKED] といった形で隠蔽されることが分かりました。このことから aws sts assume-role-with-web-identity で一時的な認証情報が取得できていそうです。 ( 中略 ) $ echo ${CI_JOB_JWT_V2} [ MASKED ] $ STS = ( $ ( aws sts assume-role-with-web-identity --role-arn ${ROLE_ARN} --role-session-name " GitLabRunner- ${CI_PROJECT_ID} - ${CI_PIPELINE_ID} " --web-identity-token $CI_JOB_JWT_V2 --duration-seconds 3600 --query ' Credentials.[AccessKeyId,SecretAccessKey,SessionToken] ' --output text )) $ export AWS_ACCESS_KEY_ID= " ${STS[ 0 ]} " $ export AWS_SECRET_ACCESS_KEY= " ${STS[ 1 ]} " $ export AWS_SESSION_TOKEN= " ${STS[ 2 ]} " $ aws s3 ls 2020-06-23 06:08:59 analytics-blobs-dev-log-personal 2020-06-23 06:20:23 analytics-blobs-dev-personal : 注意点 冒頭でも記載しました通り、(2022/07/07 時点で) 本機能はアルファ版であり 本番環境での使用はまだ適していない ことにご注意ください。 まとめ 今回は、GitLab でサポートされた OIDC(OpenID Connect) 機能を利用して、よりセキュアな環境で GitLab CI/CD を実施するようにした内容をご紹介しました。 あらかじめ認証情報を払い出し GitLab シークレットに登録することなく、クラウドインフラのリソースを操作できることが確認出来ました。これにより GitLab CI/CD におけるセキュリティ面の向上はもちろん、CI/CD を利用したいが認証情報の取り扱いなどから生まれる心理的な障壁の一端を無くすことができると考えています。 本記事が少しでも参考になりましたら幸いです。
アバター
はじめに DX推進事業本部 クロスインダストリーグループの渡辺です。 ソリューションアーキテクトを担当しています。 「ソリューションアーキテクト」(以降SAと省略)という言葉が使われる機会は増えてきましたが、まだまだ馴染みの無い方は多いと思います。 今回は弊社のSAの仕事について紹介したいと思います。本記事で、SAという仕事に少しでも興味を持ってもらえると嬉しいです。 はじめに ソリューションアーキテクトとは アプトポッドのソリューションアーキテクト 例えばこんなこと ソフトウェア開発は初めてです こんなご相談も ソリューションのご提案もいたします スマートインフラメンテナンス ロボットフリート&遠隔制御 5G通信技術展でご覧頂けます おわりに ソリューションアーキテクトとは 自社のサービス・技術力を深く理解するとともにお客様の業務を理解して、お客様が課題解決を行うサポートをさせて頂く仕事です。 カッコ良く書けばこういう事になりますが、 お客様の「やりたいこと」を聴いて一緒に実現する。 お客様の「課題・問題」を聴いて一緒に解決する。 という仕事です。 アプトポッドのソリューションアーキテクト アプトポッドは、自社製品である「 IoTプラットフォーム intdash 」を活用して、産業IoTに取り組まれるお客様のデータ伝送、データ収集、データ活用に関する課題を解決するためのソリューションを提供する会社です。 intdash の特長としては以下があげられます。 サーバを介しても低遅延であり、高いリアルタイム性でデータ伝送が行える 不安定な通信環境化でも、伝送時にロストしたデータの完全回収ができる エッジサイドでデータに時刻を付与することで、多種多様なデータを時刻同期させてデータ管理が行える リアルタイムのデータ活用だけでなく、サーバに保存された過去データも容易に分析、学習などに利活用できる 上記の特長を活かし、intdashは、自動車、重機・建機・農機、工場機械、ロボットなど様々な分野のお客様にご活用頂いています。 と、intdashの説明するのは簡単なのですが、これだけではお客様が 「自社サービスにintdashを利用したら何が出来るか?」 を明確にイメージして頂くには至りません。 そこには、ギャップが在る、と言えます。 このギャップを埋めるお手伝いをするのが、SAの役割です。 新しいサービスを実現したい。intdashを使えば出来そうなんだけど。 日常業務でコストを削減したい。intdashを使えば出来そうなんだけど。 とりあえずintdashって使えそうな気がする。 そんなお話をお聴きするところからSAの仕事が始まり、お客様が望むサービスや業務改善を実現するまでのお手伝いをさせて頂いています。 例えばこんなこと ソフトウェア開発は初めてです IoTに関わっていますと、専らハードウェア開発に専念してきた、というお客様とお仕事をさせて頂く機会があります。 「監視パネルをWEB上で実現したい」 とご相談を受けました。 やや特殊なハードウェアを扱っておられるお客さまでしたので 操作パネルがどういう動作をするのか なぜそういう動作をするのか とお客様にヒアリングしながら私たちは勉強をし、 お客様にも WEBアプリにはどういう特性があるのか どういう操作がWEB上では使いやすいのか の様なことをレクチャーして勉強して頂きながら、膝を突き合わせて議論を重ねて最適なWEBシステムを作り上げていく。 とこういったお手伝いをさせて頂きました。 こんなご相談も 「空はどこからが空なのかを知りたい」 こんなご相談を受けたこともありました。 ドローン開発に当たってintdashを導入して頂くことが決まっていたお客様からのご相談です。 空を飛ぶドローンでモバイル回線を使用する場合は特別な許可が必要です。 では許可を得なければいけない条件の 「空を飛ぶ」 とはどういうことなのか、10mを超えれば空なのか、それとも1cmでも飛んでいれば空なのか? そういう疑問から出てきたご相談でした。モバイル回線を用いて確実なデータ伝送を行えるのが当社サービスの強みでもあるので、当社にとっても大事なことです。 関係する法規を調査し、モバイル回線会社にも問い合わせをし、結果は 「1cmでも飛んでいれば空」 でした、なんだか腑に落ちたような、落ちないような、、、 ソリューションのご提案もいたします ソリューションアーキテクトはお客様からのご相談をお受けするだけではなく、お客様のニーズに対応したソリューションを企画立案しご提案いたします。 スマートインフラメンテナンス 工事現場情報など、インフラ事業者様が必要とする環境情報を、モビリティから取得する動画像を元にAIによるリアルタイム検知とGPSによる位置特定を行い、リアルタイムに自動検知、収集、可視化するソリューションです。 「 EDGEPLANT T1 」に接続されたUSBカメラを用いて映像を撮影しながら、デバイス上で動作させるAIモデルで画像を検出しています。 当社製デバイスの特徴を有効活用したソリューションです。 社会インフラに関わっているお客様にご提案させて頂いています。 【USBカメラで撮影しながら画像検出を行っている様子です】 youtu.be ロボットフリート&遠隔制御 5G 時代に本格化する自動化と遠隔化を見据えて、「intdash」はROS1およびROS2にも対応しています。こちらはその機能を活かして、ロボットや各種モビリティなど、機体の遠隔制御、機体データの遠隔モニタリングとデジタルツイン対応などを実現するソリューションです。 「 IoTプラットフォーム intdash 」は様々なデバイス同士を繋ぐことが出来ます。 その特徴を活かし、リアルとバーチャルリアリティを繋ぎました。 バーチャルリアリティ空間に存在するロボットを、リアル空間に存在するコントローラーを用いて操作しています。また、バーチャルリアリティ空間のロボットからクラウド空間にアップロードされるデータを「 Visual M2M Data Visualizer 」を用いて可視化しています。 AGVをはじめとする様々なロボット開発、自律/遠隔制御されるモビリティの開発に関わっているお客様にご提案させて頂くソリューションです。 【VRロボットをコントローラーで操作している様子です】 youtu.be 【ロボットの各種データを可視化している様子です】 youtu.be 5G通信技術展でご覧頂けます この度、 5G通信技術展 でこれらのソリューションの幾つかをデモンストレーションさせて頂くことになりました。 是非当社ブースまでお越しください。 www.aptpod.co.jp おわりに 本記事で、当社のサービス、またソリューションアーキテクトに少しでも興味を持ってもらえると嬉しいです。 そして 新しいサービスを実現したい。intdashを使えば出来そうなんだけど。 日常業務にコストを削減したい。intdashを使えば出来そうなんだけど。 とりあえずintdashって使えそうな気がする。 と思ったお客様はぜひお気軽に当社までお問い合わせください。 5G通信技術展 にもお越しいただけることを楽しみにお待ちしております。 www.aptpod.co.jp
アバター
はじめに プロフェッショナルサービス本部 SRE グループのkawamata です。 今回は at least once を保証するメッセージングシステムを構成可能なミドルウェアである、 NATS JetStream を試してみました。 弊社 intdash を構成する intdash Server でもメッセージングシステムが動作しています。 現状は、そのメッセージングシステムの一部に NATS Streaming を採用しています。 NATS Streaming は現在 deprecated となっており、 2023/06 にはサポートが終了 する予定です。本件への対応方針はプロダクト開発本部で検討中ですが、 SRE グループでも情報収集のため、 NATS Streaming の後継にあたる NATS JetStream をまずは試してみました。 はじめに NATS とは NATS NATS Streaming NATS JetStream NATS JetStream の特徴 設計思想 特徴 実際に試してみた デプロイ メッセージストリーミング Key Value ストア オブジェクトストア おわりに NATS とは まず、 NATS とは何か?について概要を説明します。 そもそも、メッセージングシステムに関して簡単に触れておきます。 メッセージングシステムはサービス間の非同期処理を実現します。 メッセージングシステムの利用者はPublisher, Subscriber と呼ばれます。 Publisher はメッセージングシステムにメッセージを送信し、Subscriber はメッセージングシステムからメッセージを受信します。 Publisher とSubscriber のメッセージングの間にシステムが入ることによって、両者は疎結合となり相互の処理の依存度が下がるため、非同期の処理が可能になります。 また、メッセージングシステムは主に送達されるメッセージの保証要件によって大別されます。 メッセージの送達保証には保証度の低い順に以下の種類があります。 at most once 1 回 は 送信 メッセージの送達は保証されない at least once 少なくとも 1 回送信 1 回 以上 のメッセージ送達を保証 メッセージの重複が有り得る exactly once 必ず 1 回送信 1 回 だけ のメッセージ送達を保証 メッセージの重複は発生しない NATS NATS は、 CNCF(Cloud Native Computing Foundation) のプロジェクトでホストされているシンプルかつ高性能なメッセージングシステムを構成可能なオープンソースのミドルウェアです。 NATS 単体では、 at most once なPub/Sub(Publish/Subscribe) システムを構成可能です。 at most once ですので、Subscriber は NATS サーバに接続していない間にPublisher から送られたメッセージを受信することが出来ません。 NATS Streaming NATS Streaming は NATS Server にStreaming module と永続化のためのstorage を組み合わせ、 at least once を保証するメッセージングシステムを構成可能にしたものです。 at least once が保証されますので、Subscriber(この場合Consumer) はPublish されたメッセージを文字通り必ず1 回は受信可能です。 NATS Streaming はシンプルでスケールさせやすい優れたメッセージングシステムですが、いくつかの問題点がありました。 例として、以下の課題があります。 at least once のためメッセージが重複する可能性がある 確認済みのメッセージをシステムから削除出来ない Consumer はメッセージをpull 出来ない Subscribe の制限が柔軟に出来ない クラスタのスケールアウトにおいて、スケーラビリティが低い NATS 2.0 のセキュリティモデルに対応していない NATS JetStream NATS JetStream は前述の課題を解決するべく、 NATS Streaming の後継として開発されたものです。 NATS Server とは別に開発されていましたが、 NATS 2.2.0のタイミングでGA(Generally Available) となり、現在は NATS Server に組み込まれています。 NATS JetStream は exactly once を保証するメッセージングシステムを構成することが可能です。 NATS Streaming と違いメッセージの重複は発生しないアーキテクチャになっています。 NATS JetStream の特徴 ここで、 NATS JetStream の設計思想や特徴をいくつか説明します。 設計思想 設計思想に関して、以下に 公式ドキュメント から抜粋します。 構成と操作が簡単で監視が容易 NATS 2.0 セキュリティモデルに準拠 スケールアウトによる高いスケーラビリティを有する 多様なユースケースをサポートする 自己回復性を有する メッセージペイロードに依存しない動作をする 特徴 NATS JetStream にはいくつか特徴があります。 以下に注目すべき特徴を説明します。 exactly once なメッセージングを保証 メッセージの再生ポリシーが多様 再生レートが選択可能 instant (Consumer が処理可能な最速で受信) original (Publisher が送信したレートで再生) メッセージの開始番号や時刻を選択可能 メッセージの確認応答の種類が豊富 メッセージストリーミング以外の機能も利用可能(JetStream は単なる永続層という位置づけ) Key Value Store 所謂、一般的なKVS として利用可能 KVS イベントをメッセージングとして受信可能 Object Store メッセージの容量制限を大きくすることでバイナリ等のデータオブジェクトをメッセージとしてストア可能 その他の特徴や制限事項は 公式ドキュメント をご確認ください。 実際に試してみた それでは、実際に NATS JetStream を試してみます。 公式の Helm charts がありますので、こちらを利用して 3 Node のNATS JetStream クラスタを Kubernetes 上で動作させます。 Kubernetes Operator 用に NATS Operator も存在していますが、こちらはNATS JetStream には対応していないため注意が必要です。 デプロイ 公式ドキュメント を参考に、helm を使用してデプロイしていきます。 repository の追加 $ helm repo add nats https://nats-io.github.io/k8s/helm/charts/ 設定ファイルの準備 以下の設定ファイル(YAML) を準備 NATS JetStream の有効化および、クラスタの設定定義 $ cat jetstream/app-config.yaml nats: image: nats:alpine jetstream: enabled: true memStorage: enabled: true size: 2Gi fileStorage: enabled: true size: 10Gi storageClassName: gp3 cluster: enabled: true replicas: 3 $ インストール $ helm install nats-jetstream nats/nats -n aptpod-demo -f jetstream/app-config.yaml NAME: nats-jetstream LAST DEPLOYED: Wed May 25 18:13:20 2022 NAMESPACE: aptpod-demo STATUS: deployed REVISION: 1 NOTES: You can find more information about running NATS on Kubernetes in the NATS documentation website: https://docs.nats.io/nats-on-kubernetes/nats-kubernetes NATS Box has been deployed into your cluster, you can now use the NATS tools within the container as follows: kubectl exec -n aptpod-demo -it deployment/nats-jetstream-box -- /bin/sh -l nats-box:~# nats-sub test & nats-box:~# nats-pub test hi nats-box:~# nc nats-jetstream 4222 Thanks for using NATS! $ 確認 pod が動作していれば成功 $ kubectl -n aptpod-demo get pods nats-jetstream-0 3/3 Running 0 73m nats-jetstream-1 3/3 Running 0 73m nats-jetstream-2 3/3 Running 0 73m nats-jetstream-box-65d7d89987-gbrg4 1/1 Running 0 73m $ メッセージストリーミング 公式ドキュメント を参考にメッセージストリーミングを試してみます。 nats-box にログイン $ kubectl exec -n aptpod-demo -it deployment/nats-jetstream-box -- /bin/sh ~ # stream の作成 intdash_test という名前で作成 ~ # nats stream add intdash_test ? Subjects intdash ? Storage file ? Replication 3 ? Retention Policy Limits ? Discard Policy Old ? Stream Messages Limit -1 ? Per Subject Messages Limit -1 ? Total Stream Size -1 ? Message TTL -1 ? Max Message Size -1 ? Duplicate tracking time window 2m0s ? Allow message Roll-ups No ? Allow message deletion Yes ? Allow purging subjects or the entire stream Yes Stream intdash_test was created Information for Stream intdash_test created 2022-05-25T09:46:03Z Configuration: Subjects: intdash Acknowledgements: true Retention: File - Limits Replicas: 3 Discard Policy: Old Duplicate Window: 2m0s Allows Msg Delete: true Allows Purge: true Allows Rollups: false Maximum Messages: unlimited Maximum Bytes: unlimited Maximum Age: unlimited Maximum Message Size: unlimited Maximum Consumers: unlimited Cluster Information: Name: nats Leader: nats-jetstream-0 Replica: nats-jetstream-1, current, seen 0.00s ago Replica: nats-jetstream-2, current, seen 0.00s ago State: Messages: 0 Bytes: 0 B FirstSeq: 0 LastSeq: 0 Active Consumers: 0 ~ # consumer の作成 以下2 つのconsumer を作成 intdash_consumer intdash_consumer2 ~ # nats consumer add ? Consumer name intdash_consumer ? Delivery target (empty for Pull Consumers) ? Start policy (all, new, last, subject, 1h, msg sequence) all ? Acknowledgement policy all ? Replay policy instant ? Filter Stream by subject (blank for all) intdash ? Maximum Allowed Deliveries -1 ? Maximum Acknowledgements Pending 0 ? Deliver headers only without bodies No ? Add a Retry Backoff Policy No ? Select a Stream intdash_test Information for Consumer intdash_test > intdash_consumer created 2022-05-25T09:52:54Z Configuration: Durable Name: intdash_consumer Pull Mode: true Filter Subject: intdash Deliver Policy: All Ack Policy: All Ack Wait: 30s Replay Policy: Instant Max Ack Pending: 1,000 Max Waiting Pulls: 512 Cluster Information: Name: nats Leader: nats-jetstream-1 Replica: nats-jetstream-0, current, not seen Replica: nats-jetstream-2, current, seen 0.00s ago State: Last Delivered Message: Consumer sequence: 0 Stream sequence: 0 Acknowledgment floor: Consumer sequence: 0 Stream sequence: 0 Outstanding Acks: 0 out of maximum 1,000 Redelivered Messages: 0 Unprocessed Messages: 10 Waiting Pulls: 0 of maximum 512 ~ # ~ # nats consumer add ? Consumer name intdash_consumer2 ? Delivery target (empty for Pull Consumers) ? Start policy (all, new, last, subject, 1h, msg sequence) all ? Acknowledgement policy all ? Replay policy original ? Filter Stream by subject (blank for all) intdash ? Maximum Allowed Deliveries -1 ? Maximum Acknowledgements Pending 0 ? Deliver headers only without bodies No ? Add a Retry Backoff Policy No ? Select a Stream intdash_test Information for Consumer intdash_test > intdash_consumer2 created 2022-05-25T09:53:42Z Configuration: Durable Name: intdash_consumer2 Pull Mode: true Filter Subject: intdash Deliver Policy: All Ack Policy: All Ack Wait: 30s Replay Policy: Original Max Ack Pending: 1,000 Max Waiting Pulls: 512 Cluster Information: Name: nats Leader: nats-jetstream-1 Replica: nats-jetstream-0, current, not seen Replica: nats-jetstream-2, current, seen 0.00s ago State: Last Delivered Message: Consumer sequence: 0 Stream sequence: 0 Acknowledgment floor: Consumer sequence: 0 Stream sequence: 0 Outstanding Acks: 0 out of maximum 1,000 Redelivered Messages: 0 Unprocessed Messages: 10 Waiting Pulls: 0 of maximum 512 ~ # メッセージのpublication subject は intdash 1 s 毎に10 メッセージを送信 ~ # nats pub intdash --count=10 --sleep 1s "publication #{{Count}} @ {{TimeStamp}}" 09:49:38 Published 37 bytes to "intdash" 09:49:39 Published 37 bytes to "intdash" 09:49:40 Published 37 bytes to "intdash" 09:49:41 Published 37 bytes to "intdash" 09:49:42 Published 37 bytes to "intdash" 09:49:43 Published 37 bytes to "intdash" 09:49:44 Published 37 bytes to "intdash" 09:49:45 Published 37 bytes to "intdash" 09:49:46 Published 37 bytes to "intdash" 09:49:47 Published 38 bytes to "intdash" ~ # intdash_consumer でメッセージ受信 メッセージ再生レート instant publishe 時のレートとは無関係に受信しているのがわかる ~ # nats consumer next intdash_test intdash_consumer --count 10 [09:55:04] subj: intdash / tries: 1 / cons seq: 1 / str seq: 1 / pending: 9 publication #1 @ 2022-05-25T09:49:37Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 2 / str seq: 2 / pending: 8 publication #2 @ 2022-05-25T09:49:38Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 3 / str seq: 3 / pending: 7 publication #3 @ 2022-05-25T09:49:39Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 4 / str seq: 4 / pending: 6 publication #4 @ 2022-05-25T09:49:40Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 5 / str seq: 5 / pending: 5 publication #5 @ 2022-05-25T09:49:41Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 6 / str seq: 6 / pending: 4 publication #6 @ 2022-05-25T09:49:42Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 7 / str seq: 7 / pending: 3 publication #7 @ 2022-05-25T09:49:43Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 8 / str seq: 8 / pending: 2 publication #8 @ 2022-05-25T09:49:44Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 9 / str seq: 9 / pending: 1 publication #9 @ 2022-05-25T09:49:45Z Acknowledged message [09:55:04] subj: intdash / tries: 1 / cons seq: 10 / str seq: 10 / pending: 0 publication #10 @ 2022-05-25T09:49:46Z Acknowledged message ~ # intdash_consumer2 でメッセージを5 ずつ受信 メッセージ再生レート original publishe 時のレートで受信しているのがわかる ~ # nats consumer next intdash_test intdash_consumer2 --count 5 [09:55:37] subj: intdash / tries: 1 / cons seq: 1 / str seq: 1 / pending: 9 publication #1 @ 2022-05-25T09:49:37Z Acknowledged message [09:55:38] subj: intdash / tries: 1 / cons seq: 2 / str seq: 2 / pending: 8 publication #2 @ 2022-05-25T09:49:38Z Acknowledged message [09:55:39] subj: intdash / tries: 1 / cons seq: 3 / str seq: 3 / pending: 7 publication #3 @ 2022-05-25T09:49:39Z Acknowledged message [09:55:40] subj: intdash / tries: 1 / cons seq: 4 / str seq: 4 / pending: 6 publication #4 @ 2022-05-25T09:49:40Z Acknowledged message [09:55:41] subj: intdash / tries: 1 / cons seq: 5 / str seq: 5 / pending: 5 publication #5 @ 2022-05-25T09:49:41Z Acknowledged message ~ # nats consumer next intdash_test intdash_consumer2 --count 5 [09:55:52] subj: intdash / tries: 1 / cons seq: 6 / str seq: 6 / pending: 4 publication #6 @ 2022-05-25T09:49:42Z Acknowledged message [09:55:53] subj: intdash / tries: 1 / cons seq: 7 / str seq: 7 / pending: 3 publication #7 @ 2022-05-25T09:49:43Z Acknowledged message [09:55:54] subj: intdash / tries: 1 / cons seq: 8 / str seq: 8 / pending: 2 publication #8 @ 2022-05-25T09:49:44Z Acknowledged message [09:55:55] subj: intdash / tries: 1 / cons seq: 9 / str seq: 9 / pending: 1 publication #9 @ 2022-05-25T09:49:45Z Acknowledged message [09:55:56] subj: intdash / tries: 1 / cons seq: 10 / str seq: 10 / pending: 0 publication #10 @ 2022-05-25T09:49:46Z Acknowledged message ~ # nats consumer next intdash_test intdash_consumer2 --count 5 nats: error: no message received: nats: timeout ~ # purge サブコマンドや rm サブコマンドでstream やメッセージの削除も可能になっています。 Key Value ストア 公式ドキュメント を参考にKey Value ストアも試してみます。 kvs の作成 intdash_kv という名前で作成 ~ # nats kv add intdash_kv Information for Key-Value Store Bucket intdash_kv created 2022-05-25T10:04:10Z Configuration: Bucket Name: intdash_kv History Kept: 1 Values Stored: 0 Backing Store Kind: JetStream Maximum Bucket Size: unlimited Maximum Value Size: unlimited JetStream Stream: KV_intdash_kv Storage: File Cluster Information: Name: nats Leader: nats-jetstream-1 ~ # key のput 2b126b37-f663-40e6-9ad6-2665b8ef2de3 というkey に intdash_X1 というvalue をストア ~ # nats kv put intdash_kv 2b126b37-f663-40e6-9ad6-2665b8ef2de3 intdash_X1 intdash_X1 ~ # key のget ~ # nats kv get intdash_kv 2b126b37-f663-40e6-9ad6-2665b8ef2de3 intdash_kv > 2b126b37-f663-40e6-9ad6-2665b8ef2de3 created @ 25 May 22 10:07 UTC intdash_X1 ~ # key のdelete ~ # nats kv del intdash_kv 2b126b37-f663-40e6-9ad6-2665b8ef2de3 ? Delete key intdash_kv > 2b126b37-f663-40e6-9ad6-2665b8ef2de3? Yes ~ # ~ # nats kv get intdash_kv 2b126b37-f663-40e6-9ad6-2665b8ef2de3 nats: error: nats: key not found, try --help ~ # key event の確認 ~ # nats kv watch intdash_kv [2022-05-25 10:08:09] DELETE intdash_kv > 2b126b37-f663-40e6-9ad6-2665b8ef2de3 ^C ~ # 特に問題なくKVS として動作しています。 オブジェクトストア 公式ドキュメント を参考にオブジェクトストアも試してみます。 オブジェクトストアの機能は現時点では Experimental Preview です。 オブジェクトバケットの作成 intdash_objbucket という名前で作成 ~ # nats object add intdash_objbucket Information for Object Store Bucket intdash_objbucket created 2022-05-25T10:12:48Z Configuration: Bucket Name: intdash_objbucket Replicas: 1 TTL: unlimited Sealed: false Size: 0 B Maximum Bucket Size: unlimited Backing Store Kind: JetStream JetStream Stream: OBJ_intdash_objbucket Cluster Information: Name: nats Leader: nats-jetstream-1 ~ # オブジェクトのput aptlogo-dark.png という弊社のロゴ画像ファイルをput ~ # nats object put intdash_objbucket aptlogo-dark.png Object information for intdash_objbucket > aptlogo-dark.png Size: 5.1 KiB Modification Time: 25 May 22 10:16 +0000 Chunks: 1 Digest: sha-256 43bcdf3b98e2e96bbd4f5bde5d32c29520f30fea7253cedba910334442ad ~ # nats object ls intdash_objbucket ╭───────────────────────────────────────────────────╮ │ Bucket Contents │ ├──────────────────┬─────────┬──────────────────────┤ │ Name │ Size │ Time │ ├──────────────────┼─────────┼──────────────────────┤ │ aptlogo-dark.png │ 5.1 KiB │ 2022-05-25T10:16:45Z │ ╰──────────────────┴─────────┴──────────────────────╯ ~ # ~ # nats object info intdash_objbucket Information for Object Store Bucket intdash_objbucket created 2022-05-25T10:12:48Z Configuration: Bucket Name: intdash_objbucket Replicas: 1 TTL: unlimited Sealed: false Size: 5.5 KiB Maximum Bucket Size: unlimited Backing Store Kind: JetStream JetStream Stream: OBJ_intdash_objbucket Cluster Information: Name: nats Leader: nats-jetstream-1 ~ # オブジェクトのget ~ # nats object get intdash_objbucket aptlogo-dark.png Wrote: 5.1 KiB to /root/aptlogo-dark.png in 0.00s ~ # ls aptlogo-dark.png ~ # オブジェクトのdelete ~ # nats object del intdash_objbucket aptlogo-dark.png ? Delete 5.1 KiB byte file intdash_objbucket > aptlogo-dark.png? Yes Removed intdash_objbucket > aptlogo-dark.png Information for Object Store Bucket intdash_objbucket created 2022-05-25T10:12:48Z Configuration: Bucket Name: intdash_objbucket Replicas: 1 TTL: unlimited Sealed: false Size: 267 B Maximum Bucket Size: unlimited Backing Store Kind: JetStream JetStream Stream: OBJ_intdash_objbucket Cluster Information: Name: nats Leader: nats-jetstream-1 ~ # nats object ls intdash_objbucket nats: error: nats: no objects found, try --help ~ # オブジェクトストアも機能上は問題なく動作しました。 ちなみに、pod のログを確認すると分かりますが、KVS やオブジェクトストアを作成した場合は以下のような接頭語をつけたstream が作成され、consumer は都度ランダムな文字列で作成される仕様になっています。 KVS KV_ 今回の例では KV_intdash_kv になる オブジェクトストア OBJ_ 今回の例では OBJ_intdash_objbucket になる おわりに 今回は exactly once を保証するメッセージングシステムを構成可能なミドルウェアである、 NATS JetStream を試してみました。 今の所、機能上は問題なく、 NATS Streaming が抱えていた種々の課題や制約が解消されていそうなため 弊社の次期メッセージングシステムの1つとして有望です。 今後、SRE グループでは以下に取り組みつつ、プロダクト開発本部と連携して他も鑑みミドルウェアの検討を続ける予定です。 詳細な仕様確認 特にstream 制限仕様の確認 詳細な機能試験 特に多様なAck 周り 非機能試験 特にスケーラビリティの確認 デプロイ方式の確立 既存の公式helm chart やOperator を参考にしながら自前でOperator を用意できると良いと考えています。
アバター
プロダクト開発本部EDGEPLANTグループのやべです。 皆さんGPS、使ってますか? 一昔前なら利用シーンも限られていましたが、今や生活の一部といってもいいレベルなので「使う」というイメージすらないかもしれません。 aptpod の計測ユースケースでも、GPSは不要、というものはあまりなく、自社で開発している EDGEPLANT T1 (以下T1、 Amazon で販売中 )にもGPS機能は搭載されています。 そんなGPSですが、スマホなど画面が一体となっていて確認しやすいものはともかく、機器に組込まれている場合はデータを確認するのにひと手間必要です。今回はその中でも、遠隔、つまり別のPCからデータを見る方法をいくつかご紹介します。 aptpod にお願いする 機材の設置 データの取得方法 u-center をネットワーク越しで利用する ser2netでシリアルデータをネットワーク上に流す ser2netのインストール ser2netの設定 u-center を使ってGNSSデータを見る u-centerのインストール u-centerの設定 おわりに aptpod にお願いする まず、aptpod がお客様に提供している方法を紹介したいと思います。先日、千葉県にある茂原ツインサーキットまで走りに行ったので、その際の映像を使いながら説明します。茂原ツインサーキットは手軽にサーキット走行できるとても楽しい場所で、年一くらいでお世話になっています。 www.mobara-tc.com 機材の設置 今回の記事とはあまり関係ないですが、こんな風に車両への取り付けを行いました。 せっかくだからといろいろセンサ類を取り付けています。ちなみに、機材を組んでいる銀色のラックは社内で工作されたものです。展示会向けにも作ることもあります。 データの取得方法 以下の図にあるような形で、 intdash Edge の機能を利用してGPSのデータをサーバーに送ります。冒頭で述べたようにGPSの利用は非常に多いため、デフォルトで機能は組込まれており、インストールするだけで利用可能です。 サーバーのデータは Visual M2M を利用してブラウザ上で確認することができます。 今回茂原サーキットにお邪魔した際は、T1のGPSモジュールに搭載されているIMU *1 の情報も表示させました *2 。以下は、複数のIMUを同時に計測して比較したグラフです。なかなかうまく取れています。 aptpodがintdashという形で遠隔監視を提供するとこのような形になります。しかし、T1にせよほかの機器にせよ、GPSのデータを見るためだけにintdashのシステムを使うのはコストが高いのでおススメできません。 そこでほかの方法として、u-centerを利用する方法を紹介します。 u-center をネットワーク越しで利用する T1に搭載されているGPSのモジュールは、u-blox社のNEO-M8シリーズになります。u-bloxは、Windows用にu-centerというGUIツールを提供しています。 www.u-blox.com 通常、このツールはWindows PCにUSBなどで接続したモジュールとシリアル通信してデータを見ることが多いです。T1の開発中にGPS設定の調査などを行う際には、これを使いたかったので、基板を改造してWindowsからシリアル通信できるようにしました。 ネックになるのは 基板を改造して という部分です。外側に出すかLinux側に流すかをDIPスイッチで切り替えられるようにする必要もあり、あくまで試験用途だったので試作基板1枚分しか作っていません。 ser2netでシリアルデータをネットワーク上に流す そこで、ser2netという便利なものを使います。これを使うと、Telnet/TCP経由でシリアルポートの先にあるデバイスと通信できるようになります。 linux.die.net 今回の例でいえば、T1に搭載されているGPSモジュールに対して、Windows PCからネットワーク経由でシリアル通信する、という形になります。 ser2netのインストール T1の場合であれば、aptコマンドで簡単にインストール可能です。 $ sudo apt update $ sudo apt install ser2net ser2netの設定 使っている環境に合わせて設定を変更します。T1の場合、初期値はボーレート 9600 であるため、そのように設定します。 $ cat /etc/ser2net.conf 2000:raw:600:/dev/ttyTHS1:9600 8DATABITS NONE 1STOPBIT banner 設定出来たら、ser2net のサービスを起動します。 $ sudo systemctl restart ser2net これだけでGPSのデータがネットワーク上に流れていきます。例えば、Tera Termを使うとこのような形でデータを見ることができます。 u-center を使ってGNSSデータを見る では、実際にWindows上でu-centerを利用してみます。 u-centerのインストール M10用にu-center 2というものも存在していますが、T1に搭載されているのはM8シリーズですので無印のu-centerを利用します。 u-centerの設定 u-center起動後、Receiver -> Connection -> Network connectionを選択し、先ほどser2netで設定した内容を追加します。ここでは私が使用しているT1 のIPアドレスが 192.168.11.43 、設定したポートが 2000 なので、そのように設定します。 この設定を行うだけで、u-centerがネットワークを経由してT1のGPSモジュールの情報を表示できるようになります。 シリアルで直接つなげたものと比較してもそん色ないように思います。 また、u-bloxのモジュールは詳細な設定を行うために、UBX Protocolという独自プロトコルで通信をしてメッセージのやり取りを行う必要があり、これが若干面倒です。Linux向けだと ubxtool というものもありますが、少し凝った設定を行おうとすると仕様書とにらめっこしてコマンドを調べる必要があります。その点、u-centerであれば、GUI上で設定項目を眺められるため、目当ての設定やデータが扱いやすくなります。この点も、この方式で実施するメリットの一つと言えるでしょう。 おわりに aptpod では基本的に自社のサービスを使用してデータの可視化を行っていますが、開発での試験やお客様のニーズなど、状況に合わせて汎用的な方法を取ることもあります。いろいろな手法を試すことで知見もたまり、自社製品にフィードバックできることもあります。 T1の活用方法についてはテックブログだけではなく、 デベロッパーガイド という形でもいろいろご紹介しているので、興味を持った方はぜひそちらも見てみてください。まずT1の詳細を知りたい、という方は以下の紹介ページでご確認ください。 www.aptpod.co.jp *1 : Inertial Measurement Unit、慣性計測ユニット *2 : 試験用に機能実装したもので、2022/5 時点ではintdash Edgeには組込まれていません
アバター
製品開発グループintdashチームの呉羽です。 今回は標準化が進められているWebTransportの紹介と、実際にブラウザでの動作検証を行います。 本記事の参考資料として、Webの標準化団体W3C(World Wide Web Consortium)が公開している WebTransport Explainer を用いています。 WebTransportとは何か WebTransportとは、ブラウザとサーバー間での利用を目的とした新しい双方向通信プロトコルです。ではなぜ今さら双方向通信プロトコルなのでしょうか? WebTransport標準化の目的 まず現状の課題をお伝えします。Web上の双方向通信プロトコルとしてWebSocketが存在しますが、WebSocketは単一のTCPコネクション上で動作します。ゆえにTCPによるメッセージの到達保証と順序保証が提供されますが、それらが不要なユースケースに対してはオーバスペックです。 そこでWebTransportでは、到達保証・順序保証のあるWebSocketのような通信方法に加え、それらの保証が不要な通信方法の提供を目的としています。 WebTransport ExplainerにはWebTransportの目的として以下の3つが設定されています。 Provide a way to communicate with servers with low latency, including support for unreliable and unordered communication. Provide an API that can be used for many use cases and network protocols, including both reliable and unreliable, ordered and unordered, client-server and p2p, data and media. Ensure the same security properties as WebSockets (use of TLS, server-controlled origin policy) 先に述べた通信方法の提供に加え、従来のWebSocketと同じセキュリティ水準が必要です。 WebTransportを実現する技術 WebTransportの実装として、HTTP/3を利用した仕様が策定されています。 https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-02.html HTTP/3はQUICのコネクション上で動作するため、QUIC Datagram[RFC9221]による到達保証が不要な通信を提供できます。またQUICのストリームという抽象化された通信経路により、ストリーム間での順序保証が不要な通信を実現できます。 以下に、HTTP/3でのWebTransportセッションの確立・メッセージのやり取りを要約します。 HTTP/3の :protocol ヘッダーに webtransport を指定し、拡張CONNECTメソッドでサーバーにリクエストを送る。 1に用いたQUICのストリームIDを、WebTransportのセッションIDとする。 信頼性が必要な通信をする場合は、QUICのストリームを開き、セッションIDとともに送信する。 信頼性が不要な通信をする場合は、DatagramのメッセージをセッションIDとともに送信する。 このように実装のほどんどはQUICの機能を利用するため、WebTransportの実装にはそれほど手は掛からなさそうです。 WebRTCという代替案 WebSocket以外にもWebRTCという双方向通信プロトコルが存在します。 web.devが公開している記事 では、以下のようにWebRTCと比較しています。 WebRTCはサーバー側でICE, DTLS, SCTPといった珍しいプロトコルの実装が必要だが、WebTransportはQUICやHTTP/3のみでよい(ただしWebTransportは実装方法による)。 WebRTCはWeb Worker内で動かないが、WebTransportは動く。 WebRTCよりもWebTransportはモダンなAPIデザインである。 しかし、既にWebRTCで満足のいくアプリケーションが実装されている場合、WebTransportに変更する大きなメリットは無いそうです。 WebTransportをブラウザ上で動かしてみる それでは実際にWebTransportを実装し、ブラウザ上で動かしてみます。今回は弊社のサーバーサイドチームが主に用いているGo言語で実装します。 Go言語では既に quic-go と、それを用いた webtransport-go というライブラリが存在しています。しかしWebTransportに関する仕様は未だに不安定であるため、すべての機能は実装されていません。例えばHTTP上でDatagramを扱う仕様( draft-ietf-masque-h3-datagram )は過去に互換性のない変更が起きており、それが影響してかquic-goにも実装されていません。 よって、安定していそうな箇所やWebTransportに依らない箇所は quic-goへPullRequestを出し 、他の箇所は自己実装して進めます。 実装したGoのWebTransportライブラリ: github.com/hareku/webtransport-go ブラウザ側のコードは、 GoogleChromeが公開しているサンプルコード を一部修正して利用します。 修正したコード: github.com/hareku/samples/tree/aptpod-webtransport/webtransport それでは、WebTransportサーバーとクライアント側のサーバーを起動してみます。動作にはGo v1.18とNode.js v14.18.0を用います。これらのバージョンが異なる場合、動作しない場合があります。 $ git clone https://github.com/hareku/webtransport-go.git -b aptpod-webtransport $ cd webtransport-go/cmd/echo $ go run . cert.pem key.pem # localhostの自己証明書を用意して指定 $ git clone https://github.com/hareku/samples.git -b aptpod-webtransport $ cd samples/webtransport $ ./launch.sh GoogleChromeが公開しているWebTransportのブラウザ検証用画面 簡素な検証にはなりますが、全ての通信方法で「Hello」というメッセージを送信し、同一メッセージが返ってくることを確認します。 データグラム Sent datagram: Hello Datagram received: Hello 片方向ストリーム Sent a unidirectional stream with data: Hello New incoming unidirectional stream #1 Received data on stream #1: Hello Stream #1 closed 双方向ストリーム Opened bidirectional stream #2 with data: Hello Received data on stream #2: Hello Stream #2 closed ブラウザ上のイベントログを見ると、同一メッセージが返ってくることを確認できました。 まとめ 以上がWebTransportの紹介と動作検証でした。WebTransportはまだRFCとして公開はされていませんが、WebSocketの次世代である双方向通信プロトコルとして弊社は期待しています。
アバター
はじめに ソリューションプロフェッショナルグループのみよしです。 ROSソリューションの担当をしています。 今回はROSでrosbridge_serverの処理速度改善を試した結果についてご紹介します。 はじめに intdash ROS Bridge ROS Bridge Server 高頻度データに対する問題点 ROS(Melodic)でrosbridge_tcpの速度改善 rosbridge_tcp 改善策 計測と結果 環境 計測する時間 テスト用のクライアント (test_client_tcp.py) 手順 結果 - incoming_bufferの設定なし 結果 - incoming_buffer=1024 ROS(Noetic)でrosbridge_tcpを試す 環境 結果 - incoming_bufferの設定なし 結果 - incoming_buffer=1024 rosbridge_websocketを試す 環境 テスト用のクライアント (test_client_ws.py) 手順 結果 - Melodic 結果 - Noetic まとめ intdash ROS Bridge 弊社が提供する DX Functions の 遠隔制御 のユースケースとして、ROSを利用したロボット開発が挙げられます。 これらのユースケースに対して、弊社ではROSメッセージの遠隔リアルタイムデータ伝送を行うintdash Bridge / intdash ROS2Bridgeというプロダクトを提供しています。 intdash Bridge / intdash ROS2Bridgeを使うことで、遠隔地のROS1 / ROS2空間をつなぎ、ROSのメッセージをやり取りすることによる遠隔制御やモニタリングなどのユースケースが実現できます。 intdash Bridge デベロッパーガイド intdash ROS2Bridge デベロッパーガイド 弊社の過去Blogでも、intdash Bridge / intdash ROS2Bridge、rosbridge_serverに関してのご紹介をしてきました。 tech.aptpod.co.jp tech.aptpod.co.jp tech.aptpod.co.jp intdash Bridge / intdash ROS2Bridgeは、intdash Edge Agentを介してintdash Serverに接続することが可能です。 (下記は一例です) ROS Bridge Server これらの弊社製品とは別に rosbridge_suiteのrosbridge_server を使用する場合もあります。 rosbridge_serverは、rosbridge protocolを使用するクライアントであれば、接続することが可能です。 rosbridge protocolを使用するクライアントからrosbridge_serverへ接続するので、LANやVPN等でつながっている、もしくはrosbridge_serverが動く環境でグローバルIPを有している場合に使用可能です。 (下記は一例です) rosbridge_serverの通信方式として、ROS1では、TCP・UDP・WebSocketが提供されています。 rosbridge_tcp rosbridge_udp rosbridge_websocket また、ROS2では、現在、WebSocketのみ提供されています。 *1 rosbridge_websocket 高頻度データに対する問題点 過去のROS環境向けの開発でも、rosbridge_serverを度々使用していますが、高頻度のデータでは処理速度が遅くなり、その影響がrosbridge_serverの後段にあるノードやクライアント側にも生じるという問題がありました。 例えば、下記計測で行なっている環境(条件)では、テスト用クライアントでs.send()の後にtime.sleep()をして、送信する間隔を調整した場合、time.sleep()が0.0001secよりsleep時間が短くなると、rosbridge_serverを起動している側で行うrostopic echoの表示が明らかに遅れ、クライアント側でも送信待ち状態が生じます。 (rosbridge_serverで受信するメッセージ(サイズや要素数)によって、送信する間隔がどれぐらいであれば影響を受けないかは異なります。) ROS(Melodic)でrosbridge_tcpの速度改善 rosbridge_tcp 最初に結論を述べると、Melodic(Python2.7)ではProtocolクラスのincoming()内で、文字列の連結を + から join() とすることで処理速度が改善できました。 Pythonの文字列連結については、 こちらの “String Concatenation” に記載されています。 RosBridgeTcpSocketクラスのhandle()内で、recv()したデータの処理が終わるまで次recv()が行なわれないので、一度にrecv()するデータのサイズが大きくなると、recv()後に行なう処理で時間が掛かる為、遅くなります。 構成として SocketServerのTCPServerを使用 TCPServerでrequest_queue_size=5と設定 RosbridgeTcpSocketでincoming_buffer=65536byte(64k)と設定 となっているので、MAX 64k x 5 = 320k 分のデータが溜まる可能性があります。 incoming_bufferは起動時にパラメーターとして変更可能です。 改善策 rosbridge_library/src/rosbridge_library/protocol.py で文字列の連結をしている箇所を変更します。(下記diff参照) rosbridge_tcpが動く環境のスペックによってはincoming_bufferの値を小さくします。 diff --git a/rosbridge_library/src/rosbridge_library/protocol.py b/rosbridge_library/src/rosbridge_library/protocol.py index c9424ad..3ca2c47 100644 --- a/rosbridge_library/src/rosbridge_library/protocol.py +++ b/rosbridge_library/src/rosbridge_library/protocol.py @@ - 124 , 10 + 124 , 11 @ @ class Protocol: message_string -- the wire-level message sent by the client """ - if self.bson_only_mode: - self.buffer.extend(message_string) - else: - self.buffer = self.buffer + str(message_string) + if message_string != "": + if self.bson_only_mode: + self.buffer.extend(message_string) + else: + self.buffer = ''.join([self.buffer, str(message_string)]) msg = None # take care of having multiple JSON-objects in receiving buffer 計測と結果 実際に、改善策の効果を計測します。 計測の方法として DockerのContainerで動かすrosbridge_serverに対して、Hostからテスト用のクライアントでメッセージ 100,000 件を送信する。 rosbridge_serverで受信したメッセージはpublishされるので、rostopic echoで表示する。 ということを行ないます。 環境 DockerのContainerを使用(下記イメージを使用) *2 $ docker pull ros:melodic-robot 計測する時間 メッセージ送信開始(クライアント起動)から、メッセージ送信終了(クライアント終了)まで。 メッセージ送信開始(クライアント起動)から、rosbridge_serverを起動している側(DockerのContainer)でrostopic echoを行い、送信したメッセージの表示が終了するまで。 テスト用のクライアント (test_client_tcp.py) #!/usr/bin/env python3 import sys import socket import time import json def advertise_msg (topic, type ): return json.dumps( dict (op= 'advertise' , topic=topic, type = type )).encode( 'utf-8' ) def publisher_msg (topic, data): return json.dumps( dict (op= 'publish' , topic=topic, msg=data)).encode( 'utf-8' ) def publish (host= None , port= None , topic= None ): if not host or not port or not topic: print ( 'invalid params host={0} port={1} topic=({2}, {3})' .format( host, port, topic[ 'name' ], topic[ 'type' ])) try : s = socket.socket() s.connect((host, port)) s.send(advertise_msg(topic=topic[ 'name' ], type =topic[ 'type' ])) time.sleep( 0.5 ) cnt_max = 100000 for cnt in range (cnt_max): data = dict (data= 'hello, {0}, {1}' .format(cnt, time.time())) s.send(publisher_msg(topic[ 'name' ], data)) print (data) except KeyboardInterrupt : pass except Exception as e: print (e) finally : s.close() if __name__ == '__main__' : host = '127.0.0.1' port = 9090 topic = { 'name' : '/chatter' , 'type' : 'std_msgs/String' } if len (sys.argv) > 1 : host = sys.argv[ 1 ] publish(host, port, topic) 手順 DockerのContainerでrosbridge_serverを起動する。 incoming_bufferの設定なし $ roslaunch rosbridge_server rosbridge_tcp.launch incoming_buffer=1024 $ roslaunch rosbridge_server rosbridge_tcp.launch incoming_buffer:=1024 DockerのContainerでrostopic echoを行なう。 $ rostopic echo /chatter Hostでテスト用のクライアントを起動する。 $ python3 test_client_tcp.py 172.17.0.2 結果 - incoming_bufferの設定なし 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 変更なし 01h 12m 14.94s 02h 45m 22.59s 変更あり 00h 01m 30.16s 00h 04m 15.50s 結果 - incoming_buffer=1024 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 変更なし 00h 03m 26.07s 00h 03m 37.23s 変更あり 00h 00m 13.99s 00h 00m 16.06s 文字列の連結をしている箇所の変更で大きく変わりました。 計測した環境がDockerのContainerで非力な為、incoming_bufferを小さくすることで更に効果が得らました。 ROS(Noetic)でrosbridge_tcpを試す 先のMelodicで行なった変更を試してみました。 テスト用のクライアント、手順はMelodicと同様です。 環境 DockerのContainerを使用(下記イメージを使用) $ docker pull ros:noetic-robot 結果 - incoming_bufferの設定なし 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 変更なし 00h 01m 59.37s 00h 04m 15.79s 変更あり 00h 01m 28.01s 00h 04m 09.26s 結果 - incoming_buffer=1024 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 変更なし 00h 00m 10.53s 00h 00m 11.87s 変更あり 00h 00m 10.45s 00h 00m 11.90s Melodicでの結果とは異なり、文字列の連結をしている箇所の変更をしても大した違いは見られませんでした。 rosbridge_suiteはPythonで作られているので、PythonのVersionの違いが影響していると思われます。(MelodicはPython2.7、NoeticはPython3) incoming_bufferを小さくすることについては、効果が得られました。 rosbridge_websocketを試す 構成として autobahnのWebSocketServerFactoryを使用 TornadoのWebSocketHandlerを使用 となっています。 環境 DockerのContainerを使用 テスト用のクライアント (test_client_ws.py) #!/usr/bin/env python3 import sys import json import time import asyncio from autobahn.asyncio.websocket import (WebSocketClientProtocol, WebSocketClientFactory) class TestClientProtocol (WebSocketClientProtocol): def onOpen (self): self._sendDict({ 'op' : 'advertise' , 'topic' : '/chatter' , 'type' : 'std_msgs/String' , }) time.sleep( 0.5 ) self._sendDictLoop() def _sendDict (self, msg_dict): msg = json.dumps(msg_dict).encode( 'utf-8' ) self.sendMessage(msg) cnt = 0 cnt_max = 100000 def _sendDictLoop (self): data = 'hello, {0} {1}' .format(self.cnt, time.time()) msg_dict = { 'op' : 'publish' , 'topic' : '/chatter' , 'msg' : { 'data' : data } } msg = json.dumps(msg_dict).encode( 'utf-8' ) self.sendMessage(msg) print (data) self.cnt += 1 if self.cnt >= self.cnt_max: return self.factory.loop.call_later( 0 , self._sendDictLoop) def onMessage (self, payload, binary): self.__class__.received.append(payload) if __name__ == '__main__' : host = '127.0.0.1' port = 9090 if len (sys.argv) > 1 : host = sys.argv[ 1 ] url = 'ws://{0}:{1}' .format(host, port) factory = WebSocketClientFactory(url) factory.protocol = TestClientProtocol loop = asyncio.get_event_loop() coro = loop.create_connection(factory, host, port) loop.run_until_complete(coro) loop.run_forever() loop.close() 手順 DockerのContainerでrosbridge_serverを起動する。 $ roslaunch rosbridge_server rosbridge_websocket.launch DockerのContainerでrostopic echoを行なう。 $ rostopic echo /chatter Hostでテスト用のクライアントを起動する。 $ python3 test_client_tcp.py 172.17.0.2 結果 - Melodic 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 WebSocket 変更なし 00h 00m 05.18s 00h 00m 16.20s WebSocket 変更あり 00h 00m 04.99s 00h 00m 15.50s TCP 変更あり(incoming_buffer=1024) 00h 00m 13.99s 00h 00m 16.06s 結果 - Noetic 文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了 WebSocket 変更なし 00h 00m 04.63s 00h 00m 06.17s WebSocket 変更あり 00h 00m 04.49s 00h 00m 06.05s TCP 変更あり(incoming_buffer=1024) 00h 00m 10.45s 00h 00m 11.90s WebSocketが、文字列の連結をしている箇所の変更あり・なしに関わらず速いのは、常に1メッセージずつ処理を行なっているからだと思います。 まとめ 本記事では、Melodicでのrosbridge_serverの処理速度改善と、NoeticやWebSocketとの比較を行いました。 rosbridge_tcpを使用する場合、Melodicでは文字列の連結をしている箇所を変更することで大きく処理速度を改善できました。 同様の変更をNoeticで行っても、大きな効果は得られませんでしたが、それはPythonのVersionの違いによるものだと思われます。 rosbridge_tcpが動く環境のスペックによって、incoming_bufferを小さくすることはMelodic・Noeticともに効果はありました。(潤沢なスペックの場合、大きな効果は得られない可能性があります。) 文字列の連結をしている箇所の変更は、rosbridge_tcp・rosbridge_websocketともに使用している箇所ですが、変更なしのMelodicでもrosbridge_websocketが速いのは、常に1メッセージずつ処理を行なうからだと思います。 可能であるなら、rosbridge_websocketを使用するのが良いと思います。 弊社ではROSコミュニティへの貢献もしていきたいと思っております。 上記も含めて弊社製品、またはROSに関連した開発に興味を持って頂けた方は是非、こちらの 弊社採用ページ もご覧ください。 製品に関するお問い合わせはこちらへ! www.aptpod.co.jp 最後までご覧いただきありがとうございました。 *1 : v1.1.1(2021-12-09)のリリースでTCP・UDPは削除されました。 *2 : HostのCPUはIntel(R) Core(TM) i7-10870H CPU @ 2.20GHz、メモリは64Gです。
アバター
こんにちは。Visual M2M Data Visualizer の製品開発を担当している白金です。 以前に、「 WebCodecs の VideoDecoder を使用してH.264の動画を再生してみた 」の記事を紹介させていただきました。 tech.aptpod.co.jp その後、弊社が提供する Visual M2M Data Visualizer に含む標準ビジュアルパーツ「Video Player パーツ」に WebCodecs の VideoDecoder を適用した結果、複数の課題が改善できましたので、改善結果と苦労の軌跡について紹介したいと思います。 Visual M2M Data Visualizer を利用した動画ストリーミング再生に興味がある方、または WebCodecs の VideoDecoder を利用中・利用したい方のお役に立てたら嬉しいです。 WebCodecs について気になる方は、上記ブログで紹介しておりますのでご参照ください。(当ブログでは WebCodecs の説明については割愛します。) 改善した結果 Video Player パーツとは? 改善前の Video Player パーツ アーキテクチャー 発生していた課題 改善後の VideoPlayer パーツ アーキテクチャー 課題は解決できた? 苦労の軌跡 1フレームのデコード遅延が発生する パフォーマンス低下時に、動画が緑色に塗りつぶされて表示される LIVE 動画の再生時に、ネットワーク通信状況が不安定になるとパフォーマンスが低下する 過去動画の再生時に、デコードされた動画フレームが使用するメモリで圧迫する おわりに 改善した結果 まずは改善した結果について、5つの項目について紹介したいと思います。 動画再生の安定化、遅延の改善、及び再生中の動画情報の表示が可能になった、など以下の課題が解決できました。 特定のH.264 エンコーダーとの組み合わせで発生していた動画再生の不安定、カクつきが解消した。 LIVE 動画 15fps の再生で約70〜200msの遅延を改善し、iOS向けアプリ の Stream Video の遅延とほぼ同じになった。 Google Chrome の詳細設定で「ハードウェアアクセラレーションを使用可能な場合は使用する」を OFF にする運用が不要となり、運用コスト削減に繋がった。 再生している時刻の動画フレームのタイムスタンプの表示が可能になった。 再生している動画のコーデック、解像度、フレームレートなどの情報の表示が可能になった。 2. LIVE 動画再生の遅延比較 4., 5. タイムスタンプ、Video Info の表示 では、Video Player パーツの改善前後について詳細な情報を紹介していきたいと思います。 ボリュームが多いですが、最後までお付き合いいただけると嬉しいです。 Video Player パーツとは? Video Player パーツは、弊社が提供する Visual M2M Data Visualizer に含む標準ビジュアルパーツの一つです。 計測した H.264 の動画フレームを弊社のintdashサービスを経由して LIVE ストリーミング再生、またはストアした動画を後から動画再生するための機能として提供しています。 Video Player パーツ また、Visual M2M Data Visualizer のダッシュボードで同じ時刻の計測データの値と動画の再生時刻を同期させて再生することも可能です。 Video Player パーツを含むダッシュボード その他の表現については、 Visual M2M Data Visualizer でご確認いただけます。 改善前の Video Player パーツ アーキテクチャー LIVE 動画の再生は、H.264 の動画フレームを Google Chrome (M93以前) では直接表示する方法が API で提供されていなかったため、Media Services を経由して、Fragmented MP4 のコンテナ形式に変換し、 Media Source Extension API を使用して再生していました。 改善前の LIVE 動画の再生 Fragmented MP4 については、下記記事でも紹介されていますので当ブログでは説明を割愛します。 qiita.com 過去動画の再生は、HLS コンテナ形式でストアされた動画情報を、 hls.js の SDK を使用していました。 改善前の過去動画の再生 発生していた課題 上記アーキテクチャーの実装では以下4つの課題が発生していました。 (一部、前回の ブログ から再掲になります。) 課題1 - LIVE 動画再生時の動画フレームのタイムスタンプの判別 モバイル回線など電波状況により著しく通信速度が低下するケースがある環境では、H.264 の動画フレームがリアルタイムで intdash Core Services に届かず、 Fragmented MP4 の動画フレームが欠損した状態になるケースがありました。 正確なタイムスタンプが判別できない 課題2: LIVE 動画再生時のGoogle Chrome で動作する H.264 デコーダーの遅延 弊社で扱うことができる動画フレームは、特定の H.264 エンコーダー には限定していませんが、H.264 エンコーダーと Google Chrome が使用するハードウェアアクセラレーションの組み合わせによって、デコード対象となる動画フレームを十分に確保しないと、デコード後の動画フレームが出力されないケースがありました。 そのため、低遅延で動画をストリーミングするケースでは、動画の再生が安定しない課題が発生していました。 ハードウェアアクセラレーションを使用したときのデコーダー遅延の課題 暫定対策として、Google Chrome 詳細設定の「ハードウェア アクセラレーションが使用可能な場合は使用する」を OFF に変更することで、 ソフトウェアデコーダーを使用するようになりデコーダー遅延は解消できました。 しかし、事前に Google Chrome の設定を変更する必要がある運用コストの課題は未解決の状態になっていました。 H.264 デコーダー遅延の暫定対策 課題3 - LIVE 動画再生時の動画フレームの受信が詰まったときの再生位置のケア Google Chrome を使用して Fragmented MP4 を再生するときは、動画フレームに含まれる timescale や duration の情報を使用して、各フレームを実時間に沿って再生します。 intdash Core Services から H.264 動画フレームを受信するときに、一時的な通信速度の低下が発生すると、動画フレームの受信が詰まるケースがあり、動画再生は一時的に停止する症状が発生します。 その後、通信速度が復帰した後に、詰まっていた動画フレームを短時間に一度に取得することも想定されますが、その際、詰まっていた動画フレームが順番に再生されることで再生可能な動画フレームが余分に生じてしまい、LIVE 動画の再生遅延が発生する要因となっていました。 暫定対策として、現在の動画の再生時刻と、まだ再生していない再生可能な動画の最後の時刻を定期的に監視することで、一定以上再生可能なフレームあるときは、再生位置を再生可能な最後のフレームの位置までシークする補正を適用していました。 動画再生位置を最新のフレームで維持するための補正 課題4 - 過去動画再生時の HLS 動画のセグメント内の動画フレームのタイムスタンプの補正 intdash で扱う100ミリ秒∼1ミリ秒間隔程度の高頻度で発生する時系列データでは、データの転送遅延とタイムスタンプによる忠実な再生に関する課題を解決する必要があります。 動画の再生についても、計測した動画のタイムスタンプに忠実に再生することで、100ミリ秒∼1ミリ秒間隔程度の高頻度で計測した他のセンサーの値と同期して確認、及び解決したいケースがあります。 また、Media Services では過去再生で使用する動画として HLS コンテナ形式を採用しており、HLS コンテナ形式に含む一定間隔で区切られた各セグメントファイル内に含む動画フレームのタイムスタンプは、等間隔のタイムスタンプで再配置される仕様になっています。 そのため、動画のタイムスタンプに忠実に再生、確認したいケースを満たせないケースがありました。 *1 HLS のセグメントに変換された後の 動画フレームのタイムスタンプ 改善後の VideoPlayer パーツ ここまで、改善前の Video Player パーツの課題について紹介させていただきました。 アーキテクチャー まずは、改善後のアーキテクチャーの変更点について紹介したいと思います。 Google Chrome M94 で導入された WebCodecs の VideoDecoder を使用することで、直接 H.264 動画フレームを利用することが可能になり、構成がシンプルになりました。 改善結果は下記のとおりです。 (当ブログの冒頭に記載した改善結果と同じ内容です。) 特定のH.264 エンコーダーとの組み合わせで発生していた動画再生の不安定、カクつきが解消した。 LIVE 動画 15fps の再生で約70〜200msの遅延を改善し、iOS向けアプリ の Stream Video の遅延とほぼ同じになった。 Google Chrome の詳細設定で「ハードウェアアクセラレーションを使用可能な場合は使用する」を OFF にする運用が不要となり、運用コスト削減に繋がった。 再生している時刻の動画フレームのタイムスタンプの表示が可能になった。 再生している動画のコーデック、解像度、フレームレートなどの情報の表示が可能になった。 改善後の LIVE 動画の再生 改善後の過去動画の再生 課題は解決できた? WebCodecs の VideoDecoder を使用することで、改善前に発生していた課題が全て解決できました! VideoDecoder で利用した機能は以下2つです。 動画フレームをデコードするときに指定したタイムスタンプは、デコードされた動画フレームに引き継がれる。 VideoDecoder でデコードする事前準備でハードウェアアクセラレーションを使用しない設定を指定することができる。 以下、Video Decoder を使用したサンプルコードです。 const videoDecoder = new VideoDecoder ( { ... output: ( videoFrame ) => { // 1... decodeFrame で指定した timestamp 123456789 が参照可能。 console .log ( videoFrame.timestamp ) } , } ) videoDecoder.configure ( { ... // 2... ハードウェアアクセラレーションを使用しない設定をする。 hardwareAcceleration: 'prefer-software' , } ) videoDecoder.decode (new EncodedVideoChunk ( { type : 'key' , // 1... 動画フレームに紐づくタイムスタンプを指定する。 timestamp: 123456789 , data: encodedVideoFrameData , } )) では、各課題の解決方法について見ていきましょう。 課題1 - LIVE 動画再生時の動画フレームのタイムスタンプの判別 WebCodecs の VIdeoDecoder でデコードする前の動画フレームのタイムスタンプは、デコードされた動画フレームでも利用ができます。 この機能を利用して、intdash を使用して計測する H.264 動画フレームのタイムスタンプをそのまま使用することで、再生時刻のタイムスタンプを表示することが可能になりました。 課題1. タイムスタンプの表示 課題2 - LIVE 動画再生時のGoogle Chrome で動作する H.264 デコーダーの遅延 H.264 デコーダーの遅延については、改善前の状態でも「Google Chrome の詳細設定でハードウェアアクセラレーションを使用可能な場合は使用する」を OFF に変更することで暫定対策は実現できていました。 また、 VideoDecoder でデコードする事前準備でハードウェアアクセラレーションを使用しない設定を指定することができる。 から、Google Chrome の詳細設定を変更することなく Video Player パーツで「ハードウェアアクセラレーションを使用しない」ように自動で設定することが可能となり、暫定対策は廃止、及び運用コストの削減することができました。 課題2. Google Chrome の詳細設定の変更が不要 課題3 - LIVE 動画再生時の動画フレームの受信が詰まったときの再生位置のケア 改善前は、Fragmented MP4 と、Media Source Extension API を使用した動画再生で、最新の動画フレームの再生位置を表示するケアが必要でした。 WebCodecs の VideoDecoder を利用することによって、デコードされた最新の動画フレームが更新される毎に、デコードされた動画フレームを Video Player パーツで表示するだけのシンプルな構成に改善することができました。 最新の動画フレームを表示するための補正は不要 課題4 - 過去動画再生時の HLS 動画のセグメント内の動画フレームのタイムスタンプの補正 課題1の改善と同様に、H.264 の動画フレームのタイムスタンプをデコードされた動画フレームでも利用可能になりました。 その結果、再生したい時刻とデコードされた動画フレームのタイムスタンプを使用して忠実に再生できるようになりました。 課題4. 過去動画の再生時のタイムスタンプ 苦労の軌跡 さて、上記のとおり、複数の課題が解決できて喜ばしい限りなのですが、実現するためには何度も壁にぶち当たり、一つずつ解消していきました。 以降は、WebCodecs の VideoDecoder を使用する上で解決していった苦労の軌跡について紹介したいと思います。 1フレームのデコード遅延が発生する 15fps など 秒間に複数の動画フレームの再生では気づかなかったのですが、1fps の動画フレームでテストしているときに、動画の再生遅延が1秒遅れの症状になっていることに気づきました。 調査を進めた結果、WebCodecs の VideoDecoder で 動画フレームをデコードするときに、動画フレームが 1フレーム遅れでデコードされていることがわかりました。 1フレーム遅れでデコードされた動画フレームが出力される 当症状については VideoDecoderConfig で、下記 optimizeForLowLatency を有効にすることで解決できました。 videoDecoder.configure ( { ... optimizeForLowLatency: true , } パフォーマンス低下時に、動画が緑色に塗りつぶされて表示される Windows 環境でテストしていると、デコードされた動画フレームの画像が緑色で塗りつぶされるケースが発生しました。 調査を進めた結果、デコードされた動画フレーム VideoFrame を OffscreenCanvas の drawImage で表示している箇所に原因があるとわかりました。 Visual M2M Data Visualizer のダッシュボードで Video Player パーツを複数表示、またはその他のビジュアルパーツを同時に表示したときなど、Google Chrome のパフォーマンスが低下した際に当症状が発生しやすい状況になっていました。 OffscreenCanvas を使用した動画フレームの描画 対策として、描画方法を OffscreenCanvas から MediaStreamTrackGenerator で使用可能な WritableStream.getWriter に置き換えることで当症状を解決しました。 MediaStreamTrackGenerator を使用した動画フレームの描画 LIVE 動画の再生時に、ネットワーク通信状況が不安定になるとパフォーマンスが低下する LIVE 動画再生時にネットワークの通信速度の低下から復帰した際に、詰まっていた H.264 動画フレームを実再生時間より短い間隔で一度に受信するケースがあります。 その場合、デコードするための H.264 動画フレームのキューが想定以上に増加する可能性があります。 全て1フレームずつ逐次動画デコードする場合、必要以上にデコード処理コストの負荷が上昇し、パフォーマンスの低下、動画の再生遅延に影響します。 対策として、デコードする前の動画フレームのキューでキーフレームが含まれている場合は、キューに含む最新のキーフレームより古い動画フレームを破棄することで、デコード処理コストの削減を実現しました。 動画再生に不要なキーフレームより古い動画フレームは破棄 過去動画の再生時に、デコードされた動画フレームが使用するメモリで圧迫する LIVE 動画の再生のときは、最新の1フレームの動画のみ表示すればよかったので、過去の動画フレームは破棄する運用で問題ありませんでした。 過去動画の再生では、時系列データを意識して複数のタイムスタンプに相当する動画フレームを管理、表示する必要があります。 そこで、最も気をつける必要がある対象が、 デコードされた動画フレームのメモリ管理 になります。メモリの使用量が増加すると、Google Chrome のパフォーマンスの低下、またはブラウザのタブがクラッシュするなどの症状が発生します。 デコードされた動画フレームの ByteSize は、表示する動画の解像度に比例します。 デコード後の動画フレームの概算 ByteSize 以上から、次のポイントを気をつけるようにしました。 一度にデコードする動画フレームの流量を制限し、デコードされた動画フレームの数は必要最小限に抑える。 再生している間は、動画フレームを差分で順番にデコードし、デコードされた動画を順次に表示する。 再生時刻を巻き戻すなどシーク位置を変更した場合は、移動後の時刻を基準に直前のキーフレームから動画フレームをデコードをやり直す。 デコード実施前の動画フレームは、現在の再生時刻を含む1分未満の範囲のみメモリにキャッシュし、それ以外は都度 intdash Core Services から取得し直す。 構成は下記のようになりました。 過去動画再生時の構成 結果、動画再生で必要なメモリ使用量を 250MB 以内に抑えることができ、パフォーマンスが低下することなく再生できるようになりました。 おわりに WebCodecs の VideoDecoder を使用して、Video Player パーツ の課題を改善し、より使いやすい機能としてアップデートできたと実感しています。 また、メジャーバージョン級の開発内容の更新で良い開発経験ができ達成感を感じることができました。 Visual M2M Data Visualizer は開発をスタートしてからの期間が長い製品ではありますが、今回のように Web の新しい技術も取り込みつつ改善ができる領域は多く、これからも積極的に改善を推進できればと考えています。 上記も含めて弊社製品、またはフロントエンドに興味を持って頂けた方は是非、こちらの 弊社採用ページ もご覧ください。 製品に関するお問い合わせはこちらへ! www.aptpod.co.jp *1 : HLS 形式コンテナに含む各セグメントの先頭のフレームのタイムスタンプは H.264 の動画フレームと一致するため、正確なタイムスタンプを意識しなければ気になることはありません。
アバター
こんにちは! コーポレート・マーケティング室、デザインチームの「チェン ・ルイ」と申します。 普段は社内製品のアプリケーションのUIデザイン業務を行なっています。 現在、アプトポッド製品UIのデザインシステム運用の一環として、デザインガイドラインを明文化するツール、「Design Document Tool」を検討しています。 今回は検証した3つのツールを紹介します。 Zeroheight InVision DSM Confluence 着眼点はデザインガイドラインの管理の課題から「Design Document Tool」にたどり着くまでに、上記の3つのツールをを使用した感想です。 (具体的な操作方法を割愛します。) 「Design Document Tool」選定の参考になれば幸いです。 【背景・問題】 【デザインガイドラインをSketchでまとめてみた】 Sketchの制約 【Design Document Toolを導入したい】 Design Document Toolの選択基準 【ツール検証1: Zeroheight】 特徴1: デザインデータをアップロードしてそのままドキュメント化 特徴2: 操作が直感的 特徴3:内容自体に集中できる環境 特徴4: 気軽にルールを記入できる 特徴5: プロトタイプツールと連携できる 特徴6: 挿入できるファイル形式が豊富 特徴7: Lottie Animationも入れられる 特徴8: 見出し、本文などの余白が適切、読みやすいデザイン 【ツール検証2: InVision DSM】 特徴1: デザインデータをアップロードしてそのままドキュメント化 特徴2: アセットの階層管理できる 特徴3: 無料版でも無制限でデザインガイドを作成できる 特徴4: InVisionのデザインファイルと一括管理 【InVision DSM とZeroheightの比較】 共通点 相違点 所感 【ツール検証3: Confluence】 特徴1: 文書の構造化しやすい、すらすら書ける 特徴2: Zeplinのリンクを埋め込む可能 所感 最後に: ツールは新しい可能性をもたらす 参考記事 【背景・問題】 以前デザインガイドライン制作について書いてみました。 tech.aptpod.co.jp しかし、デザインガイドラインを実際に運用し始めると、まだたくさんの問題があります。 「メニューで表示できる最大の文字数は?」 「ローディングするとき、ボタンの表現は?」 「同じようなフォームを3回くらい作成したけど、組み立ての基準ってなんだっけ」 「メイン画面の最小幅?最大幅?」 … このように、共通のコンポーネントを、画面上で組み合わせても、デザインの判断基準を確認できない状況です。 「デザインのルールが明文化していないこと」 が原因だと思います。 デザインガイドラインを管理するのに、 デザインコンポーネントをリストアップするだけでは足りませんでした 。   2つの方向が必要だとわかりました。 コンポーネンをまとめるデザインライブラリー ルールを明文化 、言語化するドキュメント 【デザインガイドラインをSketchでまとめてみた】 アプトポッドではSketchでデザインを制作し、Zeplin経由で開発チームに共有するワークフローになります。 まずは、ツールを増やしたくないため、Sketchでまとめてみました。 しかし、Sketchの画面では、確認したい要素を検索しにくいです。 デザインツール内でガイドラインをまとめようとすると、ドキュメントの体裁を考えながら作業することが大変です。 見出しをそろうことに時間がかかった 一番悲しいことは、共有、レビューしにくいので、結局誰も確認できないファイルになったことです。 Sketchの制約 デザインに特化したツールは、ドキュメントのまとめには適しないとわかりました。 (早く気づけばよかったのに…) 更新の非効率 デザイナー以外は更新できない 文書のコンテンツ制作に集中できない 記載項目の制限 動くコンポーネントを挿入できない 関連するコンポーネントにリンクできない サンプルコードを挿入できない 共有の非効率 レビューやコメントしにくい チームを跨いで共有しにくい、最終的に誰も確認しない 【Design Document Toolを導入したい】 Sketchでは今回の課題に満足できないため、別のツールについて調べてみました。 こちらの記事がとても参考になります。 hike.one デザイン業務で使用するツールで主に 「Design Tool」「Document Tool」「Design Document Tool」 3つがあるとわかりました。 1.Design Tool 主にデザイン画面を作成するツール。Sketch、InVision、Figma、UXPinなどが該当します。 2.Document Tool 主に設計文書などの情報を文字ベースでまとめるツール。Notion、Confluence、Githubなどが該当します。 3.Design Document Tool アクセス可能なWebサイトであり、デザイン画面やテキストを含む複数のページで、デザインを文書化できるツールです。Zeroheight、InVision DSM、UXPin、Frontifyなどが該当します。 以上から、デザインガイドラインをまとめるには「Design Document Tool」が適していると考えています。 Design Document Toolの選択基準 情報の安全性(SSO対応、信頼できる会社など)を調査した上で、利便性にフォーカスして、基準を仮に作りました。 作業の利便性から考えると: Sketchのライブラリーをできるだけそのまま流用できる コンテンツの執筆に集中できる どんな職種でもアクセスしやすい環境 バージョンの管理ができる デザイン要素のインタラクション、アニメーションを挿入できる ページのデザインが見やすい、ストレスを感じない 先の記事を参考にし、現時点で重視する要素にうまく対応してくれそうな Zeroheight や InVision DSM に絞りました。 現在アプトポッド社内で使っているDocument Toolの Confluence も試しました。 【ツール検証1: Zeroheight】 デザインシステムのドキュメント化に特化したツールです。 特徴1: デザインデータをアップロードしてそのままドキュメント化 Zeroheightに載せたいSketchファイルをプラグイン経由でアップロードするだけで連携ができます。 アップロードが完了すると下記のデザインデータが同期されます。 Symbol、Artboards、Layer Style、Document Colors、Text Styles ※補足: Sketch側でまずコンポーネントやスタイルを整理する必要があります 特徴2: 操作が直感的 項目をドラッグ&ドロップして並び替えできます。 特徴3:内容自体に集中できる環境 ルールの記入自体に集中できない理由は、以下の手間があることです。 ドキュメントの見た目のデザイン   各ページ共通の項目設計 Zeroheightはこの2つの悩みを解決してくれます。 読みやすいページのデザインを持ち、ドキュメントの見た目をデザインする手間が不要です。 さらに、Color、Text、Component各自の項目名をゼロベースでから設計しなくてもよいです。 Layout、Color、Component用のテンプレート 上記項目をひたすら埋めれば、見やすいドキュメントが大まかに完成できます。 要素によって追記するルールもありますが、ヒントを得ながら記入できます。 特徴4: 気軽にルールを記入できる Rulesのテンプレートを追加し、Do、Don’t、注意などのルールを追記、画像でもアップロードできます。 整理していく中で、今までのデザインガイドラインの振り返りもできます。 気軽にルールを追加できる、画像ベースでもOK 特徴5: プロトタイプツールと連携できる InVisionやFramerと連携できます。 今回はFramerと連携してみました。 Framer側で、コンポーネントを制作し、Embed リンクを発行します。その後Zeroheight側でリンクを貼ると、随時同期できるインタラクティブコンポーネントが反映できます。 Framer側で制作、リンクを発行 Zeroheight画面にリンクを貼る Framerのアートボードを修正し、Zeroheight側でRefreshボタンをタップして最新のデザインを反映します。 特徴6: 挿入できるファイル形式が豊富 Embed機能を使い、デザインや開発ツール以外のGoogle Driveなどの業務ツールのコンテンツも簡単に入れられます。 特徴7: Lottie Animationも入れられる 凝ったアニメーションを入れたい場合、Lottie Animationのiframe URLを貼るだけでも挿入できます。 特徴8: 見出し、本文などの余白が適切、読みやすいデザイン 文書の構造化がしやすく、見出しや本文の余白が適切で、長文を読んでもストレスを感じません。 【ツール検証2: InVision DSM】 InVisonが開発している共同チーム向けの設計システムプラットフォームです。 Sketch内のデザインライブラリーをアップロードし、色、Component登録などの流れは 「Zeroheight」とあまり変わりがないので、 簡単に紹介します。 InVision Craftアプリをダウンロード、Mac Tool Barで起動し、Sketch Pluginをダウンロードする必要があります。 Sketchのプラグイン 特徴1: デザインデータをアップロードしてそのままドキュメント化 既存のコンポーネントライブラリーをアップロードするとこのような画面になります。すでに「Foundations」「Components」などの画面が存在しています。 項目はZeroheightと比べると若干少ないです。 何パターンかテンプレートが用意されており、とても参考になります。 Component用のテンプレート Color用のテンプレート しかし、Zeroheightと比べると、デフォルトの項目が少ない分、ルールの策定には少し労力がかかりそうなイメージです。 ルールの策定に時間をかけたくない、もっとヒントが欲しい場合はZeroheightの方が良いかと感じます。 特徴2: アセットの階層管理できる 共通コンポーネントを管理しているSketchライブラリーをInVisionにアップロードすると、このような管理フォルダーが作成されます。 コンポーネント以外、アイコン、画像などの一括管理もできます。 特徴3: 無料版でも無制限でデザインガイドを作成できる Zeroheightの無料版は、デザインガイドが1つしか作成できませんが、InVisionの無料版でも無制限で作成できます。 特徴4: InVisionのデザインファイルと一括管理 既にInVisionを使っているのであれば、デザイン、レビュー、共有、デザインシステム管理を一つのツールで完結できます。 【InVision DSM とZeroheightの比較】 共通点 カスタムドキュメントを使用して、チームを同じページに配置できる デザインガイドライン、コンポーネント一覧、アクセシビリティ標準など、一括で管理できる場所 Sketchのライブラリーをそのままアップして使える Color Text Style Component 開発者のツールとも連携できる Storybookの統合により、本番環境に対応したコードを直接埋め込んでリンクできる 相違点 大項目 小項目 InVision DSM Zeroheight ページ Componentページ Component詳細   Colorページ 機能 ページの複製可能か 不可 可能   デザインのDo、Don’tの比較が可能か 不可 可能 外部ツールと連携 開発ツールとの連携 【無料版】 ・Storybook ・code component library 【企業版】 ・Styleguidist ・CodePen ・CodeSandbox ・Pattern Lab 【無料版】 ・Storybook integration ・code component library (HTML/CSS/JS) ・Code snippets 直接記入 ・CodePen デザインツールとの連携 少ない ・InVision ・Sketch 多い ・Sketch ・Figma ・AdobeXD ・InVision ・Framer 動くUI Componentに動きを入れる 開発ツールと連携 ・Framer ・Lottie animation 管理 バージョン管理 Enterpriseプランのみ可能 Enterpriseプランのみ可能 ユーザー権限設置 Enterpriseプランのみ可能 Enterpriseプランのみ可能 【Free】 ・無料 デザインガイドの数無制限 ・機能の制限 【Enterprise】 ・個別相談 ・大体$35ユーザー1人(編集者、閲覧者とわず) 【Free】 ・無料 ・編集者1人、デザインガイド1つ ・機能無制限 【Starter】 ・1人の編集者$49/月、編集者5人まで ・小さいなプロジェクトチームに適する 【Professional】 ・個別相談 ・編集者5~10人(割引あり) ・中小企業の開発チーム 【Enterprise】 ・個別相談、10人以上編集者、デザインガイド10つまで ・プラットフォームを提供している企業に適する 【Agency】 ・複数の顧客のためのデザインガイドを管理 ・デザインエージェンシーに適する 編集者5人以上使用すると、月$245以上かかります。 まとめ 良いところ ・ソースが表示される ・Componentの名前はSketchと一致 、スラッシュの分割に影響されない。 例えば「 Button/Primary/32h」は名前そのままで、検索しやすい ・InVison内の他の機能も使える ・Componentごとにdescriptionを追加できる ・要素がはっきり表示できる ・ページの複製ができる ・無料版で操作できることが多い 微妙なところ ・表示がぼやける ・Component詳細画面でサイズ、余白を確認できない ・ページの複製ができない ・機能、テンプレートが多いため、どの表現を選択するか迷う可能性がある ・無料版で1つのデザインガイドしかできない 所感 操作の流れについて、InVison DSMとZeroheightは似ています。 残りの比較ポイントは、細かい操作感、デザインの好み、現状使用しているツールかと思います。 もしチーム内でのデザインツールはInVisionを採用しているのであれば、InVision DSMは一番良いでしょう。 開発やデザイン以外のメンバーも巻き込みたい、他のデザインツールを使用している場合、または少ない労力でリッチなデザインガイドを作りたい場合、個人的にはZeroheightが一番おすすめです。 また、社内で本格で運用していくと安全性を確保するために有料版がおすすめです。 【ツール検証3: Confluence】 アプトポッド社内では、情報共有ツールとしてConfluenceを利用しています。 新しいツールを導入してもいいですが、なるべくツールを増やしたくない、ミニマムから動き出す考えで、Confluenceで簡単にデザインガイドラインをまとめてみます。 特徴1: 文書の構造化しやすい、すらすら書ける Cofluenceの文章テンプレートが豊富なので活用できるものが多いです。 特徴2: Zeplinのリンクを埋め込む可能 ConfluenceはZeplinのリンクを埋め込んで試してみました。 良いところは、Zeplinに更新を行うと、Confluence側も同期できることです。 微妙なところは、余白のサイズが確認できないことや、操作が重いことです。 また、コンポーネントライブラリーをそのまま反映できず、共有専用のまとめファイルが必要です。 所感 Confluenceの良いところは 全職種アクセスできる、編集できる 文書をすらすらかける 微妙なところは デザインツールとの適切な統合がない Sketchのライブラリーをそのままアップロードして反映できない Zeplinを埋め込む機能がまだ使いにくい 最初は、内容を企画する段階では、早いスピードでレビューを回す必要があります。 Confluenceの機能や弱点を踏めると、まずはConluenceで画像を貼って、文書のコンテンツ制作に集中します。 ある程度でコンテンツができたら、Zeroheightに登録してもいいと思います。 デザインガイドラインの明文化の第一歩を踏み出すには、まずは導入ありきでなく、社内Document Toolから始めてみるといったことがよいと感じました。 最後に: ツールは新しい可能性をもたらす 最後までご覧くださってありがとうございます! 正直、普段業務内で使っている「Sketch → Zeplin」の流れに関して、すでに当たり前になってしまいました。 しかし、道具の選択ひとつで、全体のワークフローや組織の変化に対して新しい可能性をもたらすことをわかりました。 検証結果以外、私には以下のような発見がありました。 組織がツールの導入するときに考慮するポイント ツールの検証は組織のワークフローや志向に影響をもたらす 今デザインしているツールも、お客さんのワークフローに何かの変化をもたらす 価格、安全性、開発チームの作業コストなどの考慮もあり、最終的にどんなツールの選択になるかはまだ先ですが、一歩進めたかと思います。 これからは、今回の調査や思考のベースで社内のワークフロー改善につなげていきます! 参考記事 uga-box.hatenablog.com www.designsystems.io dev.classmethod.jp goworkship.com
アバター
みなさまこんにちは。プロダクト開発本部の岸田です。 以前に「ハードウェア側に機械学習環境を立てて推論を行い、クラウドに結果を収集して分析状況を確認する」ユースケースを こちら の記事で考察しました。手軽にローカルデバイスとクラウドを連携するサービスとして有名なものが「AWS IoT Greengrass」ですよね。公式のドキュメントでもエッジ推論システムをAWS IoT Greengrassで構築するユースケースが紹介されています。 docs.aws.amazon.com そこで上記を参考に、弊社の製品であるJetson TX2搭載のEDGEPLANT T1を利用して、AWSサービスを活用したエッジ推論システムをつくってみたいと思います! この記事の対象読者 EDGEPLANT T1 で手軽にエッジ推論を構築するメリット 今回のゴール 構成イメージ 実際に構築してみる モデルをAmazon SageMakerを用いてコンパイル EDGEPLANT T1で機械学習環境を構築する EDGEPLANT T1にAWS IoT Greengrassをインストールする 推論を行うコードをLambdaとしてデプロイする LambdaをGreengrass Groupに追加する SageMakerでコンパイルしたモデルをGreengrass Groupに追加する 推論実行用のローカルデバイスをGreengrass Groupに追加する サブスクリプションを定義する デプロイパッケージをテストする IoT Analytics にデータセットをためる QuickSightでダッシュボードを作り結果を確認する まとめ この記事の対象読者 NVIDIA提供のJetsonシリーズの機器上で、上記の公式ドキュメントのようなことがやりたい方 Jetson TX2を扱う場合は、JetPackの依存関係などを考慮しながら適切に進める必要があり、いくつか考慮すべきポイントがあります。本記事ではその点をいくつかまとめてます。 EDGEPLANT T1を単体で購入いただいている方 EDGEPLANT T1をつかって簡単な機械学習推論システムを構築したい... そのような方向けにサンプルとしてご参考頂ける内容をまとめています。(コンセプトの詳細は次項をご参照ください。) EDGEPLANT T1 で手軽にエッジ推論を構築するメリット (こちらはEDGEPLANT T1の宣伝も兼ねてるので、不要な方は読み飛ばしてください。) 弊社ではEDGEPLANTというハードウェアブランドを提供しており、その中でも機械学習の実行環境として利用できるハードウェアが EDGEPLANT T1 になります。 弊社が提供しているintdashと組み合わせることで、 EDGEPLANT T1で機械学習モデルを動かし、推論結果をintdashサーバーに時系列化された状態で送付することで、専用のWebアプリケーションでリアルタイムにデータを可視化したり、後から数種類のデータが時刻同期された状態でデータを確認することができます。 intdashの概要 このしくみは、既にリリース発表されている大阪ガス様とのプロジェクトでも利用されています。 www.aptpod.co.jp 一方で、EDGEPLANT T1は単体でも購入することができます。しかしながらご購入いただいた方々からは「どうやってつかえばいいの?」、「システムとして組み込むにはどうすればよいの?」など、ざっくりシステムとして作りたいイメージはあるものの、具体的な実現手段がわからない…. こういう声を頂いておりました。 そこで今回は予算の都合などで「ユースケースを試して試験運用してみたいが、intdashとの連携まではなかなか手が出せない」方々向けに、皆様に馴染みの深いAWS様のサービスを存分に利用して、エッジ推論システムを手軽に構築するサンプルをご紹介いたします。 まずはEDGEPLANT T1で単純にモデルを動かしたい!という方は、 こちら の記事をご参考ください。 今回のゴール 今回は、「ハードウェア側に機械学習環境を立てて推論を行い、クラウドに結果を収集して分析状況を確認する」システムを一人で構築したいと思います。 流れとしては 模擬クライアントから、画像を指定したメッセージを送る メッセージを待機しているEDGEPLANT T1がメッセージを受け取り、指定された画像に対して推論する 推論結果をサーバーに送る 1〜3を繰り返し、溜まったデータを分析ツールで可視化する というイメージです。 EDGEPLANT T1にはMXnetベースでトレーニングされたResNetというイメージ分類モデルをデプロイし、事前にEDGEPLANT T1に画像を数枚配置し、メッセージで指定された画像に対して何が写っているか推論してもらいます。ユーザーは遠隔で「推論結果の確信度」や「パフォーマンスの推移」を観察すると想定します。 やりとりのイメージ 最終的にはエッジ側の推論の様子を、以下のようなダッシュボードで確認できることを想定しています。 可視化イメージ 私が実施した際は各所でつまづき気が遠くなったときもありましたが、約3日程度で一通り構築できました。費用もデータ量に依存しますが、1000円かからないくらいの料金で試すことができました。 構成イメージ AWS IoT Greengrassを利用したケースによくあるアーキテクチャに近いですが、以下のような構成を検討しました。 構成イメージ モデルをEDGEPLANT T1で動作するように、Amazon SageMaker Neoを利用してコンパイルします EDGEPLANT T1で、モデルが動くように機械学習環境を構築します AWS IoT Greengrassを用いて以下のアセットをパッケージ化しデプロイします Lambda関数として作成された推論コード Amazon SageMaker Neoでコンパイルしたモデル デバイスとサーバー間のメッセージは、AWS IoT Coreを経由しMQTTメッセージでやりとりします EDGEPLANT T1からパブリッシュされたデータはAWS IoT Analyticsにて蓄積し、Amazon QuickSightでデータを可視化します 実際に構築してみる それでは、実際に構築してみましょう。 モデルをAmazon SageMakerを用いてコンパイル 今回はMXnetのGluonCVから ResNetのモデルをダウンロードします。 from gluoncv import model_zoo import mxnet as mx model_name= 'ResNet50_v2' net = model_zoo.get_model(model_name, pretrained= True ) net.hybridize() dummy = mx.nd.ones([ 1 , 3 , 224 , 224 ]) _ = net(dummy) net.export(model_name, epoch= 0 ) ダウンロードしたモデルは、S3に格納しておきます。 import boto3 import tarfile session = boto3.session.Session() # compress packname = 'model.tar.gz' tar = tarfile.open(packname, 'w:gz' ) tar.add( '{}-symbol.json' .format(model_name)) tar.add( '{}-0000.params' .format(model_name)) tar.close() # send to s3 s3 = session.client( 's3' ) bucket = 'sample-bucket' s3key = 'resnet-model/resnet-model' s3.upload_file(packname, bucket, s3key + '/' + packname) 次に、SageMaker Neoにてモデルを格納したS3のパスを指定してジョブを実行します。 # Replace with the role ARN you created for SageMaker sagemaker_role_arn = "arn:aws:iam::XXX:role/service-role/XXX" framework = 'mxnet' target_device = 'jetson_tx2' data_shape = '{"data":[1,3,224,224]}' # Specify the path where your model is stored s3_model_uri = 's3://{}/{}/{}' .format(bucket, s3key, packname) # Store compiled model in S3 within the 'compiled-models' directory compilation_output_dir = 'resnet-model/compiled-models/test' s3_output_location = 's3://{}/{}/' .format(bucket, compilation_output_dir) # Give your compilation job a name compilation_job_name = 'resnet-model5' sagemaker_client.create_compilation_job(CompilationJobName=compilation_job_name, RoleArn=sagemaker_role_arn, InputConfig={ 'S3Uri' : s3_model_uri, 'DataInputConfig' : data_shape, 'Framework' : framework.upper()}, OutputConfig={ 'S3OutputLocation' : s3_output_location, 'TargetDevice' : target_device}, StoppingCondition={ 'MaxRuntimeInSeconds' : 900 }) 上記を実行するとコンパイルジョブが発生し、問題なければ完了します。コンソール上でもジョブのステータスを確認することができます。 コンパイルジョブの確認画面 これで、コンパイルは完了しました。 EDGEPLANT T1で機械学習環境を構築する T1でモデルが動くように、機械学習環境を構築していきましょう。このステップでは、 AWSのドキュメント にもある通り Neo Deep Learning Runtime (DLR) をインストールします。 通常であれば pip install で DLRをインストールすれば完了しますが、EDGEPLANT T1ではJetson TX2を採用しており、JetPackのバージョンやOSのアーキテクチャと一致するDLRを用意する必要があります。そのため、T1上でDLRをビルドして用意します。 DLRの公式ページ を参考に、ビルドします。 cmakeのビルド&インストール sudo apt-get install libssl-dev wget https://github.com/Kitware/CMake/releases/download/v3. 17 . 2 /cmake-3. 17 . 2 .tar.gz tar xvf cmake-3. 17 . 2 .tar.gz cd cmake-3. 17 . 2 ./bootstrap make -j 4 sudo make install DLRのビルド&インストール git clone --recursive https://github.com/neo-ai/neo-ai-dlr cd neo-ai-dlr mkdir build cd build cmake .. -DUSE_CUDA = ON -DUSE_CUDNN = ON -DUSE_TENSORRT = ON make -j 4 cd ../python python3 setup.py install --user インストールし終えたら、実際にpython上でimport できるか試してみます。 root@aptuser-desktop:/greengrass/ggc# python Python 3.8.6 (default, Jan 24 2022, 21:19:25) [GCC 7.5.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import dlr CALL HOME FEATURE ENABLED You acknowledge and agree that DLR collects the following metrics to help improve its performance. By default, Amazon will collect and store the following information from your device: (省略) >>> DLRの初期ロード時の注意書きが出力され、無事importができました。 ⚠️ 注意 私が確認したときはAWS Lambdaの制約によりアーキテクチャがarm64の場合、 Python3.8 以上でないと対応しておりませんでした。バージョン3.8未満を利用されている方は Pythonのバージョンをアップデートすることをおすすめします。 EDGEPLANT T1にAWS IoT Greengrassをインストールする AWS IoT Greengrassに関連した手順は、基本的に以下手順に沿って実施しています。 docs.aws.amazon.com まず最初にEDGEPLANT T1に AWS IoT Greengrassをインストールします。初期手順については こちら の手順のModule1〜Module2を参考にしました。 Module1は、Greengrassがアクセスする用のユーザーを設定するパートです。私は以下手順を実施しました。 sudo adduser --system ggc_user sudo addgroup --system ggc_group 上記のドキュメントでは openjdk-8-jdk をインストールする手順がありますが、今回はGreengrass Managerは利用しないのでスキップしました。 Module2はAWS IoT Greengrassの画面上でデバイス情報を新規作成し、作成されたクライアント認証情報をダウンロードしてセットアップします。 最終的にEDGEPLANT T1上で以下の動作が確認できたら完了です。 root@aptuser-desktop:/greengrass/ggc# core/greengrassd start Setting up greengrass daemon Validating hardlink/softlink protection Waiting for up to 1m10s for Daemon to start Greengrass successfully started with PID: 28776 推論を行うコードをLambdaとしてデプロイする 実際にモデルの推論を行うソースコードをAWS Lambdaにデプロイします。 まず、AWS IoT Greengrass経由でSubscribeしたメッセージをトリガーに、モデルを動作させて推論した結果をPublishするコードをAWS Lambda上にデプロイします。 サンプルとして、以下のコードを用意しました。こちらも 公式サンプル をほぼ参照しております。 (一部前処理など公開の都合上省略しています) import logging import os from dlr import DLRModel from PIL import Image import greengrasssdk import numpy as np import json import time # Initialize logger customer_logger = logging.getLogger(__name__) # Create MQTT client mqtt_client = greengrasssdk.client( 'iot-data' ) # Initialize model_resource_path = os.environ.get( 'MODEL_PATH' , '/ml-model' ) print (model_resource_path) dlr_model = DLRModel(model_resource_path, 'gpu' , 0 ) # Load Synset synset_path = os.path.join( '' , '/home/aptuser/develop/t1-ml-workflow/synset.txt' ) with open (synset_path, 'r' ) as f: synset = eval (f.read()) def softmax (x): e_x = np.exp(x) sum_x = np.sum(e_x) return e_x/sum_x def predict (image_path): """ Predict image with DLR. The result will be published to MQTT topic '/resnet/predictions'. :param image: numpy array of the Image inference with. """ print ( 'start prediction process.......' ) im = Image.open(f '/home/aptuser/develop/t1-ml-workflow/{image_path}' ) im = np.asarray(im.resize(( 224 , 224 ))) im = im.astype(np.float32) if len (im.shape) == 2 : # for greyscale image im = np.expand_dims(im, axis= 2 ) input_data = { 'data' : im} print ( 'start prediction.' ) load_start_time = time.time() out = dlr_model.run(input_data) finished_time = time.time() print ( 'finished.' ) top1 = np.argmax(out[ 0 ]) prob = np.max(softmax(out[ 0 ])) prediction_time = finished_time - load_start_time exe_time = str (datetime.datetime.now()) result = { 'class' : synset[top1], 'probability' : float (prob), 'prediction_time' : finished_time - load_start_time, 'time' : exe_time } # Send result send_mqtt_message(result) def send_mqtt_message (message): """ Publish message to the MQTT topic: '/resnet-50/predictions'. :param message: message to publish """ mqtt_client.publish(topic= '/resnet/predictions' , payload=json.dumps(message)) # The lambda to be invoked in Greengrass def lambda_handler (event, context): print (f 'subscribed message: {event}' ) try : predict(event[ 'message' ]) except Exception as e: customer_logger.exception(e) send_mqtt_message( 'Exception occurred during prediction. Please check logs for troubleshooting: /greengrass/ggc/var/log.' ) Lambda関数を作成し、上記のコードをデプロイします。 Lambda関数の作成 このとき、JetsonTX2の場合は arm64をアーキテクチャとして指定する必要がありますが、残念なことにPython 3.7以下のバージョンは利用できませんでした。そのためPython3.8以上を利用します。 バージョンを発行し、エイリアスに紐付けて作成します。 Lambda関数のエイリアス詳細画面 これで、Lambda関数の準備も完了です。 LambdaをGreengrass Groupに追加する 先程作成したLambda関数をGreengrassのグループに追加します。 詳細の手順は Step 4: Add the Lambda function to the Greengrass group を参考にしてください。 SageMakerでコンパイルしたモデルをGreengrass Groupに追加する 先程作成したSageMaker Neoでコンパイルしたモデルをグループに追加します。 詳細の手順は Step 5: Add a SageMaker Neo-optimized model resource to the Greengrass group を参考にしてください。 推論実行用のローカルデバイスをGreengrass Groupに追加する 今回のGreengrassの設定で、Lambda関数をコンテナ形式で動作させる設定にしたため、GPUにアクセスできるように設定する必要があります。章の冒頭で触れている こちら のドキュメントの末尾にある”Configuring an NVIDIA Jetson TX2”に、設定すべきデバイスとボリュームのパスが書かれています。 基本的な手順は、 こちら の手順と同じです。 Greengrassの"グループ"から"リソース" を選択し、"ローカルリソースの追加"を押下します。リソースの作成画面が開くので、各デバイスのパスを入力します。 ”Configuring an NVIDIA Jetson TX2”に記載されているDevice Pathを指定してください。 name path nvhost-ctrl-gpu /dev/nvhost-ctrl-gpu nvhost-dbg-gpu /dev/nvhost-dbg-gpu nvhost-ctrl /dev/nvhost-ctrl nvmap /dev/nvmap nvhost-prof-gpu /dev/nvhost-prof-gpu ちなみに、上記を追加せずGreengrassを起動すると、Lambda関数側で以下のエラーが表示されます。 [2022-01-25T00:13:01.06+09:00][ERROR]---------------------------------------------------------------- [2022-01-25T00:13:01.06+09:00][ERROR]-An error occurred during the execution of TVM. [2022-01-25T00:13:01.06+09:00][ERROR]-For more information, please see: https://tvm.apache.org/docs/errors.html [2022-01-25T00:13:01.06+09:00][ERROR]---------------------------------------------------------------- [2022-01-25T00:13:01.06+09:00][ERROR]- Check failed: (e == cudaSuccess || e == cudaErrorCudartUnloading) is false: CUDA: no CUDA-capable device is detected 同様に、ボリュームも”Configuring an NVIDIA Jetson TX2”に記載されているパスを指定します。 他にもLambda関数側でアクセスしているボリュームがあれば、同じように指定します。 name path shm /dev/shm tmp /tmp ※ 今回はサンプルなのでセキュリティを考慮していませんが、 実際に利用する場合はGreengrassのアクセス許可がセキュリティ的に問題ないか、 しっかり検討した上で指定のリソースを参照してください。 サブスクリプションを定義する 先程のパート「推論を行うコードをLambda関数としてデプロイする」にて、プッシュされたメッセージを参照に推論を実施し、AWS IoTにPublishするコードを書いていたと思います。ここでは、そのAWS IoTとLambda関数のサブスクリプションを定義します。 今回も、 こちら の手順を実行しました。 サブスクリプションの定義 トピック 説明 /resnet/predictions IoT Cloud上のMQTTクライントからプッシュされるメッセージをLambda関数でサブスクライブするトピック /resnet/test Lambda関数からプッシュされるメッセージをサブスクライブするトピック これで、デプロイパッケージができました!! それでは、早速デプロイしてみましょう。デプロイの手順は こちら を実施します。 上記手順後、デバイス側での動作状況をCloudWatchで確認します。 CloudWatchのログ [2022-01-25T15:31:21.811+09:00][INFO]-lambda_runtime.py:149,Running [arn:aws:lambda:ap-northeast-1:XXXXXXXXX:function:ml-edgeplant-test:6] [2022-01-25T15:31:21.811+09:00][INFO]-ipc_client.py:210,Getting work for function [arn:aws:lambda:ap-northeast-1:XXXXXXXXX:function:ml-edgeplant-test:6] from http://localhost:8000/2016-11-01/functions/arn:aws:lambda:ap-northeast-1:XXXXXXXXX:function:ml-edgeplant-test:6/work 上記のログが出力され待機状態になっていればOKです! デプロイパッケージをテストする パッケージのデプロイが完了したので、実際に各サブスクリプションが正常に定義されているか試してみます。 こちら の手順と同様に、AWS IoTのテストクライアントでメッセージを送ってみましょう。 今回は、テストクライアント側でサブスクライブするトピックを /resnet/predictions に指定します。 トピックのサブスクリプションの作成 指定後、トピックに紐付いたサブスクライブの項目が出てくるので、そちらでトピックを /resnet/test と指定し発行します。今回は goldfish の画像を指定します。 発行するトピックとメッセージの指定 すると、モデルの出力結果が返ってきました! goldfish と期待通りの推論結果が返ってきていることが確認できます。 送付結果 EDGEPLANT T1側の設定はこれで完了です。 IoT Analytics にデータセットをためる ようやくEDGEPLANT T1からAWS IoTにデータを送れるところまでできたので、次はそのデータをIoT Analyticsに投下しデータセットとして溜めていきます。 こちら の手順に従い設定を行います。 チャネルの作成 チャネルの作成では、先程指定した /resnet/predictions を指定します。 データセットの作成 データセットの作成では、定期的にデータセットに溜めたいデータを出力できるクエリを定義します。今回はすべての項目をそのまま参照したいので、すべてのデータをクエリするようにしました。 SELECT * FROM edgeplant_project_datastore スケジュールは、1分間に一回実行するようにします。 パイプラインの作成 パイプラインでは、Lambda関数が送付しているJSONの各項目とデータ型を指定します。 上記の手順が終了したらテストしてみましょう。 先程のパート「デプロイパッケージをテストする」で実施した手順を複数回実施して、AWS IoT側が受け取ったメッセージがデータストアに蓄積されているか確認します。 メッセージごとに、指定の画像を切り替えながら複数回トピックを発行します。 連続してトピックを発行 AWS IoT Analyticsに戻り、データセット > コンテンツ をクリックすると、コンテンツが生成されていることが確認できます。 データセットのコンテンツ このコンテンツをクリックし、実際にデータが格納されている様子を確認します。 実際のデータ 無事、データが蓄積している様子を確認することができました。 QuickSightでダッシュボードを作り結果を確認する いよいよ最後のパートになります。QuickSightを使って、ダッシュボードを作ります。 まずデータセットを作成します。"データセット"の項目をクリックし、"新しいデータセット"をクリックします。すると、参照先のサービスの一覧がでてきます。今回は AWS IoT Analyticsを参照します。 参照先の選択画面 すると、既に作成したデータセットが表示されるので、指定のデータセットを選択して “データソースを作成“をクリックします “分析“で “新しい分析“をクリックし、対象のデータセットを参照します データセット選択画面 自分が可視化したいデータに合わせて、パーツを選びダッシュボードを構築します。 作成イメージ 最終的には、以下のようなダッシュボードが作成できます。自分が評価したい数値をベースに、オリジナルのダッシュボードを構築してみましょう。 可視化イメージ ダッシュボードが完成したら、全作業が完了です。 お疲れ様でした! まとめ 今回は、EDGEPLANT T1とAWSのサービスを活用することで、手軽にエッジ推論システムが構築できました。このサンプルを利用することで、以下のようなケースに応用することができると思います。 自分の希望するモデルに差し替えて、すぐにシステムにデプロイできる 台数を増やしたいときは、AWS IoT Greengrassでデプロイするだけでシステムに組み込める EDGEPLANT T1をシステムとして活用したいとなった場合、EDGEPLANT T1でモデルを動かすだけではなく、その推論結果をクラウドに蓄積し、分析や再学習に応用したいケースに発展することが多いです。しかしながら、そのシステムを構築するのにも費用や時間がかかってしまう… そのようなときに、今回のサンプルを利用することができます。 ちなみに以下のような要望がある場合は今回のシステムでは実現が難しいので、ぜひ弊社のintdashの活用をご検討ください。 リアルタイムで推論状況を確認したい AWS IoT AnalyticsおよびAWS Quick Sightは、基本的にバッチ処理でデータを処理するため、リアルタイムで推論状況を確認することができません。intdashでは、高粒度データのリアルタイム送受信を可能にし、再生に特化した高機能ダッシュボードをご活用いただけます。 他のセンサーデータを同期させながらデータを収集・管理したい 機械学習モデル以外に、走行中の動画データや、CANなどの他データと同期しながらデータを収集・管理したい場合、このしくみでは達成できません。intdashでは、複数に横断した高粒度データを、ミリ秒単位で同期させデータを収集し、あとから管理することができます。 intdashを検討する前の手軽な機械学習システムを構築する、という位置づけで、ぜひご参考いただけると嬉しいです。 それでは、最後までお付き合いいただきありがとうございました!
アバター
はじめまして。 SRE チームの細谷です。 弊社製品 intdash のサーバーサイドでは、 intdash Server と呼ばれるミドルウェアが動作しており、intdash の多様なデータパイプラインの構築を実現しています。 intdash Server の構成要素の 1 つに時系列データベースがあり、弊社では InfluxDB OSS を採用しております。 InfluxDB OSS は、v1( 1.X )と v2( 2.X )のバージョンがリリースされており、現在運用している intdash では v1 を利用しています。 SRE チームでは v2 への移行を検討しており、v1 との比較などを含め検証を進めています。今回は InfluxDB v2 の導入や移行などをご紹介いたします。 導入手順 インストール セットアップ v1 との互換性について v1 からの移行手順 事前準備 時系列データのマイグレーション クエリ言語について Flux 言語によるクエリパフォーマンスの向上 さいごに 導入手順 今回は Amazon Linux 2 の EC2 インスタンス上にインストールします。インストールするバージョンは以下になります。 influxdb: 2.1.1 influx-cli: 2.2.1 インストール influxdb をインストールします。systemd で起動するため、rpm パッケージ経由でインストールします。 wget https://dl.influxdata.com/influxdb/releases/influxdb2-2.1.1-x86_64.rpm sudo yum localinstall influxdb2-2.1.1-x86_64.rpm 起動前に config を設定します。 /etc/influxdb/config.toml 1 に記述するか、環境変数を介して渡すことができます。詳細オプションは、 公式ドキュメント をご覧ください。 設定が完了したら、influxdb を起動します。 sudo service influxdb start 続いて CLI でのセットアップのため、 influx-cli をインストールします。 wget https://dl.influxdata.com/influxdb/releases/influxdb2-client-2.2.1-linux-amd64.tar.gz tar xvzf influxdb2-client-2.2.1-linux-amd64.tar.gz sudo cp influxdb2-client-2.2.1-linux-amd64/influx /usr/bin/ セットアップ デフォルトの User や Bucket 2 、Org を作成します。 influx setup -f \ --name default \ # 初期設定の名前 --username influx \ # 初期ユーザー名 --password influxpassword \ # 初期パスワード --token influxtoken \ # InfluxDB へ接続する API Token --org influx \ # 組織名 --bucket intdash \ # 初期バケット --retention 0 \ # Retention Policy --shard-group-duration 168h0m0s # Shard Group Duration ここで、 --token オプションより influxdb に接続するための API Token 値を指定することができます(指定しない場合はランダムな値になります)。 設定を完了すると、以下のコマンドよりデフォルトの config 3 が作成されていることが確認できます。 $ influx config ls Active Name URL Org * default http://localhost:8086 aptpod また、デフォルトの Bucket も作成されていることが確認できます。 $ influx bucket ls ID Name Retention Shard group duration Organization ID Schema Type 14aed3a5dcc5af35 _monitoring 168h0m0s 24h0m0s 06adc337029ceb21 implicit 58a8e876e4cf0bbf _tasks 72h0m0s 24h0m0s 06adc337029ceb21 implicit 61d8afb4a9e0413c influx infinite 168h0m0s 06adc337029ceb21 implicit _monitoring 、 _tasks は System Bucket と呼ばれ、1 インスタンス毎にデフォルトで作成されます。 その他、必要な Bucket は以下のコマンドで作成できます。 influx bucket create \ --name influxdb2 \ # バケット名 --org influxdb \ # 組織名 --retention 0 \ # Retention Policy --shard-group-duration 168h0m0s # Shard Group Duration v1 との互換性について v2 では、主要言語に クライアントライブラリ が提供されていますが、v1 クライアントライブラリとの互換性もあります。InfluxDB 側に v1 auth と v1 dbrp を作成することで、v1 クライアントライブラリを使用できます。 上記は influx-cli コマンドで作成可能です。(事前に必要となる Bucket を作成し、ID を控えておく必要があります。) v1 auth InfluxDB にBasic 認証で接続できる認証設定をします。 --read-bucket 、 --write-bucket オプションにて、read/write 可能な Bucket を指定します。 influx v1 auth create \ --username <ユーザー名> \ --password <パスワード> \ --org <組織名> \ --read-bucket <Bucket ID> \ --write-bucket <Bucket ID> v1 dbrp Database と Retention Policy を、Bucket にマッピングする設定をします。 influx v1 dbrp create \ --bucket-id <Bucket ID> \ --db <DB 名> \ --rp <Retention Policy> 以上で v1 クライアントライブラリを使用可能な状態になります。 v1 からの移行手順 移行手順は「Automatically upgrade」と「Manually upgrade」の 2 通りがありますが、「Manually upgrade」で移行する手順をご紹介いたします。 こちらは時系列データを line protocol ファイルで書き出してマイグレーションするといった内容になっており、初期セットアップした InfluxDB v2 への移行を想定しています。 事前準備 移行前の準備として、 /etc/influxdb/config.toml を作成します。 v1 で設定していた config を元に、v2 の config を作成します。詳しいマッピングは 公式ドキュメント をご確認ください 4 。 時系列データのマイグレーション InfluxDB v1 サーバー上で、各 DB ごとに以下のコマンドを実行します。 line protocol ファイル形式で時系列データをエクスポートしています。 influx_inspect export \ -datadir /var/lib/influxdb/data \ # datadir を指定 -waldir /var/lib/influxdb/wal \ # waldir を指定 -database influxdb \ # DB 名 -retention autogen \ # Retention Policy -out /var/tmp/influxdb.lp \ # line protocol ファイルを出力するパス -lponly 次に InfluxDB v1 をアンインストールして InfluxDB v2 をインストールします。 5 その後、事前準備にて作成した /etc/influxdb/config.toml を配置して、上述した初期セットアップと Bucket 作成、v1 互換性の設定を実施します。 最後に、以下のコマンドから時系列データの書き込みを実行してマイグレーションは完了になります。 influx write \ --bucket influxdb \ # バケット名 --file /var/tmp/influxdb.lp # line protocol ファイルパス クエリ言語について v1 では クエリ言語として influxql が使用されていましたが、v2 では flux が使用されます。 (flux 言語自体は、時系列データベースだけでなく RDB や CSV などを扱うことができます。) influxql も API から使用可能ですが、上述した v1 dbrp 設定をする必要があり サポートされているクエリ が限られています。 また、Continuous queries は tasks という名前に置き換わり、flux での作成が必要です。詳細は 公式ドキュメント をご覧ください。 Flux 言語によるクエリパフォーマンスの向上 Flux 言語では、 Pushdowns という関数がサポートされています。 Pushdown とは 1 つ以上から成る関数であり、その関数はデータ操作をメモリ上で行わずデータソース上で実行します。 こちらを活用することで、CPU やメモリ使用率などの向上が期待できます。 実際に検証中のベンチマークテストにて InfluxQL と Flux の比較を実施しました。 参照するデータ量が大きいクエリを実行したところ、InfluxQL は OOM によって処理が止まってしまいましたが、Flux は正常に動作することが確認できました。 以下がメモリ使用率のグラフになります。 InfluxQL 実行時のメモリ使用率 Flux 実行時のメモリ使用率 顕著に差が出たのは、Pushdowns によるものと思われます。 長期間のデータ参照が必要なユースケースなどにおいて、Flux が有効な選択肢の 1 つとなることに期待できそうです。 さいごに InfluxDB のインストールや構成についてご紹介いたしました。 今回は検証向けの手順などがベースの内容となっておりますが、SRE チームでは運用環境に対する移行環境の整備など検証を進めています。 今後も引き続き進展あれば InfluxDB についてご紹介していきたいと思います。 json, yaml 形式でも記述可能です。 ↩ v2 では Database が Bucket として置き換わり、各 Bucket に Retention Policy / Shard Group Duration が設定されます。 ↩ ここでは Client 側( influx-cli )の config を指しています。検証では同一インスタンスに CLI をインストールしましたが、別インスタンスからもセットアップなどができます。 ↩ v1 と v2 の config option は大幅に変更されており、v2 から使用不可のものもあります。 ↩ 紹介した手順では、同一インスタンスを v2 に移行する内容を記載しています。運用中でダウンタイムなどを考慮して移行する場合は、 Dual write などを活用することをお勧めします。 ↩
アバター
Aptpod Advent Calendar 2021 24日目の記事です。 (土日休みにしているので最終日です) CTOの梶田です。 今年もあっという間でAdvent Calendarなんとか走りきれそうです。始まる前は、集まりがあまりよくなく焦りましたが、ロビー活動!?とかとかでなんとかなりました💦 これもそのひとつ。。。 tech.aptpod.co.jp Techブログでの挑戦も3年となり、今年も去年と同様に健全に(!?)土日は基本抜く形で投稿できました。慣れてきた部分もありつつ、なかなか続けるのは大変だなと痛感しています😓 (みんな忙しい中、頑張った!💪) というわけで! 早いもので2021年も終わろうとしています。毎年言ってますねw 年末ネタとして(勝手に)定着しつつある!?ので2021年も 昨年 と同様に振り返ろうと思います。 ちなみに メルペイ VPoE の @hidekさんも 今年も Merpay Advent Calendar を始めるにあたって、1年を振り返ってみたいと思います。 ちなみに、このネタで書くのは2019年、2020年に続き、三年連続になるわけですが、 このメソッドは大変楽なので、毎年ネタで困っている CTO/VPoE の方々に改めてオススメします。 とのことでした。そう思います!(同じく三年連続振り返りネタ💦) engineering.mercari.com はじめに 組織 ツール 技術ピックアップ 個人的に 2022年に向けて はじめに 2021年もCOVID-19は終息することなく、引き続き猛威を振るっており、リモートワークも定着してきた感はあります。最近は少し落ち着いていますが、まだまだどうなるか。。。 昨年 来年早々からいろいろリリースネタもありますし と書いた通り、 今年大きなトピックがいくつかありました。 シリーズCの資金調達 アプトポッドとマクニカ、資本業務提携 ハードウェアブランド "EDGEPLANT" 立ち上げ エッジコンピュータ "EDGEPLANT T1" の販売開始 アナログ信号利用を可能にするインターフェイス機器 "EDGEPLANT ANALOG-USB Interface" の販売開始 開発周りの拡充 iOS用のアプリケーション開発キット 「intdash SDK for Swift」の提供 可視化ダッシュボード「Visual M2M Data Visualizer」 最新バージョン及びパーツ開発キット「Visual Parts SDK」の提供 ... とけっこう今年の前半に固まった形にはなりましたが、さらなる資金調達も行い、組織規模も少しづつ大きくなってきています。 特にハードウェアブランド "EDGEPLANT" については大きなトピックで、小さい会社規模ながらのチャレンジでもあります。 今回のAdvent Calendarでもいくつかありました。 tech.aptpod.co.jp tech.aptpod.co.jp というところで! 今回の振り返りでも昨年同様、いろんなことがありすぎたので時系列というよりは、今年のトピックを個人的なところ含めていくつかピックアップして書いていきます。 組織 年初から将来を見据え事業を戦略的に実行するための組織の再編もあり、いろいろ紆余曲折ありながら進んできました。 またまた激動な年だったな と記憶ない部分もありますが💦 各マネージャやチームリーダーのおかげでグループやチームとしてよくなってきてチームとして作り上がってきている部分もあり、来年のさらなる飛躍に向けて今までのものをベースに積み上げていけたらと思っています。 ツール 昨年からトライアルしていた プロジェクト/課題管理のツールとして Jira(ジラ) ドキュメントコラボレーションツール、ナレッジ管理として Confluence(コンフルエンス) を全社導入しました。 Confluenceについては、最近では同じ領域のツールで Notion を使っている企業も多くなっていますが、Jiraとの親和性もあり、機能的にも遜色なく現在の利用においては、問題ありません。以前に比べるとだいぶ知の集約/構造化は進んできた感はあります。(まだまだこれからな部分もありますが。。) Notionについては、こんなものも出ているぐらいでだいぶ意識はしている感じですね。 www.atlassian.com 個人的にはNotion使っています👍 あとまだ一部の利用ですが、 アプリケーション向けのワイヤーフレーム作成やブレスト向けのコラボレーションツールとして Whimsical を使い始めています。 個人的には学習コストが低く、サクッと頭の中を整理したり形にしやすく、コラボレーションしやすいので重宝しています。 技術ピックアップ 個人的にはここのあたりをトピックとして。 www.aptpod.co.jp Visual M2M については、事業の当初から開発され、年々進化してきています。 2012年頃の1stから始まり、2ndとして2015年頃から今のVisual M2M Data Visualizerの開発が始まり今に至ります。 歴史があり長く継続して開発された製品となります。 特にフロントエンド界隈は進化が早く、トレンドも移り変わりやすいといったなかで、TypeScriptが浸透する前に、型宣言を導入するため Flow を使用したり。。等々試行錯誤しながらやってきた部分があります。 プロジェクト自体も巨大でなかなかテコ入れも難しい面もあり、 外部の開発者自身で拡張したいニーズも前々からずっとあった状態でした。 そんな中、担当の白金がFlow,TypeScriptを共存し本体の一部の機能を段階的に分離する方法を見出し、その最初として以前から要望の強かったVisualPartsの切り出し、SDK公開に至ったのでした。 事業的にも大きな機能でインパクトがあり、今後のVisual M2Mとしての光が見えたといったところでした。 Flow,TypeScript共存に関する詳細はまたテックブログにて! VisualPartsのテックブログはこちら↓ tech.aptpod.co.jp 他にもまだまだあったりするのですが、公開が来年なネタも多いのでここまでとします。 個人的に 個人的なところを少し。 今年は一回目のワクチン摂取で副反応起きたり、 引っ越ししてワークスペースを確保(生産性もアップ!)したり、 ... いろいろありました。 製品開発のWeb周りを埋めるために実装に入ったりのチャレンジもありました。(今も続いています) ざっと製品のWeb周りのスタックは TypeScript React Next.js *1 あたりを中心にマイクロフロントエンド *2 という小さいWebアプリが集まっているような構成です。 martinfowler.com 最初はうまく時間をとりつつ、学びつつ苦労しました。だいぶ慣れましたが、リリースする際に数が多いのでけっこう大変です💦 まだまだ恩恵が受けられていない部分や改善はありますが、やりがいはあると思います! というところで採用を挟んでおきます😅 open.talentio.com また、関連して、内部的に部署横串のフロントエンドのコミュニティ会を始めまして、 製品 / 技術 / 課題共有 問題の解決 / 相談(壁打ち) Webフロントエンドに関するトピックやブラウザ/ライブラリの更新状況 をトピックとして相互の助け合い、フロントエンドの技術追求、技術課題の解決や実装の効率化/スピードアップ、品質向上を目的として定期的に実施しています。 既に30回程度を重ね、振り返りしつつ改善を重ねて良い方向に向かっており、今年やってよかったものの一つでした。 2022年に向けて 2021年も期待の通りにはいかず、状況としても2020年と大きく変わることはなく、お客様の計画自体がズレることもあり、難しい状況は続きました。 ただ、EDGEPLANTの立ち上げやSDK等の開発環境の充実化を進めたことにより、2020年に比べ新規のお客様やパートナー様が増加し、基盤として少しづつですが積み上がってきています。 取り組んできたものが少しづつ実ってきた感触はあります。 来年2022年は、昨年より改善の兆しは見えてきていて、DXというのも浸透し一般的になりつつある *3 状況ですので、来年のネタもまだまだ仕込んでありますし、引き続き、アクセル踏んで頑張っていきます!💪 来年もまたアプトポッドにご期待ください!! メリークリスマス!🎄 *1 : ※すべてにNext.jsが使われているわけではありません。SPAもあったりします。 *2 : 参考: 『Micro Frontends』という記事を読んだのでまとめる *3 : 急速に拡大するDX市場の現状と今後の動向・対策を解説
アバター
aptpod Advent Calendar 2021 の 23 日目を担当する、製品開発グループ intadsh チームの佐藤 (TK)です。 多言語化されたアプリケーションのフォームの開発で Yup を使ったスキーマを作成する機会があり Formik や、 React Hook Form などのフォーム用のライブラリに適用する前に単体テストを実行したかったので作成してみました。 API ドキュメントをテスト仕様として定義するところから紹介したいと思います。 ゴール 技術 API ドキュメントからテスト仕様を定義する レッドパターンを洗い出す レッドパターンから文言を決める 多言語ライブラリを作成する 日本語 英語 スキーマのテストを書く スキーマを定義する テストを実行する ゴール 多言語されたバリデーションメッセージが定義されたスキーマのテストが通ること 技術 TypeScript i18next Yup Jest API ドキュメントからテスト仕様を定義する レッドパターンを洗い出す 使用するエンドポイントの仕様を読んで、レッドパターン (= バリデーションメッセージを出す条件) を洗い出します。 今回は [POST] /contacts というエンドポイントに対してリクエストを投げることとします。 サンプルのAPI ドキュメント name required なので、未入力の場合 age type が integer なので、数値かつ整数ではない入力の場合 0 以上になっているので、-1 以下の入力の場合 email type が string (email) なので、メールアドレスの形式ではない場合 message required なので、未入力の場合 レッドパターンから文言を決める 洗い出せたので、それぞれの条件で表示する文言を日本語と英語と決めていきます。 箇所 メッセージ (日本語) メッセージ (英語) 条件 name 名前を入力してください。 Please enter name. 未入力の場合 age 数値を入力してください。 Please enter a number. 数値ではない入力の場合 age 整数を入力してください。 Please enter an integer. 整数ではない入力の場合 age 年齢は 0 以上の整数を入力してください。 The integer of Age must be greater than or equal to 0. -1 以下の入力の場合 email メールアドレスが不正な形式です。 Email is in invalid format. メールアドレスの形式ではない場合 message メッセージを入力してください。 Please enter Message. 未入力の場合 これで、それぞれの条件に対応した表示したいメッセージが定義できました。 こちらをテスト仕様とし、次は多言語ライブラリを作成していきます。 ※ 今回はサンプルなので最低限のバリデーションを定義しています。 多言語ライブラリを作成する i18next の Basic sample を参考に文言を定義していきます。 import i18next from "i18next" ; import { translations as translationsEN } from "./en/translations" ; import { translations as translationsJA } from "./ja/translations" ; i18next.init ( { lng: "en" , debug: false , resources: { en: { translation: translationsEN , } , ja: { translation: translationsJA , } , } , } ); export { i18next } ; 今回は日本語と英語で言語ファイルをそれぞれ分けて定義しました。 表示したいメッセージは先程定義したので、それを当てはめていきます。 日本語 import { Translations } from "../types" ; export const translations: Translations = { "name.required" : "名前を入力してください。" , "age.invalid-type" : "年齢は数値を入力してください。" , "age.integer" : "年齢は整数を入力してください。" , "age.min" : "年齢は 0 以上の整数を入力してください。" , "email.invalid-format" : "メールアドレスが不正な形式です。" , "message.required" : "メッセージを入力してください。" , } ; 英語 import { Translations } from "../types" ; export const translations: Translations = { "name.required" : "Please enter name." , "age.invalid-type" : "Please enter a number." , "age.integer" : "Please enter an integer." , "age.min" : "The integer of Age must be greater than or equal to 0." , "email.invalid-format" : "Email is in invalid format." , "message.required" : "Please enter Message." , } ; 念の為、言語 (lng)と Key を指定した場合の挙動を確認しておきます。 Jest で簡単な単体テストを書きました。 import { i18next } from "." ; test ( "name.required en" , () => { i18next.changeLanguage ( "en" ); expect ( i18next.t ( "name.required" )) .toBe ( "Please enter name." ); } ); test ( "name.required ja" , () => { i18next.changeLanguage ( "ja" ); expect ( i18next.t ( "name.required" )) .toBe ( "名前を入力してください。" ); } ); ... テストを実行して問題ないようであれば、この言語設定を実際にスキーマに適用させていきます。 まずはスキーマのテストを書きます。 スキーマのテストを書く 「多言語されたバリデーションメッセージが定義されたスキーマ」が取得されることを期待しているので、 まずは、言語 (lng) を渡して、それに沿ったスキーマが返却されるような関数を想定します。 const { schema } = getSchema ( "en" ); これで指定した言語のスキーマを取得する関数が定義できたので、先に作ったテストパターンを使ってテストケースを定義していきます。 import { getSchema } from "./schema" ; describe ( "英語のスキーマ" , () => { const { schema } = getSchema ( "en" ); it ( "名前が未入力の場合" , () => { const testValues = { name: "" , email: "simple@example.com" , message: "message" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "Please enter name." ); } ); it ( "年齢が数値ではない入力の場合" , () => { const testValues = { name: "name" , age: "age" , email: "simple@example.com" , message: "message" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "Please enter a number." ); } ); it ( "年齢が整数ではない入力の場合" , () => { const testValues = { name: "name" , age: 0.1 , email: "simple@example.com" , message: "message" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "Please enter an integer." ); } ); it ( "年齢が-1 以下の入力の場合" , () => { const testValues = { name: "name" , age: -1 , email: "simple@example.com" , message: "message" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "The integer of Age must be greater than or equal to 0." ); } ); it ( "メールアドレスの形式ではない場合" , () => { const testValues = { name: "name" , age: 29 , email: "Abc.example.com" , message: "message" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "Email is in invalid format." ); } ); it ( "メッセージが未入力の場合" , () => { const testValues = { name: "name" , age: 29 , email: "simple@example.com" , message: "" , } ; expect (() => schema.validateSync ( testValues )) .toThrow ( "Please enter Message." ); } ); } ); エラー条件に沿った値を schema.validateSync に引数で入れると、 ValidationError がスローされる ので、 Jest の .toThrow を使ってバリデーションメッセージを取得します。 さらに念の為、全ての値がエラー条件に沿った場合のテストケースも定義しておきます。 it ( "すべての値が不正な場合" , () => { const testValues = { name: "" , age: -1 , email: "Abc.example.com" , message: "" , } ; schema .validate ( testValues , { abortEarly: false } ) . catch (( validationError ) => { const validationErrors: Record < string , string > = {} ; const _validationError = validationError as Yup.ValidationError ; _validationError.inner.forEach (( error ) => { if ( error.path ) { validationErrors [ error.path ] = error.message ; } } ); expect ( validationErrors ) .toEqual ( { name: "Please enter name." , message: "Please enter Message." , age: "The number of Age must be greater than or equal to 0." , email: "Email is in invalid format." , } ); } ); } ); schema.validate のオプションに abortEarly: false をセットすることによって、 すべての検証が実行された後に ValidationError がスローされる ようになります。 スローしたエラー の inner から validationErrors としてオブジェクトを生成し、toEqual で検証します。 スキーマを定義する テストから実際にスキーマが取得できる関数を作成します。 import * as Yup from "yup" ; import { i18next } from "../i18n" ; import { Schema } from "./types" ; export const getSchema = ( lang: "en" | "ja" ) => { i18next.changeLanguage ( lang ); const schema: Yup.SchemaOf < Schema > = Yup. object () .shape ( { name: Yup. string () .required ( i18next.t ( "name.required" )), age: Yup. number () .transform (( value , originalValue ) => originalValue === "" ? undefined : value ) .typeError ( i18next.t ( "age.invalid-type" )) .integer ( i18next.t ( "age.integer" )) .min ( 0 , i18next.t ( "age.min" )), email: Yup. string () .email ( i18next.t ( "email.invalid-format" )), message: Yup. string () .required ( i18next.t ( "message.required" )), } ); return { schema } ; } ; テストを実行する スキーマとテストを定義したら、実際にテストを実行してみます。 PASS src/schemas/schema.test.ts (7.414 s) 英語のスキーマ ✓ 名前が未入力の場合 (16 ms) ✓ 年齢が数値ではない入力の場合 (30 ms) ✓ 年齢が整数ではない入力の場合 (1 ms) ✓ 年齢が-1 以下の入力の場合 (1 ms) ✓ メールアドレスの形式ではない場合 (1 ms) ✓ メッセージが未入力の場合 (1 ms) ✓ すべての値が不正な場合 (1 ms) ✓ すべての値が正常な場合 (1 ms) Test Suites: 1 passed, 1 total Tests: 8 passed, 8 total Snapshots: 0 total Time: 7.921 s すべてのテストケースが通ったので、最後に正常な値を入れたパターンのテストを定義します。 it ( "すべての値が正常な場合" , () => { const testValues = { name: "name" , age: 10 , email: "simple@example.com" , message: "message" , } ; expect ( schema.isValidSync ( testValues )) .toBe ( true ); } ); ✓ すべての値が正常な場合 (1 ms) このように多言語化されたアプリケーションのフォームの開発で Yup を使ったスキーマを作成することで、バリデーションがより安全に実装できます。 また、 Formik や、 React Hook Form に結合する前にテストをすることによってバリデーションの品質向上につながればと思います。
アバター
aptpod Advent Calendar 2021 の22日目を担当する、製品開発グループintdash チームの呉羽です。 以下の要件を満たした回数記録基盤を、以前に個人開発したので紹介します。 Bluetoothで接続されたIoTボタンが押された回数を記録したい ボタンが押された時刻と共に永続化したい 永続化されたデータを集計して閲覧したい 利用したもの 構築までの流れ IoTボタンの準備 Raspberry Piの準備 IoTボタンとの接続 IoTボタン押下時の処理 AWSの準備 Amazon Timestreamによる記録の永続化 API Gatewayによる記録実行URLの実装 集計結果をAmazon SNSを通じてメール通知 まとめ 利用したもの CamKix カメラシャッターボタン Raspberry Pi 4 AWS (Lambda, Timestreamなど) 構築までの流れ 今回は以下の図のように、ボタンを押してから永続化される流れとなります。 IoTボタンによる回数記録までの流れ IoTボタンの準備 今回はIoTボタンとして CamKix カメラシャッターボタン を利用します。 この製品はBluetoothで接続可能な スマホのカメラシャッター向けボタン ですが、実質的にはボリュームアップキーが1つあるキーボードです。価格は1000円ほど(2021/12/20確認)。 Raspberry Piの準備 IoTボタンとAWSを仲介するための機器として、今回はRaspberry Pi 4(OSはUbuntu Server)を利用します。Raspberry Piでなくとも、以下の要件を満たせば代替可能です。 IoTボタンをBluetoothで接続可能 インターネットを利用可能 IoTボタンとの接続 LinuxでBluetoothデバイスの管理をする際、 bluetoothctl というソフトウェアを用います。広く利用されているため接続手順は省略します。 IoTボタン押下時の処理 IoTボタンが押下された際、後述するAmazon API GatewayのURLを叩くことで、AWSとの橋渡しを行います。 今回は evdev-trigger を利用し、接続されたデバイスの特定のキーが押下された時にコマンドを実行させます。以下のような設定ファイルを用意すれば、IoTボタン押下時に、 curl コマンドが実行されます。 # physical id of device phys : a1:b2:c3:d4:e5:f6 triggers : # Key is the input event code to trigger the command. 115 : command : [ "curl" , "https://example.com" ] evdev-triggerはフォアグラウンドで動作するソフトウェアなので、supervisorによりバックグラウンドとして常に動作させておきます。以下がsupervisorの設定ファイル例です。 # cat /etc/supervisor/conf.d/evdev-trigger.conf [ program:evdev-trigger--camkix ] command= /usr/ local /bin/evdev-trigger --config /etc/evdev-trigger/camkix.yml autorestart = true AWSの準備 最後にAWS上で以下を構築します。 Amazon Timestreamによる記録の永続化 API Gatewayによる記録実行URLの実装 集計結果をAmazon SNSを通じてメール通知 実装コードやCloudFormationのテンプレートはGitHubで公開しているため、以下の手順でデプロイ可能です。 https://gist.github.com/hareku/30023706e854015bfa289bd4a2081022 Amazon Timestreamによる記録の永続化 今回はデータの保存先としてAmazon Timestreamを利用します。保存先として、他にDynamoDBも候補として挙げられますが、Timestreamは後述する集計機能を提供しているため、今回はTimestreamを選択します。 Timestreamは時系列データベースで、AWS SDKを通して容易に扱えます。 今回のGo言語による実装コードは github.com/hareku/go-timestreamer で公開しており、Lambdaのエントリーファイルは cmd/simple/lambda/record/main.go です。 API Gatewayによる記録実行URLの実装 上述のLambdaをHTTPプロトコル経由で実行させるために、Amazon API Gatewayを用います。 また不特定多数から呼び出されないよう、API Gatewayが提供しているAPIキーを設定することが推奨されます。 集計結果をAmazon SNSを通じてメール通知 TimestreamはSQL-likeな集計機能を提供しています。そこで集計結果を以下の手順で通知させます。 回数記録基盤の結果通知の流れ CloudWatch Eventsで定期的にLambdaを実行する 実行したLambda上でTimestreamの集計SQLを発行し、結果をAmazon SNSのトピックへ送信する Amazon SNSのトピックを購読しているメールアドレスに集計結果が送信される 以下のように、一日ごとの総記録回数がプレーンテキスト形式のメールで送信されます。 2021-11-27: 7 2021-11-26: 4 2021-11-25: 5 .... 今回は以下のようなクエリをTimestreamに投げました。 SELECT bin(time, 24h) as date, SUM(measure_value::bigint) as count FROM "db"."meas" WHERE time between ago(30d) and now() GROUP BY bin(time, 24h) ORDER BY date DESC Timestreamは時系列関連の関数が多く実装されており、簡潔に集計することが出来ました。慣れ親しんだSQLで表現力高く書けますので、気になる方はAWSドキュメントの Timestreamを使った集計のSQL例 の一読をおすすめします。 今回実装した集計用Lambdaのエントリーファイルは cmd/simple/lambda/publish-sns/main.go です。 まとめ 以上が、IoTボタンによる回数記録基盤の構築の流れです。例に挙げたコードでは、AWSの利用料金は月に1ドルも掛からないため(2021/12/20確認)、低コストで運用が可能です。
アバター
aptpod Advent Calendar 2021 の21日目を担当します、SREチームの金澤です。 私が所属する SRE チームでは、担当業務の一つに、弊社の製品である intdash のインフラストラクチャをメインとしたクラウドシステムのインフラ設計・構築、監視・バックアップなどの運用業務があります。 バックアップについては、約1年半前、Amazon Web Service(AWS) のマネージドサービスである AWS Backup への移行を開始し、設定のチューニングを行いながら、現在ではバックアップ要件がある AWS リソースほぼすべてを AWS Backup にてバックアップ管理しています。 本記事では、バックアップマネージドサービスである AWS Backup への移行について、現在検討中の内容も交えてご紹介いたします。 AWS Backup とは バックアップの動作 バックアップの設定検討 環境毎の Backup Vault の作成 IaC による管理が可能 バックアップリソースの選択 RDSの継続的バックアップ バックアップのスケールが容易になった 監視 監査 さいごに AWS Backup とは 公式ドキュメント の言葉を借りますと、 一元管理型クラウドバックアップサービス です。 AWS Backup は AWS クラウド内の AWS のサービス間でアプリケーションデータを簡単かつ低コストでバックアップできる一元管理型クラウドバックアップサービスです。 スナップショットとしてバックアップするケースが多い EBS を始め、RDS や EFS など現在11種類のリソースのバックアップが可能 *1 です。さらに、今年オンライン上で開催されました re:Invent 2021 では、Amazon S3 のバックアップがサポート予定であることが発表 *2 となり、計12種類となりました。 設定においては、複雑になりがちなバックアップウインドウ *3 や、保持しておきたいバックアップファイルの世代管理が容易に設定できます。また、マネージドサービス自体の初期費用はゼロで、使用したい場合は手軽に始めることができます。 バックアップの動作 AWS Backup の動作について簡単に説明します。 AWS Backup では、Backup Plan リソースの内容にしたがってバックアップの動作を決定し、バックアップリソースは Backup Vault というリソースに整理・保管されます。 AWS Backup で行われるバックアップ動作 Backup Plan では、バックアップの頻度やバックアップウインドウ、保持期間を定めます。また、バックアップ対象を指定する方法を設定します。 Backup Plan の設定内容 バックアップの設定検討 バックアップを行うにあたり、設定検討した内容です。 環境毎の Backup Vault の作成 通常、開発環境は本番やステージングなど複数の環境があるものと思います。それぞれの環境におけるバックアップの要件も当然異なるため、複数の Backup Vault を作成し運用しています。これにより、それぞれの環境におけるバックアップリソース個数の規模感が明確になり、現状どのくらいのバックアップリソースが存在するか把握できるようになりました。 Backup Vault の作成には個数制限 *4 があるものの費用自体は発生しないため、管理したい形で構成することができます。 環境毎の Backup Vault の作成 IaC による管理が可能 弊社のインフラは基本的に IaC *5 を行っており、Terraform にて構築・管理しています。 現在では、IaCによる管理も珍しくないものとなりもはや当たり前と言われそうですが、AWS Backup も Terraform によるコード化が可能で、弊社のインフラストラクチャ管理と親和性が高く難なく組み込むことができました。 バックアップリソースの選択 バックアップするリソースを選択する方法としては、上述の通り以下が存在します。 サービス全体(EC2, EBS, ...) リソースID リソースに付与されるタグ 今後変更が見込まれない環境やバックアップ対象が明確に決められている場合は、サービス全体やリソース ID によるバックアップの方が都合が良い場合が存在しますが、変更が見込まれる環境においてはスケールしやすい(バックアップ対象の増減が容易な)リソースタグによる管理が多いと思われます。弊社もリソースタグによる対象管理を行っています。 タグとは別に、AWS Backup では サービスのオプトイン というバックアップ対象を有効または無効化出来る機能があり、併用することでタグ付与における効率化が可能か、と検討していましたが、現状ではそこまで効率化が見込めず、検討時点では hashicorp/terraform-provider-aws が対応していなかったため、採用していません *6 。 RDSの継続的バックアップ AWS RDS は自身でバックアップウインドウを設定し自動バックアップを行うことが出来ます。この自動バックアップが AWS Backup によって管理できるようになりました。バックアップ管理を統合し易くなり、管理性の向上が見込めるようになりました。弊社で稼働している RDS は稼働当初より自動バックアップが設定していますが、そのような目的のもとに AWS Backup への移行を行っています。 統合後、自動バックアップは AWS Backup 管理である旨が記載される バックアップのスケールが容易になった AWS Backup への移行のモチベーションとして一番大きかったのは、それまで稼働していたバックアップシステムのスケーラビリティを改善したいというものでした。 以前は AWS SDK を使用してバックアップを行っていました。当初は問題ありませんでしたが、弊社製品を利用していただく機会が増えることにより、バックアップリソースの数も増加し、次第にスケールの限界が見えるようになりました。その影響を被る前にマネージドサービスへの移行を検討・開始し、現在に至ります。 約1年半前の移行検討を開始した時点と比較して、現在もバックアップリソースの数は増加していますが、バックアップ失敗といったアラートはほとんど見られず、バックアップのスケールに対して特別な意識を持たなくてもよくなりました。 監視 AWS Backup の設定を行いバックアップを開始することができました。次に、バックアップが正常に実行されているかどうかの監視はどのように行えばよいでしょうか。 いくつか方法はあるかと思いますが、弊社では AWS SNS で作成した SNS topic に Backup Vault からバックアップのレスポンスを Slack に通知することで行っています。 Slack に自動投稿されたバックアップ失敗アラート バックアップするリソースの数が多い場合は、フィルターをかけた方が視認性的にも費用的にも利点が多いです。現在はレスポンスに含まれる MessageAttributes の Key-Value の値でフィルターし、通知に使用しています。 監査 現在検討中の内容もご紹介します。 先日開催された AWS のイベントである re:Inforce 2021 では AWS Backup Audit Manager が発表されました。 *7 これは、現在動作しているバックアップの結果を既定のポリシーに従って評価する、またその評価結果をレポートする機能となります。 Audit という名称から想像できるとおり、バックアップにおける監査・コンプライアンスの対応に役立つ機能です。 既定のポリシーとは、 バックアップリソースがバックアッププランによって保護されているかどうか バックアッププランの最小頻度と最小保持期間が想定通りとなっているか など計5種類が設定されていて、バックアップしているリソースに対してポリシーを満たしているかどうか評価され、レポートを指定の S3 バケットに CSV または JSON の形式で配信することができます。このレポートをデータソースとして、Amazon QuickSight を利用したダッシュボードを作成し監査対応者などの第三者がリソースバックアップの状況を閲覧するようなこともできると考えています。 レポートを元に作成したバックアップステータスダッシュボード(のようなもの) また、リソースバックアップの成功を確認出来るという意味では、上述の監視にも利用出来るものと考えており検討を進めています。 一点、この AWS Backup Audit Manager は AWS Config を用いて実現しています。 AWS Config は AWS リソースの設定状況を記録するサービスとなり、 AWS Backup Audit Manager においては、 AWS リソースの設定状況の記録 既定のポリシーによる評価 の2点を AWS Config が担当しています。対象となるリソース数が多い場合は意図せず費用が膨らむ場合があります。そのため、導入には費用算出を考慮した上で検討したほうがよいです。今回は影響が微小となるように、別途用意されている個人アカウントでの検証を行いました。 さいごに 本記事では、AWS Backup への移行として、その際に検討した内容や現在検討中の内容を交えて紹介いたしました。バックアップという比較的地味な領域ではありますが、システム運用の面では重要で不可欠な領域だと考えます。引き続き、有用なサービスや機能の利用を検討しながら、効率的な基盤構築・運用に貢献できるよう努めていきたいと思います。 *1 : 2021/11/30に発表されたVMwareで実行される仮想マシン を含む *2 : https://aws.amazon.com/jp/about-aws/whats-new/2021/11/aws-backup-amazon-s3-backup/ *3 : AWS Backup 内では「バックアッププラン」と呼ばれている *4 : Backup Vault はリージョン毎に最大100個作成できる *5 : Infrastructure as Code *6 : 後にv3.18.0にて対応されました *7 : https://aws.amazon.com/jp/about-aws/whats-new/2021/08/aws-backup-audit-manager/
アバター
aptpod Advent Calendar 2021 の20日目を担当する、製品開発グループDocumentsチームの篠崎です。 この記事では、「70人規模の会社で、ドキュメント専門の担当者はどんなことをするのか」を紹介したいと思います。 最近、いくつかのオンライン上のミートアップで、テクニカルコミュニケーションチームの素晴らしい情報が公開されていて *1 、かっこいいなあ、すごいなあと思いながら日々見ています。 これらミートアップを拝見しながら、弊社のような「もっと小さな会社のドキュメント担当の仕事」についてもご紹介したいと思いました。 私自身以前から、同規模の他社ではどのようにされているのか知りたいと考えており、「まずは自分が書いてみよう」と思ったのでした。同様の仕事をしている方の参考になるところがあれば幸いです。 職種名と所属 ドキュメント担当の仕事 (A) ライティング/リライト関連 (B) 翻訳関連 (C) UI文言関連 (D) A~Cにかかわるツールや仕組みの整備 まとめ 職種名と所属 弊社内でのドキュメント担当の職種名は「テクニカルコンテンツディレクター」です。 テクニカルコンテンツディレクターは、「製品開発グループ」(15人ほどのエンジニアにより構成)に所属し、製品・サービスの使い方・価値を、分かりやすく統一性のある形で社外に伝えることを目標に、ドキュメントを作っています。 ただ、自己紹介するときに「テクニカルコンテンツディレクターです」と言っても実際の仕事をイメージしてもらうことが難しいので、「ドキュメント担当です」「テクニカルライターです」「マニュアルを作っています」などのように言っています *2 。 実務を振り返ってみると、私としては、「ドキュメント担当」というのがいちばんしっくりくるかなと思っています。以下のような理由からです: 実際にドキュメントを書いていますが、書く以外の仕事も多いので、「ライター」という言葉ではカバーしきれない マニュアルというと一般にはPDFや冊子の形式になったものを想像することが多いと思いますが、開発者向けAPIリファレンスも扱うので「マニュアルを作っている」だけでもない 翻訳の進行もする(ただし、翻訳者ではありません)し、ドキュメント作成のためのツールの整備をしたりもする ドキュメント担当の仕事 弊社の「ドキュメント担当」の仕事は、4つの分野に整理できます。 (A) ライティング/リライト関連 (B) 翻訳関連 (C) UI文言関連 (D) A~Cにかかわるツールや仕組みの整備 現在、弊社は全体で70人ぐらいですが、社内に「テクニカルコンテンツディレクター」は1人しかいません *3 。そのため、上記のように幅広く担当しています *4 。 小さな組織で、情報システム関連のあらゆる業務を受け持つ人を「ひとり情シス」と呼ぶことがありますが、それになぞらえると、私は「ひとりドキュメント担当」です。 1年半前に入社したとき、社内にこの職種の人はいませんでした。一方、私は以前ドキュメント制作チームで働いた経験はありましたが、「ひとりドキュメント担当」として働いたことはありませんでした。 そのため、社内の既存メンバーも私も、ドキュメント担当の業務分担はどうあるべきかを手探りで確かめながら、現在のような形を作ってきました。 その「手探り」は、いまも続いています。製品のラインナップ、サービスの提供形態、組織の成長フェーズが移っていくにつれて、常に調整していかなければならないことだと思っています。 では、4つの分野について、もうすこし詳細をご紹介します。 (A) ライティング/リライト関連 弊社コーポレートサイトの Docs で公開されているマニュアル、ドキュメント類を、日本語で作成しています。 量が多く、また内容も幅広いため、すべてを一人で書いているわけではありません。 制作工程は製品や内容によってさまざまです。いくつかの代表的なパターンは以下のようになります: ドキュメント担当が、企画、構成検討から始め、ゼロからライティングをする 決まった構成に沿ってエンジニアが原案を書いて、それをドキュメント担当が整理・リライトして仕上げていく(ハードウェアマニュアルなど) エンジニアが書いて、ドキュメント担当は表現のブラッシュアップのみを行う(デベロッパーガイド、APIリファレンス) どのパターンで作成するかは、内容、スケジュールによって臨機応変に変えています。 「前回はエンジニアさんの原案をもとにして作りましたが、今回の改版部分はドキュメント担当が直接書きますね、エンジニアさんはレビューをしてくださいね」ということもよくあります。 作成に使用するツールは主に Sphinx です(APIリファレンスを除く)。 ドキュメントのデータは、社内GitLabで管理しています。ドキュメント担当がすべてのドキュメントリポジトリのオーナーになっており、修正作業は修正用ブランチで行って(対象製品の開発担当エンジニア、ドキュメント担当がともにコミット可能)、ドキュメント担当が最終的に本流にマージして完成させるというフローが定着しました。 一般的には、ドキュメントの作成(特に「マニュアル」と呼ばれる種類のもの)を、専門の制作会社に外注するケースも多いと思いますが、ドキュメント担当が社内で作成する(内製する)ことは、 継続的にドキュメントを整備していくにあたって大きな利点になる と考えています。 社内の人間であれば、常に社内の製品開発エンジニアと共に、製品を深く理解しながら作業を進めることができます。存分に社内の資料を見られますし、エンジニアに気軽に質問することができます。その気になれば、製品のソースコードを読むことも可能です。 ライティング中は、開発担当エンジニアに製品仕様や新機能の目的などを質問することがあるのですが、事前に十分に情報収集ができるおかげで、あらかじめ「きっとこういうことだな」と理解したうえで、エンジニアに「こういうことですよね?」と確認し、「そのとおり」という返事をもらって、短時間で情報収集を行うことができます。これは大きなアドバンテージです(もちろん、「そのとおり」でないこともありますが)。 また、ドキュメント担当としては、同じ製品を継続して担当することで、製品の知識を蓄積することができます。弊社の場合、ハードウェアからソフトウェア開発用SDKまで、非常に製品の幅が広く、それらが1つの目的のためにシステムとして動くので、幅広く、しかし自社製品に集中して知識を蓄積できる環境は非常に重要と考えています。 この分野で思い出深い今年の仕事(年末なので振り返ります): ハードウェア製品 EDGEPLANTシリーズ の取扱説明書 Data Visualizer Visual Parts SDKのチュートリアル (B) 翻訳関連 一部のドキュメントは英語に翻訳する必要があります。私自身は技術翻訳はできませんが、翻訳前の処理、翻訳会社への発注(一部は社内で機械翻訳を使用)、翻訳後の処理、訳文のチェックを行っています。 翻訳会社で翻訳支援ツール(CAT)を使った処理をしやすいように、また、自分でもチェックしやすいように、原文ファイル、用語集、翻訳メモリなどの材料を整えたり、工程を考えたりします。 このあたりは、 リンギスト と呼ばれる仕事に近いと思います。 原文である日本語のライティングと、この翻訳関連業務を一人で担当していると、「翻訳結果をチェックすると原文の日本語のマズいところが分かり、すぐに日本語版を直すことができる」という、特有の利点もあります。 今後、本格的に英語ドキュメントを用意していくためには、クラウド型の翻訳支援ツールを導入しないと処理しきれなくなるだろうなと思っているところです。 この分野で思い出深い今年の仕事: Automotive Proクイックスタートガイド英語版(近日公開) (C) UI文言関連 弊社製品のうち、UIを持つウェブアプリケーションについて、ボタンやメッセージの文言を考えています。 これは、最近「UXライター」と呼ばれている仕事にあたると思います。 このあたり、私は業界の動向に十分に追い付けているとは言い難いですが、それでも、画面を設計する段階から議論に加わり、エンジニア、デザイナーとともにUI文言を作っています。 UI検討のフェーズから開発に加わることで、早い時期から製品の仕様を理解し、そこで得た情報をドキュメントのライティング工程にも活かすことができます。 たった1行の文言を書くのに、調査・議論など、かなりの時間がかかる難しい仕事です。 この分野で思い出深い今年の仕事: Visual M2M Data Visualizer のバージョンアップに合わせて文言を検討(ユーザーが複雑な設定をする画面があり、もっとマニュアルで補足説明したいと思っていたのですが、エンジニア、デザイナーとともに画面そのものを分かりやすくすることで、複雑さの原因の部分で改善ができました) (D) A~Cにかかわるツールや仕組みの整備 ここまで挙げてきたような仕事をしていると、もっと効果的なものを効率よく作れるようになりたいと感じてきます。 そこで、そのためのツールを整えたり、ドキュメントを公開するための仕組みを作ったりするのも仕事です。 前述のように、マニュアルは主にSphinxで作っていますが、その設定やスタイルファイルは継続的に改善しています(SphinxでのPDF生成については 以前のテックブログの記事 にも書きました)。 また、PDF/HTMLを自動でビルド・デプロイするための仕組みを整えたり、Sphinxから出力される翻訳用ファイル( Gettext PO )を使ったワークフローを考えたり、といったことも含まれます。 このあたり、ドキュメント関連のエンジニアリング的な作業が好きな人には、とっても楽しい仕事です。 また、ドキュメント内で使う用語をできるだけ統一できるように、製品に関連する概念や機能名を集めた用語集も整備しています。 この分野で思い出深い今年の仕事: SphinxでPDFを出力するための社内スタイルファイルの統一 intdash FAQ の外部公開(これは、ドキュメント担当だけでなく部署横断チームにて、ナレッジベースサービスの選定からコンテンツ公開までを行いました。今後も内容を充実させていきます。) まとめ 以上が、弊社の「ひとりドキュメント担当」の概要です。 すべてをこなすのは大変で、十分にできていないところもありますが *5 、現在の弊社の場合のように1人(または少人数)で、「製品横断的に、また、工程縦断的に、多方面にかかわることができる」というのは面白いところだと思っています。今後も日々勉強・実践していきます。 *1 : 例えば、 LINEの開発者向けドキュメントを支える「テクニカルライティング」の専門チーム 、 Cybozu × SmartHR プロダクトに関わるライターのリアル 、 LINE Technical Writing Meetup vol. 8 など *2 : 「自分の仕事を何と呼べばよいのか迷う」というのはテクニカルコミュニケーション業界の「あるある」だと思います。業界外では最もよく知られている「テクニカルライター」という職種名でさえ、職業図鑑にはなかなか載っていないですので。 *3 : 弊社内の職種別の構成については今年のアドベントカレンダーで 人事担当が紹介 しています。 *4 : マーケティング関連のドキュメント(コーポレートウェブサイト、パンフレット、プレスリリースなど)は、別途マーケティング部門が担当しています。 *5 : 課題もたくさんあります。 まだ説明しきれていない機能がある 情報が見つけにくい場合がある このままドキュメントが増えると今のやり方ではメンテナンスが追い付かなくなる 必要とされている情報を作るだけでなくて、情報の再利用性、メンテナンス性、製品としての方向性、読者にとっての必要性、潜在的なユーザー層のありかなどを考えて、作るものの優先順位をもっと考えないといけない(コンテンツストラテジー) これらについては、今後も考え、対処していかないといけません。
アバター
aptpod Advent Calendar 2021 の17日目を担当するProtocol/Robotics Teamの酒井 ( @neko_suki ) です。 弊社では、intdash_ros2bridgeというROS2上で「任意のトピック、サービス、アクションのC++実装によるブリッジ」を実現するソフトウェアの開発を進めています。ブリッジとは、ROS2空間の内部のデータを、インターネット経由で伝送できるメッセージ形式に橋渡しをする処理のことを指します。 このintdash_ros2bridgeによって離れた2つのROS2空間をつなぐことが可能になります。このintdash_ros2bridgeによって、ROS1 *1 と同様にROS2でも遠隔制御やモニタリングなどが実現できます。 過去に書いた紹介記事では、intdash_ros2bridgeで実現できることや、技術的な実現方法をご紹介してきました。 tech.aptpod.co.jp tech.aptpod.co.jp 今回の記事では、intdash_ros2bridgeと、 ROSメッセージのブリッジによく使われる rosbridge_server のメッセージ伝送の遅延を比較しましたので、その結果をご紹介します。 遅延値による性能の比較 計測の構成 intdash_ros2bridge rosbridge_server 計測に使用したソフトウェア rosbridge_server 計測に使用するフォーマット 計測に使用するトピック Struct256 Array1k PointCloud512k 計測に使用したマシン 計測結果 Struct256 Array1k PointCloud512k 考察 ペイロードのサイズ確認 rosbridge connector 処理時間 まとめ 遅延値による性能の比較 遅延の計測には、 performance_test (release-foxy-20210519) というツールを使用しました。 performance_testは、テスト用のトピックのpublishとsubscribeを行います。配列、点群などのテスト用のトピックが用意されています。 performance_testは、メッセージをpublishするときにメッセージにタイムスタンプを含めます。このタイムスタンプと、メッセージがsubscriberで受信されたときのタイムスタンプを比較することで遅延を計算することが出来ます。 しかし、タイムスタンプは、ClockCycles 関数で取得されたマシン固有のクロック値であるため、今回のようにメッセージを別のPCで受信する場合は、この方法で遅延を求めることはできません。 そこで、以下の図に示すように、PC1からインターネットを経由した先にあるPC2で受信したときの遅延ではなく、ループバックさせてメッセージがPC1に戻ってくるまでの遅延を計測します。 遅延値は、以下の図のようにperformance_test(publisher)がメッセージをpublishするときに付けたタイムスタンプと、performance_test(subscriber)がメッセージを受信した時のタイムスタンプの差分を使用します。 遅延の計算方法 戻ってきたメッセージを受信したPC1で、performance_test(subscribe)は、トピックのリネームを行います。これにより、performance_test(publisher)がpublishしたばかりのトピックをsubscribeすることによる無限ループを回避しています。 参考までに、performance_testは以下のように実行します。 performance_test(publisher) $ ./install/performance_test/lib/performance_test/perf_test -c ROS2 -l log -t Array1k -r 1 --max_runtime 120 --num_sub_threads 0 performance_test(subscriber) $ ./install/performance_test/lib/performance_test/perf_test -c ROS2 -l log -t Array1k -r 1 --max_runtime 120 --num_pub_threads 0 /Array1k:=/Array1k_test 計測の構成 intdash_ros2bridge intdash_ros2bridgeを使用した場合は以下のような構成になります。 intdash_ros2bridge使用時の構成 計測側のPC1では、performance_test(publisher)、performance_test(subscriber)を動作させます。 PC1のintdash_ros2bridgeは、performance_test(publisher)がpublishしたトピックを受信し(①)、受信したメッセージをFIFOに書き込みます(②)。FIFOに書かれたメッセージは弊社プロダクトの intdash Edge Agent によって読み込まれ、インターネット経由で弊社プロダクトのintdash Serverを経由し、PC2のintdash Edge Agentに渡されます(③)。 PC2のintdash Edge Agentは、受信したメッセージをFIFOに書き込みます(④)。PC2のintdash_ros2bridgeはメッセージをFIFOから読み込みpublishします(⑤)。publishされたメッセージは、intdash_ros2bridge自身によって受信されます。そして、PC2のintdash_ros2bridgeはFIFOにそのメッセージを書き込みます(⑥)。 PC1の時と同様に、メッセージはintdash Edge Agent、intdash Serverを経由して、PC1のintdash Edge Agentに届きます(⑦)。PC1のintdash Edge AgentはメッセージをFIFOに書き込みます(⑧)。PC1のintdash_ros2bridgeは、ループバックしてきたメッセージをFIFOから読み込み、トピックをリネームしてpublishします(⑨)。 performance_test(subscriber) はリネームされたメッセージを受信します。最後にperformance_test(subscriber)は、受信した時点のタイムスタンプを取得し、遅延値を計算します。 rosbridge_server rosbridge_serverを使用する場合は以下のような構成になります。 rosbridge_server使用時の構成 intdash_ros2bridgeを使う場合との大きな違いは、rosbridge_serverとintdash Edge Agentが直接メッセージをやり取りする手段を持たない点です。 これを解決するために、rosbridge connector というc++で書かれたソフトウェアを用意します。rosbridge connectorはWebSocketを使ってrosbridge_serverとメッセージのやり取りを行います。また、rosbridge connectorはFIFOへの読み書きを行うことで、intdash Edge Agentとメッセージのやり取りを行います。 PC1で、トピックをsubscribeしたrosbridge_server(①)はメッセージをJSONに変換し、WebSocket経由でrosbridge connectorに渡します(②)。rosbridge connector は渡されたメッセージをFIFOに書き込みます(③)。FIFOに書かれたメッセージがintdash Edge Agent を経由し、PC2に届く仕組みはintdash_ros2bridgeと同様です(④)。 PC2でrosbridge connectorはFIFOからメッセージを読み込みます(⑤)。rosbridge connectorは読み込んだメッセージをWebSocket経由でrosbridge serverに渡します(⑥)。rosbridge_serverは受け取ったトピックをpublishします。そしてrosbridge_server は自身がpublishしたメッセージを受信します(⑦)。そして、このメッセージはrosbridge connector、intdash Edge Agentなどを経由しPC1のrosbridge connectorに届きます(⑧、⑨、⑩、⑪)。 rosbridge connector はPC1に戻ってきたメッセージのトピックをリネームします。ここでは文字列の状態で、"topic":"topic_name" となっている部分を、"topic":"renamed_topic" に書き換えるようにしています。 rosbridge connectorはリネームしたメッセージをrosbridge_serverに渡します(⑫)。そして、rosbridge_serverはこのメッセージをpublishします(⑬)。 performance_test(subscriber) はリネームされたメッセージを受信します。最後にperformance_test(subscriber)は、受信した時点のタイムスタンプを取得し、遅延値を計算します。 計測に使用したソフトウェア rosbridge_server rosbridge_server は 実験時点の最新コミット がそのままでは使えなかったので、以下の2点の修正を加えて使用しています。 performance_test のトピックを扱えるように修正 qosをbest_effortに変更 *2 計測に使用するフォーマット 遅延を計測するためのフォーマットは、intdash_ros2bridgeはCDR *3 、rosbridge_serverはJSONを使用します。 rosbridge_serverのフォーマットは、性能の観点ではサイズが一番小さくなると考えられるCBOR *4 を使うのが望ましいと考えられます。しかしリポジトリからcloneした状態では動作しなかったので今回はJSONとの比較をしました。 本家の方のバグが直ったらCBORとの比較も行いたいと思います。 計測に使用するトピック 今回は以下の3種類のトピックで遅延時間を計測しました。 Struct256 Array1k PointCloud512k それぞれのトピックは、int64のタイムスタンプと、uint64型のID、そのトピック独自の構造を保持しています。 タイムスタンプは、 ClockCycles 関数で取得されたマシン固有のクロック値です。 計測では、トピックを発行する頻度を1 Hz、10 Hz、100 Hz、1000 Hzの4通りとしました。ただし、サイズ上計測できた上限までとなっています。 また、トピックのQoSはベストエフォートに設定しています。 そして、計測は120秒間実行しました。 Struct256 Struct256 は、256のバイトデータを含んだ構造体です 16個の1バイトの要素を持ったStruct16 という構造体を16個持っています。 Struct16 struct160 Struct16 struct161 Struct16 struct162 Struct16 struct163 Struct16 struct164 Struct16 struct165 Struct16 struct166 Struct16 struct167 Struct16 struct168 Struct16 struct169 Struct16 struct16a Struct16 struct16b Struct16 struct16c Struct16 struct16d Struct16 struct16e Struct16 struct16f int64 time uint64 id Array1k Array1k は1024個の要素を持つバイト列を含む情報です。 byte[1024] array int64 time uint64 id PointCloud512k PointCloud512k は524288個の点群データを持つデータ構造です。 点群部分の中身は、 PointCloud2 と同様です。 std_msgs/Header header uint32 height uint32 width bool is_bigendian uint32 point_step # Length of a point in bytes uint32 row_step # Length of a row in bytes uint8[524288] data # Actual point data, size is (row_step*height) bool is_dense # True if there are no invalid points int64 time uint64 id 計測に使用したマシン PC1には、Intel社のNUCというミニPCを使っています。CPUは、Core(TM) i7-8559U CPU、メモリは8GB積んでいます。 PC2の環境は、AWS EC2の c6g.xlarge というARMのインスタンスを使用しました。 計測はDocker上で行いました。 計測結果 performance_test に含まれる解析ツールから遅延の最小値、平均値、最大値が得られるため、それらを比較します。 これらの値は、PC1→PC2→PC1という往復の遅延です。なので、絶対的にXX msec速いという比較ではなく、相対的にどちらが速いかが比較できます。 Struct256 平均値を見ると、トピックのpublish頻度によらず、intdash_ros2bridgeは47.5~49.2msec、rosbridge_serverは90.2~113.3msec となっています。 ここから、Struct256 の場合、intdash_ros2bridgeの方が相対的に速いことがわかります。 Hz intdash_ros2bridge rosbridge_server min (msec) mean (msec) max (msec) min (msec) mean (msec) max (msec) 1 40.2 49.2 61.9 74.0 96.2 201.1 10 39.3 48.8 59.8 88.5 113.3 134.9 100 27.7 47.5 70.9 63.1 90.2 132.2 1000 39.5 48.9 68.3 71.5 102.3 146.3 Array1k 平均値を見ると、intdash_ros2bridgeは43.9msec~53.1msec、rosbridge_serverは96.3~111.1msec となっています。したがって、Array1kも同様に、intdash_ros2bridgeの方が相対的に速いことがわかります。 また、rosbridge_serverは1000 Hz まで頻度を上げると、計測が出来ませんでした。 Hz intdash_ros2bridge rosbridge_server min (msec) mean (msec) max (msec) min (msec) mean (msec) max (msec) 1 34.1 44.6 65.6 62.35 96.3 146.6 10 43.1 53.1 63.2 89.5 111.1 130.6 100 37.6 43.9 60.9 64.2 98.2 147.1 1000 39.8 50.8 75.2 計測不能 計測不能 計測不能 PointCloud512k 平均値を見ると、intdash_ros2bridgeは181.6~189.1msec、rosbridge_serverは 389.3msecとなっています。 PointCloud512kの場合も、intdash_ros2bridgeの方が相対的に速いことがわかりました。 Hz intdash_ros2bridge rosbridge_server min (msec) mean (msec) max (msec) min (msec) mean (msec) max (msec) 1 178.1 181.6 394.2 340 389.3 621.3 10 177.3 189.1 204.9 計測不能 計測不能 計測不能 これらの結果から、 intdash_ros2bridgeの方が、rosbridge_serverよりも低遅延でのROS2のブリッジを実現できる と言えそうです。 考察 ペイロードのサイズ確認 ブリッジされたトピックのサイズを確認します。 トピック intdash_ros2bridge(CDR) rosbridge_server(JSON) Array1k 1.06KByte 10.10KByte Struct256 583Byte 5.89 KByte PointCloud512k 512.27 KByte 683.37 KByte ここから、やはりバイナリ形式であるCDR形式の方が、効率的なデータ伝送が出来ている可能性がありそうです。 ただし、PointCloud512kだけは、intdash_ros2bridgeとrosbridge_serverの出力するトピックのペイロードサイズの差が小さい結果になりました。 これは、rosbridge_serverがuint8の配列をbase64エンコーディングしているのが理由です。 rosbridge connector 処理時間 rosbridge_serverとintdash Edge Agentの間のrosbridge connectorは実験用に開発したものです。これがボトルネックになっていないことも確認しておきます。 rosbridge connectorの処理は、WebSocket経由でメッセージを受信してFIFOに書き込む処理と、FIFOからメッセージを読み込んでWebSocketに渡す処理が行われています。 実際の実行時間は以下のようになります。 トピックの種類 WebSocket受信からFIFOに書き込む前まで FIFOから読み込んでからWebSocketに渡すまで Array1K 0.2msec 3msec Struct256 0.1msec 2msec PointCloud512k 5msec 55msec ここから、大きなボトルネックにはなっていないと言えそうです。 まとめ 本記事では、ROS2メッセージの遠隔リアルタイムデータ伝送を実現する新プロダクト「intdash_ros2bridge」と、rosbridge_serverのメッセージ伝送時の遅延時間比較を行いました。 実験の結果から intdash_ros2bridgeの方が、rosbridge_serverよりも低遅延でのROS2のブリッジを実現できる と言えそうです。 弊社では現在のintdash_ros2bridgeの利用拡大やROSコミュニティへの貢献を視野に入れたOSS化に向けた計画も進めています。 プロダクト開発の進捗やOSS化の進捗がありましたらまた続報をお届けできればと思います。 最後までご覧いただきありがとうございました。 *1 : 弊社では、 ROSの任意トピックをC++ノードでPublish/Subscribeする方法 - aptpod Tech Blog に書かれている技術を用いた intdash_bridge というROS1で離れた2つのROS1空間をつなぐプロダクトを提供しています。 *2 : その後対応されたようです。 https://github.com/RobotWebTools/rosbridge_suite/commits/ros2 *3 : rosbag2 でも使われている バイナリ形式に近いフォーマット。詳細は ROS2で任意のメッセージをC++ノードでブリッジする方法 - aptpod Tech Blog で紹介しています。 *4 : RFC 8949 Concise Binary Object Representation っで規定されている格納効率の良いバイナリフォーマット
アバター
aptpod Advent Calendar 2021 の 16日目を担当する、プロジェクト開発グループの 松下 です。 本記事では、PythonでBluetooth Low Energy (BLE)のデバイスからデータを収集するGATTのクライアントアプリを実装したので紹介します。 背景 BlueZでの GATT Clientの実装について モチベーション 想定読者 プログラムの紹介 本プログラムの特徴 実行環境 事前準備 スクリプトの実行 接続処理 接続後 工夫したポイント デバイス一覧の取得と、サポートしている GATT Serviceの判定 接続機能 自動接続機能 GATTの受信処理 免責事項 まとめ 背景 2018年の弊社のAdvent Calenderにて BlueZのAPI/サンプルコードのメモ と題して、BlueZの簡単な紹介をしてみました。 おかげさまで、今でも定期的に LGTM を頂いている記事になっています。 この記事の最後で、 次回は、実際にこのサンプルコードを使って、ラズパイに接続した温度センサーの値を通知するGATT Serverを構築できればと思います。 と書いていたのですが、更新する暇もなく放置していたのが心残りでした。 その後、BLE の GATT を使って市販のセンサー(心拍とか)の値を取得する機会があり、BlueZでGATT Clientを実装する事にしました。 BlueZでの GATT Clientの実装について BlueZを使ってGATTのclientを実装する場合、blueZの testフォルダにある example-gatt-client がサンプルコードとして提供されています。 このコードを見れば、 dbus-pythonを使ってblueZ経由で GATTのServiceおよびCharacteristicを扱うテクニックがわかります。 モチベーション しかし、このサンプルコードは bluetoothctl 等で事前にBluetooth自体が接続されている状態で、サンプルコードを実行する必要があります。 毎回手動でBluetooth接続してから、このサンプルを実行してもいいのですが、ラズパイの電源を入れたら自動で接続してデータを取得したいと考えました。 更に、複数のServiceを扱うのが難しい実装であり、複数のデバイスに対する同時接続もしたいと考えました。 結構ニーズがありそうなモチベーションなのですが、調べてみてもあまりサンプルコードがみつかりませんでした。 そこで、今回は実際にPythonでスクリプトを作成してみたので、スクリプトの簡単な動作の説明と、作成にあたって工夫した(ハマった)点を紹介したいと思います。 想定読者 BluetoothのGATTの基礎知識がある方 blueZの test/example-gatt-client をベースに実装したいが、更にProfileの追加、自動接続や同時接続を実装したい方 blueZのdbus-pythonの コード がなんとなく読める方 1 筆者はあまりpythonは得意ではないので、書き方は初心者丸出しです。温かい目で見てくれる方 プログラムの紹介 ソースコードは github に公開していますので、参考にして頂ければと思います。 本プログラムの特徴 自動接続機能に対応 複数デバイスに同時接続可能 以下のProfileに対応 Cycling Speed and Cadence Profile Heart Rate Profile 以下のServiceに対応 Cycling Speed and Cadence Service Heart Rate Service Device ID Service Battery Service GATT の Read および Notify に対応 ただし、以下の制限事項があります。 同一プロファイルのデバイスを複数同時に接続できません。(各Profileに対してそれぞれ1台接続します) 一部のOptionのパラメータや Writeなどは非対応です。 blueZ 5.50の場合、Batteryの通知を受け取れません。 2 実行環境 筆者の環境は以下の通りです。 RaspberryPi 4B+ (Raspbian OS) 3 blueZ 5.50 / 5.58 python 3.5.5 dbus-python 4 事前準備 今回のプログラムを実行する前に、事前に接続したいBLEデバイスを探索 (Scan) し、blueZのデバイスリストに登録しておく必要があります。 まずは bluetoothctl を開きます 5 $ bluetoothctl 次に、 scan on で周辺のデバイスを探索します。 [bluetooth]# scan on お目当てのデバイスがみつかりました。 [NEW] Device F9:04:D8:2B:5E:8E SPD-BLE0047853 [bluetooth]# devices Device 00:12:A1:70:3F:26 bluetooth ... Device F9:04:D8:2B:5E:8E SPD-BLE0047853 発見したデバイスはペアリング済みではないため、一定時間が経過すると消えてしまいます。 [DEL] Device F9:04:D8:2B:5E:8E SPD-BLE0047853 そこで、 trust を onにした上で [bluetooth]# trust F9:04:D8:2B:5E:8E [CHG] Device F9:04:D8:2B:5E:8E Trusted: yes Changing F9:04:D8:2B:5E:8E trust succeeded 一回 connect で接続しておきます。 [bluetooth]# connect F9:04:D8:2B:5E:8E Attempting to connect to F9:04:D8:2B:5E:8E [CHG] Device F9:04:D8:2B:5E:8E Connected: yes Connection successful [NEW] Primary Service /org/bluez/hci0/dev_F9_04_D8_2B_5E_8E/service000a 00001801-0000-1000-8000-00805f9b34fb Generic Attribute Profile [NEW] Characteristic /org/bluez/hci0/dev_F9_04_D8_2B_5E_8E/service000a/char000b 00002a05-0000-1000-8000-00805f9b34fb Service Changed ... こうする事で、意図的に remove しない限り、ホスト側のデバイスリストからは削除されなくなります。 本来は、ペアリングしたいのですが、agentを使った SSPの挙動が不安定で困っていたところ、この手順にたどり着きました。(デバイスリストから消えなければOKなので) スクリプトの実行 以下の通り、スクリプトを開始します。 6 $ ./ble_client.py 開始すると、最初にbluetoothのpower をOFF/ONして、諸々リセットします。(不要であれば、コメントアウトしてもいいです) power off power on 接続処理 まずは、ホスト側に登録されているデバイスリストから、サポートしているServiceの属性を持つデバイスをピックアップし、接続先テーブルを構築します。 connection_table: {'HRM': '/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F', 'SPEED': '/org/bluez/hci0/dev_F9_04_D8_2B_5E_8E'} HRM:/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F SPEED:/org/bluez/hci0/dev_F9_04_D8_2B_5E_8E その後、 device_connect_thread() にて接続テーブルの中で未接続のデバイスを見つけて、自動で接続していきます。 mainloop.run() start SCAN *************** can't find device near side: /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F can't find device near side: /org/bluez/hci0/dev_F9_04_D8_2B_5E_8E RSSI=-59 /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F start connect /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F connecting..../org/bluez/hci0/dev_DF_78_9E_7E_D3_7F connecting..../org/bluez/hci0/dev_DF_78_9E_7E_D3_7F 接続に成功してもすぐに処理は開始せず、Device Interface の Connected が Trueになるのを待ちます。この時、blueZは SDPによって接続したデバイスのService & Characteristic の情報を取得しているようです。 connection successful Service resolved! :/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F その後、接続したデバイスの D-busのdeviceのpathを手がかりに、 BlueZの interface org.bluez.GattService1 に関係するobjectを取得します。 これが、接続したい対象のServiceであった場合は、更に interface org.bluez.GattCharacteristic1 に関係するobjectを取得し、Serviceが持っているCharacteristicの属性(uuidなどProperty)を取得していきます。 以下のログは、Device ID Service (uuid = 0x180a )と Heart Rate Service ( uuid = 0x180d )が取得でき、各Serviceの CharacteristicのUUIDとFlagを取得しています。 configure_service(/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F HRM) ********************************************** detect service:0000180a-0000-1000-8000-00805f9b34fb SERVICE_DEVICE /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018 ==================== ============ 00002a28-0000-1000-8000-00805f9b34fb DEV_SW_REV /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001f Flag: dbus.Array([dbus.String('read')], signature=dbus.Signature('s'), variant_level=1) ============ 00002a26-0000-1000-8000-00805f9b34fb DEV_FW_REV /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001d Flag: dbus.Array([dbus.String('read')], signature=dbus.Signature('s'), variant_level=1) ============ 00002a27-0000-1000-8000-00805f9b34fb DEV_HW_REV /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001b Flag: dbus.Array([dbus.String('read')], signature=dbus.Signature('s'), variant_level=1) ============ 00002a29-0000-1000-8000-00805f9b34fb DEV_MANFUC /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char0019 Flag: dbus.Array([dbus.String('read')], signature=dbus.Signature('s'), variant_level=1) detect service:0000180d-0000-1000-8000-00805f9b34fb SERVICE_HRM /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service000e ==================== ============ 00002a38-0000-1000-8000-00805f9b34fb SNSR_LOC /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service000e/char0012 Flag: dbus.Array([dbus.String('read')], signature=dbus.Signature('s'), variant_level=1) ============ 00002a37-0000-1000-8000-00805f9b34fb HR_MEAS /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service000e/char000f Flag: dbus.Array([dbus.String('notify')], signature=dbus.Signature('s'), variant_level=1) configured service success: /org/bluez/hci0/dev_DF_78_9E_7E_D3_7F そして、dbus上のCharacteristic のpathとuuidの紐づけ情報を事前に保持しておきます。 接続後 接続に成功すると、 Readの属性を持つCharacteristicに対して Readを要求し、順次データを取得していきます。 同時に、Notifyの属性を持つCharacteristicに対しては Notify の開始を要求します。 以下の例では、各デバイスの device IDをReadして表示しています。 hrm_dev_sw_rev -> HR40 V1.0.0 speed_snsr_loc -> 0 /org/bluez/hci0/dev_F9_04_D8_2B_5E_8E/service0025/char0028 Notifying = 1 notifications enabled speed_csc_feat -> 1 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 all device is connected hrm_dev_fw_rev -> HR40 V1.0.0 speed_dev_sw_rev -> 2.30.0 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 speed_dev_fw_rev -> 2.30.0 speed_dev_hw_rev -> 3 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 hrm_dev_hw_rev -> HR40 V1.0.0 speed_dev_serial -> 3335568109 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 speed_dev_model -> 3192 speed_dev_manfuc -> Garmin speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 hrm_dev_manfuc -> iGPSPORT speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 hrm_snsr_loc -> 1 speed_csc_meas -> wh_rev:13100 update_time:16.8134765625 all device is connected また、Notify の通知データをパースして、値をログで出力しています。 以下の例では、心拍センサーデバイスが通知してくる心拍数と、ケイデンスメータが通知してくる回転数を出力しています。 all device is connected hrm_hr_meas -> hr_meas = 99 bpm speed_csc_meas -> wh_rev:13093 update_time:10.0107421875 speed_csc_meas -> wh_rev:13094 update_time:10.9296875 hrm_hr_meas -> hr_meas = 97 bpm speed_csc_meas -> wh_rev:13094 update_time:10.9296875 hrm_hr_meas -> hr_meas = 97 bpm speed_csc_meas -> wh_rev:13095 update_time:11.98046875 speed_csc_meas -> wh_rev:13096 update_time:12.853515625 hrm_hr_meas -> hr_meas = 94 bpm speed_csc_meas -> wh_rev:13096 update_time:12.853515625 hrm_hr_meas -> hr_meas = 91 bpm speed_csc_meas -> wh_rev:13098 update_time:14.701171875 speed_csc_meas -> wh_rev:13098 update_time:14.701171875 ここまで動けば、後は煮るなり焼くなり好きにできます。 工夫したポイント デバイス一覧の取得と、サポートしている GATT Serviceの判定 デバイスリストの取得は blueZの test/list-device を参考にしました。 最終的には、以下のようなコードを実装しました。 DeviceのInterfaceのUUIDを見ることで、このデバイスがサポートしている Serviceと Characteristicを特定できます。 7 下記のコードでは、 Heart Rateと Cycling and Cadence のServiceのUUIDを持つデバイスのobject pathを dict で保持しています。 objects = get_managed_objects() all_devices = ( str (path) for path, interfaces in objects.items() if IFACE_DEVICE in interfaces.keys()) connection_table = {} for device_path in all_devices: dev = objects[device_path] properties = dev[IFACE_DEVICE] uuids = properties[ "UUIDs" ] for uuid in uuids: if uuid == UUID.SERVICE_HRM: connection_table[ "HRM" ] = device_path if uuid == UUID.SERVICE_SPEED: connection_table[ "SPEED" ] = device_path 接続機能 そもそも、PythonからどうやってBluetoothを接続するのでしょう。 BlueZの doc/device-api.txt に答えがありました。 Service org.bluez Interface org.bluez.Device1 Object path [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX Methods void Connect() This is a generic method to connect any profiles the remote device supports that can be connected to and have been flagged as auto-connectable on our side. If only subset of profiles is already connected it will try to connect currently disconnected ones. 実際に test/simple-agent を見ると、dbus-pythonでの呼び出し例が書いてあります。 def dev_connect (path): dev = dbus.Interface(bus.get_object( "org.bluez" , path), "org.bluez.Device1" ) dev.Connect() なるほど、pathに紐づく org.bluez.Device1 のobjectを取得して Connect() を呼べばよさそうです。 実際に試したところ、この書き方だと Connect() が同期処理になってしまう 8 ため、非同期処理にしたいと考えました。 最終的に、以下のように実装しました。 reply_handler , error_handler を指定する事で、接続処理が終了した時に非同期でhandlerが呼ばれます。 IFACE_DEVICE = 'org.bluez.Device1' def fetch_object (path): try : return bus.get_object(BLUEZ_SERVICE_NAME, path) except Exception as e: LOG.error( "faital error in fetch_object({}): {}" .format(path, str (e))) mainloop.quit() def device_connect (device_path): # Connect Device if no connected dev_object = fetch_object(device_path) dev_iface = dbus.Interface(dev_object, IFACE_DEVICE) dev_iface.Connect(reply_handler=device_connect_cb, error_handler=device_connect_error_cb, dbus_interface=IFACE_DEVICE) device_connect(dev_path) 自動接続機能 自動接続は接続対象のデバイスをテーブル化して、ループで順番に接続処理を投げ続ける。という方式を考えました。 しかし、電源が入っていないデバイスに対して、連続して Connectを要求し続けると、BlueZの内部状態がおかしくなってしまうのか、正常に動作しなくなってしまう(接続ができなくなる)問題が発生しました。 そこで、接続処理の前に scan を実行し、周辺にデバイスが存在する事を事前に確認してから、接続する処理に変更しました。 周辺にデバイスが存在する事の判断材料は、Device Interfaceで取得できる RSSI に注目しました。 scanした結果、デバイスから応答があると、DeviceのPropertyに RSSI が追加されます。すなわち、周辺に電源が入った(接続待ちの)状態で存在すると判定しています。 int16 RSSI [readonly, optional] Received Signal Strength Indicator of the remote device (inquiry or advertising). 最終的には、以下のようなコードになりました。 def is_alive_device (device_path): dev_props = fetch_property(device_path, IFACE_DEVICE) if dev_props is None : return False rssi = dev_props.get( "RSSI" , None ) if rssi: return True else : return False return False while True : if is_connected_device(dev_path) is True : LOG.info( "already connected {}" .format(dev_path)) continue if not is_alive_device(dev_path): LOG.info( "can't find device near side: {}" .format(dev_path)) continue device_connect(dev_path, profile_key) GATTの受信処理 BLEのGATTの受信メッセージ (Read, Notify)は、D-bus上では PropertiesChanged で通知されるようです。 そこで、D-bus上の PropertisChanged をキャッチする Signal Receiver を登録します。 bus.add_signal_receiver(property_changed, bus_name= "org.bluez" , dbus_interface= "org.freedesktop.DBus.Properties" , signal_name= "PropertiesChanged" , path_keyword= "path" ) 登録する関数は以下の通りです。 def property_changed (interface, changed, invalidated, path): if interface != IFACE_GATT_CHRC: return if not len (changed): return notify = changed.get( 'Notifying' , None ) value = changed.get( 'Value' , None ) if notify: LOG.info( "{} Notifying = {}" .format(path, notify)) return if not value: LOG.warning( "value is None" ) return recv_message_queue.put((path, value)) このハンドラを経由して、BLEの通信で Read / Notify を受信すると、dbusのメッセージから interface, path, value が取得できます。 しかし、pathだけでは、この通知がどの Characteristic によるものなのかがわかりません。 取得したpathがどの Characteristic (UUID)を持っているのかを、都度 D-Bus経由でBlueZに問い合わせてもいいのですが、ちょっと無駄な処理に感じます。 そこで、接続時に事前にobjectを探索して、pathとuuidの関係を保持しています。 これにより、このSignalのpathからどの Characteristicの受信データなのかを簡単に紐づけています。 path_uuid_dict= { "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001f" : "00002a28-0000-1000-8000-00805f9b34fb" , "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001d" : "00002a26-0000-1000-8000-00805f9b34fb" , "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char001b" : "00002a27-0000-1000-8000-00805f9b34fb" , "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service0018/char0019" : "00002a29-0000-1000-8000-00805f9b34fb" , "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service000e/char0012" : "00002a38-0000-1000-8000-00805f9b34fb" , "/org/bluez/hci0/dev_DF_78_9E_7E_D3_7F/service000e/char000f" : "00002a37-0000-1000-8000-00805f9b34fb" } 受け取ったPropertyChangeのSignalがGATT関連の通知の場合は、 queueにpathとvalueを入れておきます。 別のthreadでqueueからpathとvalueを取り出します。 path, value = recv_message_queue.get() 接続時に生成した pathに紐づくCharacteristicのUUIDを探索し、 uuid = path_uuid_dict.get(path, None ) そのCharacteristic毎に定義したパース用の関数を呼び出しています。 if uuid not in ble_parser.uuid_to_parser_dict: LOG.warning( "Unknown uuid in cb_table: {}" .format(uuid)) return parse_func = ble_parser.uuid_to_parser_dict[uuid] parse_func( id , value) 関数は関数テーブルの仕組みを使う事で、パース処理を共通化しています 9 。 uuid_to_parser_dict = { # Heart Rate UUID.CHRC_HRM_HR_MEAS: parse_hrm_meas, UUID.CHRC_HRM_SNSR_LOC: parse_integer, # Common Characteristic UUID.CHRC_SNSR_LOC: parse_integer, # Cycling Speed and Cadence UUID.CHRC_SPEED_CSC_MEAS: parse_speed_csc_meas, UUID.CHRC_SPEED_CSC_FEAT: parse_integer, # Device Information UUID.CHRC_DEVICE_SYSTEM_ID: parse_integer, UUID.CHRC_DEVICE_MODEL: parse_string, UUID.CHRC_DEVICE_SERIAL: parse_string, UUID.CHRC_DEVICE_FW_REV: parse_string, UUID.CHRC_DEVICE_HW_REV: parse_string, UUID.CHRC_DEVICE_SW_REV: parse_string, UUID.CHRC_DEVICE_MANFUC: parse_string, # Battery UUID.CHRC_BATTERY_LEVEL: parse_integer, } 免責事項 今回公開したサンプルコードは、あくまでも本記事の参考資料です。 弊社は本サンプルコードの動作保証・サポートは致しかねます。 まとめ 今回対応したProfileはHeart Rateと Cycling Speed and Cadence だけでしたが、他のProfileの追加もパース関数を実装して登録すれば可能かと思います。 少なくとも、自動接続や BlueZの使い方など、PythonでBluetoothを扱うプログラミングの参考になれば幸いです。 最後になりますが、BLEデバイスのデータ収集&遠隔からの可視化などのお仕事も募集中です! お問い合わせは こちら まで! 最初に dbus-python tutorial でノリをつかんでから、blueZの test/ のサンプルコードを見ると理解が進みます。 ↩ BatteryはD-Bus上独自の扱いになっていたのですが、5.56から Add support for battery D-Bus interface. と、他のキャラクタリスティックと同様に扱うことができるようになりました。 ↩ RaspbianのblueZでは、Notifyの通知を受け取れたのですが、Lenovo Thinkpad X1 CarbonのblueZでは Notifyを受信できませんでした… 謎。BlueZのversionを変えても違いが無かったので、別の要因だと思われます。だれか、原因わかる方がいれば教えてください… ↩ $ sudo apt-get install python-dbus で事前にインストールしてください。 ↩ Raspbianの場合は sudo が必要っぽいです。 ↩ Raspbianの場合、sudoで実行する必要があるかもしれません。 ↩ uuidの定義は bluetooth.com の 16-bit UUIDs から確認できます。 ↩ 同期処理で実行すると、他のthreadなどが並列動作してくれませんでした。 GObjectってこういうものなのか.. ↩ Device IDやBatteryの各Characteristicは1個の数字 or 文字列のみというパターンが多いです。ただ、Notify時は1個のパケットに複数のパラメータをのせる仕様が多いため、個別にパース処理を実装する必要があります。 ↩
アバター
Advent Calendar 2021 15日目を担当します、QAエンジニアの板倉です。 アプトポッドのQAって何するの?私が入社する前にホームページを見て思ったことです。 弊社の製品ページを見て同じように感じるQAエンジニアは多いのではないかと思います。 そこで普段アプトポッドのQAがどんな仕事をしているのか紹介させて頂きます。 まずは製品ページの一文を紹介します。 intdashとは intdashは、100ミリ秒∼1ミリ秒間隔程度の高頻度で発生する時系列データを モバイル網などのベストエフォート型ネットワークを介して、 高速・大容量かつ安定的にストリーミングするための双方向データ伝送プラットフォームです。 www.aptpod.co.jp ふむふむなるほど、、、でQAはどう関与していけばいいの??  QA経験者が最初に身構えるところではないでしょうか。 それでは、入社時のアプトポッド第一印象を踏まえながら実際の作業について記載させて頂きます。 【自己紹介】 【アプトポッドの第一印象】  アプトポッドの強み  スピード感 【QA作業について】  プロダクト  プロジェクト 【QAメンバーに求められる人物像】 【最後に】 【自己紹介】 最初に私の経歴等を紹介させて頂きます。 ・家電メーカー(自動車部門)の品質管理部 不具合品の一次解析という、ともかく不具合を再現&怪しいところを突き止めろ!という仕事。 カーメーカー関連の純正返品は回答必須、カー用品店からの市場返品は回答必要ではないのですが、とにかく不具合情報が無く「動作不良」という内容だけ、、、 品質の向上はまず自分が再現出来るかにかかっている、という軽いプレッシャーはありましたが、それでもある意味脱出ゲームの様な面白さに目覚め始めた頃でした。 ・テストエンジニアの世界に本格的に入り込む その後、世間的に派遣への格差が強い時代に突入。このままじゃいかんと転職を試みました。 ただ、これ又たまたま入った会社が同じ家電メーカーから業務委託を受けているテストエンジニアの会社。 経験が活きると共に、テスト作成やマネジメント、テスト方針の立案・決定、チームのハンドリングと新たにやる事が増えましたが、それでも品質管理にのめり込んでいく毎日でした。 そしてテスターあるある。高稼働過ぎる。。。 結婚して会社から家が遠くなると、早朝に出勤(早朝電車は席取りでみんなギスギス)、帰りは終電ギリギリの午前様。家についても脳が覚醒したままで中々眠れない=睡眠2時間 という日が続く様になりました。 会社でめまいをしながら分刻みのスケジュール、、、 これが自分だけならいいのですが、付き合う家族も体調を崩し始め、退職を決意。 それから紆余曲折を経て入社した会社から、昨年6月に出向した先が アプトポッド でした。 (出向→入社については後述とさせて頂きます) 【アプトポッドの第一印象】 まず最初に思ったこと。やっていることが難しそうで自分が役に立てるのかな、、、でした。 基本的にブラックボックステストを中心としたシステムテストを行っていたので、ソフトウェア知識は乏しく品質の保証するべきところを見れないのではないか?という不安がいっぱいでした。 では実際どうなのか。確かに知識や経験があるほうが内容の理解やシステムの弱点、QAが力を入れるところなどをいち早くキャッチする事ができ、スムーズに仕事が進むと思います。 ただ、仕組みの理解が出来れば深い知識が無くても仕事は可能でした。  アプトポッドの強み 上記の「仕組みが理解できればまずは大丈夫」というのはどういう事なのか。 それはアプトポッドがSA・PM、開発チームなど関係する人々でワンチームとして活動するところにあります。 一つのプロジェクトに対し、小規模であれば関係部署から一名ずつアサインされる小さなチーム編成、大きなプロジェクトならば各部署から数名となる事もありますが、基本的にはQAは一名orサポートメンバー追加の2名体制。 なので一人で対応する事が多い。 それでも大丈夫だと言えるのは、チーム全体で品質に対する意識が高く、成功させる為のチームワークが非常に強力だからです。 例えばソフトウェア内部の動作確認を重点的に行う必要がある場合は、どのように確認すべきか、どんなツールを使うと良いか、を開発者から情報展開してもらったり合同テストの実施、時にはQA方法についてミーティングを開いてもらったりとフォロー体制がすごく厚いと感じる場面がとても多いです。 テスター業界にありがちな(教本にも載っていたり、、、)    "開発の敵役"になる事がない のは、私が入社して一番驚いたことでした。 更に「テストしてくれている」と言われたのは今までのQA業務で初めての経験です。  スピード感 協力体制がしっかりしている=期間的に余裕が多いから? と思いがちですが、実際のところ仕事のスピード感は早いと感じています。 どちらかと言うと個人個人の能力が高くて仕事が早い!大企業ではみれないメリット部分でもありますが、問題発生から解決までのスピードも最初感動してしまうくらいでした。 勿論QAにもスピード感は必要になるのでエンドユーザーがどんな人物で、どんなことをしたい製品なのかを考え、補償すべき点・重点的にテストする点などを的確に判断することは求められます。 【QA作業について】 ここでは実際の作業について紹介したいと思います。 区分は大きく分けて2つ。 ① ベースとなる製品の開発 :プロダクト ② お客様案件に合わせてカスタマイズを施す:プロジェクト   プロダクト 四半期に分けて製品開発が行われます。 既存機能への追加・新機能の追加・新しい製品の誕生 と都度折り込まれる機能は盛りだくさん! QAは機能ごとメンバーで担当を決め各々で 詳細のヒアリング・テスト範囲の合意・テスト設計が終わればレビュー依頼 といった流れで開発者と連携をしていきます。 その為、テスト完了日にはその開発者と近い関係になれていることも多くあります。 また、intdashは車両データを取得し可視化することが機能の根幹ですので(カスタム次第で変化しますが)走行テストも行っています。 走行テストはオリンピック会場の国立競技場近くを走行する近距離走行と、箱根ターンパイクまで向かう長距離走行があります。 ドライバーと可視化データを監視するメンバーの二名体制で、各テスト期間2〜3回は行われます。有名な銀杏並木や芦ノ湖を季節毎に見れるのは良い気分転換にもなります。 先日はどうしても気になるポイントがあったので原因追求のため社用車で通勤し測定データを溜めることもしました。 机上だけでは判らない事も多く、そこを突き詰めるのも走行テストの楽しさの一部です。  プロジェクト ベースとなる製品はミドルウェアなので、お客様のご要望に対応してカスタマイズすることが可能です。 遠隔操作・自動検出・データ測定などなど… 基本は同じでも全く違う製品が誕生するのがこの会社の面白いところ。 その為、プロジェクト毎にQAのアプローチの仕方も異なります。 じゃあどんな対応をしようか・どこを重点的に進めるか・この仕様は問題ない?を考えるところにQA経験やスキルを求められると感じます。 ※カスタマイズの一例はこちらをご参照ください。 www.aptpod.co.jp プロジェクトはこの他にも出荷テストを行うフェーズがあります。 Webアプリのみ納品する場合はリモート作業での出荷確認が可能ですが、多くの場合はまずintdashの導入になる為、 エッジコンピュータ や周辺機器といった”モノ”の納品が必要です。 その為、QAチームの出社率は高めですが、フレックスタイム制で無理なく通勤することが可能です。 【QAメンバーに求められる人物像】 アプトポッドで働き始めて1年半ほどになり私が感じたQAメンバーに求められる人物像は、  ・協調性がある  ・建設的である  ・気遣いができる  ・視野が広い  ・柔軟性がある  ・想像力が高い といった人間性の部分が大切かなと思いました。 勿論スキル面でいえばテストスキルやマネジメント経験、改善活動の推進力、幅広い知識があれば尚可ではありますが、正直なところアプトポッドの開発陣はレベルが高い!スピードも速い!ので QAは「ユーザーがどう使うのか、この仕様で困らないか」というお客様の困りごとに気づけることのほうが大切だと感じます。 難しいことを解消するために協力する文化はこの会社には備わっているので、どちらかと言えば当たり前のことに気づく=母親の「ハンカチ持った?」が出来ることが全体の助けになると思います。 また、特にプロダクトではQAの「こうだったらいいのにな」という意見が通りやすく、そこがQAに期待されている部分でもあります。 その為、テスト設計・実行ができるだけではなく主観的・客観的・俯瞰的に物事を捉え、考え想像できる、発言できる人にとってはやりがいを感じる職場だと思います。 【最後に】 私は昨年6月にアプトポッドに出向となり参入、もともと新しい物に興味があり次々と新技術・新製品が生み出されていく日々に魅了されていったこともありますが、なによりQAとしての働きやすさ・人間関係の素晴らしさに惹かれ「もっとこの会社に携わりたい」との思いで今年7月、転職に踏み切りました。 自由さがある中でもちゃんとしたルール・規則があるので、私は通勤も長時間ですがそれでも前職の出社時間・帰宅時間よりもかなり余裕を持てるようになり、転職して公私共に本当に良かったと思っています。 覚えることは勿論多いですがそれを楽しく思えたり、人付き合いが好きだったり、こうなったら良いのになという想像力が高い人には かなりオススメの職場 です。 もし少しでもやってみようかな、と興味をお持ち頂けましたらお気軽にご応募ください。 一緒に品質を高めましょう! アプリだけでなくクルマ好き・メカ好きの方もお待ちしています! 先進技術に関われるなんて面白すぎる! QA関係者に是非ご覧頂きたい、求人情報です!  ↓ open.talentio.com QA以外の職種も勿論募集しております! www.aptpod.co.jp
アバター