アプトポッドのブログ - TECH PLAY

TECH PLAY

アプトポッド

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

254

はじめに こんにちは、 aptpod Advent Calendar 2020 の21日目を担当するハードウェアグループの おおひら です。 もう今年の稼働日もあと数日になりましたね。 例年、年末年始のお休みにむけて帰省や旅行を計画される時期と思いますが、今年はコロナウイルスの感染拡大もあって自宅でお過ごしになる方も多いと思います。本文に入る前のアイスブレイクとしてNetflixの最近のお勧めをひとつご紹介します。 www.netflix.com 『スタートアップ×少女漫画』とでも言えばいいのか(いや、どちらかというと優先度的に並びが逆で『少女漫画×スタートアップ』か…?)、私は韓国ドラマを観るのは初めてなんですが、コテコテな人情・恋愛要素を入れながらもスタートアップ/ベンチャーでよく聞く話がひとしきり押さえられていて面白いです。男性諸氏におかれましては夜中にお酒を飲みながらひとりで観ることをお勧めします(既婚の身で妻と一緒に観るのはすごく恥ずかしいぞ😇) 閑話休題。 さて、本記事では 日置電機株式会社 様の製品である 【非接触CANセンサー SP7001】 を、弊社の自動車計測のためのパッケージソリューション Automotive Pro と組合わせて評価した結果を紹介させていただきます。なお本記事はハードウェアグループおよびソリューションアーキテクトの複数メンバー *1 のコラボレーション記事です。 はじめに 非接触CANセンサーの説明 検証車両への機器設置 Automotive Proの紹介 計測システムへの非接触CANセンサーの追加 計測データの可視化 Visual M2M Data Visualizer CSVダウンロード おわりに 非接触CANセンサーの説明 www.hioki.co.jp 日置電機株式会社様からご提供いただいた非接触CANセンサーの詳細に関しては上記の製品HPをご確認ください。簡単に特徴を述べさせていただくと以下の通りです。 ハーネスに特別な加工や改造をしなくても被覆ごと挟むことでCAN信号を計測できるため、試作車両や市販車に対する計測の工数削減が可能 CAN/CAN-FDバスに対して電気的な影響を与えず信号品質劣化のリスクが低いため、公道でのテストと計測における安全性の確保につながる CANの極性検知は自動検知もOK プロービングした信号がそのままD-Sub 9ピンのコネクタから出力されるので、他社製の既成CANロガーを利用してデータ計測が可能 通常、車両内部の制御信号はゲートウェイECUがあるために一般ユーザーは直接アクセスできません。弊社の過去の記事でも、Raspberry Piを利用してOBDⅡポートから車両の情報を取得する試みが掲載されましたが、こういった整備端子を経由する通信はリクエスト&レスポンスのやりとりによって得られるものであり、リアルタイム性やサンプリング周期の観点で車両本体の制御情報とは隔たりがあります。 tech.aptpod.co.jp そこで非接触CANセンサーの出番というわけですね。 特に近年、CASEと呼ばれる技術革新のトレンドに伴って完成車メーカー様以外の企業様でも車両の制御信号を計測したいというニーズが高まっており、弊社としても高い関心を持って試用をさせていただきました。 以下に弊社の計測システムを利用して、非接触CANセンサーから得られた計測データのリアルタイム性や一致性を検証した結果をご紹介させていただきます。 検証車両への機器設置 Automotive Proの紹介 Automotive Pro は弊社が自動車の開発に携わるお客様向けに販売しているパッケージソリューションです。LTE通信機能とGNSS測位機能を持つ車載コンピューターに弊社のソフトウェア一式をインストールし、サーバー環境も含めて構築した状態でご提供 *2 しますので、お客様は面倒な設定作業なく即日車両計測を開始することが可能です。ハードウェアとしては前述の車載コンピューター、車室外を撮影するカメラ、およびCAN-USBインターフェースを利用して、動画とCANバスの制御信号を統合した時系列で計測することができます。さらにお客様のご要望にあわせてiPhoneアプリを利用した計測も実現可能です( intdash Motion 2.0のニュースリリース 参照)。 aptpodが提供するパッケージソリューション "Automotive Pro" 今回はこのCAN-USBインターフェースに日置電機様の非接触CANセンサーの出力を接続しました。 計測システムへの非接触CANセンサーの追加 計測システムの模式図を下に示します。 CANバス計測システムの模式図 弊社が普段の開発業務やお客様へのデモに利用している社有車は特別に車両のCANバスが取り出せるようにハーネスが改修されており、ここから得られる3系統のCANバス(図中のA,B,C)を2つのCAN-USBインターフェースで取得します。1つのCAN-USBインターフェースにつき2チャンネルのCANバスが取得可能ですので、余ったチャンネル1つに非接触CANセンサーの出力を接続し、通常のハーネス経由で計測したCANバスとのデータの差分を確認します。なお、弊社自社設計のCAN-USBインターフェースは複数の機器間で時刻同期をするためのクロック共有機能があり、各チャンネルの計測時刻を10マイクロ秒の精度 *3 で打刻することができます。 実車の写真は以下の通りです。非接触CANセンサーの電源は車両のシガーソケットから給電し、CANバスの入力極性モード設定は "FIXED"にしました。 物理的な接続が完了したら、続いてエッジ側の機器群(ターミナルシステム)の設定を行います。本稿では詳細を割愛させていただきますが、Linux OSで動作する intdash Edge というミドルウェアに対して、GUIでCANバスのチャンネル追加やサンプリングレートの設定変更が可能です。 設定変更したらエンジンを始動してひとっ走りしてきましょう。車両のイグニッション信号に連動してターミナルシステムが自動的に計測を開始してサーバーにデータを送信します。弊社のIoTプラットフォームである intdash はデータの完全回収が優れたポイントで、仮にネットワーク伝送路においてデータの欠損(LTEの通信環境悪化は分かりやすい一因)が生じても、ローカルストレージに一時保存されたデータが適切なタイミングで再送されることで 時系列が保証されたデータをサーバーに完全回収すること が可能です。 計測データの可視化 サーバーに保存されたバイナリデータの可視化にはWebブラウザで動作する Visual M2M Data Visualizer を利用します。 制御データのバイナリファイルを物理値に変換するためのDBCファイルをインポートし、可視化したい物理値を適切なビジュアルパーツに紐付けて配置することができます。 Visual M2M Data Visualizer Data Visualizerのスクリーン表示例 (ステアリング操舵角/速度/位置/映像/音声) 画面上の青色のグラフが車両のCANバスをハーネス経由で直接計測した情報で、赤色のグラフは非接触CANセンサー経由で計測した情報を時系列に沿って可視化しています。CANバスを流れる ステアリングの操舵角 および 車両の速度 の情報と、同時にGNSSから得られる車両の座標、前方視界を撮影するビデオカメラの動画、車室内のマイク音声もビジュアルパーツとして配置しています。直感的に理解しやすいですね。 さて青色と赤色の計測データはそれぞれ同じように見えますが、詳細はどうでしょうか?更に踏み込んで精緻な検証をしたい場合、計測データをCSVファイルでダウンロードすることができます。 CSVダウンロード ダウンロードしたCSVファイルにはTimestampの情報(マイクロ秒)と選択したデータが含まれます。今回は簡易的にMicrosoftのExcelを利用してCSVファイルを確認してみました *4 。 ハーネスから計測した速度データと非接触CANセンサーから計測した速度データを比較してみると、10マイクロ秒オーダーまで時刻が一致するデータが約82%存在し、時刻が一致しないデータに関しても20マイクロ秒の範囲内に同一のデータが存在することが分かりました *5 。 弊社のCAN-USBインターフェースの打刻誤差が10マイクロ秒であることを考慮すると、非接触CANセンサーの遅延は十分に小さいと言えます。 なお答え合わせ的にネタばらしすると、 こちら で謳われている性能として、 非接触方式でも信号を取りこぼさず、接触方式と同様に正確なCAN信号の取得が可能です。また、CAN信号検出における遅延が130nsと非常に小さく、リアルタイム性を失いません。 というスペックが保証されていますので、安心して利用できることが分かります。 おわりに 本記事では日置電機株式会社様からご提供いただいた非接触CANセンサーを、アプトポッドが提供する自動車計測ソリューションであるAutomotive Proと組合わせて利用し、車両の制御信号を可視化するユースケースをご紹介させていただきました。 CANバスのハーネス経由で直接得られるデータを基準に非接触CANセンサーを評価した結果、低遅延でデータの取りこぼしが発生せず、通常のCAN計測と全く遜色なく利用できることが分かりました。 自動車の研究開発に携わっている方々をはじめとして、移動体全般のデータ計測にご興味・ご関心をお持ちの方はぜひ 弊社HPの問合せフォーム からご連絡をいただけますと有難く思います。 最後になりますが、今回製品の試用と本ブログでの情報発信にご快諾いただいた日置電機株式会社の新津様、高橋様、柳澤様、誠にありがとうございました。この場を借りて厚く御礼申し上げます。 *1 : Macさん、加藤さん、富田さん、ご協力ありがとうございました *2 : 弊社ではこのエッジ側のシステムを総称して"ターミナルシステム"と呼んでいます *3 : 2020年12月時点の最新ファームウェアでは1マイクロ秒の打刻精度に性能向上しています *4 : が、当然ながら数十分~数時間の計測を行う場合はデータ量が多くてExcelでは力不足と思いますのでデータ処理に適したツールを利用しましょう^^ *5 : 厳密に評価する際には机上で任意のCANデータを生成する信号発生器や治具を利用することが必要ですが、今回はあくまで簡易的な評価ですのでご容赦を
aptpodフロントエンドエンジニアの黒川です! aptpod Advent Calender2020 の19日目を担当します。 2020年は新型コロナウイルスの世界的流行により全てが一変した年でした。 オリンピックも延期になりましたし、私達の生活様式や働き方、価値観まで変わりました。 そんな2020年にReactの状態管理を大きく変えるライブラリがリリースされました。それが Recoil です。 Recoilについては、私の 以前書いた記事 でも名前だけ触れました。 2020年の5月に行われたReact Europe2020で発表され、瞬く間に注目を浴びまして、2020年12月現在GitHubスター数1万を超えるなかなかの人気ライブラリとなっております。 とはいえ、 npm trends などを見ても、同じく状態管理ライブラリである Redux や MobX には大きく水をあけられており、まだまだ実際に使われている機会は少ない、これからのライブラリです。 今回は、そんなRecoilについて基本的な使い方と思想、そしてこういう使い方をすると嬉しいんじゃないかというお話をしたいと思います。 Recoilについて Recoilの代表的なAPI atom selector useRecoilState/useRecoilValue/useSetRecoilState useRecoilCallback まとめ Recoilについて Recoilは2020年5月にリリースされたReact用の状態管理ライブラリです。 2020年12月現在はまだexperimentalということもあり、まだまだ製品へと本格投入されるフェーズにありません。しかし、その利便性やReactの本家本元であるFacebookが開発しているという話題性から注目を集めています。 Recoilのコンセプトは非常にシンプルです。 atom と呼ばれる関数から生成された RecoilState というRecoil専用の状態をそのままコンポーネントにsubscribeさせるか、あるいは selector と呼ばれる純粋関数を通してコンポーネントにsubscribeさせるか、これだけです。Reduxのようなreducerやactionといった多くのボイラープレートをRecoilは必要としません。 これはRecoilの開発が、Reactの元々持つ状態管理(useStateやContext)のシンプルさと便利さを損なうことなく、逆にそれらを扱う上で制約となっていた不自由さを取っ払うことを目的に行われていることによるものです。以下に実際に従来のコードとRecoilを用いたコードを書いたので見比べてみましょう。 // 従来の書き方 import React , { useCallback , useState } from "react" ; export const Conventional = () => { const [ count , setCount ] = useState < number >( 0 ); const onIncrement = useCallback (() => { setCount (( prev ) => prev + 1 ); } , [] ); const onDecrement = useCallback (() => { setCount (( prev ) => prev - 1 ); } , [] ); return ( < div > < p > { count } < /p > < button onClick = { onIncrement } > インクリメント < /button > < button onClick = { onDecrement } > デクリメント < /button > < /div > ); } ; // Recoilを用いた書き方 import React , { useCallback } from "react" ; import { useRecoilState } from "recoil" ; import { counterState } from "../../atoms/state" ; // counterStateはこのような定義がされています // const counterState = atom<number>({ // key: "counterState", // default: 0 // }); export const RecoilWay = () => { const [ count , setCount ] = useRecoilState ( counterState ); const onIncrement = useCallback (() => { setCount (( prev ) => prev + 1 ); } , [] ); const onDecrement = useCallback (() => { setCount (( prev ) => prev - 1 ); } , [] ); return ( < div > < p > { count } < /p > < button onClick = { onIncrement } > インクリメント < /button > < button onClick = { onDecrement } > デクリメント < /button > < /div > ); } ; このようにほとんど変わりません。 変更点は、Recoilの方でimportしている atom 関数によって定義された counterState が useState 関数と入れ替わった useRecoilState 関数によってsubscribeされたことだけです 1 。 これだけで今やRecoilで定義されたほうの count は、他のコンポーネントからも counterState をsubscribeすることで参照可能となったのです。簡単で便利ですね! ここではRecoilが既存のReactのシンプルさを引き継ぎつつ、簡単に他のコンポーネントからも RecoilState が参照可能になることを紹介しました。続いて、Recoilの代表的なAPIと基本的な使い方を紹介したいと思います。 Recoilの代表的なAPI atom atom はRecoilで用いられる RecoilState を生成する関数で、使い方は非常にシンプルです。以下のように atom 関数で宣言するだけで完了です。 const counterState = atom < number >( { key: "counterState" , default : 0 } ); ほとんど初見でも分かりそうなくらいにシンプルですが、簡単に説明します。 atom は key と default という2つのプロパティを引数として求めます。 key はアプリケーション全体でユニークなものを渡す必要があります。これは内部的に key によってatomを判別しているからです。 default は useState やReduxにおける初期値になります。 以上を設定すれば、あとはこの atom から生成されたStateを使用するコンポーネントにおいてsubscribeすれば参照可能になります。 アプリケーションを通してこの atom は何度も使用することになるかと思います。 そのような時に、例えばユーザーネームとパスワードなど同じ input を用いたコンポーネントでありながら、別々の RecoilState を扱うコンポーネントに対して userNameState , passwordState などといちいち atom から RecoilState を生成するのは面倒ですよね。 そんな時は、 atomFamily を使えば解決です。 atomFamily は動的にatomを作成してくれる関数であり、宣言もほとんど atom と同様です。 import React from "react" ; import { useRecoilState , atomFamily } from "recoil" ; const inputTextState = atomFamily < string , string >( { key: "inputTextState" , default : "" } ); export const InputFamily = () => { // もしパラメータを同じものにすると変数名が違っても、同じ値になる const [ userName , setUserName ] = useRecoilState ( inputTextState ( "userName" )); const [ password , setPassword ] = useRecoilState ( inputTextState ( "password" )); const [ remarks , setRemarks ] = useRecoilState ( inputTextState ( "remarks" )); return ( <> < input value = { userName } onChange = { ( e ) => setUserName ( e. target .value ) } / > < input value = { password } onChange = { ( e ) => setPassword ( e. target .value ) } / > < input value = { remarks } onChange = { ( e ) => setRemarks ( e. target .value ) } / > < p > { userName } < /p > < p > { password } < /p > < p > { remarks } < /p > < / > ); } ; ほとんど atom を用いた宣言と同様な事がわかります。異なる点は、 RecoilState をsubscribeする際に任意のパラメータを atomFamily に渡すことです。ちなみに atomFamily の型引数の1つ目はStateの型で、2つ目はパラメータの型になります。 これにより内部的に atomFamily が生成した atom に key をマッピングしてくれるのでこちらで細かいことを気にする必要はありません。 今回はinputで簡単に説明しましたが、例えばユーザーが動的に追加していく項目やボタンなどにも atomFamily は用いることができますので、非常に活用の幅は広いです。 selector selector は atom で生成された RecoilState をコンポーネントにsubscribeする際、使いやすい形に加工するための関数です。 まずはサンプルコードをお見せして説明しようと思います。こちらは Recoilの公式ドキュメント から引用しました。 const tempFahrenheit = atom ( { key: 'tempFahrenheit' , default : 32 , } ); const tempCelcius = selector ( { key: 'tempCelcius' , get: ( { get } ) => (( get ( tempFahrenheit ) - 32 ) * 5 ) / 9 , set: ( { set } , newValue ) => set ( tempFahrenheit , newValue instanceof DefaultValue ? newValue : ( newValue * 9 ) / 5 + 32 ), } ); selector を使うときには、2つのプロパティを定義する必要があります。1つ目はおなじみの key ですね。ユニークなものを定める必要があります。 2つ目は、 get です。ここでコールバック関数を定めることで selector がどんな加工を行うか定義できます。また、このコールバック関数は引数に get という関数を受け取れます。このget関数に atom で定義した RecoilState を渡すことでselector内で値を読み込むことができます。ここからは自由に加工可能です。上の例では、温度の単位変換として華氏(°F)の値を読み込んで摂氏(℃)に変換しています。 さて、2つのプロパティと言いましたが、上記の例では3つ定義していますね。3つ目のプロパティ set はオプションです。定義せずに読み込み専用の selector とすることも勿論可能ですが、定義することで selector と atom の値を連動させることができます。 set の役割は、 RecoilState を新しい値に上書きすることです。 get プロパティ同様に set プロパティもset関数を引数に受け取ります。また、上書きするための新しい値も受け取ります。このset関数の第1引数に更新対象である RecoilState 、そして第2引数に set プロパティが受け取った新しい値を渡すことで、 RecoilState が更新されます。上の例では tempFahrenheit が更新の対象です。また、 tempCelcius は tempFahrenheit の値をもとに生成されているので、こちらも連動して更新されます。このように selector で RecoilState を更新させた際は、結果的にその selector の get から得られる値も更新されることとなります。 また、上記のコードを見るとset関数の新しい値を DefaultValue のインスタンスかどうかを判別しているかと思います。これは、 useResetRecoilState という RecoilState の値をdefaultに戻すHooksの使用を考慮したものです。すなわち、 useResetRecoilState で newValue が DefaultValue のインスタンス = defaultの値の時はそのままdefaultの値に更新し、それ以外の時は摂氏を華氏に変換して更新しています。文字で説明すると少々ややこしいですが、実際に使ってみるとわかりやすいAPIです。 さらに selector は非同期処理も組み合わせることが可能です。 これを利用して以下のように、例えばあるidのデータをAPIを通して取り出したいというものが簡単に実装できるようになります。 const asyncSelector = selector ( { key: "asyncSelector" , get: async ( { get } ) => { const res = await getSomething ( get ( idState )); return res.data ; } } ); ただし、非同期処理に selector を用いる時は一点だけ注意が必要です。 この asyncSelector から返ってくる値は非同期であるため、同期的にコンポーネントをマウントした場合はまだ取得されていないデータを参照する可能性があり、アプリケーションの動作に支障を来してしまうということです。これを回避するための方法は2つあります。 1つ目はコンポーネント内は同期的に取得して、親コンポーネントで回避する方法です。以下のコードをご覧ください。 // 親コンポーネントで回避するパターン const SomethingAsync = () => { const value = useRecoilValue ( asyncSelector ); return < div > { value } < /div >; } ; const ParentComponent = () => { return ( < Suspense fallback = { < div > Loading... < /div > } > < SomethingAsync / > < /Suspense > ); } ; ここでは Suspense を使っています。 Suspense について簡単に説明しますと、 SomethingAsync が読み込まれるまで待機するためのコンポーネントです。 Suspense の fallback に渡したコンポーネントを待機中に表示してくれますので、非同期中であってもアプリケーションは正常に動作できます。 もう1つは、コンポーネント内で回避する方法です。この回避をするためのHooksとしてRecoilは useRecoilValueLoadable を提供しています。 この useRecoilValueLoadable は値をそのまま返すのではなく、Loadableオブジェクトというものを返します。Loadableオブジェクトは state と contents という2つのプロパティを持ち、 contents は state の状態によって中身が変わります。 以下のコードを見て、 state と contents がどのような形になりうるか確認したいと思います。 // コンポーネント内で回避するパターン const SomethingAsync = () => { const valueLoadable = useRecoilValueLoadable ( asyncSelector ); switch ( valueLoadable.state ) { // 非同期処理が終わっていない時、stateはloadingとなり、contentsはPromiseとなる // そのままだと表示できないので、<Suspense>と同じ扱いをする case "loading" : return < div > Loading... < /div >; // 非同期処理が終わればstateはhasValueとなり、 // contentsには実際の値が入るので、コンポーネント内で使用できる case "hasValue" : return < div > { valueLoadable.contents } < /div >; // もしも非同期処理中にエラーが生じるとstateはhasErrorとなる // contentsにはErrorオブジェクトが入るので、これをthrowすることでErrorBoundaryに検知させる case "hasError" : throw valueLoadable.contents ; } } ; このような感じです。実際にどちらのパターンを取るかは好みやコンポーネントの設計によると思いますが、 selector で非同期処理を扱う時はこのような処理が必要ということだけ留意する必要があります。 useRecoilState/useRecoilValue/useSetRecoilState 今まででRecoilを扱うためのStateを作るAPIを紹介してきました。ここからは実際にコンポーネントにsubscribeするためのHooksを紹介していきます! ただ、実はRecoilでsubscribeするためのHooksはほとんど中身は一緒なんです! 違いは、 「値」と「更新するための関数」両方必要か 「値」だけ必要か 「更新する関数」だけ必要か の3つに応じて使い分けるだけです。 そして、それぞれ①のケースが useRecoilState 、②が useRecoilValue 、③が useSetRecoilState となります。 すなわち、 useRecoilValue + useSetRecoilState = useRecoilState です。簡単ですね。 useRecoilState は今までのサンプルコードで何度も用いてきましたが、以下に3つのサンプルコードを書きます。 Reactのhooksに慣れている方であれば、全く違和感なく書けると思います。 // ReactのuseStateと全く一緒です const [ count , setCount ] = useRecoilState ( counterState ); // ReactのuseStateからstateだけを取り出したものです const count = useRecoilValue ( counterState ); // ReactのuseStateからsetStateだけを取り出したものです const setCount = useSetRecoilState ( counterState ) いかがでしょうか。本当にシンプルなAPIなのでありがたいですね。 さて、ここまでで RecoilState を作り、それをコンポーネントにsubscribeさせる方法を説明してきました。これによりもう <RecoilRoot> 内のどこからでも自由に RecoilState を参照できるようになっています。ただ、まだ気になる点が1つあります。 onClick などイベント発火時のコールバック関数です。従来の useCallback は便利ですが、下手に依存関係を持たせてしまうと依存している変数の更新に伴い、 useCallback の更新も行われ、意図しない再レンダリングが生じていました。 RecoilState が更新されても不要な時はsubscribeせず、必要になった時に RecoilState を読み込む、そんなコールバック関数があれば理想的です。そのような希望を叶えてくれるのが次に紹介するAPIです。 useRecoilCallback Recoilには snapshot というRecoilの現在のStateを読み取れるオブジェクトがあります。 useRecoilCallback はこの snapshot を用いて、必要な時にだけStateを読み込み発火することができます。この必要な時に読み込むことの何が嬉しいのかは、上述の通り不要な再レンダリングを避けられることです。 この再レンダリングを説明するために、姓と名を入力して送信ボタンを押すと、フルネームが表示される簡単なアプリケーションのコードをReactのAPIのみで書きました。 gyazo.com import React , { useCallback , useState } from "react" ; type InputProps = { value: string ; onChange: ( e: React.ChangeEvent < HTMLInputElement >) => void ; } ; const Input = ( props: InputProps ) => { return < input value = { props.value } onChange = { props.onChange } / >; } ; type ButtonProps = { onClick : () => void ; } ; const Button = ( props: ButtonProps ) => { return < button onClick = { props. onClick } > 送信 < /button >; } ; export const Form = () => { const [ lastName , setLastName ] = useState < string >( "" ); const [ firstName , setFirstName ] = useState < string >( "" ); const [ fullName , setFullName ] = useState < string >( "" ); const onChangeFirstName = useCallback ( ( e: React.ChangeEvent < HTMLInputElement >) => { setFirstName ( e. target .value ); } , [] ); const onChangeLastName = useCallback ( ( e: React.ChangeEvent < HTMLInputElement >) => { setLastName ( e. target .value ); } , [] ); const onClick = useCallback (() => { setFullName ( `${firstName} ${lastName}` ); } , [ firstName , lastName ] ); return ( < div > < Input value = { firstName } onChange = { onChangeFirstName } / > < Input value = { lastName } onChange = { onChangeLastName } / > < p > FullName: { fullName } < /p > < Button onClick = { onClick } / > < /div > ); } ; こちらのGIFでわかりますように、Inputの文字を変更するたびにInputそのものとForm、そしてButtonに再レンダリングがかかっています。(白い明滅がレンダリングがかかっているということです) これはInputの変数をButtonに渡すために、その親コンポーネントであるFormがInputの変数も持っているため、 lastName や firstName 更新時にInputだけではなくFormまで再レンダリングがかかるためです。また、Buttonに降ろすコールバックも lastName と firstName に依存しているため、このコールバックを使用するButtonにも再レンダリングがかかります。勿論、いくつかの工夫により多少は抑制されますが、やはりレンダリングは必要なコンポーネントに留め、不要な再レンダリングは可能な限り避けたいものです。 これを解消するのがRecoil、そして useRecoilCallback になります!同じような構成をRecoilで書き直してみたのが以下になります。 gyazo.com import React , { useCallback , useState } from "react" ; import { atomFamily , useRecoilState , useRecoilCallback } from "recoil" ; const nameState = atomFamily < string , string >( { key: "nameState" , default : "" } ); type Props = { param: string ; } ; const Input = ( props: Props ) => { const [ value , setValue ] = useRecoilState ( nameState ( props.param )); const onChange = useCallback (( e: React.ChangeEvent < HTMLInputElement >) => { setValue ( e. target .value ); } , [] ); return < input value = { value } onChange = { onChange } / >; } ; type ButtonProps = { onClick : () => void ; } ; const Button = ( props: ButtonProps ) => { return < button onClick = { props. onClick } > 送信 < /button >; } ; export const RecoilForm = () => { const [ fullName , setFullName ] = useState < string >( "" ); const onClick = useRecoilCallback ( ( { snapshot } ) => async () => { const firstName = await snapshot.getPromise ( nameState ( "firstName" )); const lastName = await snapshot.getPromise ( nameState ( "lastName" )); setFullName ( `${firstName} ${lastName}` ); } , [] ); return ( < div > < Input param = { "firstName" } / > < Input param = { "lastName" } / > < p > FullName: { fullName } < /p > < Button onClick = { onClick } / > < /div > ); } ; いかがでしょうか。明滅が変更のかかっているコンポーネントのみに留まりましたね! Inputには atomFamily で nameState というものを作り、複数のInputであっても1つの RecoilState で対応できるようにしました。 そして、RecoilFormでは自身が使うローカル変数である fullName を useState で作り、Buttonに渡すコールバックは useRecoilCallback で作成しています。 useRecoilCallback は高階関数のような形をとっていまして、冒頭で説明した snapshot オブジェクトは1つ目の引数に受け取ります。この snapshot から他の atom の RecoilState を読み込むことができるのですが、ここで注意してほしいのは snapshot からはPromiseの形でしか読み込めないことです。なぜPromiseかといいますと、非同期 selector を考慮した設計だそうです。そして、Promiseを扱うので2つめの引数にはasyncを付ける必要があります。 コールバック内では snapshot オブジェクトの getPromise という関数に他のAPI同様に RecoilState を渡すとその値を読み込むことができます。これにより、 useCallback のように再レンダリングをかけることなくInputの値を取り扱うことができます! 一点注意してほしいのは、今回は useState の更新を行うために setState を useRecoilCallback 内部に置きましたが、もしRecoilの値を更新したい時は useSetRecoilState を用いてはいけないということです。したがって、 useRecoilCallback は selector 同様に set 関数を用意しています。使い方も selector と一緒で第一引数に更新したい RecoilState 、第2引数に新しい値を指定します。 useRecoilCallback を用いる時に RecoilState を更新したい場面は多いと思いますので、この点だけ気をつけてください。 まとめ 今回はRecoilのAPIを中心にどのような点が嬉しいのか、どのような点に気をつければいいのかを中心に書かせていただきました。 Recoilは、私自身も書いていてとても書き心地がいいですし、便利なのでもっと広まればいいなっと思っています。今回は書ききれませんでしたが、便利なAPIや機能はまだまだたくさんあります。 一方で、今回は伝えきれなかったのですが、 atom などは油断するとすぐに無秩序になりがちなので治安の良い書き方というのも意識する必要がありそうです。このあたりのベストプラクティスはまだまだ溜まっていないので、これからに期待ですね。 また、 Facebook Experimental ということで、まだまだRecoil自体もどうなっていくか分かりません。最初のリリースからAPIも変わったりしていますので、製品にホイホイと投入できるほど安心はできないというのも事実です。この点についても趨勢を見守っていきたいですが、2021年には何らかの進展が見られることを期待して、本タイトルをつけさせていただきました!2021年に活用していきたいです!(願望) さて、来週月曜日はハードウェアグループおおひらさんの「非接触CANセンサーで車両の制御信号を可視化してみた」になります。 自動車の非接触センサーを用いてリアルタイムにちゃんと可視化できるかを検証する弊社らしい記事になっておりますので、是非こちらもご注目ください! <参考文献> Recoil公式サイト  https://recoiljs.org/ recoiljs.org 正確にはRecoilを使用しているコンポーネント群のトップに <RecoilRoot> を置く必要があります。 ↩
マニュアル等のドキュメント制作を担当している私は、世の中の機械翻訳エンジンの進歩を日々驚きながら観察しています。 実際に業務で使うかは別途判断するとして、「この文をこの機械翻訳エンジンに与えたら、どんな訳文が得られるのだろうか」と、試してみることもあります。 この記事では、そんなお試しの例として、Google Cloud Translation APIの「用語集」の機能を使ってみます。 aptpod Advent Calender 2020 の18日目、テクニカルライターの篠崎が担当します。 (なお、この記事で扱うのは、プログラムから利用する機械翻訳ウェブAPI Google Cloud Translation API です。ウェブブラウザーから利用する機械翻訳サービス Google Translate ではありません。) この記事について Google Cloud Translationの用語集機能とは 試してみる 準備 お試しPythonコード 得られた翻訳結果 おわりに 参考文献 この記事について この記事は、軽く「試してみる」だけの内容です。機械翻訳一般やGoogle Cloud Translation APIの性能や有用性を検証するためのものではありません。 2020年12月に試してみた結果に基づいています。翻訳エンジンは日々更新されると思われます。また、用語集機能もアップデートされるかもしれません。 Google Cloud Translation APIで翻訳を実行するには料金がかかります。また、以下に挙げるコードではGoogle Cloud Storageのバケットに用語集を保存しますが、それに対しても料金が発生します。 日本語から英語への翻訳を例とします。 あくまで1つの例としてご覧ください。 Google Cloud Translationの用語集機能とは Google Cloud Translation APIには、 用語集(Glossary)の機能 が存在します。 対訳形式の用語集をあらかじめ登録しておくことで、指定された訳語を訳文の中で使用してもらうことができます。 対訳用語集の例 Google Cloud Translation APIの公式ドキュメント では、用語集を使用する場合の例として、「商品名」、「あいまいな単語」(意味が全く違うのに同じつづりの語)、「借用語」が挙げられています。 実際に用語集を使用するとどのような翻訳結果が得られるのか、試してみました。 試してみる 以下では、必要な準備を行ったうえで、実際に用語集を使って機械翻訳をしてみます。 準備 手元のPCからGoogle Cloud Translation APIを使用するには、一般に、以下の準備が必要です(Google Cloud Platformのアカウントは取得済みとします)。 これらについては、 公式ドキュメント に詳しく説明されています。 Google Cloudコンソール(以下「GCPコンソール」)で新しいプロジェクトを作る。 GCPコンソールで、プロジェクトのCloud Translation API機能を有効にする。 GCPコンソールで、APIを使用するためのアカウントである「サービスアカウント」を作り、適切な権限を設定する。 GCPコンソールで、サービスアカウントのキー(JSONファイル)を作成し、手元のPCにダウンロードする。 サービスアカウントのキーのファイルを、環境変数 GOOGLE_APPLICATION_CREDENTIALS で指定する。 クライアントライブラリー(Python用ならば google-cloud-translate )をインストールする。 Cloud SDK をインストールする。 また、この記事で紹介する「お試しPythonコード」を実行するには、以下の準備も必要です。 使用するサービスアカウントには「Cloud Translation API 編集者」( roles/cloudtranslate.editor )と、「Storage Object管理者」( roles/storage.objectAdmin )のロールを持たせる。 サービスアカウントに設定されたロール 用語集のアップロード先としてGoogle Cloud Storage(GCS)バケットを作っておく。 GCSバケットを作っておく お試しPythonコード 上記の準備をしたうえで、実際に翻訳を試してみます。 このコードには、用語集データのアップロードから後片付けまでの手順が入っています(エラー処理は省略しています)。また、用語集とテスト用の原文もコード内に入っています。 from google.cloud import storage from google.cloud import translate_v3 as translate # 1. 各種設定 # 日英対訳用語集(公式ドキュメントにある「単一言語ペアの用語集」の形式で作成) # ここでは、簡単のためにCSV形式の1つの文字列として用意しました。 GLOSSARY = ''' \ "メールアドレス","email address" "健司","Takeshi" "国立","Kunitachi" "試験","test" "選ぶ","select" ''' # テスト翻訳対象の原文 SOURCE_TEXTS = [ 'メールアドレスを入力してください。' , 'メールアドレス設定画面' , '健司さんは国立の病院に行きます。' , '学校で試験があります。' , '3つの選択肢から選ぶことができます。' , '彼が選ぶものはいつも同じです。' , '3つの選択肢から選ばなければならなないため、Aを選んだ。' , '3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。' ] # GCPのプロジェクト情報(プロジェクトは事前に作成済みとする) PROJECT_ID = '<プロジェクトID>' # ロケーションはいまのところus-central1のみ対応とのこと LOCATION = 'us-central1' # 用語集CSVのアップロード先バケット名(バケットは事前に作成済みとする) BUCKET_NAME = '<GCSバケット名>' # バケットに用語集を保存するときのオブジェクト名 GLOSSARY_OBJECT_NAME = 'test_ja_en_glossary.csv' # 上記により、バケット内のCSVファイルオブジェクトへのURIが決まる。 GLOSSARY_URI = 'gs://' + BUCKET_NAME + '/' + GLOSSARY_OBJECT_NAME # 新しく作成する用語集のID GLOSSARY_ID = 'test_ja_en_glossary' # 2. 用語集をアップロードして、GSCバケット内にCSVファイルとして保存する # GCSクライアントを作成する storage_client = storage.Client() # 用語集のアップロード先バケットと、新たに作るオブジェクト名を指定 bucket = storage_client.bucket(BUCKET_NAME) glossary_blob = bucket.blob(GLOSSARY_OBJECT_NAME) # 用語集CSVを文字列として送信し、GCS上のオブジェクトにする glossary_blob.upload_from_string(GLOSSARY) # 3. GCSにアップロードされたCSVをもとにして、 # Google Cloud Translate用の「用語集リソース」を作る # Google Cloud Translate v3 APIのクライアントを作成 trans_client = translate.TranslationServiceClient() # 用語集リソースのパスを決める glossary_name = trans_client.glossary_path( PROJECT_ID, LOCATION, GLOSSARY_ID) # ソース言語とターゲット言語のペアを指定する language_pair = translate.types.Glossary.LanguageCodePair( source_language_code= 'ja' , target_language_code= 'en' ) # 先ほどGCSにアップロードした用語集CSVを指定する gcs_source = translate.types.GcsSource(input_uri=GLOSSARY_URI) input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) # 上記で準備した情報を組み合わせて、用語集の情報を準備する glossary = translate.types.Glossary( name=glossary_name, language_pair=language_pair, input_config=input_config) project_location_path = trans_client.location_path(PROJECT_ID, LOCATION) # 用語集リソースを作成する glossary_create_operation = trans_client.create_glossary( parent=project_location_path, glossary=glossary) # 用語集リソースの作成完了を待つ glossary_create_result = glossary_create_operation.result(timeout= 90 ) # 4. 翻訳してみる # 使用する用語集リソースを指定する glossary_config = translate.types.TranslateTextGlossaryConfig( glossary=glossary_name) # 翻訳対象、言語名、用語集リソース等を指定して翻訳する response = trans_client.translate_text( SOURCE_TEXTS, 'en' , project_location_path, source_language_code= 'ja' , glossary_config=glossary_config) # 結果を出力する for (source, trans, trans_with_glossary) in zip ( SOURCE_TEXTS, response.translations, response.glossary_translations): print ( '原文: ' + source) print ( '用語集なし: ' + trans.translated_text) print ( '用語集あり: ' + trans_with_glossary.translated_text) print () # 5. 後片付け # 用語集リソースを削除する glossary_delete_operation = trans_client.delete_glossary(name=glossary_name) # 用語集リソースの削除完了を待つ glossary_delete_result = glossary_delete_operation.result(timeout= 90 ) # GCS内のCSVファイルを削除する glossary_blob.delete() 得られた翻訳結果 以下のような結果が得られました。 原文: メールアドレスを入力してください。 用語集なし: Please enter your e-mail address. 用語集あり: Please enter your email address. ↑ 用語集で指定したとおり、ハイフンなしの"email address"となりました。 原文: メールアドレス設定画面 用語集なし: Email address setting screen 用語集あり: email address setting screen ↑ 文書内でタイトルとして使うことを想定した名詞句です。 先頭は大文字がよいのですが、用語集に入れたハイフンなしの"email address"に置換され、 小文字になってしまいました。 なお、「メールアドレス」という1つの原語に対して、"email address"と"Email address"のような2種類の訳語を登録することはできません("Can't create glossary. There are conflicting entries."というエラーになります)。 原文: 健司さんは国立の病院に行きます。 用語集なし: Kenji goes to a national hospital. 用語集あり: Takeshi goes to a Kunitachi hospital. ↑ 「たけし」さんと「くにたち」の話であることがあらかじめ分かっているときは、このような置換は効果を発揮しそうです。 ただし、"hospital in Kunitachi"のようにはならず、単純な置き換えになっているようです。 原文: 学校で試験があります。 用語集なし: I have an exam at school. 用語集あり: I have an test at school. ↑ ここでは、単純に"exam"が"test"に置換された結果、"a test"ではなく"an test"となってしまいました。 原文: 3つの選択肢から選ぶことができます。 用語集なし: You can choose from three options. 用語集あり: You can select from three options. ↑ 動詞の例です。「選ぶ」が"select"に置換されました。(このような一般的な動詞は用語集に入れないほうが良いと思いますが、試しにやってみました。) 原文: 彼が選ぶものはいつも同じです。 用語集なし: The one he chooses is always the same. 用語集あり: The one he select is always the same. ↑ 単純に"select"に置換された結果、"he selects"ではなく、"he select"となってしまいました。 原文: 3つの選択肢から選ばなければならなないため、Aを選んだ。 用語集なし: I chose A because I had to choose from three options. 用語集あり: I chose A because I had to choose from three options. ↑ 「選ぶ」を用語集に入れましたが、活用形である「選ば」、「選ん」は用語集に入れていないので置換されないようです。 原文: 3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。 用語集なし: I was instructed to choose from three options, so I decided to choose A. 用語集あり: I was instructed to select from three options, so I decided to select A. ↑ 「選ぶ」に完全に一致する文字列(連体形)の場合は置換されました。 おわりに 用語集の効果がどのようなものか、軽く試してみることができました。 用語集の項目の立て方を工夫して、他のケースも試してみたいですが、 上記のようなコードを用意しておけば、用語集のアップロードから、翻訳結果取得、用語集削除まで、いっぺんにできるので便利です。 なお、Cloud Translation APIには、対訳コーパスを使ってカスタム翻訳モデルをトレーニングし、 それを使って翻訳を行う AutoML Translation という機能もあります。 こちらも、いつか試してみたいところです。 参考文献 Google Cloud Translation公式ドキュメント 特に、 用語集の作成と使用(高度な機能) Python Client for Google Cloud Translation公式ドキュメント Improving Machine Translation with the Google Translation API Advanced (Translation APIの使用法がよく分かるチュートリアルです。) GoogleのCloud Translation API v3を触ってみる (つまづきがちな、サービスアカウントのロール(役割)の設定についても解説されています。)
Advent Calendar 2020 17 日目を担当します、 SRE チームの川又です。 SRE チームでは自社開発プロダクトである intdash のサーバサイドインフラにおいて、主に以下の職務を行なっています。 設計・構築・運用 可用性・パフォーマンスの向上のための改善 セキュリティの維持 今回は、エッジロケーションにおけるセキュリティを維持したパフォーマンス向上のために、サイト間VPN 接続を用いて弊社 intdash のトラフィックフローを効率化した話をご紹介します。 はじめに 課題 解決策 エッジ側vRouter の構成 結果 おわりに はじめに 弊社では年に1 回程度、サーキットを1日貸し切って、自社プロダクトを用いた車両走行・データ計測を行う イベント を行います。 本イベントは以下を目的としています。 自社プロダクトに触れる (ドッグフーディング) デモ用データ取得 自社新規プロダクトの検証 通算何回か行なっている本イベントですが、 現地サーキットの通信環境の悪さを原因とする課題 を抱えていました。 この記事では、ネットワーク技術でその課題の解決を試みた話をご紹介します。 ※ この実験を行ったのは昨年度です。今年度は、主に感染症対策のため本イベントは実施しておりません。 課題 例年先述のイベントを行なっているサーキットはモバイル網の通信速度が遅く、さらには、サーキット内にモバイル網がエリア外の敷地もありました。 したがって、インターネット通信を行える帯域が小さく 満足にクラウド側とデータ通信ができない課題 がありました。 弊社の intdash Server は多様なデータパイプラインを備えており、車両等の計測対象からアップストリームされるデータをリアルタイムに取得することも可能です。また、同じく弊社のWebベースダッシュボードアプリケーション Visual M2M Data Visualizer を用いることでデータの多彩な可視化が行えます。 イベント当日は、サーキットに居る弊社社員のほぼ全員が Visual M2M Data Visualizer で車両計測データの確認を行うことが予測されました。ですが、先述の通りデータ通信帯域が限られているため 何らかの対策 を行う必要がありました。 本ネットワーク環境の概要図を以下に示します。 ネットワーク環境概要図 何らかの対策を実施しなければ、下の図の様にクライアントにあたる各人の Visual M2M Data Visualizer がそれぞれインターネットを通してクラウド上の intdash Server と通信を行い、帯域を逼迫させてしまいます。 問題点のイメージ図 解決策 この課題を解決するために、エッジロケーションにあたるサーキット側にも intdash Server を設置し(以下、これを エッジサーバ と呼びます)、インターネット上ではサーバ間のみで通信を行う様に環境を構築しました。この場合、 クライアントはエッジサーバのみと通信を行う ため、インターネット側の通信量は削減されます。 問題解決構成のイメージを以下の図に示します。 解決構成のイメージ図 弊社 intdash Server はリアルタイムストリームのPub/Sub メッセージングに NATS が利用可能です。この NATS を用いてクラウド側の intdash Server とエッジ側の intdash Server で連携を行い、クライアントへのリアルタイムストリームを提供可能にしました。概念としては、CDN( Content Delivery Network) におけるキャッシュサーバに近いです。 サーバ間の通信に関しては、以下の理由からサイト間VPN接続としました。 セキュアに通信を行う サーバ側に追加設定を必要としない サイト間VPN接続には以下を用いました。 クラウド側: AWS 仮想プライベートゲートウェイ (VGW) エッジ側: vRouter VyOS 概要図を以下に示します。 解決構成概要図 エッジ側vRouter の構成 エッジ側のvRouter ではクライアントがサーバの接続先を意識しないで良い様に以下の機能を提供させました。 DHCP サーバ ( Dynamic Host Configuration Protocol ) DNS サーバ ( Domain Name System ) NAT ( Network Address Translation ) ルーティング(静的) VPN ゲートウェイ エッジ側ネットワークの機能およびリソースをvRouter の管理下に置くことで、クライアント側が接続先サーバのドメイン名を意識せずアクセス可能な構成としました。 結果 結果としては、理論通りの挙動をさせることには成功しました。ただし、他の検証施策もあった兼ね合いで、全てのクライアント側で同時にエッジサーバを利用して貰うことが叶わず、効果測定としては満足に行うことが出来ませんでした。次回、同じ構成を試す機会があれば、インターネット通信量の削減効果を測定してみたいと思います。 おわりに 今回は、インターネット通信環境が十分でないエッジロケーションでの弊社 intdash の利用に際し、エッジの intdash Server とサイト間VPN接続を用いてトラフィックフローの効率化を行なった話をご紹介しました。 インフラのエンジニアリングとしては、利用する製品の挙動を理解した上で、状況・要件に応じた設計・構築、可用性の選択が重要だと考えます。課題に対して柔軟に対応できる様に努めていきたいです。 最後までご覧くださいまして、ありがとうございました。
Advent Calendar 2020 16日目担当の ソリューションアーキテクト の岩坪です。 aptpodのソリューションアーキテクトという役割は、自社プロダクトであるIoTプラットフォーム intdash をベースに、お客様の課題解決やDX(Digital Transformation)実現に向けたソリューション提案、システム全体のアーキテクチャ設計を行い、納品までのプロジェクトリードを行っていきます。 今回は弊社の intdash と、近年ロボット開発だけでなく自動運転車両開発など様々な産業で採用されている ROS(Robot Operating System ) との連携についてご紹介します。 はじめに intdashのシステム構成 『intdash x ROS』で実現できること 『intdash x ROS』のユースケース例 おわりに はじめに ソリューションアーキテクトである我々の仕事は お客様の課題に対して、いかに自社プロダクトintdashを効果的かつ効率的に活用した解決手段をご提案できるか に尽きると考えています。 お客様のニーズ、課題、悩みは様々ですが、それぞれに対して全てをただ求められるままに対応していたのでは、費用と期間がいくらあっても足りませんし、出来上がったものもお客様にとって本当に望んでいたものになるとは限りません。 そこで提案に当たっては次のようなことを考えていく必要があります。 お客様に対してintdashが最大限に価値提供できるのはどの領域であるか 要件に対してintdashの特長が当てはまる領域はどこであるか 短期間でお客様の求めるシステムを提供する為に自社とお客様側システム/機器との境界をどこに持つべきか その産業のプロフェッショナルであるお客様に実現頂くべき領域はどこまでの範囲であるか 自社以外の技術/サービスで優れている領域があれば、その技術/サービスといかに連携を行うべきか その道のエキスパートの会社があるのであればその会社の技術に頼るべき これらを考える上で重要なのは、自社の技術/自社のプロダクトの特長を意識しつつ、 お客様 ↔︎ aptpod、世の中の技術 ↔︎ intdashの技術 との適切な境界を考え、また、その境界をいかにスムーズに繋ぐことができるか?ということです。 境界をスムーズに繋ぐ為には、自社プロダクト以外との連携を行いやすいように、プロダクトとして 世の中の標準的な技術 への対応を進めていく必要があります。それにより、新規にお客様へシステム提供するに当たってのコスト、リードタイムは大幅に削減できます。弊社のintdashもこういった進化を徐々に行っています。 前置きが長くなってしまいましたが、今回は自社プロダクト intdash と、 近年、ロボット開発だけでなく自動運転車両開発などでも採用されている ROS(Robot Operating System ) との連携について自社の取り組みをご紹介したいと思います。 intdashのシステム構成 弊社のプロダクトであるIoTプラットフォーム intdash の特長としては以下があげられます。 高いリアルタイム性でデータ伝送できること サーバを介しても低遅延でデータを届けることができる 伝送時にパケロスしたデータの完全回収ができること 不安定な通信環境化においても全てのデータを回収できる 多種多様なデータに対して時刻を同期してデータ管理が行えること エッジサイドでデータに対してタイムスタンプを付与することで種々のデータを同一の時間軸で扱うことができる サーバに保存されたデータを利活用できること リアルタイムのデータ活用だけでなく、保存された過去データも容易に分析、学習などに利活用できる intdashのシステムでは、データが実際に流れている自動車、ロボット、建機などの ターゲットデバイスに intdash Server と接続するためのゲートウェイデバイスを配置して、ゲートウェイデバイス上に自社プロダクトであるエッジ向けのミドルウェア intdash Edge を搭載することで、デバイスに流れるデータを intdash Serverまで伝送することが可能になります。 また、intdash Serverに伝送されたデータを、Webベースのダッシュボードアプリケーションである Visual M2M Data Visualizer を使用することで、多彩に可視化することができます。 intdash Edgeが扱うデータはお客様側のシステム、デバイスにより様々であり、自動車業界におけるCANなど、intdash Edgeが標準で対応しているデータ形式もあれば、お客様ごとにカスタム開発することで対応するケースもあります。 intdash Edgeでは、デバイスとデータをやり取りする機能部位を、 Device Connector と呼んでいます。上記の カスタム開発 の対応を少しでも減らして、お客様に取って 短期・低コスト でシステムに導入ができるものを提供する為には、世の中で求められるデータ形式に対してプロダクトとして標準対応を進めておく必要があります。 そして近年、ROSを取り扱うシステムが多く見られるようになってきたこともあり、 弊社のプロダクトとしてintdash Edgeにおいて、 Device ConnectorがROSに標準対応 しました。 ROS nodeとしての ROS用のDevice Connector ができたことで、お客様のROSを取り扱うシステムに、intdash Edge搭載のゲートウェイデバイスを接続し、 容易に、すぐに、低コスト でintdash ServerへのROSメッセージのリアルタイム伝送が可能となりました。 また、複数台のデバイス上にROS用のDevice Connecterを使用したintdash Edgeを搭載することで、 遠隔のデバイス間におけるintdash Serverを介したROSメッセージのやり取りも可能 になります。 これは、 遠隔制御、協調制御、遠隔監視や、遠隔におけるROSのデータ利用 などのユースケースが考えられ、ROSを取り扱うシステムの用途を大きく広げることができます。 『intdash x ROS』で実現できること これまでにご紹介の通り、 intdash EdgeのROS用のDevice Connector を使用することでROSを取り扱うシステムの様々な連携の可能性を広げることができます。 一般的にROSを取り扱うシステムのアプリケーション開発のワークフローは、下記のような流れになると思います。 特徴としては、開発しているアプリケーションを シミュレータ上で検証 を行い、ソフトウェアの修正を進めます。さらに、実機を用いて フィジカル環境における検証 を行い、またここでのフィードバックをソフトウェアに反映します。これらの作業を繰り返すことで、開発が進み、アプリケーションが成熟していきます。 このワークフローの中には下記のような要素が必要となると考えられます。 要素として、 「可視化」「シミュレーション」「テスト」「学習」「エッジ管理」 など多岐に渡りますが、これらに共通することは、エッジサイドのデバイス性能には限界があることから、 様々なクラウドサービス等と連携してこれらの要素を実現する必要がある ことです。また、クラウド上のシミュレーション環境と連携して実機を開発・検証することで、フィジカル(実機)とバーチャル(シミュレーション)のデータを統合管理できるようになり、 フィジカルとバーチャルの比較や相互フィードバックが容易になる 利点もあります。 そして、これらクラウドサービスとの連携には、 システム間のスムーズなデータ伝送なしには実現が難しい という課題があげられます。 『intdash x ROS』ではまさにこの「 スムーズなデータ伝送 」の手段を提供できます。 『intdash x ROS』で提供できる価値は具体的には下記の内容があげられます。 遠隔のROS空間を繋ぐ 同一のROS空間同様にintdash Serverを介してROSメッセージのやり取りを実現することが可能 リアルタイムデータ伝送 リアルタイムの遠隔制御、遠隔監視などの用途に耐えうる低遅延によるデータ伝送を実現することが可能 ROSのデータ利活用 intdash Serverに保存したROSメッセージを可視化・分析・学習・シミュレーションなどに活用することが可能 『intdash x ROS』のユースケース例 『intdash x ROS』の具体的なシステム構成の一例として、こちらの記事 『AWS re:Invent 2019 で AWS RoboMakerとintdash によるTurtlebot3の遠隔制御の展示を行いました!』 でも紹介している「intdash x AWS RoboMaker」による Turtlebot3の遠隔制御、デジタルツイン環境のシステム構成を紹介します。 実際にTurtlebot3とAWS RoboMakerのシミュレーションが動いている動画はこちらです。 後ろのモニタに表示されているのが、Visual M2M Data Visualizerと、AWS RoboMaker上のGazeboの画面表示です。 Visualize remote control TurtleBot3 with AWS RoboMaker+intdash RoboMakerのシミュレーション環境(Gazebo)と、実環境のController、Turtlebot3を各デバイスに搭載されたintdash Edgeがintdash Serverを介して接続することで、 実環境のTurtlebot3の遠隔制御、データ可視化だけでなく、シミュレーション環境のロボットに対する遠隔制御、データ可視化 までを行うことができます。 一例ではありますが、『intdash x ROS』が実環境とシミュレーション環境である他システムを スムーズなデータ伝送により連携 できることがお分かり頂けたのではないでしょうか。 おわりに 今回は弊社のIoTプラットフォーム intdash と ROS の連携についてご紹介しました。 繰り返しになりますが、『intdash x ROS』で解決することは下記です。 遠隔のROS空間を繋ぐことができる リアルタイムデータ伝送することができる ROSのデータ利活用ができる ROSを取り扱うシステムを開発される中で課題をお持ちで、『intdash x ROS』の世界観に興味を 持たれた方はぜひaptpodへ コンタクト 下さい。 弊社では、intdashを AWSマーケットプレイス でも順次提供していきますので、興味ある方には 容易にお試しいただける環境をご用意できます。 『アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始』 また、ロボティクスでバリバリROSを扱っています!や、ROSって興味あったんだよね。というような方やintdashの技術、価値に興味を持って頂けたエンジニアの方は是非、こちらの 弊社採用ページ もご覧になって頂けましたらと。 aptpodと一緒にリアルタイムデータ伝送が実現する明るい未来をめざしましょう。
自己紹介 はじめまして。株式会社アプトポッド ビジネスデベロップメントグループの小宮です。 aptpod Advent Calendar 2020 の15日目を担当します。 ビジネスデベロップメントグループとは、 個別の商談からイベント出展まで、対外的な情報発信の取りまとめ 製品の価格決めから売り方検討 納品物のデリバリーまでの支援 その他ビジネスサイドのプロセスの穴埋めと補強 などをしているチームです。要するに営業サイドの何でも屋さんです。 実は私、そろそろアプトポッドに入社して、ちょうど一年が経つところです。まだ「例年のアプトポッド」がどのようなものかわからないのですが、それでも今年はお客様との接点が例年より少なかった一年でした。 そんな中、今年は9本のプレスリリースを出しまして、その半分が製品のアップデートに関するものです。総括すると、2020年は 今後増えるであろう様々なスケールの案件に最適に対応するための、準備を色々と仕込んだ年 だったと言えます。 コロナ禍の折、なかなかちゃんとご説明する機会もないので、今年のプレスリリースのうち製品に関わるものが、弊社製品のどの部分をどうアップデートしたものだったのか、という観点でご説明させてください。 自己紹介 はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です intdashの構成 プレスリリース振り返り プレスリリース1:intdash 2.0リリース プレスリリース2:テラデータ様との協業 プレスリリース3:SDKの提供開始 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 おわりに はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です アプトポッドは、産業向けに強みを持つIoTプラットフォーム”intdash”をご提供している会社です。 IoTに必要な、データ伝送やデータ管理などのコア機能は、上図上側のサーバーサイドで動く ミドルウェア に含まれています。さらに、上図下側の、「 エッジ 」と呼ばれる各種産業機器に取り付けるハードウェアや、エンドユーザーが直接操作する、時系列データの可視化ツールや各種ユーティリティ類も一気通貫してご提供しています。 このように、サーバーサイド、フロントエンド、ハードウェアの機械系から組み込みソフト、さらにはスマホアプリまで、いわゆるフルスタックに対応できる点が弊社の強みであり、このテックブログで取り扱うテーマがめちゃくちゃ幅広い所以でもあります。 intdashの構成 改めて弊社の製品群を、データ活用の流れに沿って分解したのが上図です。 一番左、つまり一番エッジに近いところから行くと、エッジのセンサーやアクチュエータと通信する ペリフェラル があり、エッジとクラウドの間のゲートウェイの役割を果たす ターミナルシステム があります。これらが弊社で取り扱う ハードウェア になります。 ターミナルシステム上で動く組み込みソフトも、一連の仕組み”intdash”に対応したものである必要があります。この組み込みソフト(エッジサイドのミドルウェア)を intdash Edge 、そしてそのうち特にintdashの特徴である、データの完全回収やリアルタイム伝送など、データ伝送機能を担うソフトウェアを intdash SDK for Edge Device (intdash Edge Agent) と呼びます。 続いて真ん中、サーバーサイドで動くミドルウェアの部分を、 intdash Server と呼びます。またこのミドルウェアには外部連携するためのAPIをご用意しており、 intdash SDK for Python を用いてカスタムの後処理やデータ分析などを手軽に行うことができます。 最後、一番右側の、データ活用の手段として、データ可視化ツールである Visual M2M Data Visualizer をご用意しております。こちらはブラウザ上で手軽に時系列データを可視化する、デザイン性に優れたツールです。 Visual M2M Data Visualizer DEMO 今年の夏に、弊社のコーポレートサイトのデザインが一新されたのをご存知でしょうか。この「PRODUCTS」以下には、弊社製品がカテゴリごとにならんでいます。各製品の詳細に興味がある方は、ぜひ コーポレートサイト も覗いていただけると幸いです。 プレスリリース振り返り プレスリリース1:intdash 2.0リリース 今年の製品関連のプレスリリース第1弾は、7月28日の「 DX プラットフォームintdash 2.0 をリリース 」でした。これは、intdash Serverのメジャーアップデートであり、本運用などにも使えるスケーラブルな環境を提供できるようになったことを指します。まず真ん中のコアの部分を、様々な規模の案件に対応できるようにした、ということです。 プレスリリース2:テラデータ様との協業 続いて第2弾は、8月4日の「 アプトポッドとテラデータ、自動車開発向けDXソリューション提供で協業 」です。これは、intdashで取得したデータの活用の手段を広げるため、日本テラデータ様のもつ次世代分析基盤「 Teradata Vantage 」と連携できるようになったことを指します。Visual M2M Data Visualizerが扱えるのは時系列データの分析のみであり、統計処理をやろうと思うと何かしらのカスタムコードをPythonで書く必要があったのですが、Vantageとの連携により、データ取得から分析まで、エンドユーザー側はコーディングすることなく、本来やるべき作業に集中できるようになりました。 プレスリリース3:SDKの提供開始 第3弾は、9月30日の「 高速IoTプラットフォーム「intdash」 アプリケーション開発キット「intdash SDK」の提供を開始 」です。上のintdashの構成で、しれっと2つのSDK: intdash SDK for Edge Device と intdash SDK for Python 「があります」とご紹介しましたが、これらは今年新規にリリースされたものです。 intdash SDK for Pythonについては以下のウェブサイトをご参照ください。 https://pypi.org/project/intdash/ また以下の情報もご参照ください。 ドキュメント サンプルコード(GitHub) intdash SDK for Edge Deviceについては以下のデベロッパーガイドをご参照ください。こちらは誰でも入手可能というわけではなく、ご利用される方に都度アクセス情報を払い出す形でご提供しています。 intdash Edge Agentデベロッパーガイド これまでのintdashの標準構成では、弊社の指定するターミナルシステム(エッジとクラウドのゲートウェイとなるコンピュータのこと)と、その下にぶら下がる弊社製のペリフェラルでしかシステムを構築できませんでした。しかしintdash SDK for Edge Deviceにより、ターミナルシステムとして扱えるコンピュータの種類が広がり *1 、システム構築の自由度が一つ上がりました。またこの開発環境を公開したことにより、弊社のエンジニアでなくとも、エッジサイドの組み込みソフトや、データ分析用のツールなどを作ることができるようになりました。 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 続いて第4弾は、同じく9月30日の「 アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始 」です。これにより、お客様の持つAWS環境内に簡単にintdashを構築することが可能になりました *2 。先にご説明した各種SDKと組み合わせると、弊社の直接のご支援なくとも、弊社が構築するのと同機能のintdash環境をお客様の手で構築することができるようになります。 おわりに 以上、各プレスリリースの位置づけをざっと説明してきました。さらにざっくりまとめると、 データ活用の流れの上流から下流まで、弊社自身のマンパワーが制約にならないように、要所要所を強化、または汎用化してきた 、ということになります。 intdashの正式リリースは2018年5月 で、まだリリースから2年半の比較的若い製品です。実はこの後も、年始にかけていくつか製品関連のプレスリリースを予定しております。本投稿が「このプレスリリースはどの部分をどう強化することを意図しているのか」の理解につながれば幸いです。 *1 : 現時点で対応しているプラットフォームは、AMD64 アーキテクチャー上の Linux、Raspberry Pi 上の Raspbian、NVIDIA Jetson 上の NVIDIA L4Tのみになります。 *2 : 2020/12時点ではAWS Marketplaceは一般公開ではなく、Private Offerのみ
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の14日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 今回の記事は前回の「5Gのネットワークを計測してみた」の続きになります。 tech.aptpod.co.jp 今回の記事では、以下の2つを紹介します。 モバイルルーターを Wi-Fi STATION SH-52A に固定して行った4G/5G のping/iperf3 の計測結果 4G/5G でのTCP/QUIC/UDP の計測結果 計測機材の紹介 計測場所の紹介 ネットワーク計測の結果 pingの計測 iperf3の計測 UDPの結果 TCPの結果 トランスポートプロトコルの評価 実験内容 評価結果 TCPの結果 QUICの結果 UDPの結果 考察 まとめ 計測機材の紹介 計測機材は前回と同じ機材を使用しています。 計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。計測用のサーバは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はUbuntu 20.04、サーバ側はUbuntu 18.04 を使用しています。 また、5G/4Gの接続にはDocomoから販売されている モバイルルーター Wi-Fi STATION SH-52A を使用しています。SH-52Aは「5G/4G/3G」で接続するモードと「4G/3G」で接続するモードを選択可能です。「5G/4G/3G」モードでは5Gの接続に固定されるわけではありませんが、今回計測を行った場所では安定して5Gに接続が可能でした。 以下のような構成で計測をします。 前回の記事の再掲になりますが、計測に使った機器は以下のように接続しています。 計測場所の紹介 前回の記事で計測を行った場所と同じ日産スタジアム東ゲート付近の広場で、12/4に計測を行いました。 ネットワーク計測の結果 pingの計測 pingの計測では、pingコマンドを使って1秒間隔で60秒間RTT (Round Trip Time)を計測しました。 比較のために、12/4 に計測した結果と前回の記事の計測結果の両方を載せます。前回の記事では4Gの計測は モバイルWi-FiルーターL-01G というモバイルルーターを使っていました。 以下の表には、前回の結果と今回の計測結果の、平均値、10パーセンタイル値、50パーセンタイル値、90パーセンタイル値を載せています。 4G(2020/12/4計測) 5G(2020/12/4計測) 4G(2020/12/2掲載) 5G(2020/12/2掲載) 平均値 40.3msec 43.3msec 74.5msec 38.5msec 10パーセンタイル値 31.7msec 32.9msec 65.2msec 30.4msec 50パーセンタイル値 40.5msec 44.0msec 75.6msec 39.15msec 90パーセンタイル値 47.9msec 52.12msec 83.9msec 46.09msec 前回の記事では、5Gの方が4GよりもRTTが小さいと紹介しましたが、モバイルルーターをSH-52Aに統一した結果を見ると、4Gと5GのどちらもRTTには差分がなさそうです。 L-01G を使用した場合のみ大きく違う値が出ていますが、この原因は今後調査を行い機会があればお伝えしたいと思います。 iperf3の計測 iperf3の計測では、UDPとTCP(輻輳制御はBBRを使用)のそれぞれについて、クライアントからサーバに20秒間データを送信する計測を3回行いました。 UDPの結果 UDPの計測は律速するまで帯域を上げています。4Gは25Mbps、5Gは50Mbpsを設定しています。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 20.9 Mbps 45.2 Mbps 17.4Mbps 45.2Mbps 2回目 21.2 Mbps 18.4 Mbps 24.6Mbps 47.0Mbps 3回目 20.7 Mbps 39.4 Mbps 25.0 Mbps 47.0 Mbps このように、おおむね5Gの方が高いスループットを得られています。 UDPの2回目の時は、未到達のパケットがかなり多かったため4G側より低い値になっていますが、ネットワークの変動によるものではないかと思います。 TCPの結果 TCPの場合はビットレートを指定しなくても流せる上限まで流そうとするのでビットレートの指定はしません。 計測結果は以下のようになりました。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 10.3 Mbps 40.3 Mbps 9.33Mbps 15.9Mbps 2回目 13.6 Mbps 43.4 Mbps 11.1Mbps 17.8Mbps 3回目 16.2 Mbpbs 34.2 Mbps 12.3 Mbps 34.4 Mbps おおむね5Gの方が高いスループットが出ています。ただし、5Gの結果は前回の記事の結果と今回の結果で大きく差が出ています。したがって、変動の影響はありうると考えるのがよさそうです。 トランスポートプロトコルの評価 ここからは、トランスポートプロトコルの評価についてお伝えします。 実験では、 TCP(輻輳制御はCUBICを使用) QUIC(輻輳制御はCUBICを使用) UDP をそれぞれ4G/5Gで評価しています。 計測は4G/5Gに対してそれぞれ5回ずつ行いました。 実験内容 実験は、高頻度なデータの塊をクライアントからサーバに伝送するユースケースを設定します。 送信するデータは1unit (データの単位)を8byteとします。これを1000 unit/secで送信します。そのために1msec毎に1unitのデータをクライアント側で生成します。 弊社製品のintdashでは、IPやTCPヘッダのオーバーヘッドを低減するために、一定期間のユニットをバッファリングして送信する仕組みを導入しています。 なので、それに倣って10個の連続したデータを一つの塊として送信します。このバッファリングしたデータ単位はflush *1 と呼ばれています。 以下の図のように、送信時刻までの10msec分のデータが1つのflushとしてまとめて送信されます。 flush クライアント-サーバ間の送信遅延を評価するために、データがクライアントで生成された時刻とサーバ側で受信した時刻の差分を遅延時間として定義します。 サーバでは1flush分のデータをまとめて受信します。そのため、flushの先頭のunitは、ネットワークの上りの遅延+10msec程度の遅延が発生します。 クライアントとサーバ間のデータのやり取り この場合、遅延時間は以下のようになります。 生成時刻 受信時刻 1unit 0msec ネットワークの上りの遅延 + 10msec 2unit 1msec ネットワークの上りの遅延 + 9msec 3unit 2sec ネットワークの上りの遅延 + 8msec ... ... ... 10unit 9msec ネットワークの上りの遅延+1msec ここから、理想的な状態では「ネットワークの上りの遅延+10msec」の範囲に、遅延時間が収まることが期待されます。 評価結果 それでは実際に結果を見ていきます。 TCPの結果 ここでは5回の計測結果の中から一つの結果を取り上げていますが、他の結果も同様の傾向になっています。 まずはヒストグラムです。横軸は、遅延時間、縦軸は頻度になっています。 TCP ヒストグラム 理想的には10msec以内に収まってほしいのですが、5Gの場合は20msec、4Gの場合は40msec の範囲にデータが収まっていることがわかります。pingの結果が4G/5Gで差異がなかったことを考えると、4G/5Gの回線のみを変えて結果に差分が出た点は疑問が残ります。 原因は、クライアントからサーバの間のどこかで想定外のバッファリングが行われたのではないかと予想しています。 ヒストグラムだけだと理解が難しいので、時系列でのプロットも見てみます。 まずは、5Gの結果です。以下のグラフは横軸が10秒間に送信される10000unitのデータの番号で、縦軸がそれぞれの遅延時間になります。5Gの場合は19msec~36msec 辺りに収まっていることがわかります。 TCP(5G)の結果 次に4Gの結果です。4Gの場合は多くの値が、10msec~50msecの間にあることがわかります。 TCP(4G)の結果 原因は調査が必要ですが、ヒストグラムの違いの理由は明らかになりました。 QUICの結果 QUICの結果のヒストグラムは以下のようになります。 QUIC ヒストグラム ここでも、理想的には10msec以内に収まっていてほしいものが、5Gの場合は20msec、4Gの場合は40msec の範囲に収まっていることがわかります。 5Gの場合はTCPと同じような結果になっています。一方で4Gの場合は2つの山が見える結果になっています。 TCPの時と同様に、時系列のプロットを見てみます。 5Gの結果は以下のようになります。ヒストグラムで見たように、18msec~38msecの間にデータが収まっていることがわかります。 QUIC(5G)の結果 次に、4Gの結果です。4Gの場合は、18msec~34msecの範囲と、15msec~45msecの二つのパターンがあることがわかります。これはネットワーク上で別の経路を通った結果が反映されている可能性がありそうです。この結果、ヒストグラムには二つの山ができていると考えられます。 QUIC(4G)の結果 UDPの結果 UDPの結果のヒストグラムは以下のようになります。 UDP ヒストグラム TCP/QUICの結果と異なり、5Gの方が遅延時間のとる範囲が大きくなっています。 ここでも時系列のデータを見てみます。 5Gの結果はこれまでと異なり、値のとる範囲が安定していないことがわかります。その結果がヒストグラムにも反映されていたようです。 5G(UDP)の結果 逆に4Gの結果は、最初の2秒(2000unit)の振れ幅は大きいですが、それ以降は 19msec~29msec 辺りに落ち着いているデータが多いように見えます。 4G(UDP)の結果 考察 疑問が多く残る結果になりましたが、少なくとも以下の3点は言えそうです。 10unit以上のデータがまとまって届いているため、経路上のどこかで予期していないキューイングが発生している可能性が考えられる ネットワーク上の経路の違いが遅延時間に影響を与えている TCP/QUIC/UDPすべての結果を踏まえると、4G/5Gの違いがあると言い切れる根拠は今回の実験では得られなかった 疑問が多く残っているため、今後も調査・検証を続けていこうと思います。 まとめ 今回の記事では「5Gでのトランスポートプロトコルの評価」について紹介しました。 ネットワークの再計測の結果やトランスポートプロトコルの評価結果は疑問が多く残る結果となりました。今後も調査・検証を継続、新しい事実が判明したらお届けしたいと思います。 最後までご覧いただきありがとうございました。 *1 : https://www.aptpod.co.jp/products/white-paper/iscp.pdf
みなさま、こんにちは。研究開発グループと製品開発グループ に兼務で所属しております、きしだです。 aptpod Advent Calendar 2020 11日目を担当します。今回は機械学習に関わるエンジニア向けに、最近AWSがリリースしたAWS Lambdaの新機能を利用して、サクッと推論APIを作るネタをご紹介します。 aws.amazon.com 推論をすばやくAPI化する意義 その前に、推論箇所をAPIとしてすばやく用意できる必要性について簡単に触れたいと思います。 機械学習関係の案件では、お客様側も理解できるKPIを立てることが非常に重要視されています。例えば「モデルの正解率を〜にしたい」や「モデルの動く速さを〜にしたい」などですね。これらの具体的な数値はお客様と議論を重ね、お互いにしっくりくる数値に落とし込む必要があるのですが、これがとてもむずかしいのです。なぜでしょうか。 お客様と開発側で共に運用イメージがわかず、モデルに対して運用時の影響力がピンとこない 具体的な運用イメージをすべて仮説ベースで作り上げるため、仮説にほころびが生まれやすく議論が錯綜する 上記に心当たりがある方もいらっしゃるのではないのでしょうか。これらはいずれも「システム運用を前提としたモデルの使い方をお客様側でイメージできていない」ことに起因します。このときにAPIまでを用意してお客様に共有すると、以下のようなことが期待できます。 APIを叩くだけで推論結果が返ってくるので、お客様側もモデルを試しやすい APIとして用意されているので一時的にシステムに組み込むことができ、試験運用が可能になる 必要最低限の費用で、お客様側で運用しながらモデルをさわることができる ガチガチにシステムを作るより、まずは小さく簡単なシステムから始めることでイテレーションを回せる いいこと尽くめですね。お客様側もモデルにふれる機会がグッと増え、議論を活発にまわすことができ、より納得感をもってすすめることができます。そのためにモデル開発に留めず運用側に一歩踏みこむ姿勢が大事になり、モデルの推論箇所をAPIとしてすばやく用意することが必要になります。 今回の記事はその一歩のヒントになるよう、私が検証した内容の一部をご紹介するものです。 AWS Lambda コンテナサポートの嬉しいポイント AWS Lambdaといえば、 「サーバーのことを意識しないサーバーレスコンピューティングを提供してくれるサービス」で、アプリケーションをすばやく実行環境に持っていきたい時に重宝する素晴らしいサービスです。それだけでなく、Amazon API Gateway などの他サービスをトリガーにしたイベント定義も楽々できることで有名ですよね。 機械学習の実行環境もAWS Lambda上で動かせたら楽だろうなぁと思う時がありつつも、以下の制約により断念していました。 パッケージの割り当て量が 250MB Lambda向けにパッケージを作る際、依存パッケージが多いと手間がかかる ローカルテスト/デバッグはAWS Lambdaだけでお手軽にできない とくに容量制限が厳しく、機械学習の場合はTensorFlowやらKerasなどの大きめのパッケージを使用することが多いため、到底250MBに抑えるなんてことはできません。 しかしながら冒頭に記載のAWSのリリース内容では、 大きく以下のような内容がピックアップされていました。 コンテナイメージをそのままLambda関数としてデプロイできる コンテナイメージは 10GB までならデプロイ可能 ローカルで実行できるLambdaのRuntime APIツール Lambda Runtime Interface Emulator がオープンソース化される パッケージの容量が大きく緩和され、コンテナイメージ向けのローカルテストツールも提供されるとのことです。これは、従来のLambdaに対して抱いていた課題感をすべて払拭してくれる予感がしますね。 前置きが長くなってしまいましたが、試してみないわけにはいかない! ということで、実際に動かして見ましょう。 構成 今回は、 こちら のTensorFlowを使用した「車両を検出するモデル」を、AWS Lambda上で動かしてみます。 *1 以下のように、Amazon API Gatewayと連携させてクライアントPCから画像をPOSTすると、車両の検出結果を返すAPIを作ります。 構成図 TensorFlowで作成されたモデルが動くコンテナイメージを用意しAmazon ECRにPushします AWS Lambda上でLambda関数を作成、Amazon API Gatewayと紐付けて画像のPOST用APIを用意します コンテナイメージを作ってローカルでテストする まずはコンテナイメージを作ってみましょう。 手元にあるコンテナをそのまま使えばよいのではなく、AWS Lambda向けにコンテナを作りなおす必要があります。 理由としては、Lambda Runtime API を実装する Lambda Runtime Interface Clients を取り込む必要があるためです。このクライアントを実行するためにいくつか依存ライブラリをインストールしなければいけません。 上記をふまえ、以下のようなDockerfileを準備します。 # Define function directory ARG FUNCTION_DIR="/function" FROM tensorflow/tensorflow:1.14.0-py3 # Install aws-lambda-cpp build dependencies RUN apt-get update && \ apt-get install -y \ g++ \ make \ cmake \ unzip \ libcurl4-openssl-dev \ libsm6 \ libxrender1 \ libxtst6 \ # Install for vehicle detection python3-dev \ libsm6 \ libxext6 \ libxrender-dev \ libgl1-mesa-dev \ python3-tk # Include global arg in this stage of the build ARG FUNCTION_DIR # Create function directory RUN mkdir -p ${FUNCTION_DIR} # Copy function code COPY app/app.py ${FUNCTION_DIR}/app.py COPY app/lib/ ${FUNCTION_DIR}/lib/ # other python file RUN mv ${FUNCTION_DIR}/lib/* ${FUNCTION_DIR} # Install the runtime interface client & other python package COPY requirements.txt / RUN pip install --upgrade pip && \ pip install awslambdaric && \ pip install --target ${FUNCTION_DIR} --no-cache-dir -r requirements.txt # Set working directory to function root directory WORKDIR ${FUNCTION_DIR} # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] いくつかポイントを記載します。 # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie コンテナをローカルでテストする際は、冒頭で紹介の通り Runtime interface emulator を使います。ローカルでテストしたい場合は AWSの公式ドキュメント に従い、インストールしておきましょう。 (※コンテナにいれなくても、コンテナ実行時にマウントする方法もあるのでここは任意です) ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] 実行時は、 Lambda Runtime Interface Client により対象の関数が呼び出されます。上記を実行するため ENTRYPOINT でクライアントの実行コマンド、 CMD で対象関数を指定します。 (今回は app.pyの handler 関数を対象) 上記の準備ができたら、buildします。 docker build -t serverless-function:latest . buildできたら、まずはローカルで動作するか確認します。 以下を実行します。 docker run -p 9000:8080 --entrypoint /usr/bin/aws-lambda-rie --name serverless --rm serverless-function:latest /usr/local/bin/python -m awslambdaric app.handler すると、以下のようにコンテナ内のLambda Runtime APIが動作します。 time="2020-12-08T07:30:14.459" level=info msg="exec '/usr/local/bin/python' (cwd=/function, handler=app.handler)" 早速エミュレーターが提供しているエンドポイントを叩いて見ましょう。 以下の画像をエンドポイントに投げてみます。 車の画像 以下はクライアント側をPythonで簡単に書いたものです。 # Amazon API Gatewayの代わりに base64にencode data = base64.b64encode(f.read()) data = { "body" :data} response = requests.post( "http://localhost:9000/2015-03-31/functions/function/invocations" , json=data) contents = json.loads(response.content) imageBody = base64.b64decode(contents[ "body" ]) Image.open(io.BytesIO(imageBody)) 上記を実行すると、コンテナ側のログが出力されていました。 START RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Version: $LATEST /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:516: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. ・・・ 2020-12-08 07:30:45.707671: W tensorflow/compiler/jit/mark_for_compilation_pass.cc:1412] (One-time warning): Not using XLA:CPU for cluster because envvar TF_XLA_FLAGS=--tf_xla_cpu_global_jit was not set. If you want XLA:CPU, either set that envvar, or use experimental_jit_scope to enable XLA:CPU. To confirm that XLA is active, pass --vmodule=xla_compilation_cache=1 (as a proper command-line flag, not via TF_XLA_FLAGS) or set the envvar XLA_FLAGS=--xla_hlo_profile. END RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 REPORT RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Init Duration: 2.54 ms Duration: 7020.34 ms Billed Duration: 7100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB Jupyter notebook側でもデータが返ってきました。 推論もできている様子です👍 推論結果 ECRにプッシュし、AWS Lambda上で実行してみる いよいよ実環境へのデプロイです。 ECRに先ほど作成したコンテナイメージをpushします。(docker loginが必要ですのでご注意ください) docker tag serverless-function:latest {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest docker push {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest コンソールを見ると無事pushできていました。 ECRの様子 次に Lambda関数を作成してみます。関数の作成時、「コンテナイメージ」を選択するとECRにコミットされているコンテナイメージを選択することができます。 関数の作成 これで無事作成できました。 次にAmazon API Gateway を連携させて、推論したい画像データをPOSTするためのAPIを構築します。 構築後のURL 無事構築できたので、ローカルの時と同様に画像を送付してみます。  ローカルテストと同様に、クライアント側として以下のコードを用意します。 response = requests.post( "https://{API_GATEWAY_CODE}.execute-api.ap-northeast-1.amazonaws.com/default/vehicle-endpoint-test" , data=data, headers=header ) imageBody = base64.b64decode(response.content) Image.open(io.BytesIO(imageBody)) 上記を実行すると....以下のように車が推論された結果が返ってくることが確認できました! 推論の結果 予想より簡単に推論エンドポイントを立てることができました! 🎉 使ってみた感想 一通り試してみた感想をまとめておきます。 良かった点 予め用意したコンテナをベースにLambda向けにビルドし直せばいいので、環境の作り方が楽! AWS Lambda Runtime API との連携テストもローカルで実施しやすくなり、環境準備がしやすい! 今後の課題となりそうな点 基本Lambda関数の実行時の仕様は変わらず。AWS Lambdaの仕様でデータのキャッシュはできないので、推論する場合毎度モデルをロードする必要がある ローカルでテストできるのは Lambda Runtime API経由のみなので、Amazon API Gatewayなどの他サービスとの連携までを見据えたテストまで完結することはむずかしい まとめ AWS Lambda上に推論環境として構築したコンテナイメージをデプロイし、Lambda関数として動かすことができました。外部に共有できるAPIとしてはまだ課題はありそうですが、とても簡単に試すことができるのでケースに当てはまりそうな方がいたらぜひ活用してみてください! ちなみに余談ですが、弊社製品のintdashというデータストリーミングプラットフォームと今回の推論APIのような機械学習ツールと連携すると、データの収集から推論まで行えるシステムをサクッと作れます。 もしまた機会があれば、今年リリース発表された intdash SDK for Python を使って、今回構築したAPIとintdashを連携したパターンもやってみたいと思います。 intdash SDK for Python のリリース発表はこちら www.aptpod.co.jp ご覧いただきありがとうございました! *1 : https://github.com/ahmetozlu/vehicle_counting_tensorflow より一部引用
はじめに Advent Calendar 2020 10 日目を担当する、SRE チームの柏崎です。 nginx をお使いのみなさま、TLS 導入が当然となっている現代、どんな設定をされていますでしょうか。 NGINX Config や SSL Server Test のようなサイトを利用し、セキュリティに気を配っている方も多いと思います。 そんな nginx ユーザのみなさま、最近 nginx 1.19.4 がリリースされ、 イケてる TLS 設定ができるようになった事はご存じでしょうか? ChaCha20-Poly1305 について まず前提知識として、ChaCha20-Poly1305 について、ほんの少し書いておきます。 ChaCha20-Poly1305 は、ストリーム暗号である ChaCha20 とメッセージ認証符号である Poly1305 を組み合わせた、認証付き暗号です。 RFC 7905 (2016/06) で TLS における利用が標準化され、TLS 的には比較的新しい暗号方式となっています。 OpenSSL だと 1.1.0 以降で利用可能です。 これまで、現実的に使える暗号方式として一択だった AES-GCM に加え、ChaCha20-Poly1305 が加わった形になります。 クライアントによる 2 方式の性能差 ChaCha20-Poly1305 はソフトウェア処理に向いた、簡潔なアルゴリズムです。 一方 AES-GCM は、ソフトウェア処理ではあまり性能が出ませんが、ハードウェア処理が利用できる環境ではとても高速です。 Intel や AMD の CPU には AES をハードウェア処理できる AES-NI という拡張機能が載っていますが、ARM 等には載っていません。 つまり、AES-GCM と ChaCha20-Poly1305 のどちらが性能が高いかは、環境によって違ってくるということです。 Android スマートフォンや Raspberry Pi、最近発売された M1 Mac など、ARM で動くデバイスは意外と多いです。 特に、弊社の製品のエッジ向けミドルウェア intdash Edge は、Raspberry Pi を含め様々なデバイスで利用されるので、この性能差は気になるところです。 実際に、openssl speed コマンドを使って、8192 バイトブロックにおける、AES-GCM と ChaCha20-Poly1305 の暗号化・復号化の性能を測ってみました。 ざっくりとした性能差を知るのが目的のため、それぞれ 1 回ずつの計測です。 利用したコマンドは下記です。 ### aes-128-gcm の暗号化 $ openssl speed -evp aes-128-gcm ### aes-256-gcm の暗号化 $ openssl speed -evp aes-256-gcm ### chacha20-poly1305 の暗号化 $ openssl speed -evp chacha20-poly1305 ### aes-128-gcm の復号化 $ openssl speed -evp aes-128-gcm -decrypt ### aes-256-gcm の復号化 $ openssl speed -evp aes-256-gcm -decrypt ### chacha20-poly1305 の復号化 $ openssl speed -evp chacha20-poly1305 -decrypt 確かに、Intel では AES-GCM の方が高性能ですが、ARM では逆転して ChaCha20-Poly1305 が高性能ですね。 クライアントの暗号スイートリスト TLS クライアントによっては、ネゴシエーション時の暗号スイートリストが、前述の性能差を考慮したものになっています。 クライアントの暗号スイートリストは Qualys SSL Labs の SSL Client Test で確認することができるので、実際に Intel MacBook と M1 MacBook で比較をしてみました。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome 確かに、Intel MacBook では AES-GCM が優先されているのに対し、M1 MacBook では ChaCha20-Poly1305 が優先されていますね。 これまでの課題 TLS のネゴシエーションにおける暗号スイートは、クライアントとサーバの暗号スイートリストを比較することで、1 つに決定されます。 しかし、一般的な TLS サーバは、「サーバ側の暗号スイートリストを優先して」暗号スイートを決定するような設定がされています。 つまり、TLS クライアントが適切な優先度で暗号スイートリストを送ってきたとしても、サーバ側の優先度により決まった一つに決定されてしまうのです。 例えば、Raspberry Pi は ChaCha20-Poly1305 を使ってラクしたいのに、限られたリソースのなか苦手な AES-GCM で頑張らないといけない、みたいな事が起きてしまいますね。 nginx 1.19.4 でどうなる? nginx 1.19.4 では、 ssl_conf_command ディレクティブ を使って、OpenSSL の設定を行うことができるようになりました。 下記の設定を追加すると、「クライアントが ChaCha20 の暗号スイートを優先している場合はサーバも優先する」という動作をさせることができます。 イケてる! ssl_conf_command Options PrioritizeChaCha; この設定は、 僕が勝手に nginx の実験場にしている アプトポッドのコーポレートサイト にも適用しています。 Chrome でアクセスして開発ツールを開くと、下記の画像のように環境によって適切な暗号方式でつながるはずです。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome さいごに ちょっとマニアックになってしまいましたが、nginx の TLS 設定についてお話をしました。 ちなみに M1 Macbook でのスクショは、同僚に協力していただきました。ありがとうございます!M1 羨ましい! nginx 1.19.4 は mainline のバージョンなので、intdash のサービス用サーバでは、まだこの設定を適用できておりません。 stable 版の 1.20.0 が待ち遠しいですね。 この記事は、筆者の個人ブログの記事をベースに加筆修正したものです nginx 1.19.4 以降で ChaCha20-Poly1305 の運用が現実的になった件
aptpod Advent Calender 2020 、 9日目の記事です。 本日の担当は、組込み開発チームでFW開発を担当している矢部です。 はじめに 組込み機器の開発に関わって1x年ですが、入出力の自動化が難しい機器も多く、結局手作業になって無駄にボタン押下やUI操作の速度が洗練されたりします。私の場合、ゲームをよくやっていたからか、効率よく操作させることができるとちょっと嬉しかったりもします。 とはいえリグレッションテストなどを考えると極力自動化したいところなので、過去いろいろと試行錯誤してきました。 今回は、その中でも比較的簡単な、USB通信をインターフェースに持つ機器のテストを自動化した際の手法について簡単に解説します。 前提知識 今回の記事で詳しくは解説しない部分をさらっと。 BDDとは 振る舞い駆動開発(Behavior Driven Development)と呼ばれるもので、TDDの一種。 システムに期待する挙動や制約などを自然言語に近い形式で記述し、テストを実行できるようにしたもの。自然言語で書けるため、非開発者であってもシステムの仕様がわかるように書ける(らしい)。 テストファーストな開発だと「テストコードは仕様である」という考え方になるので、それを突き詰めたものがBDDかな、と思っています。 BDDのフレームワーク 主要な言語であれば、たいていBDDを実現するためのフレームワークが存在します。 Ruby: RSpec Python: Behave C/C++: Catch2 etc... 私が最初に触れたのはRSpecですが、最近はPythonを利用する機会が多いため、今回はBehaveで実装します。 Pythonの開発環境 色々ありますが、最近はpipenvを使っています。設定や実行方法など、バランスが取れていて使いやすい。 公式: https://pipenv-ja.readthedocs.io/ja/translate-ja/ Pipenvを使ったPython開発まとめ: https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a USB機器をテストする 実行環境 Ubuntu MATE 20.04 今回はLinuxのデバイスドライバを利用する都合上、テスト環境はLinuxになります。 Python環境構築 テスト用のディレクトリに、pipenvを利用してBehaveをインストールします。 $ mkdir usbtest $ cd usbtest $ pipenv --python 3 $ pipenv install behave テスト対象 今回は私が開発に関わった、aptpodの CAN-USB Interface - AP-CT2A 1 を対象にします。実際に AP-CT2A ではこの方法で全機能の自動テストコードを書いています。なお、2020/12 時点ではデバイスドライバなどのコードを公開していないため、細かいところはぼかしたり改変しています。 PythonでUSBデバイスを操作する 方法はいくつかあると思いますが、今回はデバイスドライバがあるため、 ioctl を利用して操作します。 基本的な手順は以下になります。 デバイスファイルをopenする。 ファイルディスクリプタを利用して、ioctlでIO制御する。 ioctlのリクエストは汎用的なものであればPython側にありますが、今回のように特殊なものは自前で設定する必要があります。 リクエストの種別として IO、 IOR、_IOWなど 2 があり、C/C++であれば以下のように実装されます。 struct fw_data_s { int value; }; typedef struct fw_data_s fw_data_t; #define CMD1 _IO( 'A' , 0x1 ) #define CMD2 _IOR( 'A' , 0x2 , fw_data_t) #define CMD3 _IOW( 'A' , 0x3 , fw_data_t) 上記のコマンドがデバドラ側にあると仮定して、デバイス操作のためのクラスをPython上で実装すると以下のようになります。 __make_xxx のメソッドが、リクエストの生成部分になります。 import ctypes import fcntl import logging import os class FwData (ctypes.Structure): _fields_ = [( "value" , ctypes.c_int32)] class ApCt2a : def __init__ (self, path): self.__path = path def open (self): self.__fd = os.open(self.__path, os.O_RDWR | os.O_NONBLOCK) if self.__fd == - 1 : logging.critical( "[APTTRX] failed open path:{0}" .format(self.__path)) def close (self): os.close(self.__fd) self.__fd = None def cmd1 (self): fcntl.ioctl(self.__fd, self.__make_io_req( 0x1 )) @ property def data (self): data = FwData() fcntl.ioctl(self.__fd, self.__make_ior_req( 0x02 , ctypes.sizeof(data)), data) return data.value @ data.setter def data (self, value): data = FwData() data.value = value fcntl.ioctl(self.__fd, self.__make_iow_req( 0x03 , ctypes.sizeof(data)), data) def __make_io_req (self, nr): return ord ( "A" ) << 8 | nr def __make_iow_req (self, nr, size): return 1 << 30 | ord ( "A" ) << 8 | nr | size << 16 def __make_ior_req (self, nr, size): return 2 << 30 | ord ( "A" ) << 8 | nr | size << 16 テストする仕様 AP-CT2Aのメイン機能は、入力したCAN 3 データに時刻情報を付与してホストに渡すというものです。さらに複数台あった場合、同期用のケーブルで機器間を接続することで、付与する時刻を同期させることができます。 AP-CT2A接続図 上の写真で、機器間を繋いでいるのが同期ケーブルです(開発用の機材なので汚いのはご容赦を)。今回は、この同期機能を確認するためのテストを例に取ります。 テストしたいシーケンス 2台のAP-CT2Aを接続する 2台のAP-CT2Aを時刻同期する CANデータを500kbpsで受信できるよう設定する CANデータを入力する 2台のAP-CT2A受信したCANデータの時間が一致していることを確認する 実際に書いてみる ファイルは以下のような形で構成されます。 .feature にテストシナリオ、 steps/ 以下に実際にシナリオ内で実行される処理(step)を実装します 4 。 features/ features/everything.feature features/steps/ features/steps/steps.py 今回の例だと、まずテストシナリオをこのように書きます 5 。 Feature: AP-CT2A テスト Scenario: 複数台で時間同期する Given 2台のAP-CT2Aを用意する And AP-CT2Aを時刻同期する And AP-CT2Aを500kbps入力に設定する When AP-CT2AにCANデータを500kbpsで10個入力する Then AP-CT2AがCANデータを10個取得している And AP-CT2Aが受信したCANデータの時刻情報が一致している そして、各stepの処理について、以下のように書きます。 @~ のところで対応するstepを指定し、変数として利用したい箇所は {} で記載します 6 。 from behave import given, step, then, when @ given ( "AP-CT2Aを時刻同期する" ) def step_impl (context): for d in context.devices: d.sync() @ when ( "AP-CT2AにCANデータを{baudrate:d}kbpsで{num:d}個入力する" ) def step_impl (context, baudrate, num): send_candata(baudrate, num) @ then ( "AP-CT2AがCANデータを{num:d}個取得している" ) def step_impl (context, num): for d in context.devices: data = d.receive() assert len (data) == num 実行例 上のシナリオを実行した例。 $ pipenv shell $ behave Feature: AP-CT2A テスト # features/ap_ct2a.feature:1 Scenario: 複数台で時間同期する # features/ap_ct2a.feature:3 Given 2台のAP-CT2Aを用意する # features/steps/ap_ct2a_steps.py:6 0.001s And AP-CT2Aを時刻同期する # features/steps/ap_ct2a_steps.py:11 0.001s And AP-CT2Aを500kbps入力に設定する # features/steps/ap_ct2a_steps.py:16 0.000s When AP-CT2AにCANデータを500kbpsで10個入力する # features/steps/ap_ct2a_steps.py:21 0.000s Then AP-CT2AがCANデータを10個取得している # features/steps/ap_ct2a_steps.py:26 0.000s And AP-CT2Aが受信したCANデータの時刻情報が一致している # features/steps/ap_ct2a_steps.py:31 0.000s 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 6 steps passed, おわりに 今回はUSB機器を例に取りましたが、何らかのプロトコルで通信することで動作するものについてはほぼ自動テスト化できるものと思っています。 外部からの操作を必要とするものに関しては(例えばUSBの挿抜とか)なかなか自動化は難しいですが、aptpodには優秀なHWエンジニアも在籍しており、テスト用の治具も自作したりします。 組み込み機器であっても、既存のフレームワークをうまく使ったり、アイデア次第で煩雑なテスト業務を軽減させることができますので、トライしてみてはいかがでしょうか。 https://www.aptpod.co.jp/products/hardware/ のペリフェラルデバイスの項に載っています。 ↩ https://www.quora.com/What-is- IO- IOR-and-__IOW-in-ioctl ↩ https://ja.wikipedia.org/wiki/Controller_Area_Network ↩ https://behave.readthedocs.io/en/latest/tutorial.html#features ↩ 普段は日本語では書きません。マルチバイト文字怖い。 ↩ https://behave.readthedocs.io/en/latest/tutorial.html#python-step-implementations ↩
Advent Calendar 2020 8日目 を担当します。ハードウェアグループの加藤です。 私は2020年2月にハードウェアエンジニアとして入社しました。10か月間業務を行って思ったこと、感じたことなどを書きます。 自己紹介 ハードウェアエンジニアの仕事 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある 社員同士の互いの信頼や尊重もある。 やらされている感がない 事業に無理がない 技術的に本質的な方向を向いている 業務環境、開発に使用しているツール アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 未経験の業務もやらせてもらえるのでスキルアップ 妻が喜んだ 最後に 自己紹介 簡単に私がアプトポッドに入社するまでに勤めた会社についてご説明します。特に意識はしていませんが、入社して思ったこと感じたことというのはこれまで勤めた会社との比較になっていると思います。 ゲーム機等の会社 1994年に入社。ゲームセンター向けのゲーム機器を6年間、海外カジノ向けのスロットマシンを13年間、業務用フィットネスバイクの開発を2年間、各グループ会社の開発部門でハードウェアエンジニアとして商品開発を行いました。海外カジノ向けスロットマシンのグループ企業は、米国のラスベガスに本社を設置するところからのスタートでいろいろ大変でした。 業務用フィットネスバイクの企画から販売まで行うスタートアップ企業 設立されたばかりの会社に入社しました。社員数は10名以下。ハードウェアもソフトウェアも自分以外はわかる人はおらず大変でした。業務用フィットネスバイクというのは、スポーツクラブ等においてある自転車型の運動機器です。この会社では大学・企業・プロの運動部にターゲットを絞った製品の開発を行いました。ここでは4年ちょっと勤めて、立ち上げを成功させることができました。この後、アプトポッドに入社しました。 ハードウェアエンジニアの仕事 アプトポッドのハードウエアエンジニアの業務内容はこんな感じです。 ハードウェア製品の企画、投資(費用・工数)回収計画の立案 設計、開発、評価等 規格・認証試験の対応 製造会社の選定、その他いろいろも含めて量産できるようにする。 量産対応(生産を継続する。生産効率や品質の向上も。) 顧客案件のHWカスタマイズ対応 企画から量産まで全部ですね。 製造そのものは必要な設備等を有する会社に委託しています。そうは言っても部品のEOL対応などは自分達で判断する必要があります。設計や開発は都度終わらせることができますが、生産はそうはいきません。製品の種類が増えてきた時にどうするかはこれからの課題となっています。それでも自分たちで効率よく行っていくのが最も良いのかなぁ。と考えています。 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある パソコンを自宅に持ち帰ることができます。 ”何言っているの?”という人の方が多いでしょうか? 私がこれまで勤めていた会社では平社員はパソコンの持ち出しができませんでした。 オフィスには、お菓子コーナーがあってお菓子食べ放題です。カップ麺も栄養ドリンクも置いてあります。コーヒーも飲めます。自動販売機の飲み物も1本30円です。 しかし、無茶苦茶なことをする人はいません。無茶苦茶な事というのは、本当に食べ放題食べてしまったり、カップ麺や自販機の飲み物を大量に持ち出して外で売りさばいたりということです。 新型コロナウィルス騒ぎ以降、会社全体として在宅勤務が主体となったのですが、「在宅勤務だからやらないといけないこと」というのもありません。 第一印象は、「会社は社員を信頼していて、社員のモラルも高い。すごいなこの会社。」でした。 社員同士の互いの信頼や尊重もある。 社内でなにか人にお願いする時、人からお願いされる時、期限を設定しないのです。誰もがすぐに対応します。なので、必要最低限の時間で事が進んでいきます。いやぁ、すごいです。 打ち合わせを設定する時は、参加者のカレンダーを確認して空いてさえいれば、好きなところに入れられます。極端な話、今すぐ!でもOKです。それで注意されたり怒られたりすることはありません。 やらされている感がない 上司の都合で書類を作成したり提出したりを求められることがありません。業務のスケジュールも自分で立てますし、レビューも自分で設定します。準備ができたらカレンダーに設定するだけなので、参加者の都合の調整などは発生しません。 事業に無理がない ハードウェアは量産時に材料費がかかります。販売価格の設定に無理があるとずーっと赤字のままになってしまいます。しかし、競合他社と同じ内容の製品を販売することになってしまう場合、営業部門の人は価格を安く設定することを求めざるを得なくなります。 アプトポッドの場合、独自性と技術力の高い製品・サービスを提供することができるので、販売価格や原価で無理を求められることはありません。 一般的に、独自性の高い商品であったとしても、ハードウェア単体だと同等品は簡単に作られてしまうので、売れて目立ってしまうとすぐ価格競争になってしまいます。アプトポッドの場合、そういう心配も不要であり、設計・開発に集中できるのはいいなぁと感じています。 技術的に本質的な方向を向いている 設計開発部門が向いている技術的な方向性に対して、私個人が安心感を得られるかどうかということです。主観なので正しい/間違っているはないのですが、自分にとっては大事に考えていることです。何かというと、例えば掃除機を作っている会社があったとして、吸引力に優れた掃除機を作れている会社であるなら安心で、(お客さんの要望だったとしても)本体の重量が軽いとか紙パックの容量が大きいといった掃除機を売ったり売れちゃったりしている会社であれば不安といった具合です。ディスプレイだったら、画面大きく、解像度高く、発色に優れた製品を出せていれば大丈夫で、”専用の眼鏡をかけると立体に見えます。”ということだと、「大丈夫かな?」って感じです。 データをクラウドに保存し、そこから閲覧する製品・サービス( IoTプラットフォーム intdash )を行っている会社で「データの完全回収」「低遅延」「高速」(これだけではありませんが)に対する方向性が社内全体で見受けられ、これに対し私は安心感を得ました。 業務環境、開発に使用しているツール 出社する/自宅で仕事するは本人の自由 オフィスは新宿区四谷四丁目 勤務時間も完全に裁量労働制 社内コミュニケーションはSlack 打ち合わせはGoogleMeetが基本、GoogleCalendarで互いの予定を確認 ドキュメント作成はConfluence、課題管理はJIRA 基板CADはQuadcept、筐体設計CADはFusion360 CADは「必要な機能が一通り揃っており手頃な価格帯のもの」を使用していると思います。選定する人の好みや経験に影響されやすいと思うのですが、こんなところにも柔軟で合理的な会社の色がでているように感じました。 アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 成功体験がないとなかなか大きい成長が得られない。そして、成長市場でないと成功体験を得るのは難しい。さらに、成長市場であっても人や組織や方向性がしっかりと確立されていないと長続きしない。アプトポッドは全てを満たしており、そんな中で仕事をできることに幸せ(と書くと胡散臭いですが)を感じ、大いにモチベーションをもって仕事に取り組むことができるようになりました。 未経験の業務もやらせてもらえるのでスキルアップ 必要とされる業務であれば、やりたいと言えばやらせてもらえます。私の今年の業務の場合、全く経験はありませんでしたが3DCADで基板筐体用のデータや加工用の図面を作成したりといった事をやらせてもらえました。今回のような記事の投稿というのも初めての体験でした。ただ、「やって良い」というだけでなく、周りの多くの方々に教えていただきながら業務を進めることが出来るので、自身のスキルアップが実現できました。 妻が喜んだ アプトポッドに勤めることになって、私の妻も「よかったわぁ」と大変喜んでいます。主な理由はふたつです。 ほとんど在宅勤務なので、「いつもそばにいるので安心」「家なので食事も制御可能」 私の妻の場合、「私が見える場所にいる。自分が帰ってきた時に私が家にいる」のが安心ということでした。 新型コロナウィルス騒ぎ以降9.5カ月間で出社したのは9日程度、EMC試験の為の外出が5日程度でした。 「将来性のある業種なので安心」「社員数も多い会社なので安心」 思うところはありますが、まぁ喜んでくれるのはありがたいことです。 社員数はその会社の成長フェーズによって変化するもので、多ければ良いというものではないと思います。しかし、社員数10名以下の中小企業で社員1人が頑張っているだけでは、なかなか会社も個人も成長は難しく、妻の言っていることも100%間違っているというわけでもないかな。とも思います。人数が少ない(特に1人の場合)と得られる情報が少なく進歩に時間がかかるのでキツイです。 アプトポッドよりも社員数の多い会社は多数あるでしょうが、各社員が優秀であることと効率の高いツールを使いこなしていることから、共有している情報の質と量は高いレベルにあるのだろうなと思っています。 最後に 社外の方は、良い事ばっかり書いてあるようにお感じになる方もいらっしゃるかもしれません。しかし、今回の記事は私が思ったこと・感じたことで嘘はありません。 もしアプトポッドに興味を持っていただけたら、 弊社の採用ページ に足を運んでいただけるとありがたく思います。
Advent Calendar 2020 7日目担当デザイン室の上野です。 デザイン室では製品や案件のUIデザインを行っています。 弊社のWebベースのダッシュボードアプリケーション Visual M2M Data Visualizer で可視化できるVisual Partsには3Dモデルを表示するパーツも存在します。 そこで用いられる3Dモデル作成も担当しています。 パーツをパネルに貼り付けてデータを疎通させると以下の画像のようにモデルがサーバに送られてきたデータと同期してリアルタイムに動くようになっています。 こういった3Dモデルでのデータ可視化は直感的でわかりやすいため、製造業のお客様の現場で需要があります。 今回はそこで使われている3Dモデルの作成フローと気をつけている3つのポイントを書いていこうと思います。 モデリングツールについて 3Dモデルの作成フロー モデリング時に気をつけている3つのポイント ちょうど良いくらいにポリゴン数を調整する 法線の向きを正しく ゴミを残さない モデリングツールについて 弊社では Maya LT を主に使用しています。 MAYA LTは元々MAYAと呼ばれるピクサーなどが使っている有名な3DCGソフトがあり、そこからレンダリングなど映像に特化した機能を制限し、ゲームなどをはじめとするリアルタイムに描画するモデリングに特化したソフトウェアになっています。 MAYA自体が業界標準的な側面もあり、ドキュメントがとても豊富です。 他には Blender というツールも使い始めています。 こちらは無料で間口が広く様々なユーザーがおり拡張機能も豊富なのが魅力です。 UIなどが独特で一昔前は使いにくい印象でしたが、最近は大きいアップデートがあり有料ソフトウェアと遜色ないほど進化してきました。 書き出しできるフォーマットやそれぞれ特化した機能の違いがあるので要件によって使い分けています。 3Dモデルの作成フロー モデルの作成は0から作るパターンと、CADなどの3Dデータをいただいて作り直すパターンがあります。 0から3Dモデリングを作る場合のフローは以下の3つのステップになります。 ① 下準備として三面図を用意 ② Maya上に三面図を引いてモデリング ③ 完成 CADなどの3Dデータをいただいて作り直す場合は、そのままのフォーマットではブラウザで表示することはできないので、一度MAYA LT上に取り込みます。 その際にそのまま書き出そうとするとポリゴン数があまりにも膨大(データ量も肥大)なので、ガシガシ減らして動作に耐えうるくらいまでモデルを修正していきます。 モデルが完成したら、そこからOBJファイルを書き出し、Photoshopに読み込んでサムネイルを作成します。 ここまでがデザイン室の作業となります。 あとはOBJファイルとサムネイル画像をエンジニアに渡し、実装をお願いしていく形になります。 他にもボーンを入れてアニメーションできるようにFBXファイルというフォーマットで実装した例もあります。 アバターロボットを遠隔でリアルタイム制御する未来へ 〜Aptpod×MELTIN MMI エンジニアクロストークインタビュー〜 こちらのロボットの3Dモデルは、アバターロボットの操作側から上がってくるデータで各関節にあるボーンの回転を制御しました。 こういったアニメーションをする3Dモデルは、外観ができたあと内側にボーンを配置していき、各ポリゴンの頂点のウェイトを紐付けしていく作業があります。 モデリング時に気をつけている3つのポイント 自分がモデリングをする際に気をつけているポイントは ポイント1 ちょうど良いくらいにポリゴン数を調整 ポイント2 法線の向きを正しく ポイント3 ゴミを残さない の3つです。 ちょうど良いくらいにポリゴン数を調整する 実はこちらの3Dパーツですがマテリアルは全てThree.jsでマテリアルを制御してライン描画するようになっています。 リアルタイムで描画するにあたってポリゴン数が多いと動作が重くなります。 しかし、ポリゴンが極端に少なすぎると密度がスカスカになり形がわかりにくくなってしまいます。 動作を保ちながら形も分かりやすいようにちょうどいいポリゴン数になるように心がけて作っています。 ポリゴン数は ディスプレイ -> ヘッドアップディスプレイ -> ポリゴン数 にチェックを入れることでビューポートに表示されるようになります。 作る対象によって差はありますが多くても7000ポリゴン以内を目安に作っています。 (今回例にだしている飛行機のモデルは汎用的なものになるので、特徴を出さずに△1028ポリゴンと少なめに作っています) お客様からCADベースのモデルデータを表示したいと依頼されるケースがあります。 ポリゴン数50万〜データ容量も50MBなど、Webブラウザで扱うには重たいデータなので、扱えるようにローポリ化します。 例えば下図のように約70万ポリゴンを2500ポリゴンに減らすとこれだけ軽くなり、パフォーマンスを保ってブラウザで表示できます。 法線の向きを正しく 法線とはポリゴンのフェースの向きを表します。 緑の線が飛び出ている方向がフェースの向きになります。 左側が上向きのフェース、右側が下向きのフェースです。向きによって描写が変わるのがわかりますね。 メッシュの結合やポリゴンの穴を埋めたりしているとこの向きが反転してしまうことがあります。 そのままだと描画がおかしくなるため対象のフェースを選び メッシュ -> 法線 -> 反転 で直していきます。 確認方法としては、対象のオブジェクトを選択し、 ディスプレイ -> ポリゴン -> フェース法線 をONにすることで画面のように表示されます。 ゴミを残さない ポリゴンの結合などを繰り返したりしていると気づかないうちに不整合な要素ができてしまいます。 そういった要素も描画時に問題がおきる元なのでそちらもしっかり取り除いて綺麗なデータを渡すようにしています。 メッシュ -> クリーンアップオプション を開き該当項目にチェックして適用を押すと該当するフェースなどを綺麗にしてくれます。 他にもヒストリの削除など作業中に色々整理しながら作っていきます。 以上がWebアプリケーション用の3Dモデルの作成フローと気をつけている3つのポイントになります。 ありがとうございました!
aptpod Advent Calendar 2020 の4日目を担当します、研究開発グループの大久保です。 2日目 では、Rustと Quinn でechoサーバを作成しました。今回は応用として、Quinnを使って大量のデータを送信し、パフォーマンス評価をしてみたいと思います。弊社内のユースケースとして、エッジ側で溜まったデータをサーバに送信したい、という状況が考えられるため、それを想定した評価となります。 実装と検証 1プロセス内でサーバ、クライアント両方立てます。 Cargo.toml に追記する依存関係は以下のようになります。 [dependencies] anyhow = "1" futures = "0.3" quinn = "0.6" rand = "0.7" rcgen = "0.8" tokio = { version = "0.2", features = ["full"] } 評価用のコードは以下のようになります。 use anyhow :: * ; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, ClientConfigBuilder, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use rand :: {thread_rng, Rng}; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: time :: Instant; // 送信用のデータ(10KiB) const MSG_SIZE: usize = 1024 * 10 ; #[tokio::main] async fn main () -> Result < () > { let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); let cert_der_clone = cert_der. clone (); // 送信用の適当なデータを乱数で用意する let mut send_data = vec! [ 0u8 ; MSG_SIZE]; thread_rng (). fill ( &mut send_data[..]); // サーバを動かす tokio :: spawn (async move { run_server (cert_der_clone, priv_key).await. unwrap (); }); // クライアントを動かし、所要時間(ミリ秒)を表示する let start = Instant :: now (); run_client (cert_der, & send_data).await?; let elapsed = start. elapsed (); println! ( "Elapsed time: {} ms" , elapsed. as_secs () * 1000 + elapsed. subsec_millis () as u64 ); Ok (()) } // サーバを動かす async fn run_server (cert_der: Vec < u8 > , priv_key: Vec < u8 > ) -> Result < () > { let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); let cert = Certificate :: from_der ( & cert_der)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & priv_key)?, )?; let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), 33333 ); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); while let Some (conn) = incoming. next ().await { tokio :: spawn (async { match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // サーバへの接続を扱う async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } println! ( "connection closed from {}" , connection. remote_address ()); Ok (()) } // クライアントを動かす async fn run_client (cert_der: Vec < u8 > , send_data: & [ u8 ]) -> Result < () > { let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & cert_der)?)?; let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 )), 33333 ); let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; let NewConnection { connection, .. } = endpoint. connect ( & addr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } connection. close ( 0u8 . into (), & []); endpoint. wait_idle ().await; Ok (()) } echoサーバと基本的に同じように構築していきますが、いくつかの変更点があります。 let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); main 関数のこちらの部分では、 rcgen クレートを用いて、自己署名証明書を作成しています。テスト目的ならこれで十分でしょう。 while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } handle_connection 関数では、サーバに対して張られた単方向ストリームを読み込みます。読み込むだけで返信は行いません。 for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } run_client 関数では、用意したデータ(10KiB)を1000回ループを回して送信します。 こちらを実行すると、以下のような出力になりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:47158 closed by peer: 0 Elapsed time: 131 ms 1000回送信したので、1回あたり0.13ms程度で10KiBを送信していたことが分かります。 送信タスクを非同期にする せっかく tokio で非同期にコードを書いているので、 FuturesUnordered を使って送信タスクを非同期にしてみます。その場合、送信を1000回行っているfor文を次のように書き換えます。 let mut tasks = futures :: stream :: FuturesUnordered :: new (); for _ in 0 .. 1000 { let connection = connection. clone (); let send_data = send_data. to_vec (); let task = async move { let mut send_stream = connection. open_uni ().await. unwrap (); send_stream. write_all ( & send_data).await. unwrap (); send_stream. finish ().await. unwrap (); }; tasks. push (task); } while let Some (_) = tasks. next ().await {} この場合の実行結果は以下のようになりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:42291 closed by peer: 0 Elapsed time: 83 ms 1回あたり0.083msで、すこし短縮されます。 なお、送信するデータを10KiBから100KiBにした場合、1回送信ごとにブロッキングする実装で663ms、非同期で668msと、ほとんど差が無くなります。1ストリームあたりに流す量に応じて使い分けるのが良さそうです。 最後に 今回は、RustとQuinnを使って、大量のデータを送信した場合のパフォーマンスを調査しました。Quinnは、tokioをベースとして実装されているため、tokioやfuturesといったRustの非同期機能と組み合わせて使うことができ、コネクションやストリームを開くのも非常に直感的にできます。今後もこれらの機能を活用しながら、引き続きQUICの利用法を調査していく予定です。
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の3日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 aptpodの製品は車載などの移動体の組み込み機器もターゲットにしているため、これらのトランスポートプロトコルは4G/5G 上での使用が想定されます。 そこで、5Gを使用して、ネットワークの計測やトランスポートプロトコルを評価しようということになりました。 今回の記事では、5GのネットワークでGoogleのスピードテスト/ping/iperf3などの計測を行った結果を紹介します。 なお、トランスポートプロトコルの評価については、12/15 「5G上でのトランスポートプロトコルの評価」にて投稿する予定です。 計測用機材の紹介 Wi-Fi 接続の影響について 計測場所について 都庁周辺の調査 新宿区立新宿中央公園 新宿住友ビル1F フリースペース 新宿三井ビル前のオープンスペース 渋谷ストリーム 日産スタジアム周辺 計測結果 googleのスピードテスト 計測場所① 計測場所② 計測場所③ pingの計測 iperf3の計測 UDPの結果 TCPの結果 まとめ 計測用機材の紹介 まずは計測用機材を紹介します。 Googleスピードテストは、 Xperia XZ2 SO-03K で計測しています。 pingとiperf3の計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。pingとiperf3計測用のサーバーは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はubuntu 20.04、サーバー側はubuntu 18.04 を使用しています。 以下のような構成で計測をします。 構成図 5Gへの接続には、Docomoから販売されているモバイルルーター Wi-Fi STATION SH-52A を使用しています。 5G接続時には、画面が青く光り表示が「5G」に代わるため5Gに接続しているのかどうかの判別が可能です。 5G接続時 Raspberry PiとSH-52Aは有線で以下のように接続します。Raspberry Piと赤いケーブルで接続されているのはモバイルバッテリーです。 5G を計測する機器の構成 Wi-Fi STATION SH-52A では、手動で5Gと4Gを選択することはできない 1 ため、4G計測用には モバイルWi-FiルーターL-01G を使用しています。 L-01Gは有線接続がないためRaspberry Piとモバイルルーター間はWi-Fiで接続しました。 4G 計測用機器の構成 Wi-Fi 接続の影響について 公正に評価できるように、Raspberry PiとWi-Fi接続影響について調べました。 まずはRaspberry PiとL-01G間のRTTについてです。 Raspberry PiからL-01Gに対して、1秒間隔で1分間の設定でpingコマンドを実行しました。RTTの値は最小値3.1msec、平均値 7.952msec、最大値26.8msecでした。状況による変動はあると思いますが、今回は4GでpingによるRTTの計測を行うと8msec 程大きい値が出ると想定します。 次に、Wi-Fiの帯域確認のために、Raspberry PiとパソコンをそれぞれL-01G経由で接続しiperf3を使って両者の間の帯域を調べました。 結果は、TCP 31.5Mbps、UDP 32.1Mbpsとなりました。もし4Gの計測を行ったときにこの値で律速した場合は、Wi-Fiの影響があるかもしれません。 計測場所について ドコモが5Gを提供しているエリアは、 サービスエリアマップ から確認することが出来ます。 最初に会社のオフィスの近くにある新宿区の都庁周辺と渋谷ストリームで計測ができないか確認に行きました。下記の画像の赤い部分が5Gが入るといわれている場所です。 都庁周辺の5Gエリアマップ 渋谷ストリーム周辺の5Gエリアマップ 両者ともあまり芳しくなかったため、周辺に大きなビルなどがない日産スタジアムを調査対象に選びました。 日産スタジアム周辺の5Gエリアマップ 都庁周辺の調査 都庁周辺は後述する3か所で確認をしました。5Gが入りますが、すぐに4Gに切り替わってしまい計測を行えるほど安定した接続は難しかったです。 新宿周辺は高いビルなどが多いため、5Gの直進性が高く減衰しやすいという特性による影響があったのかもしれません。 新宿区立新宿中央公園 新宿中央公園はエリアマップの範囲から少し外れていますが、時々5Gが入ることがありました。 新宿中央公園 新宿住友ビル1F フリースペース 新宿住友ビルの1Fのフリースペースは屋内ですが、5Gが入りました。コンクリートではなくガラス張りだったのが影響しているかもしれないです。 新宿三井ビル前のオープンスペース ここは上記の2か所よりは安定して5Gが拾えました。しかし、4Gに切り替わることが多かったため計測には不向きと考えられます。 新宿三井ビル前 渋谷ストリーム 渋谷ストリームはイベントホール内での接続が可能のようです。イベントホールには入れませんでしたが、4Fで一瞬だけ5Gを拾えました。しかしそれ以降5Gは拾えませんでした。 日産スタジアム周辺 都庁周辺・渋谷では厳しそうだったので、日産スタジアムを訪問しました。 日産スタジアム 日産スタジアムの周りは高いビルなどもなく、5Gの接続状況はかなり良かったです。 ということで、日産スタジアム周辺で計測を行いました。 計測結果 googleのスピードテスト スピードテストはgoogleが提供している回線速度計測のサービスです。上りと下りのスループットを計測することが出来ます。 スピードテストはSH-52A、L-01G をXperia XZ2 SO-03KとWi-Fiで接続して行いました。 場所による変化を見るために、スタジアムの東ゲート付近の3か所で計測しています。 計測場所① Smile Table という建物の脇の場所で計測しました。 日産スタジアム計測場所① 5Gの結果は、ダウンロードが76.1Mbps、アップロードが63.4Mbpsでした。 日産スタジアム場所①5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが7.38Mbps、アップロードが18.6Mbpsになりました。 日産スタジアム場所①4Gのスピードテストの結果 計測場所② 次に計測したのは東ゲート前です。 日産スタジアム場所② 5Gの結果は、ダウンロードが50.7Mbps、アップロードが25.1Mbpsでした。 日産スタジアム計測場所② 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが4.18Mbps、アップロードが17.5Mbpsになりました。 日産スタジアム計測場所② 4Gスピードテストの結果 計測場所③ 3か所目は、東ゲート付近の広場です。 日産スタジアム計測場所③ 5Gの結果は、ダウンロードが92.7Mbps、アップロードが36.7Mbpsでした。 日産スタジアム計測場所③ 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが16.5Mbps、アップロードが27.3Mbpsになりました。 日産スタジアム計測場所③ 4Gスピードテストの結果 ばらつきはありますが3か所すべてでダウンロード・アップロードともに5Gの方が4Gよりもスループットが高い結果になりました。 pingの計測 pingの計測は、計測場所③で行っています。 pingコマンドを使って1秒間隔で60秒間RTTを計測しました。 Raspberry PiからAWS上のEC2インスタンスまで60秒の間に1秒間隔で計測しています。 ヒストグラムのプロット結果は以下のようになります。(外れ値は除いています) pingの結果 RTTにはばらつきがありますが、傾向として5Gの方が小さい値になっています。 2 また、最小値、平均値、最大値は以下のようになりました。 最小値 平均値 最大値 5G 26.8msec 38.5msec 47.9msec 4G 58.3msec 74.8msec 93.9msec 参考までに、L-01GとのRaspberry Pi間のRTTの平均で8msecを引いてプロットしても、傾向は変わらないと言えそうです。 pingの結果 4GはWi-Fi分8msecを引いた iperf3の計測 iperf3の計測も計測場所③で行っています。 ここでは、UDPとTCP(輻輳制御はBBRを使用)それぞれ20秒間の上りの計測を3回行いました。 UDPの結果 UDPの計測は律速する上限まで帯域を上げてみて計測をしてみました。5Gは50Mbps、4Gは25Mbpsを設定しています。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 45.2Mbps 47.0Mbps 47.0 Mbps 4G 17.4Mbps 24.6Mbps 25.0 Mbps L-01Gの上限は30Mbpsと思われるので、4GはWi-Fiの接続で律速されているわけではなさそうです。 TCPの結果 TCPの場合はビットレートの指定をしないと上限まで流そうとするので、ビットレートの指定はしません。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 15.9Mbps 17.8Mbps 34.4 Mbps 4G 9.33Mbps 11.1Mbps 12.3 Mbps 同じ計測場所③で計測したスピードテストとは差分がありますが、これは計測方法の違いが影響している可能性がありそうです。 iperf3の結果からUDP/TCPともに5Gの方が4Gよりもスループットが高いことが期待出来そうです。 まとめ 今回は「5Gのネットワークを計測してみた」について紹介しました。 今回の計測結果から5GによるRTTやスループットの改善には期待が出来そうです。 3 次回は、12/15 に 「5Gでのトランスポートプロトコルの評価」についてご紹介する予定です。 最後までご覧いただきありがとうございました。 この記事を書いているときに気が付きましたが実際には「5G/4G/3G」モードと「4G/3G」モードを選べるようです。12/15 投稿予定の「5G上でのトランスポートプロトコルの評価」の記事は4Gの計測もSH-052Aを利用して計測した結果を載せたいと思います ↩ モバイルルーターを統一した結果、RTTの値はほぼ同じ値になることを確認しました。詳細は次の記事をご確認ください。: 5Gでのトランスポートプロトコルの評価 - aptpod Tech Blog ↩ 2 と同上 ↩
aptpod Advent Calendar 2020 の2日目を担当します、研究開発グループの大久保です。 弊社では、新しいプロトコルであるQUICの利用法を調査しています。そこで今回は、RustのQUIC実装の1つである Quinn を用いて、受け取ったリクエストをそのままクライアントへ返送するechoサーバを実装してみます。RustのQUIC実装には、他に quiche というものもありますが、Quinnは tokio の上に実装されているため、Rustのasync機能を活用して楽に書くことができます。 構成 quinn-echo-server と quinn-echo-client という2つのクレートを作り、それぞれの Cargo.toml に以下の依存関係を追記します。 [dependencies] anyhow = "1" clap = "3.0.0-beta.2" futures = "0.3" quinn = "0.6" tokio = { version = "0.2", features = ["full"] } QUICを使うのに quinn 、async周りの機能のために futures と tokio が必要です。また、エラー処理には、最近おなじみになった anyhow を、CLI引数のパースには clap を入れておきます。 サーバ側 quinn-echo-server は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] port: u16 , #[clap(short, long)] ca: PathBuf, #[clap(long)] privkey: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); // 証明書の設定 let cert = Certificate :: from_der ( & std :: fs :: read (opts.ca)?)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & std :: fs :: read (opts.privkey)?)?, )?; // QUICを開く let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), opts.port); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); // クライアントからの接続を扱う while let Some (conn) = incoming. next ().await { tokio :: spawn (async { // クライアントとの処理を行い、エラーが起きたら表示 match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // echoの処理をする関数 async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); // 受信用のストリームを開く if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; // ストリームを読み出す let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); // 送信用のストリームを開く let mut send_stream = connection. open_uni ().await?; // 返信を書き込む send_stream. write ( & data).await?; send_stream. finish ().await?; connection. close ( 0u8 . into (), & []); } else { bail! ( "cannot open uni stream" ); } println! ( "closed" ); Ok (()) } 下準備と接続を開くまで main 関数で行っています。コマンドライン引数で渡されたポート番号と、証明書ファイルのパスを基にサーバを立ち上げ、クライアントとの接続が起きたら handle_connection 関数に渡します。tokioランタイムの上で動作するので、 main 関数には #[tokio::main] 属性を追加しておきます。 handle_connection 関数では、受信用に単方向ストリームを開き、内容を全て読み出します。その後、送信用の単方向ストリームを開き、受け取った内容をそのまま書き出したら接続を終了します。 クライアント側 quinn-echo-client は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: {Certificate, ClientConfigBuilder, Endpoint, NewConnection}; use std :: net :: SocketAddr; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] ipaddr: SocketAddr, #[clap(short, long)] ca: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & std :: fs :: read ( & opts.ca)?)?)?; let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; // サーバへ接続 let NewConnection { connection, mut uni_streams, .. } = endpoint. connect ( & opts.ipaddr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); // メッセージの書き込み let msg = "hello" ; let mut send_stream = connection. open_uni ().await?; send_stream. write (msg. as_bytes ()).await?; send_stream. finish ().await?; println! ( "sent \" {} \" " , msg); // 返信の読み込み if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); } else { bail! ( "cannot open uni stream" ); } // 終了 endpoint. wait_idle ().await; Ok (()) } main 関数で接続、書き込み、返信の読み込みまで行います。QUICの接続を確立したら、送信用の単方向ストリームを開き、文字列 "hello" を書き込みます。その後、受信用の単方向ストリームを開き、文字列に変換して表示したら終了します。 実行 適当なDER形式の証明書ファイル cert.der と秘密鍵 priv.der を用意した場合、サーバを次のように立ち上げます。 quinn-echo-server --port 33333 --ca cert.der --privkey priv.der このサーバに接続するクライアントは、次のように実行します。 quinn-echo-client --ipaddr 127 . 0 . 0 .1:33333 --ca cert.der すると次のような出力が得られます。 サーバ側 listeing on 0.0.0.0:33333 connected from 127.0.0.1:32820 received "hello" closed クライアント側 connected: addr=127.0.0.1:33333 sent "hello" received "hello" これでechoが返ってくることを確認できました。 最後に 今回は、tokioをベースとして構築されたQuinnをつかって、簡単なQUICのechoサーバを構築してみました。別のQUIC実装であるquicheの方は、自分でイベントを扱うループを書く必要があったりしますが、Quinnの方はRustのasync/await機能の基本が分かっていれば、比較的簡単に使いこなすことができます。コネクションやストリームを開いたり、ストリームの読み書きも、とてもRustらしい書き方で行えます。今回実装したのはechoサーバですが、これでQUICの特徴の1つである ストリームの使い方がわかるので、ここから応用することもできるでしょう。 新しいプロトコルであるQUICも、Rustの知識がある程度あればQuinnで簡単に使えるので、ぜひお試し下さい。
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の1日目を担当します。 今回の記事では、ROSに対応している3つのオープンソースの自動運転シミュレータと弊社製品の intdash を連携させて、自動運転シミュレータの画像やセンサー情報をウェブブラウザ上で可視化してみました。 自動運転シミュレータは、「 LGSVL 」、「 CARLA 」、「 AirSim 」の3つを試してみました。 まずはそれぞれのシミュレータとintdashを連携させた動画をご覧ください。 動画では左に自動運転シミュレータ、右側にウェブブラウザ上で可視化を行う弊社製品の「 Visual M2M Data Visualizer 」を配置しています。 intdash LGSVL連携 youtu.be intdash CARLA連携 youtu.be intdash AirSim連携 youtu.be 動画上では撮影のために自動運転シミュレータとウェブブラウザを単一のPCで実行しています。 実際には、以下の図のように可視化しているセンサー情報や画像はクラウドを経由してウェブブラウザに届いています。 自動運転シミュレータ上の映像とウェブブラウザ上の映像を比較すると、クラウドを経由していますが大きな遅延がないことが確認できると思います。 このように、弊社の製品群を活用することで、ROSに対応した様々な自動運転シミュレータとの連携が実現できます。 ちなみに、今回は可視化を行っていますが、intdashを使うとクラウドにデータを保存することも可能です。保存されたデータをダウンロードして解析するなどの活用も可能です。 ここからはintdashとそれぞれの自動運転シミュレータをどのように連携させたかについて触れたいと思います。 intdashとROSの連携 LGSVLとROSの連携 CARLAとの連携 AirSimとの接続 まとめ intdashとROSの連携 最初に、3つの自動運転シミュレータに共通している部分について説明します。 ROSとintdashを連携させてウェブブラウザ上で可視化を行うまでは3つの自動運転シミュレータに共通しています。 それぞれの自動運転シミュレータ *1 がROSトピックをpublishします。 ここで、発行されたROSトピックのうち画像以外のセンサー情報はintdash bridge という弊社製品のROSノードが、画像はConverter *2 というROSノードがsubscribeします。 ROSトピックをウェブブラウザで可視化するためにintdash bridgeはROSトピックをJSON *3 に、ConverterはROSトピックから画像のバイナリのみを取り出します。 JSONとバイナリのデータはintdash Edge Agentに渡されます。そしてintdash Edge Agent がクラウドにデータをアップロードします。 ウェブブラウザ上のVisual M2M Data Visualizerは、クラウドのintdash ServerからJSONと画像のバイナリを受け取り、可視化します。 このように、intdashを使用して、自動運転シミュレータのセンサー情報や画像をウェブブラウザ上で可視化しています。 LGSVLとROSの連携 LGSVLはLGエレクトロニクス社が開発しているUnityベースの自動運転シミュレータで、自動運転ソフトウェアのAutowareと連携できるシミュレータとしても知られています。 LGSVLをROSと連携させるためには、 rosbridge server が必要になります。 intdashとLGSVLを連携させたときの構成図は以下のようになります。 intdashとLGSVLの連携 LGSVLは内部にrosbridge serverと接続するためのrosbridge clientを内包しています。 rosbridge clientにrosbridge server と接続するための設定をしてからLGSVLを起動するとrosbridge serverに接続します。 rosbridge clientは接続後に、JSON形式の情報をrosbridge serverに渡します。 rosbridge serverはrosbridge clientから受け取ったJSONをROSトピックに変換してpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとLGSVLの連携が可能になります。 CARLAとの連携 CALRAはUnreal Engineベースの自動運転シミュレータで、intel社やToyota Research Instituteなどがスポンサーを行っています。 intdashとCARLAを連携させたときの構成図は以下のようになります。 intdashとCARLAの連携 CARLAはROSと連携するために carla_ros_bridge というROSノードを提供しています。 このcarla_ros_bridgeがCARLAからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとCARLAの連携が可能になります。 AirSimとの接続 AirSimはUnrealEngineベースのドローンや車の自動運転シミュレータで、Microsoft社が開発しています。 intdashとAirSimを連携させたときの構成図は以下のようになります。 intdashとAirSimの連携 AirSimはROSと連携するために、 airsim_ros_pkgs を提供しています。 airsim_ros_pkgsに含まれる airsim_node というROSノードをROSとの連携のために使います。 airsim_ros_nodeは、AirSimのシミュレータからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとAirSimの連携が可能になります。 まとめ 今回は「intdashと自動運転シミュレータを連携させてみた」について紹介しました。 このように、弊社の製品群を活用することで、ROSに対応した3つのオープンソースの自動運転シミュレータとの連携が簡単に実現できました。 それぞれのシミュレータとの連携で見ていただいたように、弊社製品部分の構成は変えずにそのまま使用しています *4 。 研究開発グループではROSに限らず、プロトコルや機械学習に関連したテーマなど様々な技術テーマの調査・検証を進めています。今後も継続的に調査・検証の結果を記事として投稿できればと思います。 最後までご覧いただきありがとうございました。 *1 : 実際には後述する通り自動運転シミュレータの情報をpublishするROSのノードがいます。 *2 : 製品ではないですが自作のROSノードです *3 : ROSトピックをバイナリのまま送信することも可能です。現状ではウェブブラウザ上での可視化にはJSONが使われているのでJSONに変換しています。 *4 : subscibeするROSトピック名などの設定ファイルは変更しています
弊社では最近、PDF形式のドキュメントを作成するために Sphinx を使っています。 1 Sphinxを使って、reStructuredTextからPDFを作成する 例えば、弊社では先日AWS Marketplaceにて intdash LE All-in-One をリリースしましたが、そのドキュメントはSphinxで作成しました。 intdash概要 AMIを使ったintdashサーバー構築手順 Sphinxは非常に使いやすいツールですが、本格的に使うようになってから気づいた、使いこなしのコツやノウハウがいくつかあります。 その1つとして今回は、表紙タイトルの折り返しについてご紹介したいと思います。テクニカルライターの篠崎がお届けします。 長い文書タイトルで発生する折り返し 表紙テンプレートをカスタマイズする (準備)表紙のテンプレートを確認する 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する まとめ―Sphinxの柔軟性 長い文書タイトルで発生する折り返し 文書のレイアウトを自動化するとき、表紙も自動で作るか、または、別途手作業で作った表紙をあとで結合するか(例えばAdobe Illustratorで1枚のグラフィックとして作るというのがよくある手法でしょうか)は、判断の分かれるところだと思います。 【後日付記】 この記事の内容は、PDFの表紙のカスタマイズの方法としては有効ですが、表紙タイトルの改行位置を調整するだけであれば、Sphinxプロジェクトの設定ファイル conf.py で latex_documents を設定するほうが簡単です。 latex_documents による設定例: latex_documents = [('index', 'test.tex', r'長い\\タイトルは\\改行できます', author, 'manual')] 設定 latex_documents には、タプルのリストを指定します。タプル内の3番目(インデックス2)の要素で、PDF出力時の表紙タイトルを設定することができます。文字列はそのままLaTeXソースに書き込まれますので、上の例のように \\ を入れると改行することができます。 詳細については、公式ドキュメントの latex_documents についての説明 をご覧ください。 レイアウトを自動化するからには、表紙も自動で作成したいところです。一方、表紙は読者の目に最初に触れるものですから、できるだけ格好よくしたいと考えます。 そこで、バランスのよいレイアウトのテンプレートを作っておいて、決められた位置にテキストや画像を当てはめるのが定番だと思います。 表紙レイアウトのテンプレート例 それでも、大きな文字で長いタイトルを入れると、行末で折り返しが発生し、見栄えが悪くなることがあります。 英語であれば基本的に単語の区切り(スペース)でしか折り返されませんが、日本語では区切りのスペースを入れませんので、良いところで折り返されるとは限りません。 例として、長いタイトルを持つ文書をSphinxで作ってみます。Sphinxでは、プロジェクト設定ファイル conf.py の変数 project の値がPDFの表紙タイトルになります。以下のようにしてみました。 # 最小限の設定をしたconf.py project = 'とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント' author = 'test-author' language = 'ja' これでPDFを生成すると以下のようになりました。 行末で折り返された長いタイトル あまり格好よくないですね。 Sphinxを使い始めたころは、PDF出力後に編集ツールを使って手作業で改行を入れていたのですが、毎回このような修正をするのは大変です。 Acrobatで手動で改行を入れる やはり、自動でレイアウトしているのだから、できるだけ自動化したいですよね。 そこで以下のような方法を採ることにしました。方法はいろいろあると思いますので、一例としてお読みください。 使用した環境は以下の通りです。 Windows 10 Sphinx v3.3.1 TeX Live 2020(pLaTeXとdvipdfmxを使用) 表紙テンプレートをカスタマイズする 適切なレイアウトを実現するため、表紙テンプレートを新しく作ります。そこに、折り返し位置を指定したテキストを埋め込むことにします。 2 なお「表紙テンプレート」と呼んでいるものは、実体はLaTeXコマンドです。 以下の(A)~(C)を行います。 タイトル内の適切な折り返し位置にLaTeXの改行コマンド \\ を入れておく(A) 表紙テンプレートを再定義する(B) 表紙テンプレート(B)には、改行コマンドが入ったタイトル(A)を挿入する(C) (準備)表紙のテンプレートを確認する 先まわりして、 (B) の表紙テンプレートから考えてみます。 SphinxでPDFを出力する際に使用されるデフォルトの表紙テンプレートは、 \sphinxmaketitle というLaTeXコマンドです。 Sphinxソースの中の sphinx/texinputs/sphinxmanual.cls にあります。 \newcommand { \sphinxmaketitle }{ % \let\sphinxrestorepageanchorsetting\relax \ifHy @pageanchor \def\sphinxrestorepageanchorsetting { \Hy @pageanchortrue } \fi \hypersetup { pageanchor = false } % avoid duplicate destination warnings \begin { titlepage } % \let \footnotesize\small \let\footnoterule\relax \noindent\rule { \textwidth }{ 1pt } \par \begingroup % for PDF information dictionary \def\endgraf { } \def\and {\& } % \pdfstringdefDisableCommands { \def \\{ , }} % overwrite hyperref setup \hypersetup { pdfauthor = { \@author } , pdftitle = { \@title }} % \endgroup \begin { flushright } % \sphinxlogo \py @HeaderFamily { \Huge \@title \par } % <--- ここで、タイトル\@titleが出力されています ... \end { flushright } %\par ... \end { titlepage } % ... } タイトルの文字列は最終的に \@title コマンドにバインドされ、このテンプレートを使って出力されることが分かります。 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する そこで、デフォルトのテンプレートに少しだけ変更を加えて、改行コマンドの入ったタイトルを出力できるようにします。 Sphinxのたくさんのパラメーターの1つ latex_elements で preamble に文字列を設定すると、その文字列はLaTeXファイルのプリアンブルに書き出されます。この仕組みを利用して、テンプレートを再定義( \renewcommand )します (B) 。詳細についてはこの下のコード例をご覧ください。 また、タイトルには折り返し位置を指定するための改行コマンドを入れます (A) 。設定ファイル conf.py はPythonコードであるため、設定ファイル内で文字列操作も行うことができます。これを表紙テンプレートに挿入します (C) 。 # タイトルの折り返し位置を指定したconf.py author = 'test-author' # 変更なし language = 'ja' # 変更なし # タイトルは分割された状態で用意 document_title_lines = [ 'とても長いタイトルを持ち' , '秋の夜に世界の片隅で作成された' , 'PDF出力テストのためのドキュメント' ] # 改行コマンド `\\` を挟んで連結する(A) # バックスラッシュが4つなのは、エスケープのため my_latex_title_lines = ' \\\\ ' .join(document_title_lines) # => とても長いタイトルを持ち\\秋の夜に世界の片隅で作成された\\PDF出力テストのためのドキュメント # LaTeX出力の設定 latex_elements = { 'preamble' : r''' % my_latex_title_linesをLaTeXの世界に持ち込む \newcommand{\mylatextitlelines}{''' + my_latex_title_lines + r'''} % 表紙テンプレート内でアットマークが使われているため、アットマークを通常の文字として扱う \makeatletter % 表紙テンプレートを再定義(B) \renewcommand{\sphinxmaketitle}{% \let\sphinxrestorepageanchorsetting\relax \ifHy@pageanchor\def\sphinxrestorepageanchorsetting{\Hy@pageanchortrue}\fi \hypersetup{pageanchor=false}% avoid duplicate destination warnings \begin{titlepage}% \let\footnotesize\small \let\footnoterule\relax \noindent\rule{\textwidth}{1pt}\par \begingroup % for PDF information dictionary \def\endgraf{ }\def\and{\& }% \pdfstringdefDisableCommands{\def\\{, }}% overwrite hyperref setup \hypersetup{pdfauthor={\@author}, pdftitle={\@title}}% \endgroup \begin{flushright}% \sphinxlogo \py@HeaderFamily {\Huge \mylatextitlelines \par} % <--- ここで\mylatextitlelinesを使用(C) {\itshape\LARGE \py@release\releaseinfo \par} \vfill {\LARGE \begin{tabular}[t]{c} \@author \end{tabular}\kern-\tabcolsep \par} \vfill\vfill {\large \@date \par \vfill \py@authoraddress \par }% \end{flushright}%\par \@thanks \end{titlepage}% \setcounter{footnote}{0}% \let\thanks\relax\let\maketitle\relax %\gdef\@thanks{}\gdef\@author{}\gdef\@title{} \clearpage \ifdefined\sphinxbackoftitlepage\sphinxbackoftitlepage\fi \if@openright\cleardoublepage\else\clearpage\fi \sphinxrestorepageanchorsetting } % 表紙スタイル終わり % アットマークを特殊文字に戻す \makeatother ''' } # 変数`project`は、各行をそのまま連結したもの # PDFのメタ情報には正しいタイトルが入る project = '' .join(document_title_lines) # => とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント これで、PDFを出力してみます。 指定どおりに折り返されたタイトル 指定した位置で折り返されています。これで、PDF生成のたびに手作業で折り返し位置を修正する必要はなくなりました。 3 新しい表紙テンプレート \sphinxmaketitle は、他のドキュメントにも使いまわしができます。 この説明ではテンプレートを conf.py に直接書き込みましたが、LaTeXのスタイルファイル(.sty)として分離することで、より使いまわしを楽にすることも可能です。 まとめ―Sphinxの柔軟性 Sphinxは非常に柔軟にカスタマイズできるツールです。柔軟性を高めている特徴として、以下があります: たくさんの パラメーター が変更可能であること(しかも、痒い所に手が届くパラメーターが存在すること) レイアウトのテンプレートがモジュール化されており、それぞれ再定義可能であること 設定ファイル conf.py がPythonコードであり、ドキュメント生成時に評価されるものであること 今回の例ではこれらの特徴を活用し、パラメーター内で新たにテンプレートを定義しました。また、改行コマンドを挿入するという単純な操作ではありますが、PythonでLaTeXコードの断片を生成しました。Sphinxの持つ柔軟性の一端をご紹介できたかと思います。 今後もうまく使いこなしたいと考えています。 Sphinxを採用した背景や、LuaLaTeXによる日本語PDFの作成方法については、以前のエントリー SphinxとLuaLaTeXで、日本語PDFマニュアルを作る - aptpod Tech Blog にて公開しています。 ↩ 折り返し位置を指定するために、変数 project にLaTeXの改行コマンド \\ を挿入し、 project = 'とても長いタイトルを持ち\\\\秋の夜に世界の片隅で作成された\\\\PDF出力テストのためのドキュメント' のようにしてもうまくいきません。Sphinxの処理のなかでバックスラッシュがエスケープされるためです。 ↩ この新しい表紙テンプレートのコードは、 { 、 } 、 $ 、 % のようなLaTeXの特殊文字を最初から含むタイトルには対応していません。 ↩
はじめに こんにちは、SRE チームの柏崎です。 先日、 intdash が AWS Marketplace にて提供開始 されました。 これを期に、サーバサイドアプリをパッケージングするために、 fpm というツールを使う機会がありました。 すっかりコンテナな世の中には地味めな話題ですが、今回は fpm について、rpm パッケージの作成例を交えながら紹介したいと思います。 経緯 弊社では、intdash を組み合わせた PoC プロジェクトが多くあり、製品である intdash 自体のカスタマイズがよく行われています。 その昔、intdash のサーバサイドアプリはモノリシックアーキテクチャだったため、カスタマイズのために製品本体に手を入れなければならず、派生製品が乱立していました。 故にデプロイにおいては、派生製品ごとの実行バイナリを管理する手間を減らすため、「都度サーバ上でビルドを行う」という、ちょっと斜め上の方式をとっていました。せっかく Go で作っているのにもったいないですね…。 現在ではマイクロサービスアーキテクチャへの移行が進み、製品に手を入れずに柔軟なカスタマイズが可能な構成になっていますが、デプロイは従来の方式を踏襲してしまっており、下記のようなたくさんの課題を抱えています。 Ansible タスクでビルドしているので、冪等性があいまい Ansible の実行が長時間になりがち サーバがそれぞれビルドのためのソースを持つので、ディスクスペースが無駄 お客様管理のサーバへのデプロイも、アプトポッドエンジニアがやらなければならない そんなとき intdash の AMI 公開の話が持ち上がり、「こんなダサい方式でデプロイされた AMI を晒すのは恥ずかしい」と重い腰を上げたのでした。古いバージョンの利用者にアップデート手段を提供しないといけないですしね。 fpm とは? 目的 Linux (に限らずですが) のほとんどのディストリビューションはパッケージ管理システムを利用しています。 RHEL 系で rpm 形式を扱う yum、Debian 系で deb 形式を扱う apt がメジャーですね。 パッケージ管理システムによって、利用者はソフトウェア一式の追加削除や、依存関係の解決などを手軽に行うことができます。 便利なパッケージ管理システムですが、パッケージの作成者にとってはちょっと大変です。 様々な形式に対応するために、パッケージ作成手順を形式ごとに学習していかなければなりません。 rpm ひとつとっても、spec ファイルの複雑な書式や rpmbuild コマンドの使い方など、覚える事が多いです。 fpm は、覚えなければならない事を極力省き、シンプルに様々な形式のパッケージを作ることを目的として作られています。 どうやって動くのか fpm は、様々な形式から入力と出力を指定すると、その間の変換を行ってくれます。 README にも書いてありますが、様々な入力形式・出力形式に対応しています。 npm モジュールから rpm を作ったり、deb から rpm を作ったり、色々な用途が思いつきますね。 使いかた それでは、実際にパッケージの作成を行いながら、使い方を見ていきましょう。 入力形式として dir を、出力形式として rpm を使い、intdash の認証認可を担当するマイクロサービス「auth」の rpm パッケージを作ります。 構成ファイルの準備 入力形式 dir は、ディレクトリ配下に配置されたファイル一式をパッケージの構成ファイルとして扱ってくれます。 必要なファイルを用意し、ディレクトリ buildroot 配下に配置します。 buildroot/ usr/ bin/ authd # 実行バイナリ lib/ systemd/ system/ intdash-service-auth.service # systemd ユニットファイル share/ doc/ intdash-service-auth-1.6.0/ copyright # コピーライトファイルなど ... etc/ intdash/ authd.conf # 設定ファイル また、出力形式 rpm は、ヘルパユーティリティとしてインストール時やアンインストール時に実行されるスクリプトを指定できます。 これらをディレクトリ rpm_helper 配下に配置します。 rpm_helper/ pre.sh # インストール前に実行される post.sh # インストール後に実行される preun.sh # アンインストール前に実行される postun.sh # アンインストール後に実行される 例として、よくある rpm のヘルパユーティリティを以下に書いておきます。 サービスの動作に必要なユーザの作成や systemd 関連の操作などを行っています。 ### rpm_helper/pre.sh getent group intdash >/dev/null 2>&1 || \ groupadd -r intdash getent passwd intdash >/dev/null 2>&1 || \ useradd -r -g intdash -d /var/lib/intdash -s /sbin/nologin intdash exit 0 ### rpm_helper/post.sh if [ $1 -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi ### rpm_helper/preun.sh if [ $1 -eq 0 ]; then systemctl --no-reload disable intdash-service-auth.service >/dev/null 2>&1 ||: systemctl stop intdash-service-auth.service >/dev/null 2>&1 ||: fi ### rpm_helper/postun.sh if [ $1 -eq 0 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi 準備はこれだけです。 スクリプト周りで rpm のちょっとした知識は必要ですが、簡単ですね! パッケージング それでは、いざパッケージングをしていきましょう。 コマンド一発です。 $ fpm \ --output-type rpm \ --input-type dir \ --chdir ./buildroot \ --name intdash-service-auth \ --version 1.6.0 \ --iteration 1 \ --architecture x86_64 \ --license Unspecified \ --maintainer product-support@aptpod.co.jp \ --vendor "aptpod, Inc." \ --url https://www.aptpod.co.jp/ \ --rpm-summary "intdash Auth Service" \ --description "This package contains the intdash Auth Service." \ --rpm-os linux \ --depends shadow-utils \ --depends systemd \ --before-install ./rpm_helper/pre.sh \ --after-install ./rpm_helper/post.sh \ --before-remove ./rpm_helper/preun.sh \ --after-remove ./rpm_helper/postun.sh \ --directories /etc/intdash \ --directories /usr/share/doc/intdash-service-auth-1.6.0 \ --config-files /etc/intdash/authd.conf {:timestamp=>"2020-11-05T08:27:27.428159+0000", :message=>"Created package", :path=>"intdash-service-auth-1.6.0-1.x86_64.rpm"} ポイントをいくつか書いておきます。 アンインストール時にディレクトリが残ってしまわないように、 --directories オプションを忘れないようにしましょう。 --config-files を使って特定のファイルが設定ファイルであることを明示しておくと、編集済みの設定ファイルがパッケージアップデート時に上書きされたり、アンインストール時に削除されたりするのを防ぐことができます。 --iteration では、 1.6.0-1 の 1 ようなバージョンの後ろに付く文字列を指定できます。 パッケージ自体の更新で 2 3 と増やしたり、プレリリースバージョンにて 0.1.rc1 0.2.rc2 のように付与することで、パッケージ管理システムにより適切に新旧バージョンの比較が行われます。 以上で、rpm が出来上がりました。 お手軽ですね! おわりに fpm について、実例を交えながら紹介しました。 AMI は無事、恥ずかしくない状態で公開することができました。 引き続き、社内へのデプロイへの適用など、質の高い構築運用に活かしていくつもりです。 ここでは rpm の作成例を紹介しましたが、ほとんどの内容は deb など他の形式にも使い回せます。 パッケージングでお悩みの方の参考になれば幸いです。
はじめに ハードウェアグループのおおひらです。 所謂コロナ禍と呼ばれる状況になってはや半年が過ぎましたが、ハードウェア設計に携わっている皆様はいかがお過ごしでしょうか。 弊社では今年の2月中旬から原則リモートワークの勤務態勢になり、緊急事態宣言が終了したあともオフィスの出社人数を制限するよう3密を回避する対策が続けられています。ハードウェアグループのメンバーも自宅と会社での作業のバランスをとりつつ業務にあたっています。 私自身、各種計測器や試作基板を自宅に持ち帰ったり、前々から個人で購入しようとしていた機材を揃えたりして自宅の作業環境を充実させております。最近では協力会社や商社の方々との打合せがWeb会議で行われることも多く、意図的に自室の作業デスクがカメラに写り込むようにして会話ネタの提供に一役買ったり…😊 さて、与太話はこれぐらいにして、本記事ではハードウェア製品を商品化するうえで地味で大変だけれど避けては通れない、 部品表の管理 について書きたいと思います。 はじめに 部品表の管理は大変という話 OpenBOMの紹介 できること ライセンス形態 使ってみた おわりに 部品表の管理は大変という話 部品表 : BOM (Bills of Materials) という言葉は、製造業に従事されている方には馴染み深いと思います。ハードウェア製品を構成する部品の一覧のことで、各部品の属性として例えば下記のようなものが挙げられます。 品名 各部品の部品番号 (型番/型式) メーカー名 メーカー型番 リファレンス番号 単価 数量 (員数) 環境規制情報 (RoHS) 図面の情報・リンク etc... この部品表を元に調達・発注作業をしたり、また、各種製品コンプライアンスを遵守するために部品変更に対するプロセスを定めて情報追跡を行ったりします。 例えば米国仕向けのためにFCCの認証を取ったり、欧州仕向けのためにCEマーキングの自己宣言をしたり、更には車載機器としての欧州認証であるECE Regulation No.10 (通称Eマーク)の認証を取ったりと、各国と地域で適切な法規制を遵守する必要がありますよね。(弊社でも絶賛試験中です) 製品の出荷時点でこのような認証を受けることができたとしても、その後の部品の生産中止や仕様変更によって製品のスペックや構成物質が変わってしまうことは避けられません。こういった品質と製品コンプライアンスの管理・対応のために多くのメーカーでは4M変更プロセスが定められており、その活動の軸になる重要な要素が部品表と言えます。 品質管理における「4M」とは?効果を発揮するための変更管理 | デジタルトランスフォーメーション チャンネル 実際に特定の製品の部品表を作成する際には、そのフォーマットとしてスプレッドシート (エクセル)を使われる方が多いのではないかと思います。電気部品や半導体部品を実装した基板だけの製品を出荷するのであれば回路CADと統合されている専用のBOM管理ツールを使うことも考えられるのですが、多くの製品はメカ部品や副資材、シールや梱包箱、取扱説明書(紙)など、様々なマテリアルから成る部品の階層構造を持ちますので汎用的なフォーマットで作業できたほうが望ましいですよね。 しかしこれには色々と問題があって、 階層構造を作りづらい バージョン管理できない 横展開が面倒 (ある製品の一部品を、他の製品で流用しようとしたときに頑張ってコピペする) などなど、手作業および謎のエクセルマクロを含み、誰も管理できない秘伝のタレみたいなファイルが出来上がる…という未来が容易に想像できます。 前職で謎の社内イントラシステムに苦しめられた経験もあり(←製造業の方なら同意してくれる人も多いはず)、スプレッドシートで管理したくないな…と思っていろいろ調べた結果、OpenBOMというクラウドベースの部品表管理サービスに行き着きました。 OpenBOMの紹介 www.openbom.com ぱっと見て良い感じですね。 できること 上記のページにも書かれていますが、特徴は以下のとおりです。 スプレッドシートのように直感的に作業できる 画像や3D CAD(Autodesk, Solidworks)のデータを取り込んで各部品をビジュアルで見ることができる 複数人で同時にBOM編集できる ERPシステム(Netsuite)などとの統合 リビジョン管理できる 在庫管理できる クラウドサービスとしての基本的な機能はもちろん、在庫管理や経費との連携もできるということです。 ライセンス形態 www.openbom.com 個人ユーザーであれば無償、もしくは月25ドルのプロフェッショナルユーザーのライセンスがあります。違いは階層ごとのコスト計算機能の有無や、ベンダー管理機能の有無といったところでしょうか。 チームであれば月125ドルから。 企業ライセンスは月375ドルで、部品番号を割り振ったり、4M変更に対応しようとするとこのライセンス契約が妥当かなと思います。 www.openbom.com 使ってみた 企業ライセンスで14日間の無償トライアルをしてみました。 基本的にはカタログを作成して、そのカタログをもとに各製品単位のBOMを作っていく仕組みになっています。カタログというのは、例えば電気部品だったり、半導体部品だったり、または機構部品(板金)、機構部品(ネジ)といった区分の単位で整理される部品のリストのことです。これをベースに各製品のBOMを組立てていくことで、各部品の再利用がしやすくなります。 カタログとBOMの関係 個人的にGood!と感じたところは以下のとおりです。 電気CAD等、外部のCADの部品表からインポート可能 企業ライセンスだと各部品に自動的にユニークな部品番号を振ってくれる機能がある 部品が画像として可視化されていて直感的 部品の属性としてベンダーを紐付けられ、購買・発注時に複数のベンダーを比較することができる BOMの階層ごとにコスト算出できる 在庫管理ができ、試作支給部品や量産部品の管理が楽。営業・管理側と開発側との連携のハードルが下がる。 月375ドルでこれであれば使いたい…ということでこれから稟議申請書を書こうと思います。 おわりに クラウドベースのBOM管理ツール:OpenBOMを紹介させていただきました。 弊社のように小規模な組織では専任の調達/購買メンバーがいないこともあると思います。我々エンジニアとしては、OpenBOMのようなツールを効果的に使って業務効率化を進めていけると本来やりたい開発・設計業務に集中できて幸せになれるのではないでしょうか。 ※ ちなみに無償トライアルに申し込むとCEOのOlegさんから営業メールがバンバン飛んできます^^; 営業熱心でスゴイ。。。
エンベデッドチーム 久保田です。 開発環境をWSL2 (Windows Subsystem for Linux)へ移行しました。 タイミングよく、「WSL2でUSBデバイスを使ってみよう」という記事が出回っていたので、 aptpod CAN-USB Interface (AP-CT2A) もWSL2で動かせるのではないかという期待から intdash Edge Agent 含めて動作環境を整えてみましたのでご紹介します。 元記事 Adding USB support to WSL2 https://github.com/rpasek/usbip-wsl2-instructions USB support to WSL2 http://ktkr3d.github.io/2020/07/06/USB-support-to-WSL2/ intdash Edge Agent とは intdash Edge Agentは、intdash Edgeの基本機能を提供するエッジデバイス用のエージェントソフトウェアです。ユーザーは、接続するデバイスに応じたプラグインであるDevice Connectorを実装するだけで、様々なデバイスをintdashに接続することができます。このソフトウェアを使用すれば、intdash Edge が提供する自動再送、データ流量の制御といった、エッジ側の基本機能を利用したクライアントアプリケーションを、最小限の追加実装で開発することが可能となります。 aptpod CAN-USB Interface - AP-CT2A とは CANバスをターミナルアプライアンスに接続し、CAN、J1939などの制御信号を取得するためのインターフェイスデバイス 分岐線を使用して同時に2チャンネルのCANバスに接続可能 CANデータに対する高精度なハードウェアタイムスタンピングが可能 クロックの共有機構により、複数のCAN-USB Interface間のタイムスタンプを同期 外部計測装置などへの同期信号の出力が可能 動作構成 動作構成 WSL2 (Ubuntu 20.04)には、intdash Edge Agent, AP-CT2A device driverをbuild, installします。 WSL2は現時点(2020/10)で、残念ながらUSBデバイスの直接接続をサポートしていませんので、USB over IP経由でWindows側のAP-CT2Aを接続してみます。 WSL2とは WSL2は、Linux 用 Windows サブシステムが Windows 上で Linux バイナリを実行できるようにしたLinux 用 Windows サブシステム アーキテクチャの新しいバージョンです。 WSL1との違いは、ファイル システムのパフォーマンスを向上すること、システム コールの完全な互換性を追加することです。 WSL2 の初期リリースでは、ハードウェア アクセスのサポートが制限され、GPU、シリアル、USB デバイスに直接アクセスすることはできません。 図. WSL1 と WSL2 の命令実行の比較 「WSLのアーキテクチャ https://roy-n-roy.github.io/Windows/WSL%EF%BC%86%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A/Architecture/ 」から USB over IPとは USB over IPは、USBコマンドをIPパケットによってカプセル化するデバイス制御方法です。 USB/IP Design 「USB/IP PROJECT http://usbip.sourceforge.net/ 」から 手順 Windows側 USB/IPのstubドライバを配布サイト ( https://github.com/cezanne/usbip-win ) からダウンロードします。 https://github.com/cezanne/usbip-win/releases/tag/v0.1.0 PS C:\usbip-win\0.1.0> dir ディレクトリ: C:\usbip-win\0.1.0 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2020/07/11 14:05 551472 usb.ids -a---- 2020/07/11 14:05 1434112 usbip.exe -a---- 2020/07/11 14:07 1364992 usbipd.exe -a---- 2020/07/11 14:05 762 usbip_stub.cer -a---- 2020/07/11 14:05 1951 usbip_stub.inx -a---- 2020/07/11 14:07 58384 usbip_stub.sys -a---- 2020/07/11 14:05 2486 usbip_test.pfx -a---- 2020/07/11 14:05 2316 usbip_vhci.cat -a---- 2020/07/11 14:05 762 usbip_vhci.cer -a---- 2020/07/11 14:05 1960 usbip_vhci.inf -a---- 2020/07/11 14:05 87344 usbip_vhci.sys バイナリをダウンロードしたディレクトリでstubドライバをインストールします。 PS C:\usbip-win\0.1.0> .\usbip.exe install AP-CT2AをWindowsのUSBポートに接続すると、usbipコマンドでUSB接続が表示されます。 PS C:\usbip-win\0.1.0> .\usbip.exe list -l - busid 1-123 (32b2:0100) unknown vendor : unknown product (32b2:0100) <= これ - busid 1-138 (8087:0a2b) Intel Corp. : unknown product (8087:0a2b) - busid 1-99 (093a:2510) Pixart Imaging, Inc. : Optical Mouse (093a:2510) - busid 1-236 (5986:111c) Acer, Inc : unknown product (5986:111c) usbipコマンドでUSB/IPの接続登録を実行します。 PS C:\usbip-win\0.1.0> .\usbip.exe bind -b 1-123 usbip: info: bind_device: bind device on busid 1-123: complete usbipdコマンドでWSL2側からの接続待ちにします。 PS C:\usbip-win\0.1.0> .\usbipd.exe -d -4 usbipd: info: starting usbipd (usbip 1.0.0) usbip: debug: C:\work\usbip-win\userspace\src\usbipd\usbipd_sock.c:38:[build_sockfd] opening 0.0.0.0:3240 usbip: info: listening on 0.0.0.0:3240 WSL2側 Windows側で待ち受けているポートへ接続します。 $ sudo usbip attach --remote=192.168.1.8 --busid=1-123 接続が完了すると、WSL2側にAP-CT2Aが現れます $ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 002: ID 32b2:0100 aptpod, Inc. AP-CT2A Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 動作結果 動作結果 CANを流してみると、無事受信できました。 USB/IP経由することでCPU負荷が大きくなるかと考えていましたが、PCがCore i7なので余裕でした。 ・・・が、何かのきっかけで受信が0になります。エラーとなるわけではないので、USB/IPドライバの不具合かと。正常に動かすには調整が必要なようです。 まとめ WSL2でのintdash Edge Agentの動作手順を簡単に紹介しました。 結果AP-CT2Aは正常に継続動作しませんでしたが、intdash Edge Agentの動作は問題ありません。 CPUがパワフルなので、AP-CT2AのWindowsネイティブドライバがあれば、CANデータをTCPで転送してDevice Connectorで回収する手段も取れなくはなさそうです。 WSL2のUSBデバイス正式対応に期待しましょう。 参考 (WSL2の導入) WSL2導入|WinアップデートからWSL2を既定にするまでのスクショ https://qiita.com/tomokei5634/items/27504849bb4353d8fef8 WSL2におけるVcXsrvの設定 https://qiita.com/ryoi084/items/0dff11134592d0bb895c WSL2によるホストのメモリ枯渇を防ぐための暫定対処 https://qiita.com/yoichiwo7/items/e3e13b6fe2f32c4c6120