株式会社豆蔵のブログ - TECH PLAY

TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

110

はじめに # こんにちはDX戦の檜尾です。 初めての投稿になりますドキドキ。 日々の業務において、AWSQuickSightのダッシュボード定義をコードとして管理する「BI as Code」の重要性が高まっていると感じます。 従来のGUI上での直接編集はアジリティが高い反面、変更履歴の追跡や誤操作によるロールバックが困難になるという運用上の課題を抱えています。 AWSではこれらの問題に対してビジネスインテリジェンス運用 (BIOps)という考えを適用しようとしています。 DevOpsで行っていたことをBIでも適用できるのではないかということですね。 本記事では、QuickSightの定義ファイルをGitでバージョン管理し、安全に運用するためのCI/CDパイプライン構築手順を全3章に分けて解説します。 第1章:自動アップロード(本記事) QuickSight上でのダッシュボード公開をトリガーとし、定義ファイル(JSON)を自動的にGitHubへバックアップする仕組みを構築。 第2章:自動テスト(次回予定) エクスポートされた定義ファイルに対する静的解析や、依存するデータセットの整合性チェックを自動化する仕組みを解説予定。 第3章:自動デプロイ(次々回予定) GitHub上でレビュー・マージされた定義ファイルを、別環境(本番環境等)のQuickSightへ自動でデプロイするパイプラインを解説予定。 今回はベースとなる 第1章:自動アップロード環境の構築 について、具体的なアーキテクチャと実装手順、および構築時に陥りやすい技術的な仕様(Tips)を解説します。 第1章:自動アップロード環境の構築 # 本章では、QuickSightの「Asset Bundle API」を活用し、イベント駆動型でダッシュボード定義を抽出・保存します。 1. 全体の流れ # AWS CloudTrail / Amazon EventBridge : QuickSightにおけるダッシュボードの公開( UpdateDashboard / CreateDashboard )を検知。 AWS CodeBuild : EventBridgeをトリガーとして起動し、Pythonスクリプトを実行。 AWS QuickSight (Asset Bundle API) : CodeBuildからのリクエストに応じ、ダッシュボードの定義をJSON形式でエクスポート。 GitHub : 抽出されたファイルをCodeBuildが対象リポジトリへCommitおよびPush。 2. 実装手順 # Step 1: GitHub認証情報のAWS Secrets Managerへの登録 CodeBuildがGitHubへアクセスするためのPersonal Access Token (PAT) を発行し、AWS Secrets Managerに保存します。 シークレットのタイプ : その他シークレットのタイプ キー/値のペア : UIのキー・値入力ではなく、「プレーンテキスト」タブから以下のJSON形式で保存する必要がある。理由はCodeBuildのソースフェーズで ServerType is required エラーが発生する為。 { "ServerType": "GITHUB", "AuthType": "PERSONAL_ACCESS_TOKEN", "Token": "ghp_から始まるPAT" } シークレット名 : QuickSightGitHubToken Step 2: 実行スクリプトの配置 バックアップ先となるGitHubリポジトリの直下に、エクスポート処理を担うPythonスクリプトとCodeBuildのビルド仕様ファイルを配置します。 1. export.py Boto3を使用してAsset Bundle Export APIを呼び出します。今回はダッシュボード定義のみを対象とするため、 IncludeAllDependencies=False を指定しています。 import boto3 import time import requests import zipfile import io import os import uuid account_id = os.environ['AWS_ACCOUNT_ID'] raw_dashboard_id = os.environ['DASHBOARD_ID'] # EventBridgeからARNが丸ごと渡ってくる region = os.environ['AWS_REGION'] dashboard_id = raw_dashboard_id.split('/')[-1] client = boto3.client('quicksight', region_name=region) job_id = str(uuid.uuid4()) arn = f"arn:aws:quicksight:{region}:{account_id}:dashboard/{dashboard_id}" print(f"Exporting dashboard: {dashboard_id}") # エクスポートジョブの開始 client.start_asset_bundle_export_job( AwsAccountId=account_id, AssetBundleExportJobId=job_id, ResourceArns=[arn], IncludeAllDependencies=False, ExportFormat='QUICKSIGHT_JSON' ) # 非同期処理の完了待機(ポーリング) while True: response = client.describe_asset_bundle_export_job( AwsAccountId=account_id, AssetBundleExportJobId=job_id ) status = response['JobStatus'] if status == 'SUCCESSFUL': url = response['DownloadUrl'] break elif status in ['FAILED', 'FAILED_PARTIAL']: raise Exception("Export failed!") time.sleep(5) # アーカイブのダウンロードと展開 res = requests.get(url) with zipfile.ZipFile(io.BytesIO(res.content)) as z: z.extractall("quicksight_backup") print("Download and extraction complete.") 2. buildspec.yml CodeBuildの動作を定義します。 version: 0.2 env: secrets-manager: GITHUB_TOKEN: "QuickSightGitHubToken:Token" phases: install: runtime-versions: python: 3.11 commands: - pip install boto3 requests build: commands: - python export.py post_build: commands: - git config --global user.name "QuickSight Auto Backup" - git config --global user.email "bot@example.com" - git remote set-url origin https://${GITHUB_TOKEN}@github.com/YourOrg/YourRepo.git - git add quicksight_backup/ - git commit -m "Auto backup dashboard ID - ${DASHBOARD_ID}" - git push origin main Step 3: IAMロールとCodeBuildプロジェクトの作成 CodeBuildに付与するIAMロールには、最小権限の原則に従い以下のポリシーをアタッチします。 quicksight:StartAssetBundleExportJob quicksight:DescribeAssetBundleExportJob quicksight:DescribeDashboard secretsmanager:GetSecretValue CodeBuildプロジェクトを作成し、ソースプロバイダとして対象のGitHubリポジトリを指定します。アカウント認証情報から接続をしておきます。また、環境変数に AWS_ACCOUNT_ID (12桁の数字)を設定します。 Step 4: EventBridgeルールの設定 CloudTrailが有効化されている前提で、EventBridgeルールを作成します。 イベントパターン : { "source": ["aws.quicksight"], "detail-type": [ "AWS API Call via CloudTrail", "AWS Service Event via CloudTrail" ], "detail": { "eventSource": ["quicksight.amazonaws.com"], "eventName": ["CreateDashboard", "UpdateDashboard"] } } ターゲット設定 : 作成したCodeBuildプロジェクトを指定し、「入力トランスフォーマー」機能を用いてダッシュボードIDを環境変数として渡します。 入力パス : {"dashboard_id": "$.detail.serviceEventDetails.eventRequestDetails.dashboardId"} 入力テンプレート : { "environmentVariablesOverride": [ { "name": "DASHBOARD_ID", "type": "PLAINTEXT", "value": "<dashboard_id>" } ] } ここまでの実装でダッシュボードを公開すると、自動的に裏側のGithubにダッシュボードのバックアップができるようになります。 3. Tips: 構築時におけるQuickSight APIの技術的制約と解決策 # 自動化パイプライン構築において、QuickSight特有の仕様によりエクスポートが FAILED となるケースがあります。以下に代表的な事象とその解決策を提示します。 事象: APIにおけるローカルファイルデータセットの非互換性 IncludeAllDependencies=True を指定時、 File source type is not supported in Public API というエラーが発生する。 原因 : Asset Bundle APIは、ユーザーが手動でアップロードしたローカルファイル(CSV/Excel等)の抽出に非対応。 対策 : スクリプト側で IncludeAllDependencies=False を指定しダッシュボードのみを抽出する。もしくはデータソースをS3やAmazon Athena経由の参照モデルに改修する必要がある。 第2章・第3章に向けて # 本章により、ダッシュボードの変更が自動的にGitリポジトリへコミットされる環境が整いました。これにより、変更履歴の可視化とバックアップの自動化が達成されます。 次回の 第2章:自動テスト では、取得したJSON定義に対するスキーマ検証や、不要な変更が含まれていないかを自動検知する仕組みについて解説しようとおもいます。 参考資料 # 本環境を構築するにあたり、以下のAWS公式ドキュメントおよび公式ブログを参考にしています。さらに詳細な仕様やAPIのオプションについて知りたい方は、併せてご参照ください。 Amazon QuickSight BIOps – パート3 : API を使用したアセットのデプロイ (AWS公式ブログ) Boto3 Documentation: QuickSight - start_asset_bundle_export_job Python (Boto3) からエクスポートジョブを実行する際の、詳細なパラメータ( IncludeAllDependencies 等)。 AWS CloudTrail を使用した Amazon QuickSight API コールのログ記録 (AWS公式ドキュメント) QuickSightでの操作がどのようにCloudTrailに記録されるか(API Call と Service Event の違いなど)の仕様が記載。 AWS CodeBuild の buildspec リファレンス (AWS公式ドキュメント) buildspec.yml 内で AWS Secrets Manager から安全に認証情報(GitHub PAT)を取得するための構文規則について解説。
テキストエディタ難民 # 皆さん、テキストエディタは何を使っているでしょうか。 最近だと、VS Code ですかね。猫も杓子も といった感じですし。 でもわたし、VS Code は好きになれないんですよね。ゴチャゴチャしていて。 なので Sublime text をメインに使っていましたが、日本語の扱いが微妙な所があったり、巨大なファイルを開くのが遅かったりと不満もあり、状況に応じて色々なテキストエディタを切り替えて使う難民生活を送っていました。 コーディングには IDE を使えばいいので、日常のテキスト編集をストレスなく行える、ただそれだけのシンプルなエディタがほしかったのです。 具体的には以下のような感じです。 Markdown でメモ取り コードフェンスでシンタックスハイライトはほしい 表を Markdown テーブルとしてペーストしたい(特にペーストできないことで有名なパワポの表も貼り付けたい) 巨大なログ・ファイル確認 素早く開いてエラー箇所をフィルタリングしたい Grep して調査したい SQL や JSON の加工 マルチカーソルで編集したい インデント整形したい 巨大 CSV ファイル編集 カラム毎に揃ったインデントで操作したい マルチプラットフォーム 異なる環境でもキーバインドは同じにしたい 設定やプラグインをこねくりまわすことなくアウトオブボックスで使いたい 全角スペースやタブ表示 キャレットのある行だけ表示したい セッションの保存 未保存で閉じても前回内容を復元したい 難民からの脱却 # 難民生活にも疲れてきた折、自分でサクッと作ったほうが早いのではないか? そう思い立ち、テキストエディタを自作して、現在は難民生活を抜け出すことができました。 しかし、全然「サクッと」とはいかず、思った以上に大変でした。 見返してみると、初回コミットは Sep 12, 2022 となっています。 単純なテキスト編集だけであれば割とすぐに動くのですが、普段使いできるレベルになるまでには1年以上かかった気がします。その上、未だに変更加えてます。 どうしても、夜に数十分だけ実装するという細切れの進め方になってしまうので、翌日には何をどこまで進めたかも忘れているという感じで、モチベーション維持を含め、ある程度まとまった時間で集中して作業しないと進まないなぁ というのが印象です。 学生のように時間のある時期ならまだしも、この年になってやるものではないなと思いますが、ほしい機能があればすぐに追加できるので、思い通りにカスタマイズ可能なエディタが手に入ったと思うことにしています。 ということで本稿では、テキストエディタの自作をはじめるにあたって、知っておきたかった事柄について記しておこうと思います。 テキストバッファ # テキストエディタを自作するにあたり、最初に考えなければならないのが、文字シーケンスをどのようなデータ構造で扱うか になります。 世の中のテキストエディタは大抵、Gap Buffer、Linked List、Rope、Piece Table のいずれかのデータ構造を元にして、独自の工夫を施しているので、これらを見ていきましょう。 Array # 最初は、最も単純な例から始めます。文字シーケンスを配列として扱う方法です。 以下のように文字シーケンスを単にバイト配列として扱うことを考えます。 byte[] bytes = Files.readAllBytes(path); この時、 This is apple. という文字シーケンスは以下のような連続したメモリレイアウトとして確保されます。 an という文字列を挿入するには、元のサイズより 3 大きい配列を再確保し、文字を新しい配列にコピーして設定する必要があります。 当たり前の話ですが、編集の度に新しいメモリ領域を確保しなおす必要があるため、非常に効率が悪いです。 そこで、メモリ領域を余分に確保しておき、バッファとして利用することを考えます。 Buffer # テキスト編集の度にメモリ領域を再確保するのは明らかに非効率なため、メモリ領域中に未使用のバッファ領域を用意します。 概念的には、以下のような Buffer を考えることになります。 class Buffer { byte[] bytes; int length; } 5要素分の未使用領域(網掛け部分)をバッファとして確保した場合は以下のようになります。 文字列の挿入時には、新しい配列を作成することなく、値の設定と移動で編集操作が完結します(バッファ領域が不足した場合には、新たなメモリ領域を確保してコピーする必要があります)。 文字列の編集をバッファ領域で吸収することで、余計なメモリの確保が削減できました。 しかし、編集位置から末尾まで値を移動させる必要があり、もう少し工夫の余地がありそうです。 Gap Buffer # 多くのテキスト編集操作は、カーソル位置に対して行われる という事実を利用したものが Gap Buffer です。 先程の例では、メモリの末尾にバッファを用意しましたが、 Gap Buffer では、カーソル位置(キャレット位置)にバッファを設けます。 概念的には、以下のような GapBuffer として考えます。 class GapBuffer { byte[] bytes; int gapIndex; int gapLength; } 現在のカーソル位置がオレンジの矢印にあり、Gap として5要素分の未使用領域を割り当てた場合は以下のようになります。 カーソルを移動する際、その移動に合わせてバッファ位置を移動させていきます。 これにより、テキストの編集操作を常にバッファ位置で行うことができます。先の例で見た、編集点以降の値のシフトが不要になりました。 バッファの開始位置と長さがわかっているため、インデックスを指定したランダムアクセスも簡単に実現できます。 Gap Buffer は、Emacs でも使われている効率の良いテキストバッファの実現手法になります。 さて、ここまでは文字シーケンスを1つの塊として扱う例を見てきましたが、これらを複数の小さな塊として分割統治することも考えられそうです。 Linked List # 文字シーケンスを1つの連続した領域として管理するのではなく、小さな塊として分割統治できれば、編集操作を局在化して扱うことができます。 そこで真っ先に思いつくのは、文字シーケンスを特定サイズのチャンクに分割したノードとして扱い、連結リストとして管理する方法です。 概念的には、以下のようなクラスを考えることになるでしょう。 class LindedList { Node head; Node tail; } class Node { byte[] chunk; Node next; Node prev; } 例えば、先の例を、単語単位のチャンクとして扱った場合は以下のような構造になります。 テキストの編集処理は、新しいノードを作成してリンクの張り替えを行うだけで済むため効率も良さそうです。 ここでは説明のため単語単位のチャンクとして扱いましたが、細かくし過ぎるとリンクポインタのメモリ使用量が多くなるため、通常は1行をチャンクとして扱う実装が現実的です(改行の挿入でノードの分割、行の削除でノードの結合)。 文字シーケンスを連結リストとして扱うのは、実装が簡単ですが、要素へのランダムアクセスが遅いという欠点があります。 インデックスアクセスを行う場合は、先頭からリストを辿る必要があり、大きなテキストファイルでは特に非効率です。通常は、キャレット位置をカーソルとして、カーソルベースで要素にアクセスするなどの工夫が必要です。 では、文字シーケンスをチャンクに分割した上で、インデックスによるランダムアクセスを効率的に行うことはできないでしょうか。 Rope # 文字シーケンスをチャンクに分割し、インデックスアクセスを二分木で追尾できるようにしたものが Rope です。紐(String)を強化した縄(Rope)というわけです。 概念的には、以下のようなクラスを考えます。 class Rope { Node root; } interface Node { int weight(); int totalLength(); } record Branch(Node left, Node right, int weight) implements Node { Branch(Node left, Node right) { this(left, right, left.totalLength()); } public int totalLength() { return left.totalLength() + right.totalLength(); } } record Leaf(String text) implements Node { public int weight() { return text.length(); } public int totalLength() { return weight(); } } ここで、 weight は、左側ノードの文字列長の合計を表します。 Leaf ノードには文字シーケンスのチャンクを保持し、 Branch ノードで weight を管理します。 単語単位のチャンクとして扱った場合は以下のような二分木として管理します。 weight は、そのノードが分割する文字シーケンスの先頭からのインデックス位置となるため、例えばインデックス 10 の位置は、以下のように木を辿ることで到達できます。 ルートノードは、左側の this is と 右側の apple. に分割した中央部分のインデックスを表すため、 10 のインデックス位置は右側木の 2 の位置に存在し、 2 の位置は 5 より小さいため、その左側のノードに存在する といった具合です。 連結リストで問題だった、ランダムアクセスの効率が改善されていることがわかると思います。 前述までと同様に an を挿入する場合は以下のように木を更新することになります。 この更新による weight の更新範囲は、対象ノードを上に辿ったノードに対してのみに限定できる という点に注目してください。 apple と . を含む右側のノードにはなんの影響も及ぼしません。 元の木構造はそのままに、テキストの更新後の木構造を更新箇所を上に辿ったノードに限定してリンクし直すことで、変更後の文字シーケンスを表現できるのです。つまり、以下のように、文字シーケンスの変更をイミュータブルに扱うことができる ということです。 テキストエディタでのUndo操作は、1つ前のルートノードに戻すだけで実現できる点にも注目してください。 このように Rope は文字シーケンスを表現する強力なデータ構造ですが、木構造を維持するためのオーバーヘッドが必要であり、木のバランシング操作も必要です。 加えて、テキストファイルを開いた際に木を構築する必要があるため、巨大なファイルを開くのには時間を要します。 巨大なファイルを効率よく編集できるデータ構造は考えられないでしょうか。それが次に紹介する Piece Table です。 Piece Table # Piece Table は、テキストの変更操作を追記管理するデータ構造です。 Piece Table では、元の文字シーケンスを read only なバッファで管理し、そこに加えられた変更を append only なバッファに配備し、それらのバッファのインデックス位置をテーブルで管理します。 This is apple. というファイルを開いた直後は以下のようになります。 インデックス8の位置に an を挿入する場合は、以下のようにテーブルのインデックスを更新することで変更を扱います。 概念的には、以下のようなクラスを考えることになるでしょう。 record Piece(Buffer target, int index, int length) { int end() { return index + length; } } class PieceTable { List<Piece> pieces; Buffer appendBuffer; PieceTable(Buffer readBuffer) { this.pieces = new ArrayList<>(); this.pieces.add(new Piece(readBuffer, 0, readBuffer.length())); this.appendBuffer = AppendBuffer.of(); } } 元のファイルが巨大でも、 read only なバッファは直接ファイルやメモリマップドファイル として扱うことで、メモリ確保がほぼ不要になり、編集内容は変更分の Piece の管理だけで済みます。 ただし、変更を繰り返すと Piece の数が増加したり、append only なバッファが断片化するデメリットがあります。 テキストバッファのデータ構造まとめ # これまで見てきたデータ構造をまとめると以下の様になります。 データ構造 特徴 Gap Buffer 実装が非常にシンプル カーソル位置での入力が極めて高速( O(1) ) カーソル位置の大きな移動・マルチカーソル操作で遅延が生じる可能性がある バッファ領域の枯渇でメモリの再割り当てが必要 Emacs で利用されている 連結リスト 位置特定後の挿入や削除が一定時間で可能( O(1) ) ランダムアクセスが O(N) となるため、カーソルを利用するなどの工夫が必要 実装が簡単なため、初期のエディタ実装で採用されることが多い Rope 挿入やランダムアクセスが O(log N) 二分木の構築やバランシング処理によるオーバヘッドが発生 編集操作をイミュータブルにすることができる Zed Editor では Rope を基にしたデータ構造が利用されている Piece Table メモリ効率が良い 挿入や削除は追記型となるため高速( O(1) ) 編集によりPieceの数が増加するとパフォーマンスが劣化する VS Code では Piece Table を基にしたデータ構造が利用されている(インデックスアクセスにはRopeのように二分木を使用) どれも一長一短があり、どんな状況においてもベスト という解はありません。 私の場合は、巨大ファイルを扱いたいため、Piece Table を採用することにしました。 それぞれの利用用途に応じてデータ構造を選んでください。 データ構造だけでは語れない # データ構造が決まってテキストエディタを作り始めても、途中で迷うポイントが色々と発生します。 いくつか代表的なものに絞って以下に見てみましょう。 スクロール位置の特定 # テキストエディタには、通常、縦スクロールバーと横スクロールバーが表示されます。 縦スクロールバーを表示するには、ファイル全体の行数と、現在画面に表示されている行数を特定する必要があります。 横スクロールバーを表示するには、ファイル中の最長の行と現在の画面幅を特定する必要があります。 厄介なのは横スクロールバーの制御で、最長の行は、編集操作によって最長の行ではなくなる可能性があるので、編集の都度最長の行を探し出す必要があります。 私の場合は、横スクロールバーを完全にトラックすることは諦め、現在画面に表示している行に限定して最長行を見つける実装としました。 折り返し表示 # テキストエディタでは、1行をウインドウ幅で論理的に折り返して表示できるものがほとんどです。 しかし、この折り返し判定は厄介で、画面表示上のグリフに応じた文字の幅を計算する必要があります。 通常、文字の幅は文字毎に異なり、さらに面倒なのが、結合文字や合字(Ligature)という概念です。 これらは、複数の文字を繋げて一文字として表示するため、文字幅の計算が都度必要です。 厳密に対応しようとすると、全ての行に対して画面表示上の幅を計算し、適切な折り返し位置を見つける必要があり、非常に高価な処理になります。 折り返し計算は画面に表示されている部分だけ行う、という割り切りもできますが、そうすると先に述べたスクロールバーの長さと辻褄が合わなくなってしまいます。 ですので、折り返し表示は一定サイズ以下のファイルに制限したり、文字幅の計算はある程度割り切った近似値として扱うなどして、パフォーマンスとの折衷案を設けるのが現実的です。 文字シーケンスへのランダムアクセス # データ構造のまとめでは、ランダムアクセスが O(1) で効率が良い などと書きましたが、現実的には難しい面があります。 全てASCII 文字で、1文字は1バイトという理想的な世界であれば話は簡単ですが、現実はそう上手くは出来ていません。 例えば UTF-8 は、1文字(コードポイント)で 1~4byte の可変エンコーディングとなるため、N文字目にアクセスしようとしてもインデックスアクセスはできず、1つずつバイト長を見ていく必要があります。 テキストファイルを全てメモリに読み込み、メモリ上でアクセスしたとしても、多くの言語ランタイムは、文字列を内部的に UTF-16 でエンコードされたバイト列として扱うため、結局可変エンコードを加味する必要があります。 固定長の UTF-32 としてテキストを保持すれば、ランダムアクセスは容易になりますが、メモリ使用量が増大します。 例えば、Piece Table で、UTF-8 のファイルを直接 read-only buffer として利用する場合を考えてみましょう。 画面上をクリックし、その位置が画面表示上のN文字目(Unicodeコードポイント)だった場合、言語ランタイム上の UTF-16(1〜2バイト) におけるインデックス位置に変換し、ファイル上のUTF-8(1〜4バイト)におけるインデックス位置にさらに変換し、そしてようやくファイルの当該位置にアクセスできる といった具合で、いかにも非効率になります。 先に述べたスクロールバーや折り返し表示のことも考え、行数や文字数といったメタ情報とテキストバッファとのマッピング、グリフ幅などの表示上の情報も合わせて文字シーケンスを扱わなければならず、これらをパフォーマンスとメモリ効率のバランスを取りながら実装を工夫する必要があります。 このあたりがテキストエディタ実装の奥深さになります。
2025年10月8日、ロボット産業を揺るがす大きなニュースが飛び込んできました。 ソフトバンクグループがスイスの重電大手 ABB [1] から、ロボティクス部門を買収する記事でした。 ちょうどそのころ私はABBのロボットコントローラと連携するプログラムの開発で日夜格闘していました。 ロボット制御APIである PC-SDK [2] を使った連携を試みましたが、何度も落とし穴に落ちました。 まさに「死にゲー」をプレイしている感じです。何度も失敗を繰り返しながら、APIの動作を確認し、使い方を覚え、最適な手順を考えて1つ1つ問題やタスクを解決していきました。 この体験は今となってはPC-SDK攻略のための私のノウハウになっています。そこで印象に残った落とし穴を10個ピックアップしました。どのような落とし穴があるのか、どのようにして回避したのかを備忘録も兼ねて公開したいと思います。 --> ロボット開発の前提知識 オフラインティーチングやロボット制御APIとは何かを知りたい場合は「 産業用ロボットの教示方法とその応用 」をご覧ください。 PC-SDKとは # ここで紹介するPC-SDKとは、PCからABBのロボットコントローラ/ロボットを制御・監視するための開発キット(ライブラリ)を指します。 .NET Frameworkを使用して、Windows PC上で動作するカスタムアプリケーションを作成できます。.NET Framework依存かつ後述する通信ドライバがWindows専用のためLinuxには対応していないようです。 主な機能 コントローラ状態アクセス: ロボットコントローラの実行状態、ロボットの姿勢取得、I/O信号の読み書き プログラム操作: プログラムのロード、開始、停止 データアクセス: ロボットプログラムの変数の読み書き ファイル転送: PCとロボットコントローラ間でのファイル送受信処理 --> シミュレータ環境での利用 開発時に使用するツールとしては PC-SDK の他に RobotStudio [3] があります。RobotStudioは仮想ロボットコントローラを内包し、GUIアプリケーションでオフラインティーチングが行えるアプリケーションです。PC-SDKは、この仮想ロボットコントローラに対しても接続できるためRobotStudioがあれば実機が無くてもPC-SDKによる開発ができます。 落とし穴 ティア表 # PC-SDK利用時に遭遇する落とし穴をダメージレベルごとにランク付けしました。これをベースに落とし穴を評価します。 ランク ダメージレベル S あり得ないだろ!?どうやって回避するの?精神的ダメージを受けるレベル A え、何で?びっくりしたー。た、たぶん・・・なんとかなるよねレベル B なるほど、まあよくあるよね。やられたなぁレベル C 事前に回避可能 または 落ちても痛くないレベル あくまでも個人の感想です。 🕳️1. Web上の情報が少ない B # 落とし穴 オープンソースのライブラリを使っているとき、解らないことがあればネットで検索しますよね。同じ要領でPC-SDKに関する情報やAPIを検索すると、ほとんど情報がなくABBのサイトかStack Overflowのようなプログラミングに関する題材を扱う英語のQAサイトが表示されます。 日本語で書かれた個人サイトやABB以外のテック企業による説明などはほとんどありません。ABBのサイトもサンプルコードは非常に少ないです。そのため、英語のQAサイトを丹念に調べ、翻訳 [4] しながら内容を確認します。 ただし、あまり有用な情報が得られない場合や5年~10年前の古い情報だったりすることもあります。 対策 オフィシャルサイト(またはネット検索)からAPIリファレンスや取説などがPDFファイルでダウンロードできます。手元に置いておき1次資料としてザックリと内容を把握しておき、解らないことがあればそこから調べます。 最近はGoogleのNotebookLMを使ったりしています。APIリファレンスや取説のPDF、情報として有益なサイトをNotebookLMに登録しておけば、プロンプトで質問ができます。また要約してくれてエビデンスも表示されるので自分で検索するよりも簡単に情報にアクセスできます。 🕳️2. AIが頻繁にハルシネーションを起こす B # 落とし穴 最近は何か解らないことがあれば検索ではなくAIに問い合わせることが多いのですが、 「PC-SDKのAPI xxxx について使い方を教えて」 「xxxxを使ってxxxxxの処理のサンプルを提示して」 などプロンプト入力すると先ほどの「🕳️1. Web上の情報が少ない」の影響か、存在しないAPIや引数が間違えているコードサンプルを出力します。 さらっと自然に嘘をつきます。誤りを指摘しつつ再度プロンプト入力すると、今度は別の引数が間違っていたり、古いコードで動作しないものが出力されたりしてほとんど役に立たないことがあります。 対策 Visual Studioなどでプロジェクトを作成し、PC-SDKのライブラリを参照させ、オブジェクトブラウザでAPIを確認する PC-SDKに付属する abb.robotics.controllers.pc.xml をエディタで開きAPIに関する説明情報を参考とする 🕳️3. ロボットコントローラが見つからないことがある A # 落とし穴 APIではローカルネットワーク上で動作しているロボットコントローラを検索(UDPブロードキャスト)し、見つかったロボットコントローラに対してログインしてロボットコントローラに接続します。 RobotStudioはローカルPC上で動作しているため一瞬で接続できますが、運用環境ではロボットコントローラの検索でタイムアウトになったり、2回目の検索で検知するといった謎の現象が発生しました。 対策 ネットワークインタフェース(NIC)が複数ある環境(4とか8とか)ではどのネットワークを探せばよいのかわからないため検索時にタイムアウトとなり見つからない現象が発生していました。これを解決するには検索する前にロボットコントローラのIPアドレスを指定してあげます。 実機ロボットコントローラへの接続例 var scanner = new NetworkScanner(); // ロボットコントローラのIPアドレスを直接指定して、スキャナの探索リストに登録する // これにより、どのNICを通すべきかPC-SDKが判断する NetworkScanner.AddRemoteController("xxx.xxx.xxx.xxx"); // 事前に取得しておいたロボットコントローラのUUIDを指定して検索する var systemId = Guid.Parse("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); // systemId, 待ち時間[msec], リトライ数 var controllerInfo = scanner.Find(systemId, 1000,3); if (controllerInfo == null) { throw new Exception("コントローラが見つかりませんでした。"); } // 実機に接続 _controller = Controller.Connect(controllerInfo, ConnectionType.Standalone); IPアドレスを指定するのならFind()を使う意義は薄いと思いますが、これで解決します。 🕳️4. 制御権の獲得し忘れ C # 落とし穴 ロボットコントローラに接続し、下記のような状態変更を促す処理を行うとエラーになります。 RAPID [5] 変数の値を更新する サーボモーターをOnにする RAPIDプログラムをロード(タスク割り当て)する RAPIDプログラムを開始する 対策 コントローラにMastershipのリクエスト(書き込み権限のリクエスト)して取得してから更新します。サンプルコードや検索すれば例はいくらでも出てきますので慌てることはありません。 Mastershipのリクエスト using (Mastership.Request(_controller)) { // ここで更新処理を記述 } ただし、Mastershipのリクエストに失敗することがあります。リトライ処理を入れたり、例外処理でエラー処理を記述するなどの仕組みが必要です。 なお、コントローラの状態の取得やRAPID変数の値取得などReadOnlyなものはMastershipのリクエストは必要ありません。 🕳️5. 運用環境でロボットコントローラと接続できない S # 落とし穴 開発環境でRobotStudio上の仮想ロボットコントローラには接続できています。しかし、運用環境でロボットコントローラに接続するとIPアドレス等が正しく ping も通るのにロボットコントローラの状態が取得できない。 開発環境の構成 運用環境の構成 対策 RobotStudioをインストールすると、(裏で)ロボットコントローラと通信するためのドライバもインストールされます。これがないと通信できません。 運用環境ではRobotStudioは不要なのでインストールせず、PC-SDK(ライブラリ)だけを利用するとドライバがインストールされていないのでエラーになります。 ABBのサイトから RobotWare_Tools_and_Utilities_x.x.x.zip (x.x.xはバージョン)をダウンロードし、展開して RobotCommunicationRuntime/ABB Industrial Robot Communication Runtime.msi を実行するとドライバがインストールされPC-SDKで接続できるようになります。こんなん解らんて。 🕳️6. リモートPCから RobotStudio に接続できない C # 落とし穴 先ほどの「🕳️5. 運用環境でロボットコントローラと接続できない」を開発環境で検証しました。 PCを2台用意する 一方(Aとする)をアプリケーション動作環境とする もう一方(Bとする)をロボットコントローラとみなしてRobotStudio(仮想ロボットコントローラ)をインストールする AとBに RobotWare_Tools_and_Utilities_x.x.x.zip のドライバをインストールする AからPC-SDKでBの仮想コントローラに接続する 接続テスト環境の構成 上記を試みましたが、あえなく撃沈。接続できせんでした。 対策 RobotStudioはローカルPC上からのアクセスしか受け付けないためリモートPCからの接続はできない仕様のようです。 これはライセンスが絡んでいる(1 RobotStudio 1ライセンス)からではないでしょうか。仕方がないですね。 🕳️7. 運用環境でRAPID を実行できない A # 落とし穴 RobotStudio上ではログインして問題なくRAPIDのロードや実行ができます。しかし、運用環境ではコントローラに接続できましたが、RAPIDのロードや実行を指示すると例外が発生します。何で? 対策 ロボットコントローラに接続する際のデフォルトユーザーは Default User ですが RobotStudioと運用環境とで権限が異なっています。 様々な権限がありますが実行権限とプログラムのロード権限に違いがありました。 権限 RobotStudio 実機 実行権限 あり なし ロード権限 あり なし Default User でもRobotStudioではさまざまな権限が最初から付与されているようですが、実機では権限が付与されていないものがありました。 そのため、実機上に新しくユーザーを作成し、RobotStudio上と同じことができるように権限を付与し、そのユーザーでログインしたところ実行できました。 なお、RobotStudio上での仮想コントローラではユーザーの作成や権限を付与する機能はなく、実機コントローラでのみ可能となっているのもハマった理由として挙げられます。 🕳️8. デジタル出力ができない B # 落とし穴 ロボットコントローラには各種デバイスとデジタル信号(0 or 1)で連携するための物理インタフェース(I/Oポート)があります。この出力ポートに0または1を書き出すと例外が発生し、出力できませんでした。 対策 ABB ロボットコントローラではI/O設定で物理インタフェースのどこにデジタル出力を割り当てるかを指定します。このとき Access Level を指定します。デフォルト値では Default となっています。 Access Level はレベル毎に制御する側のコンテキストでRead/Writeが有効かどうかが異なっています。 Access Level Rapid Local Client in Auto Mode Remote Client in Auto Mode All Write Enabled Write Enabled Write Enabled AWACCESS Write Enabled Write Enabled Read Only Default Write Enabled Read Only Read Only Internal Read Only Read Only Read Only ReadOnly Read Only Read Only Read Only Auto Mode とは人が操作するのではなくプログラムでロボットを動かすモードを指します。 ロボットコントローラが Auto Mode のとき、ロボットコントローラの外部からアクセスしてI/Oを操作するときは一番右の Remote Client in Auto Mode 列となります。 ロボットプログラムの実行は Rapid 列に相当します。 今回はPC-SDKを利用して外部からロボットコントローラをプログラムで制御しているので Remote Client in Auto Mode となっています。 Access Level は Default 行となり、動作モードは Remote Client in Auto Mode 列となります。その重なり部分は Read Only となっていることがわかります。 つまり Access Level が Default だったので書き込みができない状況でした。 Access Level が All でないと書き込みできません。厳しいですね。 というわけで、デジタル出力を割り当てるときの Access Level を All にすることで無事書き込めるようになりました。 --> Information Access Level は新しく追加もできるようです。 🕳️9. 配列のデータ転送が遅い A # 落とし穴 RAPID側での配列の定義 MODULE MainModule PERS num dataArray{100}; ENDMODULE 配列に値を書き込む一般的な記述は下記となります。 // 最初に1回だけ取得しておく RapidData rd = _controller.Rapid.GetRapidData( "T_ROB1", "MainModule", "dataArray"); : using (Mastership.Request(_controller)) { for (int i = 0; i < 100; i++) { rd.WriteItem(new Num(i), i); } } このとき、rd.WriteItem()をコールするたびにネットワークアクセスします。そのためトータルで数百[msec]~数[sec]掛かります。 対策 なるべくrd.WriteItem()のコール回数を少なくし、一括でデータを設定するようにします。 RAPID側で RECORD型 で構造体を定義します。 MODULE MainModule RECORD StructData num value1; num value2; num value3; num value4; num value5; ENDRECORD : ENDMODULE C#側はその構造体をUserDefined型として参照できます。 UserDefined型に値を設定するときは以下のようにします。 // 最初に1回だけ取得しておく(RAPID側のStructDataのコピーを作成) UserDefined ud = new UserDefined(_controller.Rapid.GetRapidDataType( "T_ROB1","MainModule","StructData")); // 最初に1回だけ取得しておく(RAPID側のStructDataの参照を作成) RapidData rd = _controller.Rapid.GetRapidData( "T_ROB1","MainModule","StructData"); : using (Mastership.Request(_controller)) { int value1 = 1; int value2 = 2; int value3 = 3; int value4 = 4; int value5 = 5; // UserDefinedに設定するデータを作成 structData = $"[{value1},{value2},{value3},{value4},{value5}]"; // UserDefinedにデータを設定 ud.FillFromString2(structData); // ロボットコントローラにデータ転送 rd.Value = ud; } また、RAPIDのデータ型であるrobtargetやjointtargetは非常にデータサイズが大きいです。一部のデータのみ更新するのであればその値のみ転送し、RAPID側でデータを更新して利用することも有効です。 --> Caution ud.FillFromString2("[0,1,2,3,4,5,.....]") のように文字列リテラルで全要素を直接設定できます。しかし、巨大な構造体や配列の場合、途中までしか値が設定されていないことがありましたので注意が必要です。また、パース処理に時間が掛かりますがネットワークアクセスに比べると無視できるレベルです。 --> Caution AIでサンプルを提示してもらうと、おそらく古いAPIかと思われますが、存在しないAPIが提示されコンパイルエラーとなりました。 🕳️10. 実機でRAPIDを実行すると実行時エラーになる S # 落とし穴 RobotStudio上のシミュレータでRAPIDを実行しても問題なく動作し、プログラムの構文チェックも問題なくパスするのに、同じものを実機で動作させると実行時にエラーとなってしまう。 下記の例外が出力されたら要注意!! Operation is illegal in current execution state 実行時エラーなので状態に起因することは分かっていますが、何の状態なのかがさっぱりわかりません。コントローラのログを見ても直接的な原因が記述されていません。 「制御あるある」ですが原因不明の実行時エラーが一番辛いです。 対策 シミュレータ上では動くことから、運用環境との環境設定の違いに原因がありそうだと直感的にわかります。プログラムの開始からどこまで進むとエラーになるかをRAPIDプログラムのソースコードを全コメントアウトし、バイナリサーチ的にコメントアウトを解除して再実行する手順で探しました。(もしかしたらステップ実行で行けたかもしれません) 結果、2つ問題がありました。 I/Oの定義が実機ではされていなかった シミュレータ上で定義してあったI/Oの名前(参照するときは文字列で指定)が見つけられずに実行時エラーとなっていた 割り込みタイマーのトリガー時間が実機では早すぎた 10[msec]としていてシミュレータ上では動作していたが、実機では実行時エラーとなっていた 上記の問題の修正自体は簡単でしたが、見つけるのに手間が掛かりました。環境の違いに起因する実行時エラーにはご注意を。 リスクを軽減するための開発スケジュール # いかがだったでしょうか?大半は開発環境(RobotStudioのシミュレータ)で問題の無かったものが、運用環境(実機コントローラ)で問題となって現れたものとなっています。しかも、穴から這い上がったと思ったらまたすぐに落とされる状況がありました。挫けそうになりますよね。 開発環境では仮想コントローラに接続するユーザーに対して、セキュリティは緩く、大きな権限を持たせています。一方、運用環境ではセキュリティは厳しく、権限も最小限にしているため不具合発生するパターンがよくありました。 運用環境が遠隔地にある場合、現地での対応には人員、時間、移動距離、金銭の面で多大なコストを要する課題があります。そのため、開発・動作確認を開発環境で行い、システムテストのみを運用環境で一括実施する計画を立てた場合、不測の事態によって進捗に遅延が生じる懸念があります。 リスクを回避するため、スケジュール内に複数のマイルストーンを設け、現地での動作確認を段階的に実施することを強く推奨します。また、現地のエンジニアに検証を委託すること(なかなか難しいですが)も、費用対効果の観点から非常に有効な手段であると考えられます。 まとめ # 日本ではFANUCや安川電機(YASKAWA)といった世界トップクラスのロボティクスメーカーのマーケットシェアが高いため、欧州の雄ABBのシェアは数%程度だそうです。 ABBのロボット開発拠点はスイスにあるため、高度な技術課題については日本国内のサポートを経由し、本国の技術者へエスカレーションする場合があります。その際、時差や拠点間の連携プロセスにより、回答までに時間を要した経緯がありました。 PC-SDKについて辛口の内容ではありましたが、ロボットやロボットコントローラの機能や性能は素晴らしく、RobotStudioでのオフラインティーチング環境もトップレベルで使いやすいです。ABBのロボット部門がソフトバンクグループとなったことで日本でのシェア拡大を狙っていてもおかしくはありません。そうなると営業やサポート部門の規模や質もより重厚になると思われます。今後のABBロボット事業の展開に期待します。 今回はPC-SDKの落とし穴と題して幾つか挙げましたがRAPIDにも落とし穴が潜んでいます。機会があればそちらも記事にできればと思います。 エー・ビー・ビーと呼びます。Asea社とBrown Boveri社の合弁で設立(アセア・ブラウン・ボベリ) ↩︎ PCからABB ロボットコントローラに接続するためのSDK(ライブラリ) ↩︎ ABBが提供しているオフラインティーチング(シミュレーション)ソフトウェア ↩︎ DeepL, Google翻訳, ブラウザで右クリックして「日本語に翻訳」など ↩︎ ABBの産業用ロボットを制御するために開発された専用のプログラミング言語 ↩︎
はじめに # これまで豆蔵デベロッパーサイトで、AWS認定に関する記事( 2022年の12冠達成 やその後の新認定取得など)をいくつか執筆してきました。現在、AWS認定は最新の「Generative AI Developer - Professional (AIP-C01)」以外はすべて取得しています。 そんなAWS偏重な私が、今回は「Google Cloud認定の全冠」に挑戦しました。結論から言うと、約2か月で一気に制覇しようと挑んだものの、あと1歩のところで失敗してしまいました。本記事では、Google Cloud認定を目指した経緯や、短期集中で受験した所感、おすすめの受験順番、そしてなぜ失敗したのかについてまとめます。 --> Information 秘密保持契約(NDA)があるため、詳細な試験内容については触れることができませんので、ご了承ください。 また記載の情報は2026年3月時点のものです。 Google Cloud認定を目指したきっかけ # 事の発端は、2025年夏に携わったプロジェクトです。このプロジェクトではGoogle Cloud上で開発が行われていましたが、私の担当はAWS側でのAPI開発だったため、実際にはGoogle Cloud環境を触っていませんでした。しかし、「今後のためにも知っておいた方がいいだろう」と思い立ち、勉強がてらGoogle Cloudの「Associate Cloud Engineer (ACE)」を受験し合格しました。 ちょうどその頃はAWS認定の更新時期と重なっていたため、まずはそちらを優先しました。そして2025年12月に「Japan AWS All Certifications Engineers」の条件を満たしたのを区切りとして、本格的にGoogle Cloud認定の全冠を目指し始めました。 約2か月間の怒涛の受験ラッシュとテストセンター裏話 # 2025年12月末のProfessional Cloud Architectを皮切りに、全冠を目指して2026年1月〜2月にかけて約2か月間で一気に受験を進めました。 以下がその受験履歴です。既に取得済みだったAssociate Cloud Engineerも一覧に含めています。 No 受験日 認定名称 略称 受験言語 結果 1 2025-09-15 Associate Cloud Engineer ACE 日本語 合格 2 2025-12-27 Professional Cloud Architect PCA 日本語 合格 3 2026-01-12 Professional Cloud Developer PCD 日本語 合格 4 2026-01-17 Professional Cloud DevOps Engineer PCDOE 日本語 合格 5 2026-01-25 Professional Cloud Network Engineer PCNE 日本語 合格 6 2026-01-29 Cloud Digital Leader CDL 日本語 合格 7 2026-01-31 Professional Cloud Security Engineer PCSE 日本語 合格 8 2026-01-31 Professional Security Operations Engineer PSOE 英語 不合格 9 2026-02-05 Associate Google Workspace Administrator AGWA 日本語 合格 10 2026-02-08 Associate Data Practitioner ADP 日本語 合格 11 2026-02-08 Generative AI Leader GAIL 日本語 合格 12 2026-02-11 Professional Data Engineer PDE 日本語 合格 13 2026-02-13 Professional Cloud Database Engineer PCDBE 英語 合格 14 2026-02-21 Professional Machine Learning Engineer PMLE 日本語 合格 なお、Google Cloud認定の試験プロバイダは2026年2月23日までKryterionによる配信でしたが、3月からはPearsonVUEに変更になりました。 私が受験した地域では、Kryterion時代は特定のテストセンター一択でしたが、PearsonVUEに変わったことで複数のテストセンターが選択できるようになりました。また、Kryterionでは試験日時の変更が72時間前でロックされていましたが(手数料を払えば変更可能)、PearsonVUEでは24時間前まで変更可能になったのは受験者にとってかなりの朗報です。 ちなみに、私はこの2か月間で同じテストセンターに11回も通った(2回は同日受験)ので、すっかりスタッフの方に顔を覚えられてしまいました。スタッフの方と少し雑談した際に「PearsonVUEに対応する準備をしている」と聞いていたのですが、3月中旬からPearsonVUEにも対応したとのことで、今後も通い慣れたテストセンターを引き続き利用できそうです。 受験料は「米ドル決済」である点に注意 # テストセンターについての余談に関連してもう一つ、実際に短期間で大量受験して痛感したのが 受験料の支払い通貨 の違いです。 AWS認定の受験料は日本円(JPY)で確定決済されますが、Google Cloud認定の受験料はプロバイダ画面上で 米ドル(USD)決済 となります。 Professionalレベルの試験は1回200ドルかかるため、これだけの数を短期間で一気に受験すると、クレジットカードの請求額が為替レートの影響をダイレクトに受けます。会社の資格取得支援制度などを利用して経費精算をする場合は、予算申請の際に為替変動分のゆとりを持たせておくことをお勧めします。 全体的な難易度:AWS認定との比較 # これだけ短期間で多くの試験に合格できたのは、 「合格した認定のほとんどがAWSの知識の焼き直しでクリアできる内容だったから」 です。加えて、Googleが提唱するSRE(Site Reliability Engineering)をしっかりと理解しておくことが大事だと感じました。 また、文章量という点でも難易度に違いがありました。AWS認定のProfessionalやSpecialityは問題文も選択肢も文章が長く、内容を把握するのに苦労しましたが、Google Cloud認定のProfessionalはほとんどがAWS認定のAssociateレベルの長さであり、読み取るのにあまり苦労しませんでした。 Google Cloud試験の鍵を握る「SREの基本概念」 # 複数の試験(特にCloud ArchitectやDevOps Engineerなど)を通して、Googleが提唱する SRE(Site Reliability Engineering) のコア概念は共通言語として頻出します。ここをしっかり押さえておくと、シナリオ問題においてGoogle Cloudが推奨する「正解の行動」がすぐに選べるようになります。 SLI / SLO / SLA の違い : サービスの信頼性を測る指標(SLI)、開発チームと運用チームの共通の目標値(SLO)、顧客とのビジネス上の契約(SLA)の役割の違い。 エラーバジェット(Error Budget) : 100%の可用性を目指すのではなく「許容できる障害の予算」を定め、予算内であれば新機能のリリースを優先し、予算が尽きたら信頼性向上(バク修正など)を優先するという運用ルール。 トイル(Toil)の削減 : 手作業で反復的かつ自己修復されない運用作業(トイル)を、システム化や自動化によって徹底的に減らすこと。 非難なきポストモーテム(Blameless Postmortem) : 障害発生時に特定の個人を責めるのではなく、システムやプロセスの欠陥を分析し、自社システムにおける再発防止の仕組みづくりにフォーカスする文化。 AWSとGoogle Cloudの概念の違い(試験で注意すべきポイント) # AWSの知識があれば解ける問題が多いとはいえ、アーキテクチャの基本概念においていくつか決定的な違いがあり、試験でもここが問われます。代表的なものをいくつか挙げます。 VPCのスコープ  : AWSのVPCは特定の「リージョン」内に作成されますが、Google CloudのVPCは「グローバル」リソースです。VPCの配下に作成するサブネットが各リージョンに紐づくため、複数リージョンにまたがるネットワーク構築の考え方が大きく異なります。 リソースの管理単位(アカウントとプロジェクト)  : AWSでは環境や権限の分離に「AWSアカウント」を境界として使用しますが、Google Cloudでは「プロジェクト」という単位が基本となり、これらを「フォルダ」や「組織」で階層化して管理します。 ロードバランサの配置  : AWSの主要なロードバランサ(ALBなど)はリージョンリソースですが、Google Cloudのグローバルロードバランサ(Cloud Load Balancing)は、単一のAnycast IPアドレスを使用して世界中のユーザーからのトラフィックを最も近いリージョンに振り分けることができます。 各レベル・特徴的な試験の所感 # Google Cloud認定には、AWSのような「上位資格取得による下位資格の自動更新」の仕組みがありません。そのため、有効期限(2年または3年)が切れる前に各資格を個別に更新する必要があります。 ただし、有効期限が近づいた資格保持者向けには通常の新規受験とは異なる 「更新用の試験(Recertification Exam)」 が提供されているため、更新時期には公式の案内に従ってそちらを受験する形になりそうです。 Foundationレベル # 50~60問。90分。$99(税別)。3年間有効。 Cloud Digital Leader (CDL) と Generative AI Leader (GAIL) は、Google Cloudのどのサービスが使えるかという基本的な内容や、AIの一般論などがメインでした。試験時間は90分ですが、45分くらいで解き終わるボリューム感です。 Associateレベル # 50~60問。120分。$125(税別)。3年間有効。 Associate Cloud Engineer (ACE) と Associate Data Practitioner (ADP) は、AWSの知識を焼き直せばすぐに解ける内容でした。こちらは120分ですが、1時間もかからずに完了しました。 少し毛色が違うのが、Associate Google Workspace Administrator (AGWA) です。その名の通りGoogle Cloudに関する内容はなく、Google Workspaceの管理(監査対応、退職者対応、入職者対応、企業合併などで必要な作業等)がメインです。私は独自ドメインのメールアドレスを管理しており、Geminiなどを使うためにGoogle Workspaceに移行していたため、だいたいの概念は理解できておりスムーズに対応できました。 Professionalレベル # 50~60問。120分。$200(税別)。2年間有効。 先に書きましたが、問題文はAWS認定のAssociateレベルの長さであり、読み取るのにあまり苦労しませんでした。ただし、たまに長文が出てくることもありますので油断は禁物です。 特徴的な問題 # Professional Cloud Architect (PCA) では、画面の半分くらい(調整可能)に事例会社の説明が表示され、その内容を基に回答を選択するケーススタディ問題がありました。ただし、こちらは2026年3月30日に試験内容が更新される予定のため、今後形式が変更になる可能性があります。 また、Professional Cloud Network Engineer (PCNE) に関しては、公式が発表している問題数の上限(50〜60問)である「60問」がみっちり出題されました。他のProfessionalレベルの試験より問題数が多くなるケースがあるため、集中力を維持する体力的なハードルも少し高めでした。私は39問目の回答中に問題数に気づいたため心理的ダメージが大きかったです。最初に問題数を確認することをお勧めします。 唯一の壁、PSOEの難しさと反省点 # 順調に進んでいた全冠への道ですが、ちょうど折り返し地点であるProfessional Security Operations Engineer (PSOE) で不合格となり、ここで無敗記録がストップしてしまいました。 他の試験(AWS認定やPSOE以外のGoogle Cloud認定)は「どのように設計するか」が主に問われますが、PSOEは「問題が起きた時にどのように対処するか」に重きを置いています。扱われるセキュリティツールも特殊であり、そもそも英語が苦手なのに「英語の問題を解いて理解しようとした」のが間違いでした。 PSOEは日本語未対応です。PearsonVUEに移行するタイミングでついでに日本語化されることを願っていましたが、今のところその気配はありません。英語のみのProfessional Cloud Database Engineer (PCDBE) は内容が単調だったため何とか読み取れましたが、PSOEの複雑なシチュエーションは苦戦しました。 勉強方法の転換と「Google Cloud Skills Boost」の活用 # これまで私は、サードパーティの教材(Udemyの演習問題など)を活用して対策を進めていました。しかし、演習に使用していたUdemyの教材の一部が削除されてしまったこともあり、今後は方針を転換します。 これからは、公式の学習プラットフォームである 「Google Cloud Skills Boost」 をしっかりと使用していく予定です。 Google Cloud Skills Boostとは? # Google Cloudが公式に提供しているオンデマンドの学習プラットフォームです。主に以下のような特徴があります。 実践的なハンズオンラボ: 用意された一時的なGoogle Cloud環境を使って、実際のコンソール画面やCLIから手を動かしながら学ぶことができます。 認定試験向けの学習パス: 各資格試験の出題範囲に合わせたコースやクエストが体系的にまとめられています。 日本語での概念理解: 各種サービスの概念やベストプラクティスを解説する動画やドキュメントが充実しています。 PSOEのような実践的なトラブルシューティングが問われる試験では、単なる暗記ではなく実際の挙動を知っておく必要があります。そのため、Skills Boostのハンズオン等を活用して 「まずは日本語で概念と対処法を理解する → その後英語で読めるようにする」 という順番で対策を進めるべきだと痛感しました。 おすすめの受験順番と難易度(完全主観) # これからGoogle Cloud認定を目指す方に向けて、私がおすすめする受験順序と主観的な難易度(★5段階)を表にまとめました。 基本的な戦略として、 「インフラ系 → データ系 → 機械学習系 → 管理系」 の順に進めるのがベストです。最初のインフラ系でネットワーク境界やセキュリティ周りをしっかり理解できているため、データ系以降の学習でその知識をそのまま活かすことができます。 振り返ってみると(まだ全冠は終わっていませんが)、完全ではありませんが我ながら非常に理にかなった順番で受験していたなと感じています。 受験順 分野 認定名称 難易度 備考・おすすめの理由 1 インフラ系 Cloud Digital Leader ★☆☆☆☆ どこでも可。 ACEの前なら軽く知識が入る。 上位の資格取得後ならほぼ勉強なしで合格できる。 2 インフラ系 Associate Cloud Engineer ★★☆☆☆ 3 インフラ系 Professional Cloud Architect ★★★☆☆ 4 インフラ系 Professional Cloud Developer ★★★☆☆ 5 インフラ系 Professional Cloud DevOps Engineer ★★★☆☆ 6 インフラ系 Professional Cloud Network Engineer ★★★★☆ 7 インフラ系 Professional Cloud Security Engineer ★★★☆☆ 8 インフラ系 Professional Security Operations Engineer ★★★★★ 英語のみ。 9 データ系 Associate Data Practitioner ★★☆☆☆ 10 データ系 Professional Data Engineer ★★★★☆ 11 データ系 Professional Cloud Database Engineer ★★★☆☆ 英語のみ。 どこでも可。 データを扱うという点でPDEの付近がおすすめ。 12 機械学習系 Generative AI Leader ★☆☆☆☆ どこでも可。 PMLEの前なら軽く知識が入る。 PMLEの後ならほぼ勉強なしで合格できる。 13 機械学習系 Professional Machine Learning Engineer ★★★★★ 14 管理系 Associate Google Workspace Administrator ★★☆☆☆ どこでも可。 おわりに # 約2か月での「無敗での全冠制覇」という目標は、唯一不合格となったPSOEによって阻まれてしまいましたが、AWSの知識ベースがあればGoogle Cloudのキャッチアップも非常にスムーズに行えることが実証できました。 当初は3月中のリベンジを考えていましたが、諸事情と教材の見直しにより4月に先延ばしにしました。4月にはSkills Boostを活用した新たな勉強方法でPSOEにリベンジし、次こそは「Google Cloud全冠達成」の記事を書けるように頑張ります!
はじめに # 共通機能やAPIスキーマなどをライブラリ化して利用する場合、モジュール化したものを公開して各アプリケーションに組み込むと思います。 テストコードで動作確認すべきですが、実際に組み込むと軽微な修正が発生してしまうことがあります。 ファイルを相対参照させるとdist配下の構造が変わってエントリーポイントになるファイルの位置が変わってしまうなどの問題にも困っていました。 そんな悩みを解決してくれた yalc の活用方法を説明します。 yalcとは # yalcは、ローカルで開発中のnpmパッケージをローカルに公開し、GitHub Packagesなどに公開されているパッケージと同じようにアプリケーションに組み込んで開発できるようにするツールです。 同じような役割を持つ仕組みを持つ npm link / npm pack との違い # 観点 yalc npm link npm pack 依存参照の実態 .yalc と node_modules に展開(通常の利用形態に近い) シンボリックリンク tarballを手動で作成/配置 変更反映のしやすさ yalc push で利用先に伝搬 リンク先依存で環境差が出やすい 毎回 pack/install が必要 運用向き 複数アプリで同時検証しやすい 小規模・一時検証向き 配布物の確認向き 事故防止 yalc check で混入検知可能 標準で混入検知なし 手順の属人化に注意 利用手順 # yalcを活用した開発の流れを説明します。 下記の構成で説明します。 ライブラリ ディレクトリ: packages/math-utils パッケージ名: @sample-yalc/math-utils バージョン: 1.0.0 ライブラリを利用するプロジェクト ディレクトリ: demo-app よく使うコマンド一覧 コマンド 説明 yalc publish パッケージをyalcストアに公開 yalc push パッケージを再公開し、利用先に変更を伝搬 yalc add <package> パッケージを追加 yalc update 追加済みパッケージを更新 yalc remove <package> パッケージを削除 yalc remove --all すべてのyalcパッケージを削除 yalc installations show <package> パッケージの使用箇所を表示 yalc installations clean <package> パッケージの使用箇所をクリーン 最短で試す(3分) # ライブラリ側で公開 cd packages/math-utils yalc publish 利用側で追加 cd demo-app yalc add @sample-yalc/math-utils ライブラリ変更後に反映 cd packages/math-utils yalc push 基本はこれだけです。 以降に図解しながら詳細な流れを説明しているので、併せてご確認ください。 事前作業 # まずはyalcをインストールします。 インストール npm install -g yalc ライブラリのローカルへの公開から利用するまでの流れ # ローカルで開発中のライブラリをローカルに公開し、それを利用するまでの流れは以下の通りです。 パッケージをローカルに公開する(ライブラリ側) publishすると、パッケージがローカルのyalcストアに保存されます。 yalcストアにパッケージをコピー yalc.sig: パッケージの内容から算出した識別情報。ライブラリの変更有無を判定する際に使用します。 yalcストア上のpackage.json: yalcSigの加筆 $ cd packages/math-utils $ yalc publish @sample-yalc/math-utils@1.0.0 published in store. --> Information yalcストア yalcを使ってpublishしたパッケージが公開される場所のこと。 Windows: %LOCALAPPDATA%\Yalc (e.g. C:\Users\sample-user\AppData\Local\Yalc ) mac/Linux: ~/.yalc dirで実際のディレクトリが確認できます。 $ yalc dir C:\Users\sample-user\AppData\Local\Yalc プロジェクトにパッケージを追加する(利用側) addすると、パッケージを取り込んで、依存関係が更新されます。 installations.json: インストール先として加筆 package.json: 依存関係の追加/変更 "dependencies": { // パッケージの参照先が.yalc配下に変更されます "@sample-yalc/math-utils": "file:.yalc/@sample-yalc/math-utils" }, .yalc : yalcストアからパッケージがコピーされます node_modules/{パッケージスコープ/パッケージ名}: .yalcからパッケージがコピーされます yalc.lock: 新規作成されます $ cd demo-app $ yalc add @sample-yalc/math-utils Package @sample-yalc/math-utils@1.0.0 added ==> C:\Users\sample-user\demo-app\node_modules\@sample-yalc\math-utils ここまでで、リモートに公開されているパッケージと同じように利用できます。 ライブラリの変更を伝搬する # ライブラリの変更を、利用先に伝搬する手順は以下の通りです。 ライブラリのコードを変更する(ライブラリ側) 変更を伝搬する(ライブラリ側) pushすると、利用先に変更内容を伝搬します。 yalcストアにパッケージを再公開 更新されたパッケージの利用先に変更を反映(設定不備で失敗することがあります) $ cd packages/math-utils $ yalc push ライブラリの利用を終了する # yalcパッケージとの依存を除去する手順は以下の通りです。 yalcパッケージを削除する(利用側) removeすると、利用側にコピーされたライブラリが削除されます。 package.json : 依存関係を削除 .yalc : ディレクトリを削除 node_modules/{パッケージスコープ/パッケージ名} : ディレクトリを削除 yalc.lock : 依存関係を削除 ロック対象のyalcパッケージがすべてなくなったらファイルごと削除します。 yalcストアのパッケージ: 削除されません $ cd demo-app $ yalc remove @sample-yalc/math-utils # 特定のパッケージを指定してyalcパッケージを削除する場合 $ yalc remove --all # すべてのyalcパッケージを削除する場合 Appendix. 利用上の注意点など # yalc関連のファイルはgit管理から除外 # あくまでも開発時に利用するツールなので、yalcを使う時は .gitignore に該当ファイルを登録しておきます。 # yalc .yalc/ yalc.lock コミット前に yalc check で混入防止 # .yalc 参照( file:.yalc/... や link:.yalc/... )が package.json に残ったままコミットすると、CIや他環境で問題になりやすいです。 # package.jsonにyalc依存が残っていないかチェック yalc check pre-commitで実行するようにしておくと、誤コミットを防ぎやすくなります。 パッケージが更新されない # yalcパッケージをいったん削除して、再登録してください。 # キャッシュをクリアして再追加 yalc remove @sample-yalc/math-utils yalc add @sample-yalc/math-utils (yalcパッケージを削除してもうまくいかない場合)node_modulesを削除して、再登録してください。 # node_modulesを削除して再インストール rm -rf node_modules # Remove-Item -Recurse -Force node_modules # PowerShellの場合 npm install yalc add @sample-yalc/math-utils (それでもうまくいかない場合)インストール先のパスが誤っている可能性があります。確認して誤っていた場合はパスを修正してください。 # 特定のパッケージの情報 yalc installations show @sample-yalc/math-utils まとめ # ライブラリを開発しながら動作検証できるのは非常に助かります。 同じような苦労をされている方がいらっしゃれば、開発に組み込んでみてはいかがでしょうか。
はじめに # 普段、業務ではどのような OSを使っているでしょうか。 筆者個人では Macを使っていますが、業務では Windowsを利用しています。 Windows環境では、軽量で扱いやすく、POSIXライクな操作ができる Git Bashを利用しています。 Windows Terminalからも使えるため、普段使い慣れたコマンドをそのまま利用でき、 AWS CLIとの相性が良い点も便利です。 https://developer.mamezou-tech.com/blogs/2023/09/08/windows-terminal-with-git-bash/ このような理由から、AWS Systems Manager Session Manager(以下、SSM)経由で EC2に接続する際にも Git Bash を利用していました。 しかし、Windows 環境の Git Bash では、 lsblk や systemctl status などに含まれる一部の罫線・記号が文字化けすることがありました。 本記事では、この事象の再現内容、原因、対処方法を紹介します。 結論としては、 session-manager-plugin を最新版へ更新するのが第一選択 です。 更新が難しい場合は、 chcp.com 65001 でコードページを UTF-8 に変更する方法 が有効でした。 なお、 Macから接続した場合は同様の問題は発生しませんでした。 前提 # 以下の環境で検証しています。 OS: Windows: 11(Pro) Terminal: Git Bash AWS CLI: aws-cli/2.32.16 Python/3.13.11 Windows/11 exe/AMD64 session-manager-plugin: 1.2.707.0(検証開始時)→1.2.792.0(解決) 次のような方を対象としています。 Windows + Git Bash で AWS CLI / SSM を利用してEC2への接続している方。 lsblk や systemctl status の罫線・記号などの文字化けする方。 先に結論 # 対処方法は次の通りです。 優先度 対処方法 補足 1 session-manager-plugin を最新版( 1.2.792.0 以降)へ更新する 根本対応。まずはこちらを推奨 2 chcp.com 65001 を実行する プラグインを更新できない場合の暫定対処 3 .bashrc に設定する 暫定対処を継続利用する場合の恒久化 推奨対処: session-manager-plugin を最新版へ更新する # 検証を進める中で、 最新版の session-manager-plugin に更新すると事象が解消する ことを確認できました。 ※このバージョンは、記事の執筆・検証時点の3日前に最新版がリリースされていました。 執筆時点では、最新版の 1.2.792.0 にプラグインを更新することで文字化けが再現しなくなりました。 インストール方法は、以下の AWS 公式ドキュメントを参照してください。 WindowsでのSession Managerプラグインのインストール なぜ 1.2.792.0 で改善したのか # 執筆時点での最新版である 1.2.792.0 のリリースノートには、 Windows環境でのキーボード入力や文字処理に関する修正が含まれていました。 session-manager-plugin 1.2.792.0 リリースページ 1.2.792.0 に関連する以下のPRを見ると、本記事の事象と近い問題が修正対象になっていることが分かります。 PR: Windowsプラットフォームにおける中国語および日本語の文字表示の不具合の修正 PRでは、Windows版の session-manager-plugin が出力時に windows.WriteFile APIを使用しており、 UTF-8 の一部の言語の文字が正しく処理できていなかったことが説明されています。 その結果、中国語や日本語の入力・出力の一部で文字化けが発生していたと考えられます。 本記事で確認した文字化けも、この修正を含む 1.2.792.0 で改善した可能性が高いと考えられます。 本記事の後半は、 古いバージョン ( 1.2.707.0 ) を利用していたときの再現内容と暫定対処 をまとめたものです。 まずはプラグインを最新版へ更新し、改善するかを確認してください。 文字化けの事象(古いプラグインの場合) # 古いバージョンのプラグイン(例: 1.2.707.0 )を使用している場合、以下のようなコマンドを実行した際に文字化けが発生しました。 今回は Amazon Linux 2023 と Ubuntu 24.04 の公式AMIで検証しましたが、AMIに依存せず、同様の箇所で文字化けが発生しました。 lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 27.8M 1 loop /snap/amazon-ssm-agent/12322 loop1 7:1 0 74M 1 loop /snap/core22/2339 loop2 7:2 0 48.1M 1 loop /snap/snapd/25935 nvme0n1 259:0 0 8G 0 disk 笏懌楳nvme0n1p1 259:1 0 7G 0 part / 笏懌楳nvme0n1p14 259:2 0 4M 0 part 笏懌楳nvme0n1p15 259:3 0 106M 0 part /boot/efi 笏披楳nvme0n1p16 259:4 0 913M 0 part /boot systemctl status sshd 笳・sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled) Active: active (running) since Sun 2026-03-20 14:18:54 UTC; 1min 12s ago Docs: man:sshd(8) man:sshd_config(5) Main PID: 1553 (sshd) Tasks: 1 (limit: 1067) Memory: 2.3M CPU: 15ms CGroup: /system.slice/sshd.service 笏披楳1553 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups" ご覧の通り、罫線やステータスアイコンが文字化けしています。 原因 # この文字を調べてみたところ、以下の通りでした。 本来 Unicode 文字化け ├─ U+251C , U+2500 笏懌楳 └─ U+2514 , U+2500 笏披楳 ● U+25CF 笳 (末尾不正) 以上から、今回の文字化けは UTF-8の文字列が CP932(Shift_JIS 系)として誤って解釈されたことで発生していると考えられます。 つまり、 SSM経由で受け取った文字列と Git Bash側のコンソールのページ文字コード設定が一致しておらず、その結果として罫線や記号が正しく表示されなかった、という状況です。 Git Bash側の設定確認と暫定対処 # Git Bashの現在の設定を確認する # 今回の環境では、 chcp.com の結果は CP932でした。 自身の Git Bashの現在の設定を確認したい場合は以下のコマンドで確認できます。 chcp.com 現在のコード ページ: 932 暫定対処: コンソールの文字コードをUTF-8に変更する # --> Information まずは session-manager-plugin を最新版へ更新し、改善することを確認してください。 ここで紹介する方法は、プラグインを更新できない場合の暫定対処です。 最新版のダウンロードができない・インストールに制約がある環境の方は以下の通り、現在のページのコードをUTF-8へ変更するコマンドを試してみてください。 chcp.com 65001 Active code page: 65001 Git Bash上では単なる chcp を認識せず、期待通り動作しません。 これは Windows標準の C:\Windows\System32\chcp.com を明示的に呼び出す必要があるためです。 参考情報 Git Bash の日本語の文字化けを解消する方法 Qiita: Git Bash で 文字コードを変換する方法(Windows) Microsoft chcp Microsoft Code Page Identifiers 結果 # コンソールの文字コードを UTF-8へ変更後に再度確認したところ、文字化けが発生していた箇所が正常に出力されることを確認できました。 最新版の session-manager-plugin に更新・利用した場合は、 chcp.com 65001 を実行しなくても、文字化けせずに出力されました。 lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 27.8M 1 loop /snap/amazon-ssm-agent/12322 loop1 7:1 0 74M 1 loop /snap/core22/2339 loop2 7:2 0 48.1M 1 loop /snap/snapd/25935 nvme0n1 259:0 0 8G 0 disk ├─nvme0n1p1 259:1 0 7G 0 part / ├─nvme0n1p14 259:2 0 4M 0 part ├─nvme0n1p15 259:3 0 106M 0 part /boot/efi └─nvme0n1p16 259:4 0 913M 0 part /boot systemctl status sshd ● sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled) Active: active (running) since Sun 2026-03-20 15:22:04 UTC; 33s ago Docs: man:sshd(8) man:sshd_config(5) Main PID: 1549 (sshd) Tasks: 1 (limit: 1067) Memory: 2.3M CPU: 15ms CGroup: /system.slice/sshd.service └─1549 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups" 設定の恒久化 # 毎回コードページを変更するのが面倒な場合は、 .bashrc に設定しておくことで Git Bash 起動時に自動で UTF-8 に切り替えられます。 echo 'chcp.com 65001 > /dev/null' >> ~/.bashrc まとめ # 今回は、Git Bash で SSM を利用した際に発生する文字化けの原因と対処方法を紹介しました。 ポイントをまとめると、次の通りです。 まずは session-manager-plugin を最新版へ更新する 古いプラグインを使っている場合、Git Bash側のページ文字コードが原因で文字化けすることがある 更新できない環境では、 chcp.com 65001 によるUTF-8化が有効 継続利用する場合は .bashrc への設定で恒久化できる これまでは文字化けが起きても、表示が崩れた部分を手で読み替えながら対応していました。 最近は EC2 周りの作業が増え、文字化けに遭遇する頻度も高くなったため、本記事で対処方法を調べました。 解決方法にたどり着くまで時間がかかったので、本記事の内容がお役に立てば幸いです。 最後までご覧いただきありがとうございました。
はじめに # ロボットや製造装置のソフトウェアでは、ユーザーインターフェースと装置制御ロジックの設計が重要になります。特に装置の操作パネルは、装置の状態を分かりやすく表示するとともに、安全に操作を行えるインターフェースである必要があります。 食品盛り付けロボット「美膳®」は、製造現場でのエンドユーザー利用を想定して設計されたロボットシステムです。美膳®の本体には、システムを操作するための専用の操作パネルが用意されています。 本記事では、美膳®のUI設計を例として、次の内容を紹介します。 装置UIのソフトウェアアーキテクチャ Flutterを利用したGUI実装 gRPCによるコンポーネント間通信 双方向ストリーミングを用いたリアルタイム通信 美膳®のソフトウェアアーキテクチャ # 美膳®のソフトウェアは、役割ごとに分離された複数のコンポーネントによって構成されています。それぞれのコンポーネントが明確な責務を持つことで、システム全体の保守性と拡張性を高めています。 主なコンポーネントは次の3つです。 GUIアプリケーション ユーザー操作の入口となるアプリケーションです。装置の状態表示や操作入力を担当し、直接コアロジックにはアクセスせず、必ずAPIを経由して操作を行います。 コントローラAPI コントローラアプリケーションの機能や状態を外部に公開するインターフェース層です。GUIなどの外部アプリケーションは、このAPIを通して装置の機能にアクセスします。 コントローラアプリケーション 美膳®の中核となるコンポーネントです。装置の状態制御、登録データ管理、画像処理などを担当します。 システム構成は次の図のようになります。 このように、GUIアプリケーションは直接コントローラアプリケーションにアクセスするのではなく、APIを介して通信する構造になっています。これにより、UIとコアロジックを独立して開発・保守することが可能になります。 コンポーネント間通信 # 美膳®では、GUIアプリケーションとコントローラアプリケーションの通信に gRPC を採用しています。コントローラAPIはgRPCで実装されており、コントローラアプリケーションはgRPCサーバとして動作します。GUIアプリケーションはgRPCクライアントとして接続し、各種サービスを利用します。 この構成により、UIと制御ロジックを言語や実装に依存せず接続することが可能になります。 --> Information コントローラAPIはインターフェイス定義言語 ( Protocol Buffers )により公開インタフェースを定義し、サーバ側 / クライアント側それぞれのプログラミング言語用に変換して利用する。 通信の用途は主に次の3つです。 システム状態通知 # GUIアプリケーションはサーバに接続している間、システムの状態更新を継続的に受信します。これは サーバストリーミングRPC によって実装されています。コントローラの状態変化に応じてGUIの表示が更新され、装置の状態とUIが常に同期されます。 サービス要求 # ユーザー操作によって発生する単発の処理要求は Unary RPC によって実装されています。GUIアプリケーションから要求が送信され、サーバから処理結果が応答されます。 同期セッション # リアルタイム性が必要な操作では 双方向ストリーミングRPC を使用します。クライアントとサーバが同時にメッセージを送信できるため、リアルタイムな通信セッションを実現できます。 例えば次のような用途で利用されています。 カメラ映像を確認しながらのパラメータ調整 運転開始前の確認操作 ロボットの状態監視 通信の流れは次の図のようになります。 技術概要 # Flutterについて # 美膳®のGUIアプリケーションは、Googleが提供するUIフレームワーク Flutter を利用して実装されています。FlutterではDart言語を使用してアプリケーションを開発します。 Flutterはクロスプラットフォームのフレームワークであり、単一のコードベースから複数のOS向けのアプリケーションを生成できます。また、豊富なUIコンポーネントが提供されているため、操作パネルのようなGUIの開発を効率的に行うことができます。 さらに、Googleが提供する他の技術との親和性が高いことも特徴の一つです。 gRPCについて # gRPCはGoogleが開発したオープンソースのRPC(Remote Procedure Call)フレームワークです。gRPCでは Protocol Buffers(Protobuf) を利用してAPIを定義し、データのシリアライズを高速に行うことができます。 ProtobufでAPIを定義することで、複数のプログラミング言語から同一のインターフェースを利用することが可能になります。そのため、Flutter(Dart)で実装されたGUIと、C++で実装されたコントローラアプリケーションのような異なる言語のシステムを容易に接続できます。 豆蔵では過去のロボット開発プロジェクトでもgRPCを利用した実績があります。 サンプルアプリケーション # 双方向ストリーミングによるリアルタイム通信 # gRPCの双方向ストリーミングは、クライアントとサーバが同時にメッセージを送信できる通信方式です。この仕組みを利用することで、リアルタイム性の高いUI操作を実装できます。 ここでは、FlutterとgRPCを用いた簡単なサンプルアプリケーションを通して、双方向ストリーミングによるリアルタイム通信の仕組みを紹介します。 実際の美膳®では、Flutterで実装されたUIとC++で実装されたコアアプリケーションが通信していますが、ここでは理解を容易にするため、Dartでサーバとクライアントを実装したサンプルを作成します。 このサンプルでは、サーバが共有カウンターを管理し、複数のクライアントがカウンターの更新操作を送信できるアプリケーションを作成します。クライアントから送信された操作はサーバで処理され、その結果がすべての接続クライアントへリアルタイムに配信されます。 以降では、このサンプルアプリケーションの実装手順を紹介します。 ディレクトリ構成 # 今回のサンプルでは複数のプロジェクトを作業ディレクトリ Examples にまとめます。( Examples ディレクトリは任意の場所に作成してください) これから作成するディレクトリの構成は下図のようになります。 Examples/ | +-- counter_server/ | サーバプログラムのプロジェクトディレクトリ | +-- counter_client/ | クライアントプログラムのプロジェクトディレクトリ | +-- counter_api/ gRPCで使用するAPIの定義を格納する --> Information 本サンプルは、下記の環境で作成・動作の確認を行った。 OS: Ubuntu 22.04 LTS Flutter: 3.24.2 / Dart: 3.5.2 gRPCを用いたAPIの定義 # 最初にProtocol Bufferで、クライアント - サーバ間で使用するAPIを定義します。 ターミナルで Examples ディレクトリに入り、下記を実行してください。 mkdir counter_api cd counter_api touch counter_api.proto counter.proto をエディタで開き、gRPCのメッセージとサービスを定義して保存してください。 syntax = "proto3"; package counter_api; /// カウンターサービス定義 service CounterService { /// 双方向ストリーミング /// クライアントは操作を送信 /// サーバーは最新カウント値をストリームで返す rpc SyncCounter(stream CounterRequest) returns (stream CounterResponse); } /// クライアントからの操作リクエスト message CounterRequest { string client_id = 1; // クライアント識別子 oneof action { Increment increment = 2; Decrement decrement = 3; Reset reset = 4; } } /// +1 操作 message Increment { int32 amount = 1; // 通常は1 } /// -1 操作 message Decrement { int32 amount = 1; // 通常は1 } /// リセット操作 message Reset {} /// サーバーから配信される現在状態 message CounterResponse { int32 current_value = 1; // 現在のカウント値 string updated_by = 2; // 更新したクライアントID int64 timestamp = 3; // 更新時刻(Unix ms) } サーバ側のDartプロジェクトの作成 # Examples ディレクトリで下記をターミナルから実行する dart create counter_server cd counter_server 続けて、先に定義した counter_api.proto をコンパイルして自動生成のコードをcounter_serverのソースに加えます。 dart pub global activate protoc_plugin 21.1.2 # <-- protoc_plugin をインストール export PATH="$PATH":"$HOME/.pub-cache/bin" # <-- Protocol Buffers コンパイラ (protoc) のPATHを一時的に通す mkdir -p lib/src/generated protoc --dart_out=grpc:lib/src/generated -I../counter_api counter_api.proto # <-- counter_api.proto をコンパイルする counter.proto のコンパイルに成功すると、下記のファイルが Examples/counter_server/lib/src/ に生成されます。 Examples/counter_server/lib/src/generated | +-- counter_api.pb.dart | メッセージ型の本体定義 | +-- counter_api.pbenum.dart | enum定義 (今回は使用しない) | +-- counter_api.pbgrpc.dart | gRPCサービス用のコード | +-- counter_api.pbjson.dart JSON用メタデータ (今回は使用しない) counter_server/pubspec.yaml を編集してプロジェクトを設定します。 name: counter_server description: "gRPC bidirectional streaming counter server" version: 1.0.0 environment: sdk: ^3.5.2 dependencies: grpc: ^3.2.4 protobuf: ^3.1.0 protoc_plugin: ^21.1.2 fixnum: ^1.1.1 dev_dependencies: lints: ^4.0.0 test: ^1.24.0 counter_server/lib/counter_server.dart を編集し、サーバを実装します。 import 'dart:async'; import 'package:grpc/grpc.dart'; import 'package:fixnum/fixnum.dart'; import 'src/generated/counter_api.pb.dart'; import 'src/generated/counter_api.pbgrpc.dart'; class CounterServiceImpl extends CounterServiceBase { int _currentValue = 0; // 接続中クライアントへ配信するためのコントローラ一覧 final List<StreamController<CounterResponse>> _clients = []; @override Stream<CounterResponse> syncCounter( ServiceCall call, Stream<CounterRequest> requestStream) { final controller = StreamController<CounterResponse>(); _clients.add(controller); print("Client connected"); // 接続直後に現在値を送信 controller.add(_createResponse("server")); requestStream.listen( (request) { _handleRequest(request); }, onDone: () { print("Client disconnected"); _clients.remove(controller); controller.close(); }, onError: (e) { print("Stream error: $e"); _clients.remove(controller); controller.close(); }, ); return controller.stream; } void _handleRequest(CounterRequest request) { if (request.hasIncrement()) { _currentValue += request.increment.amount; _broadcast(request.clientId); } else if (request.hasDecrement()) { _currentValue -= request.decrement.amount; _broadcast(request.clientId); } else if (request.hasReset()) { _currentValue = 0; _broadcast(request.clientId); } } void _broadcast(String updatedBy) { final response = _createResponse(updatedBy); print("Broadcast: $_currentValue (by $updatedBy)"); for (final client in _clients) { client.add(response); } } CounterResponse _createResponse(String updatedBy) { return CounterResponse() ..currentValue = _currentValue ..updatedBy = updatedBy ..timestamp = Int64(DateTime.now().millisecondsSinceEpoch); } } Future<void> main() async { final server = Server.create( services: [CounterServiceImpl()], interceptors: const <Interceptor>[], ); await server.serve(port: 50051); print('Counter Server listening on port ${server.port}'); } クライアント側のFlutterプロジェクトの作成 # Examples ディレクトリで下記をターミナルから実行します。 flutter create counter_client cd counter_client 続いてサーバのときと同じように、 counter_api.proto をコンパイルして自動生成のコードをcounter_serverのソースに加えます。 dart pub global activate protoc_plugin 21.1.2 # <-- protoc_plugin をインストール export PATH="$PATH":"$HOME/.pub-cache/bin" # <-- Protocol Buffers コンパイラ (protoc) のPATHを一時的に通す mkdir -p lib/src/generated protoc --dart_out=grpc:lib/src/generated -I../counter_api counter_api.proto # <-- counter_api.proto をコンパイルする counter_client/pubspec.yaml を編集してプロジェクトを設定します。 name: counter_client description: "gRPC bidirectional streaming counter client" publish_to: 'none' version: 1.0.0+1 environment: sdk: ^3.5.2 dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 grpc: ^3.2.4 protobuf: ^3.1.0 uuid: ^4.4.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^4.0.0 flutter: uses-material-design: true counter_client/lib/main.dart を編集し、クライアントを実装します。 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:grpc/grpc.dart'; import 'package:uuid/uuid.dart'; import 'src/generated/counter_api.pb.dart'; import 'src/generated/counter_api.pbgrpc.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: CounterPage(), ); } } class CounterPage extends StatefulWidget { const CounterPage({super.key}); @override State<CounterPage> createState() => _CounterPageState(); } class _CounterPageState extends State<CounterPage> { late ClientChannel _channel; late CounterServiceClient _stub; late StreamController<CounterRequest> _requestController; Stream<CounterResponse>? _responseStream; final String _clientId = const Uuid().v4(); int _currentValue = 0; String _lastUpdatedBy = "-"; @override void initState() { super.initState(); _initGrpc(); } void _initGrpc() { _channel = ClientChannel( 'localhost', // サーバーアドレス port: 50051, options: const ChannelOptions( credentials: ChannelCredentials.insecure(), ), ); _stub = CounterServiceClient(_channel); _requestController = StreamController<CounterRequest>(); _responseStream = _stub.syncCounter(_requestController.stream); _responseStream!.listen((response) { setState(() { _currentValue = response.currentValue; _lastUpdatedBy = response.updatedBy; }); }); } void _sendIncrement() { final request = CounterRequest( clientId: _clientId, increment: Increment()..amount = 1, ); _requestController.add(request); } void _sendDecrement() { final request = CounterRequest( clientId: _clientId, decrement: Decrement()..amount = 1, ); _requestController.add(request); } void _sendReset() { final request = CounterRequest( clientId: _clientId, reset: Reset(), ); _requestController.add(request); } @override void dispose() { _requestController.close(); _channel.shutdown(); super.dispose(); } @override Widget build(BuildContext context) { final isMe = _lastUpdatedBy == _clientId; return Scaffold( appBar: AppBar( title: const Text("gRPC 双方向ストリーミング カウンター"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '現在値', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), Text( '$_currentValue', style: const TextStyle( fontSize: 60, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), Text( '最終更新: ${isMe ? "自分" : _lastUpdatedBy}', ), const SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _sendIncrement, child: const Text("+1"), ), const SizedBox(width: 20), ElevatedButton( onPressed: _sendDecrement, child: const Text("-1"), ), const SizedBox(width: 20), ElevatedButton( onPressed: _sendReset, child: const Text("Reset"), ), ], ), ], ), ), ); } } サンプルプログラムの実行 # ここでは作成したサンプルプログラムを実行し、gRPCの双方向ストリーミングによるクライアント間の状態同期を確認します。 このサンプルでは、1つのサーバに対して複数のクライアントが接続し、カウンタの状態をリアルタイムに共有します。 サーバは localhost:50051 で待ち受けるように実装されています。 そのため、サーバとクライアントは同じマシン上で実行することを前提としています。 下記の順にサーバとクライアントを起動します。 1. サーバ側 # ターミナルで counter_server のプロジェクトディレクトリに移動し、Dartプログラムを実行します。 cd counter_server dart run サーバが起動すると、次のようなログが表示されます。 Counter Server listening on port 50051 この状態で、クライアントからの接続を受け付けます。 2. クライアント側 # 別のターミナルを開き、 counter_client のプロジェクトディレクトリでFlutterアプリケーションを起動します。 cd counter_client flutter run クライアントアプリケーションが起動すると、gRPCを通して自動的にサーバへ接続されます。 実行結果 # クライアントの [+1] / [-1] ボタンをタップすると、操作内容が双方向ストリーミングRPCを通してサーバへ送信されます。 サーバはカウンタの状態を更新し、その結果を接続中のすべてのクライアントへストリームで配信します。 クライアントは受信した状態をもとに画面の表示を更新します。 上図はカウンタを「2」までカウントアップした状態のクライアントです(以下では「 クライアントA 」と呼びます)。 画面下部の [最終更新] には、最後にカウンタを更新したクライアントが表示されます。 この場合はクライアントA自身が更新しているため、「自分」と表示されています。 次に、別のターミナルを開いてもう1つクライアントを起動します。 これを「 クライアントB 」とします。 クライアントBもサーバから状態ストリームを購読しているため、現在のカウンタ値「2」が表示されます。 しかし [最終更新] には、更新を行ったクライアントAの識別子が表示されます。 この状態でクライアントBからカウンタを更新すると、次のような挙動になります。 クライアントBが更新操作を送信 サーバがカウンタ値を更新 サーバが全クライアントへ状態更新を配信 クライアントAとクライアントBの画面が同時に更新 この結果、 クライアントBでは [最終更新] = 自分 クライアントAでは [最終更新] = クライアントBのID と表示が更新されます。 このように、複数のクライアントが同じ状態をリアルタイムに共有していることを確認できます。 考察 # 今回のサンプルでは、双方向ストリーミングRPCを利用してクライアントとサーバ間の通信を実装しました。 ただし、このカウンタの例だけを見ると、必ずしも双方向ストリーミングを使用する必要はありません。例えば次のような構成でも同様の機能を実現できます。 カウンタ更新 → Unary RPC 状態更新通知 → Server Streaming RPC この方法でもクライアント間の状態同期は可能です。 しかし、実際の装置ソフトウェアでは次のような要件が発生することが多くあります。 操作入力をリアルタイムに送信する カメラ映像などのデータを連続的に受信する 操作と状態更新を同一セッションで同期する このようなケースでは、クライアントとサーバが同時にデータを送受信できる 双方向ストリーミングRPC が有効です。 美膳のシステムでは、この仕組みを利用して次のような操作を実装しています。 カメラ映像を確認しながらのパラメータ調整 ロボット操作の確認セッション UIと装置状態のリアルタイム同期 双方向ストリーミングを利用することで、リアルタイム性を保ちながら複雑な操作セッションを実装することができます。 まとめ # 本記事では、食品盛り付けロボット「美膳®」におけるUIアーキテクチャと、その実装技術の概要を紹介しました。 美膳®では、ユーザーインターフェースとロボット制御ロジックを明確に分離した構成を採用しています。GUIアプリケーションはFlutterによって実装され、コントローラアプリケーションとはgRPCを用いて通信します。 このような構成にすることで、UIと制御ロジックを独立して開発・保守することが可能になります。その結果、装置ソフトウェアの整備性や拡張性を高めることができます。 また、gRPCのストリーミング機能を活用することで、装置の状態通知やリアルタイムな操作セッションなど、用途に応じた通信モデルを柔軟に実装できます。 美膳®のソフトウェア設計のポイントは次の通りです。 UIと制御ロジックの分離 GUIアプリケーションとコントローラアプリケーションを独立したコンポーネントとして構成することで、責務を明確にし、開発と保守を容易にしています。 APIによるコンポーネント接続 コントローラアプリケーションの機能をAPIとして公開することで、UIからのアクセスを安全に制御し、システムの境界を明確にしています。 多言語環境を前提とした通信基盤 Flutter(Dart)とC++という異なる言語で実装されたアプリケーションを接続するために、gRPCとProtocol Buffersを用いた通信基盤を採用しています。 ストリーミング通信によるリアルタイム同期 システム状態の配信や操作セッションなど、装置のUIに必要なリアルタイム通信を効率的に実装しています。 ロボットや装置のソフトウェアでは、UI、通信、制御ロジックなど複数の要素が密接に関係します。本記事で紹介した構成はその一例ですが、装置ソフトウェアの設計を検討する際の参考になれば幸いです。
はじめに # MCPサーバーは、エージェントやツールが呼び出せる実行可能な「サービス」を定義する仕組みです。 このページでは、VS CodeのMCP拡張からMCPサーバー(今回はMarkitdownを使用)を起動して、AIエージェント/MCPクライアントで呼び出す手順を紹介します。 用語補足(この記事での使い方) MCP(Model Context Protocol)サーバー エージェントに実行可能なツールを提供する仕組み。 エージェントはMCPサーバーの「ツール」を呼び出すことで外部処理を実行できる。 Markitdown ( microsoft/markitdown ) PDFやpptxをMarkdownに変換するMCPサーバー。 uvx Markitdownのランチャー。 今回検証に使うMCPサーバーが command として期待するツール。 環境 # Windows 11 VS Code Markitdown@1.8.1: 動作確認に使用したMCPサーバー uvx@0.10.9 Copilot: MCPサーバーの呼び出しに使用したAIエージェント MCP Inspector@0.21.0: MCPサーバーの呼び出しに使用したMCPクライアント 手順 # 今回はMarkitdownを使用して、動作を確認します。 MCPサーバーをVS Codeにインストール GitHub MCP Registry からインストールする場合 GitHub MCP Registryをブラウザで開いて、インストールしたいMCPのinstallボタンを押下。 VS CodeのExtensionsからインストールする場合 VS Code上でMCPサーバーを検索できるように設定を有効化 --> Information ■設定を無効化したい場合 設定を開いてチェックを外す。 フィルタリング( @mcp )された状態でMCPが表示されるので、インストールしたいMCPのinstallボタンを押下。 MCPサーバーのインストール後、 .vscode/mcp.json にMCPサーバーの設定が追加される。 .vscode/mcp.json { "servers": { "microsoft/markitdown": { //MCPサーバー名 "type": "stdio", //MCPサーバーの種類 "command": "uvx", //MCPサーバーが期待するコマンド "args": [ //コマンドに渡す引数 "markitdown-mcp@0.0.1a4" ], "gallery": "https://api.mcp.github.com", "version": "1.0.0" } }, "inputs": [] } MCPサーバーを起動 インストール後に作成された .vscode/mcp.json の設定を読み込んで、ローカルプロセスを起動します。 VS CodeのExtensionsから起動 .vscode/mcp.json から起動 起動例 2026-03-10 12:35:30.848 [info] Starting server microsoft/markitdown 2026-03-10 12:35:30.848 [info] Connection state: Starting 2026-03-10 12:35:30.849 [info] Starting server from LocalProcess extension host 2026-03-10 12:35:30.920 [info] Connection state: Starting 2026-03-10 12:35:30.920 [info] Connection state: Running 2026-03-10 12:35:35.925 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:40.922 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:45.927 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:50.921 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:35:55.923 [info] Waiting for server to respond to `initialize` request... 2026-03-10 12:36:00.924 [info] Waiting for server to respond to `initialize` request... # 以下、略 --> Information ■uvxのインストールを促すコネクションエラー 今回、検証用に使ったMarkitdownはPythonベースでコマンドにuvxを使用します。 そのため、uvxにパスが通ってない状態だと下記のコネクションエラーが発生します。 2026-03-10 12:35:30.848 [info] Starting server microsoft/markitdown 2026-03-10 12:35:30.848 [info] Connection state: Starting 2026-03-10 12:35:30.849 [info] Starting server from LocalProcess extension host 2026-03-10 12:35:30.920 [info] Connection state: Starting 2026-03-10 12:35:30.920 [info] Connection state: Error spawn uvx ENOENT ■uvxをインストールしてパスを通せばエラーは解消します uvx をインストール PS > powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" downloading uv 0.10.9 (x86_64-pc-windows-msvc) failed to download from https://releases.astral.sh/github/uv/releases/download/0.10.9 trying alternative download URL installing to C:\Users\xxx\.local\bin uv.exe uvx.exe uvw.exe everything's installed! To add C:\Users\xxx\.local\bin to your PATH, either restart your shell or run: set Path=C:\Users\xxx\.local\bin;%Path% (cmd) $env:Path = "C:\Users\xxx\.local\bin;$env:Path" (powershell) ※ spawn uvx ENOENT は「uvx コマンド自体が見つからない(PATHにない)」という意味です。インストール済みでも、VS Codeを再起動してPATHが反映されているか確認してください。 uvxへパスが通ってることを確認 uvx --version でバージョンが表示されることを確認します。 --> Information Waiting for server to respond to initialize request... MCPサーバーの起動に時間がかかっているだけなので、辛抱強く待ちましょう。 MCPサーバーを呼び出し MCPサーバーをインストールすると .vscode/mcp.json に設定されるので、これを使用して呼び出します。 { "servers": { "microsoft/markitdown": { //MCPサーバー名 "type": "stdio", //MCPサーバーの種類 "command": "uvx", //MCPサーバーが期待するコマンド "args": [ //コマンドに渡す引数 "markitdown-mcp@0.0.1a4" ], "gallery": "https://api.mcp.github.com", "version": "1.0.0" } }, "inputs": [] } AIエージェントからの呼び出し例 MCPサーバー名と変換対象のファイルを渡して自然言語で実行。 --> Information 実行例では変換だけを指示していますが、AIエージェントから呼び出す場合、結果をファイルとして保存させるなど、プロンプト次第でさまざまな処理が可能です。 MCPのレスポンスを加工するなどの後続処理したい場合はAIエージェントから必要なプロンプトを定義して実行します。 --> Information ■期待した結果が返ってこない 今回使用しているMarkitdownはツールが convert_to_markdown しかないですが、複数のツールを持つMCPサーバーを使っている場合、期待したツールを呼び出してないと想定した結果がかえされません。 実行しているツールが想定と異なる場合は明示的に指定します。 MCPクライアントからの呼び出し例 MCP Inspectorをローカルで起動して convert_to_markdown ツールを呼び出し。 MCPサーバーを停止 確認が終わったら、以下のいずれかの方法で停止。 TerminalでCtrl+C VS CodeのExtensionsから停止 .vscode/mcp.json から停止 まとめ # VS CodeのMCP拡張で microsoft/markitdown を起動し、PDFをMarkdownに変換するまでを実際に確認しました。 例としてMarkitdownを使用しましたが、他のMCPサーバーでも mcp.json に記述されている内容に沿って準備すれば応用できます。 使用したいMCPサーバーがあれば導入して、AIエージェントに力を与えましょう。
本記事ではC#によるADS通信を使ってTwinCAT上にあるPLCデータと連携する方法についてご紹介します。 ロボット制御ではC#が人気? # システム開発では様々なプログラミング言語が利用されています。 Python, JavaScript(Node.js, Deno), C#, Java, C++, C 等がメジャーですね。最近だとRustやGoなども人気があるようです。 ロボット制御や工場の自動化においても同様に多くの言語が利用されています。 サービスや機器を提供するベンダーは自社製品をシステムに提供する際、APIやライブラリも同時に提供します。 そのため、利用者が多いプログラミング言語であったりオープンな規格で提供することが望まれます。 AI関連やオープンソースのものはPythonモジュールでの提供が非常に多いですがライセンスビジネスではC#でのライブラリ提供が多いと感じています。 理由は下記が挙げられるかと思います。 利用者が多い 使い方が簡単(プログラミング言語の敷居が低い) 便利なライブラリが豊富(組み合わせることで開発コスト削減) ベンダーが提供したWindows上のGUIアプリやシミュレータ(C#で開発)との連携で相性が良い ベンダー自体もライブラリの開発がし易い クローズドソース(ライセンスビジネスなどを想定) Linux(.NET Core)でも動作する Pythonモジュールでの提供もありますが、この場合はコアとなるライブラリは(クローズドソースや高速化のため)C++などのライブラリとし、Pythonはそのライブラリを利用するためのWrapperとして提供されます。 ADS通信とは # ADS(Automation Device Specification)はBeckhoff Automation社が開発した独自の通信プロトコルです。 TCP/IPやUDP/IPの上で動作しTwinCATシステム内外のソフトウェアモジュール間でのデータ交換で利用されます。 C#(.NET)のライブラリが提供されており、TwinCATの環境が整っていればすぐに利用することができます。 TwinCATがハブとなりADSプロトコルでTwinCAT PLC変数の監視・操作などができるようになります。 --> TwinCATおよびADS通信に関して 連載記事「TwinCATで始めるソフトウェアPLC開発」「 第1回:環境構築編 」をご覧ください。 TwinCAT ADSを利用したシステム構成例 # TwinCAT PLCを中心にシステムが構成されます。そのためXAR(実行環境)が必要になります。またアプリケーション1-3をADS通信で TwinCATと連携させるためXAE(開発環境)が必要となります。 連携方式 TwinCATがハブとなりデバイスやアプリケーションが産業用ネットワークやADSで接続され、TwinCATを介してデータ連携することができます。 TwinCAT PLC と アプリケーション1-3 は ADS で接続 TwinCAT PLC と Device1 は EtherCAT で接続 TwinCAT PLC と Device2 は EtherNet/IP で接続 --> その他のデータ連携方式 TwinCAT PLCは専用のハードウェアモジュールを追加することで温度計などで計測した温度をアナログ値として電圧値で受信することも可能です。またネットワーク通信用のソフトウェアライセンスを購入することでソケット通信でのデータ受信なども可能です。 用途 TwinCAT PLC上にグローバル変数を定義しておくと以下のような用途で利用することができます。 センサー制御 センサーデータの受信 TwinCAT PLC変数の監視・操作 デバイスやロボット等との連携 TwinCAT PLC上のプログラム(Function Block)へのRPC TwinCAT を介したプロセス間通信 TwinCAT PLC上のグローバル変数への値の設定は TwinCAT 上のプログラムで設定します。 --> グローバル変数の定義・値の設定に関して 連載記事「TwinCATで始めるソフトウェアPLC開発」「 第2回:ST言語でのプログラミング(1/2) 」をご覧ください。 ライブラリのインストール # NuGetパッケージマネージャーで Beckhoff.TwinCAT.Ads を インストールしプロジェクトの参照に設定してください。アップデートサイクル(主にバグフィクス)が比較的早く数カ月毎にマイナーバージョンがアップしています。何度かアップデートしましたが後方互換性があるので既存のコードは問題なく動作しています。 TwinCATのデータ型とC#のデータ型の対応 # TwinCATのデータ型とC#のデータ型の対応表は以下。 INTがshortに対応, REALがfloatに対応 など幾つか注意が必要です。 TwinCATデータ型 ビット幅 C#データ型 説明 BOOL 8 bit byte 真偽値/bool 1bitとの記載もあるが内部的には1byteで扱われる ( 要注意 ) BYTE 8 bit byte 符号なし 8bit 整数 SINT 8 bit sbyte 符号あり 8bit 整数 USINT 8 bit byte 符号なし 8bit 整数 INT 16 bit short 符号あり 16bit 整数 ( 要注意 ) UINT 16 bit ushort 符号なし 16bit 整数 DINT 32 bit int 符号あり 32bit 整数 UDINT 32 bit uint 符号なし 32bit 整数 LINT 64 bit long 符号あり 64bit 整数 ULINT 64 bit ulong 符号なし 64bit 整数 REAL 32 bit float 単精度浮動小数点数 ( 要注意 ) LREAL 64 bit double 倍精度浮動小数点数 ENUM 16 bit short 符号あり 16bit 整数 ( 要注意 ) STRING 1 byte/char string 1文字1バイトのバイト列 + 終端の NULL(0)文字 ( 要注意 ) TIME 32 bit TimeSpan ミリ秒単位の符号なし整数 TwinCAT側の設定 # 以下の条件で変数を登録します PlcProject プロジェクト - GVLs に グローバル変数リスト名 GVL_Test を定義(名前は任意) 変数名 TestData データ型 DINT とする TwinCAT側の設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; END_VAR それではC#からTestData変数にアクセスしてみましょう。 AdsClientによる変数アクセス # AdsClientはTwinCATとアクセスする際の窓口になります。 数値データはすべて同じ方法でRead/Writeできます。 AdsClient生成の例 using System; using TwinCAT.Ads; namespace AdsComponent { class Program { static void Main(string[] args) { // AdsClientのインスタンス作成 AdsClient client = new AdsClient(); // TwinCATへの接続 // 第1引数: AmsNetId文字列 // 第2引数: ポート番号 (TwinCAT3 PLCの場合は851) client.Connect("192.168.1.101.1.1", 851); // TwinCATのグローバル変数を参照するためのハンドルを作成 // TwinCAT側で定義した "グローバル変数リスト名.変数名"で指定する uint handle = client.CreateVariableHandle("GVL_Test.TestData"); // 書き込み操作 int writeValue = 123; client.WriteAny(handle, writeValue); // 読み取り操作 int readValue = (int)client.ReadAny(handle, typeof(int)); Console.WriteLine($"read:{readValue}"); // ハンドルの解放 client.DeleteVariableHandle(handle); // 接続を閉じて、リソース解放 client.Close(); client.Dispose(); } } } --> 読みやすさ優先のため例外処理や定数の定義等を省いています --> AmsNetIdの指定 連載記事「TwinCATで始めるソフトウェアPLC開発」 「 第1回:環境構築編 」の「ADS通信ルート設定」で表示されているAmsNetIdを指定してください --> コネクションは繋ぎっぱなしでもOKですが使い終わったら必ずリソースを解放してください --> AdsClientはSystem.IDisposableインタフェースを実装しているためusingステートメントを使うことができます データ変更コールバック通知 # TwinCAT側の GVL_Test.TestData 変数が変化したかどうかを監視するにはReadAny()による定期的なポーリングは効率が悪く、スレッドも独自で管理する必要があります。これを解決するための手段として値が変化した際に、自動でクライアント側へコールバック通知を飛ばす仕組みがあります。 データ変更コールバック通知の例 private AdsClient _adsClient; // インスタンス生成、コネクション接続済みとする private uint _handleNotification = 0; // データ変更通知開始 public void StartValueChangeNotification() { // イベントハンドラの登録 _adsClient.AdsNotificationEx += OnAdsNotified; // データ変更通知ハンドルの登録(通知開始) _handleNotification = _adsClient.AddDeviceNotificationEx( "GVL_Test.TestData", // 50[msec]毎に変更があったときに通知する // 最大遅延時間を0[msec]とする new NotificationSettings(AdsTransMode.OnChange, 50, 0), null, typeof(int)); } // イベント受信 private void OnAdsNotified(object sender, AdsNotificationExEventArgs evn) { if (evn.Handle != _handleNotification) { return; } var data = (int)evn.Value; Console.WriteLine($"notified:{data}"); } // データ変更通知停止 public void StopValueChangeNotification() { // データ変更通知ハンドルの削除(通知停止) _adsClient.DeleteDeviceNotification(_handleNotification); _handleNotification = 0; // イベントハンドラの登録解除 _adsClient.AdsNotificationEx -= OnAdsNotified; } StartValueChangeNotification() を実施した後、TwinCAT側で GVL_Test.TestData の値が更新されるとC#側で OnAdsNotified() がコールバックされます。 なお、コールバック通知処理を終える場合は必ず StopValueChangeNotification() を実施しハンドルを解放してください。 --> `GVL_Test.TestData` の値を更新するには グローバル変数の定義・値を手動で更新するには連載記事「TwinCATで始めるソフトウェアPLC開発」 「 第2回:ST言語でのプログラミング(1/2) 」の「3.3 ログインによる動作確認」よりPLCにログインして該当変数の値を直接書き換えてください コールバック周期通知 # データ変更コールバック通知のパラメータを変えることで周期的な通知も可能です。 コールバック周期通知の例 // 定期的な通知開始 public void StartCyclicNotification() { // イベントハンドラの登録 _adsClient.AdsNotificationEx += OnAdsNotified; // 周期通知ハンドルの登録(通知開始) _handleNotification = _adsClient.AddDeviceNotificationEx( "GVL_Test.TestData", // 10[msec]毎に通知する // 最大遅延時間を1[msec]とする new NotificationSettings(AdsTransMode.Cyclic, 10, 1), null, typeof(int)); } AdsTransMode.Cyclic を指定することで周期的な通知となります。通知タイミングを10[msec]、最大遅延時間を1[msec]とした場合、1[msec]の遅延が発生する場合が稀にありますが、ほぼ正確に10[msec]毎に通知されました。 TwinCAT側はカーネルモードで動作しているため正確な周期で値の通知が可能かと思われますが、C#側は普通のWindowsアプリでWindows OSのスケジューリングの精度やネットワークドライバの受信処理などによる遅延が発生するかと思いますが不思議な現象です。いつか調査してみたいと思います。 構造体の定義 # Read/Writeするデータやコールバック通知のデータ型にはプリミティブ型だけでなく構造体も使用可能です。また構造体はネストも可能です。 まずは例としてTwinCAT側で構造体 DUT_Sample を定義します。 TwinCAT側の構造体設定 // DUT_Sample構造体定義 TYPE DUT_Sample : STRUCT IsValid : BOOL; // BOOL型 Height : DINT; // DINT型 CurrentMode : EMode; // ENUM型 Status : DUT_Status; // 構造体 END_STRUCT END_TYPE // EMode ENUM型定義 {attribute 'strict'} {attribute 'to_string'} TYPE EMode : ( Vertical := 0, Horizontal := 1 ); END_TYPE // DUT_Status構造体定義 TYPE DUT_Status : STRUCT Status1 : DINT; Status2 : DINT; END_STRUCT END_TYPE グローバル変数リスト GVL_Test に Sample を追加します。 TwinCAT側のグローバル変数設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; Sample : DUT_Sample; END_VAR グローバル変数 GVL_Test.Sample をC#側でも扱えるようにC#側でも同じ構造体を定義します。 ただし、アライメントの問題(メモリ上でのデータを配置する際の整列ルール)がありますので注意が必要です。 TwinCAT 3では、デフォルトで8byteのアライメントが採用されているため、これにあわせてC#側の構造体を定義する必要があります。 構造体には 属性 [StructLayout(LayoutKind.Sequential, Pack = 8)] を付与する 構造体以外のデータ型は 「 TwinCATのデータ型とC#のデータ型の対応 」 に合わせる 変数の定義順序はTwinCAT側と合わせる なお、変数や構造体の名前はTwinCAT側と合わせる必要はありませんが合わせておくと対応関係が明らかですのでお勧めします。 C#側の構造体の定義の例 // Sample構造体 [StructLayout(LayoutKind.Sequential, Pack = 8)] public struct Sample { public byte IsValid; // BOOL型 => byte public int Height; // DINT型 => int public EMode CurrentMode; // ENUM型 => EMode public Status Status; // 構造体 => Status } // Emode定義 public enum EMode : short // ENUM型 => short { Vertical, Horizontal, } // Status構造体 [StructLayout(LayoutKind.Sequential, Pack = 8)] public struct Sample { public int Status1; // DINT => int public int Status2; // DINT => int } int readValue = (Sample)client.ReadAny(handle, typeof(Sample)); のように キャスト や typeof() を使うことでプリミティブ型と同じAPIが使えます。 文字列(string)のRead/Write # TwinCAT 側ではSTRING型で文字列を扱うことができます。しかし、1文字1バイトのASCII文字コード(Latin-1)として扱われるためそのままでは日本語を書き込むと文字化けします。また、バイト数を指定して定義する必要があります。そのためUTF-8でエンコードするように指定して定義します。 TwinCAT側の設定 {attribute 'qualified_only'} VAR_GLOBAL TestData : DINT; Sample : DUT_Sample; // UTF-8での文字列定義 {attribute 'TcEncoding':='UTF-8'} Message : STRING(1024); END_VAR 変数定義に {attribute 'TcEncoding':='UTF-8'} を付与することで文字コードをUTF-8と解釈させる 文字サイズはバイト数で指定する 文字サイズは終端のNULL(0)文字まで含めたサイズ UTF-8の場合、1文字のバイト数は可変長(1~4バイト)となります。一般的な日本語は3バイトとなりますので余裕のあるバイトサイズを指定してください。 文字列(string)Read/Writeの例 using System.Text; private const int STRING_SIZE = 1024; private AdsClient _adsClient; // インスタンス生成、コネクション接続済みとする private uint _handle; // 'GVL_Test.Message' を指しているものとする // 文字列の書き込み public void WriteMessage(string message) { // UTF-8の文字列をバイト配列に変換 byte[] utf8Bytes = Encoding.UTF8.GetBytes(message); // バイトサイズチェック if (utf8Bytes.Length > STRING_SIZE) { throw new Exception($"バイト数オーバー"); } // バッファの初期状態はNULL(0)文字で埋められている byte[] targetBuffer = new byte[STRING_SIZE]; // 変換したバイト配列を、固定長配列の先頭からコピーする Buffer.BlockCopy(utf8Bytes, 0, targetBuffer, 0, utf8Bytes.Length); _adsClient.WriteAny(_handle, targetBuffer); } // 文字列の読み取り public string ReadMessage() { // 固定長のバイト配列を取得する(終端のNULL文字も含む) var byteArray = (byte[])_adsClient.ReadAny( _handle, typeof(byte[]), new int[] {STRING_SIZE}); // バイト配列の中から最初のNULL文字(0)を探す int nullCharIndex = Array.IndexOf(byteArray, (byte)0); // NULL文字が見つかった場合は、そこまでを文字列とする if (nullCharIndex >= 0) { // GetString(バイト配列, 開始インデックス, 長さ) return Encoding.UTF8.GetString(byteArray, 0, nullCharIndex); } // NULL文字が見つからない場合 (バッファが文字列で満たされている) は、 // 配列全体を変換する return Encoding.UTF8.GetString(byteArray); } UTF-8文字列の扱いは他のデータ型に比べて少し冗長なコードになります。 文字列は定義時のバイトサイズの配列で送受信されるため、1文字だけ送りたい場合でも残りはNULL文字で埋めてバッファサイズ分データ転送されることになります。 まとめ # ADS通信で様々なデータ型の扱い方とコールバック通知の方法を理解できたかと思います。これを応用することでRPCやプロセス間通信ができます。 PC1上のプロセスA と PC2上のプロセスB とで連携するとき、データ型をJSON文字列(STRING(1024)など)で定義すれば汎用的なデータで分散処理システムを構築できます。またコールバック通知の仕組みを上手く使えばTwinCAT上の変数をTopicとみなすPublish/Subscribe型のシステムも実現できるのではないでしょうか? BeckhoffはADSの通信プロトコル仕様をWebサイト上で公開しており、オープンソースのADS通信ライブラリもあるそうです。そのためPython, Node.js, GoなどからもADS通信ができるようです。ただし、当然これらオープンソースのライブラリはBeckhoffからのサポートが受けられない点に注意が必要です。
0. はじめに # こんにちは。豆蔵R&Dグループの丹羽です。 今回はAWSセキュリティサービスの1つである「AWS Firewall Manager」(以下、FMS) [1] のポリシー設定について紹介したいと思います。 最近、私が関わるプロジェクトで「AWS Shield Advanced」を導入する必要があり、Organizationsでのアカウント管理を行っている場合、任意のOUや任意のアカウントをShield Advancedの保護対象に一括で設定するためには「AWS Firewall Manager」のポリシー設定を利用する手法が推奨されています。 このポリシー設定における挙動がややわかりにくかったため、今回実際に検証してみた結果を共有したいと思います。 現在Organizationsを利用していて、「一元的なDDoS対策やWeb ACL適用をしたい」「AWS Shield Advancedの導入を考えている」「FMSポリシーについて事前に挙動を知っておきたい」など、導入検討や挙動理解をしたい方、AWSセキュリティ運用を本格的に開始しようとしているプロジェクトにとって参考になればと思います。 1. この記事で分かること # この記事では、FMSが提供する 2つの主要なセキュリティポリシー について、実際の検証結果をもとにその挙動を解説します。 FMS Shield Advancedポリシー : 組織全体にDDoS保護を一元適用するポリシー FMS WAFポリシー : 組織全体にWeb ACL [2] ルール [3] を一元配布・適用するポリシー どちらのポリシーについても「目的」「仕組み」「検証して分かったこと」の3つの観点で整理していきます。特に、 自動修復機能(Auto Remediation) [4] の挙動や、既存のWeb ACL(独自に定義したWeb ACLという意味)との共存パターンなど、公式ドキュメントだけでは掴みにくいリアルな動きを中心にお届けします。 2. 前提知識と対象読者 # 前提知識 # 本記事は「FMSポリシー設定における挙動の検証」がメインの内容になるため、FMSや検証時に登場するWAF、Shield、それらに関連するサービスについて簡単に整理しておきましょう。 その他に登場するサービスについては記事の各所で補足していきたいと思います。 --> AWS Firewall Manager とは AWS Firewall Manager(FMS)は、AWS Organizations を利用して、複数の AWS アカウントにまたがる各種セキュリティルールを一元的に管理・適用するためのサービスです。FMS を使うことで、セキュリティポリシーの適用漏れを防ぎ、組織全体のセキュリティレベルを均一に保つことができます。 本記事で紹介するFMSのポリシーは先にも述べた以下の2つです。 FMS Shield Advancedポリシー : 組織全体にDDoS保護を一元適用するポリシー FMS WAFポリシー : 組織全体にWeb ACLルールを一元配布・適用するポリシー 上記2つ以外にもポリシーは存在していますが、今回は触れないため説明は省略します。 補足: AWS Organizationsとは 複数のAWSアカウントを組織として一元管理するサービスです。 OU(Organizational Unit:組織単位) という階層構造でアカウントをグループ化し、ポリシーを一括適用できます。FMSポリシーはこのOU・アカウント単位でスコープを指定して配布します。 --> AWS WAF とは AWS WAF は、Webアプリケーションへの不正なリクエストをフィルタリングするためのサービスです。Web ACL(アクセスコントロールリスト)の中にルールを定義し、CloudFrontやALBなどのリソースに関連付けて使います。 たとえば「SQLインジェクション攻撃をブロックする」「特定の国からのアクセスを制限する」といった防御ルールを、リソース単位で細かく設定できるのが特徴です。 補足: 関連するAWSリソース ALB(Application Load Balancer) : HTTPSレベルでトラフィックを振り分けるロードバランサー。WAF・Shield Advancedの主要な保護対象リソースの1つ。 CloudFront : AWSのCDN(コンテンツ配信ネットワーク)サービス。グローバルエッジでWAFルールを適用できる。 --> AWS Shield とは AWS Shieldは、DDoS攻撃 [5] からAWSリソースを保護するサービスです。2つのプランがあります。 プラン 概要 Shield Standard すべてのAWSユーザーに無料提供。L3/L4層(ネットワーク層・トランスポート層)の一般的なDDoS攻撃を自動防御 Shield Advanced 有料。より高度なDDoS保護、WAF利用料の無料化、DRT(DDoS Response Team) [6] への相談権利、コスト保護などを提供 Shield Advancedを利用する場合、対象リソース(ALB、CloudFront、EIPなど)に「保護(Protection)」を作成して有効化する必要があります。 FMSを利用することで、新規リソース作成時に自動的にShield Advancedで保護し、設定漏れを防ぐことができ、Organizations内の全アカウントのShield Advanced保護を、FMSから集中管理することができます。 この記事の対象読者 # Organizationsを利用した一元的なアカウント管理をしている方 Shield Advancedの導入を検討している方 AWSのセキュリティサービスに興味があり、組織全体での統制を考えている方 FMSを使って「全アカウントに共通のセキュリティルールを適用したい」と考えている方 3. FMS Shield Advancedポリシー # 3-1. 何をするためのものか(目的) # 先ほども軽く触れましたが、Shield Advancedを使うには保護したいリソース1つ1つに対して「保護」を作成する必要があります。しかし、AWS Organizationsで管理している複数のアカウントにまたがるリソースすべてに対して手作業で保護を設定するのは正直なところ現実的ではありません。 FMS Shield Advancedポリシー はこの課題を解決するためのものです。管理者アカウント(または委任管理者アカウント) [7] から「この範囲のアカウント内に存在するALBやEIPには、全部Shield Advancedの保護をかけてね」とポリシーを定義するだけで、FMSが対象リソースを検出し、保護を一元的に適用してくれます。 3-2. 大まかな仕組み # FMS Shield Advancedポリシーの基本的な動作フローを整理すると、以下のようになります。 ここで記載されている 管理者アカウント というのは、Shield Advancedをサブスクライズしたアカウントのことを指します。(基本的にはOrganizationsの管理アカウントと同一のことが多い) flowchart TD A["管理者アカウント<br>or FMS委任アカウント"] B["AWS Firewall Manager"] C["メンバーアカウント"] D_OFF["検出・報告のみ<br>Non-Compliant として記録"] D_ON["Shield保護を自動作成"] E{"L7自動緩和<br>有効?"} F["ALBにWeb ACLを<br>自動作成・関連付け"] G["準拠(Compliant)状態"] A -->|"① ポリシー作成<br>対象リソース・適用スコープ・自動修復を定義"| B B -->|"② Config Ruleを配布<br>Shield保護の未設定リソースを監視"| C C -->|"③-a 自動修復 OFF"| D_OFF C -->|"③-b 自動修復 ON"| D_ON D_ON --> E E -->|"有効(ALBなど)"| F --> G E -->|"無効"| G 準拠(Compliant)というのは、FMSが対象リソースを検出し、保護を一元的に適用している状態のことを指します。自動修復をOFFにしている場合、FMSは検出のみ行い保護対象には追加しないため非準拠となります。 ここで、AWS Configサービスが関連してくるので簡単に捕捉しておきます。 --> AWS Configとは(FMSとの関係) AWSリソースの設定状態を継続的に記録・評価するサービスです。 AWS Config Rule (設定ルール)を定義することで、「リソースがこの基準を満たしているか?」を自動評価できます。 FMSはこの仕組みを活用し、ポリシー配布時にメンバーアカウントへConfig Ruleを自動定義。「Shield保護が未設定のリソースはないか」「FMS管理のWeb ACLが適用されているか」をConfigが継続監視し、準拠(Compliant)/非準拠(Non-Compliant)として報告します。 特に重要なのは、 L7自動緩和(Automatic application layer DDoS mitigation) [8] の設定です。この機能を有効にすると、ALBのような対象リソースに対して紐づけられているWeb ACL内に、自動緩和用のWeb ACLルール( ShieldMitigationRuleGroup... という名前のルール)がFMSによって自動で作成されます。 --> Information このWeb ACLルールの自動作成は、あくまで「Shield AdvancedのL7自動緩和機能」に必要なものです。WAFポリシーで作成されるWeb ACLとは別物であり、既存のWeb ACLに影響を与えることはありません。 実際、既存のWeb ACLルール内の最後にFMSが作ったルール( ShieldMitigationRuleGroup... という名前のルール)が追加されます。最後に追加されるので、ルール評価順序としても最後になり、既存ルールでチェックしたい内容を妨げることはないです。 3-3. 実際の検証結果 # 検証は、メンバーアカウント上にALBとEIP(EC2にアタッチ)を用意し、管理者アカウントからFMS Shield Advancedポリシーを適用する形で行いました。 --> Caution 念のため断っておきますが、以下の検証はあくまで「検証」という位置づけなので、いきなり本番運用しているアカウントやOUに対して実施することはおすすめしません。もしやらざるを得ない場合でもWebACLのバックアップ(JSONによる設定定義ファイル)をダウンロードしておくなどロールバックできる準備はしておくことをお勧めします。 【検証パターンS-1: 自動修復OFF】 項目 内容 FMS設定 自動修復: 無効(Disabled) 事前状態 ALB・EIPにShield保護なし 期待値 Non-Compliant(非準拠)として検出されるが、保護は作成されない 結果 : 期待通りの挙動 FMSがメンバーアカウントにAWS Configルールを自動配布し、そのルールによってALB・EIPのShield保護未設定が検出されました。FMSコンソール上では、アカウント自体のステータスが「Non-Compliant(非準拠)」、個別リソースのステータスも「Non-Compliant(非準拠)」となります。 管理アカウントのFMSコンソール メンバーアカウントに対するステータスが非準拠 メンバーアカウント内の対象リソース(ALB, EIP)に対するステータスが非準拠の理由 メンバーアカウントのAWS Config画面 メンバーアカウントに作られるConfigルール Configルールによる非準拠リソースの検出 例えば、対象リソースALBのConfig詳細の「リソースタイムライン」を確認すると、FMSによってConfigルールが適用されたことが確認できる --> 補足 Configルールによってリソースの設定情報が「どうだったか」「どうかわったのか」を確認するには、画像にある「ルールのコンプライアンス」や「設定変更」を開くことで、 どのように設定が変更されて保護対象 になったのか(あるいは保護対象外になったのか)、 FMSルールが非準拠・準拠になったかどうか が確認できる。 (リソースの固有情報が割とバッと表示されるのでここでは開いた場合の画像は割愛します(マスクするのがめ…orz)。ご了承くださいmm) 【検証パターンS-2: 自動修復ON】 項目 内容 FMS設定 自動修復: 有効(Enabled) 事前状態 ALB・EIPにShield保護なし 期待値 Shield保護が自動作成され、Compliant(準拠)に変わる 結果 : 期待通りの挙動。( 反映にはタイムラグ があり) 自動修復を有効にしてから数分後、EIPのステータスが先にCompliantになりました。一方、ALBのステータスがCompliantになるまでにはもう少し時間がかかり、追加で数分待つ必要がありました。 ALBについては、Shield保護の作成に加えて、 L7自動緩和用のWeb ACLルール( ShieldMitigationRuleGroup... )が自動作成 されていることも確認できました。これはShield Advancedの機能によるものであり、想定どおりの動作です。 管理アカウントのFMSコンソール 自動修復の有効設定 --> 補足 画像内にある「Replace~」ですが、このチェックをONにすると、もし対象リソース(ALB、EIPなど)にWAF Classicで作られた古いWeb ACLが関連付けられていた場合に、FMSがそれをWAFv2のWeb ACLに自動的に置き換えます。 メンバーアカウントのコンソール画面 WAF&Shieldコンソール FMSによってWeb ACLが作成されます。 FMS作成のWeb ACLはALBなどのリソースへの紐づけはされず、 代わりに既存Web ACLにルールが追加されます。 「ShieldMitigationRuleGroup_...」という名前のルールが既存Web ACLのルールの最後に追加されていることが確認できます。 これがDDoS保護のためのルールのようです。 WebACLの評価は上から順に適用されるので、最後に追加されていることで既存のWeb ACLルール評価順序に影響が出ないような仕組みになっているということですね。 Configコンソール 自動修復の有効にしたことによってリソースが保護対象となりステータスが準拠になりました。 同様にリソースタイムラインから対象リソースがShield Advancedポリシーの保護対象になったことなどが見れます。(画像は割愛します。) ポリシー削除時の挙動 検証完了後にポリシーを削除したところ、メンバーアカウント上にFMSが作成したConfigルールやWeb ACLは きちんと削除 されました。ポリシー削除直後は残っていたリソースも、しばらく待つと自動的にクリーンアップされることを確認しています。 管理アカウントのFMSコンソール 削除 メンバーアカウントのコンソール画面 WAF&Shieldコンソール Configコンソール ルールが削除されるので「利用不可」になる。 4. FMS WAFポリシー # 4-1. 何をするためのものか(目的) # AWS WAFは非常に強力なサービスですが、組織内の全アカウントで「最低限これだけは守ってほしい」という共通ルールを徹底するのは意外と大変です。各アカウントの管理者に「この設定を入れてください」とお願いして回るのは、管理する側も管理される側も負荷が高いですよね。 FMS WAFポリシー は、管理者がWeb ACLの設定内容(適用するルールグループ、デフォルトアクションなど)をポリシーとして定義し、組織全体に一括で配布・適用するためのものです。 4-2. 大まかな仕組み # FMS WAFポリシーには、Shield Advancedポリシーよりも設定項目が多く、特に「既存のWeb ACLがある場合にどう扱うか」が重要な設計ポイントになります。 置換するor統合する 結論から言うと、FMS WAFポリシーを作成しWeb ACLを適用する際には、 管理者が定義したルールをFMSが管理するWeb ACLに 置換 する メンバーアカウントの独自ルールを 挟み込む ような形で統合する の2通りの設定パターンがあります。 挟みこむというのは、 flowchart TD subgraph ACL["FMS管理 Web ACL"] direction TB FRG["🔒 First Rule Groups(最初に評価)<br>管理者が設定 · メンバーは変更不可"] CUS["✏️ メンバーアカウントの独自ルール<br>各アカウントが自由に追加・編集可能"] LRG["🔒 Last Rule Groups(最後に評価)<br>管理者が設定 · メンバーは変更不可"] DEF["Default Action<br>Allow または Block"] FRG --> CUS --> LRG --> DEF end --> 補足 First / Last Rule Groups とは FMS WAFポリシーで設定できるルールグループの配置枠です。 First Rule Groups : メンバーアカウントの独自ルールより 先 に評価される。「絶対にブロックしたい通信」を管理者が強制したい場合に使う。 Last Rule Groups : 独自ルールより 後 に評価される。「最後の砦」として組織共通の後処理ルールを設定する。 Default Action : どのルールにもマッチしなかったリクエストへの最終アクション(Allow または Block)。FMSポリシーで設定する。 この構造により、管理者が強制するベースラインを保ちながら、各アカウントが独自ルールを自由に追加できます。 これにより、組織全体の セキュリティベースライン を管理者が強制しつつ、各アカウントのアプリケーション固有のルールも柔軟に追加できる仕組みになっています。 Web ACL管理の4パターンを検証してみる FMS WAFポリシーでは、リソースに対してどのようにWeb ACLを適用するか、いくつかパターンがあると思いますが、今回は以下の4パターンを試してみようと思います。 パターン 自動修復 置換オプション Retrofit [9] 挙動 W-1: 検出のみ OFF - - ポリシー違反を検知・報告するだけ。リソースには何も変更しない W-2: 自動修復(置換OFF) ON OFF OFF Web ACL未設定のリソースにはFMS作成のWeb ACLを適用。 既に独自のWeb ACLが設定されているリソースは変更しない W-3: 自動修復(置換ON) ON ON OFF すべての対象リソースに対して、 強制的にFMS作成のWeb ACLに置き換える W-4: 自動修復(置換OFF) ON OFF ON すべての対象リソースに対して、 FMSのルールを注入する 4-3. 実際の検証結果 # 検証は、メンバーアカウント上にALBと独自のWeb ACL( test-fms-waf-log )を用意し、4つのパターンに沿って設定をおこない挙動を確認しました。 【検証パターンW-1: 自動修復OFF】 項目 内容 FMS設定 自動修復: 無効 事前状態 ALBにWeb ACL関連付けなし 期待値 Non-Compliant。Web ACLは関連付けられない 結果 : 想定通りの結果。FMSコンソール上でNon-Compliantとして検出され、ALBには何も変更が加えられない。 管理アカウントのコンソール画面 該当設定箇所 メンバーアカウントのコンソール画面 ポリシー設定後のWAF/Config画面 リソースに関連付けされていない独自のWeb ACLがあり(事前に作成しているもの)、 ConfigコンソールのFMSが作成したルールでのステータスは、自動修復がOFFであり、ALBにはどのACLも関連付けされていないため、FMS WAFポリシーによる評価が非準拠になる。 管理アカウントのコンソール画面 ポリシー適用後のFMSコンソール画面 【検証パターンW-2: 自動修復ON・置換OFF】 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: OFF 事前状態 ALBにWeb ACL関連付けなし 期待値 FMS作成のWeb ACLが自動的にALBに関連付けられ、Compliantになる 結果 : 想定通りFMSが自動で作成したWeb ACLがALBに関連付けられた。ステータスもCompliant。 管理アカウントのコンソール画面 該当設定の更新 メンバーアカウントのコンソール画面 fmsが作成したWeb ACLがALBに関連付けられている Configルールも準拠になった 管理アカウントのコンソール画面 しばらくまってから管理コンソールを確認すると 【検証パターンW-3: 置換ONの状態で独自Web ACLを割り当ててみる】 ここからが面白いところです。W-2でFMS管理のWeb ACLが適用された状態から、 手動でメンバーアカウント上で独自のWeb ACL( test-fms-waf-log )に差し替え てみました。 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: ON 事前状態 ALBにFMS作成のWeb ACLが関連付け済み → 手動で独自Web ACLに変更 期待値 FMS管理のWeb ACLが強制的にALBに関連付けられ、Compliantになる 結果 : 想定通りの結果 メンバーアカウントのコンソール画面 メンバーアカウントで独自Web ACLを割り当てた直後の画面 ↓ ↓ このようにfms管理のWeb ACLが外れ、独自Web ACLが設定されている状態となる。 管理アカウントのコンソール画面 次に、管理アカウントから置換ONに変更してみる。 メンバーアカウントのコンソール画面 メンバーアカウント上でのACLの状態において、fms管理のWeb ACLは以下のようになります。 そして、独自に作ったWeb ACLは以下のようになります。 このように置換設定ONで自動修復を有効にすると、手動で独自Web ACLをALBに関連付けた場合に、一度紐づけていたFMS管理のWeb ACLは自動的に外れますが、 その後FMS管理のWeb ACLが強制的に再度ALBに関連付け られ、Compliantになります。 もし、運用メンバーが意図せずにFMS管理のWeb ACLをALBから外してしまった場合などに、FMS管理のWeb ACLの関連付けに強制的に戻すことができるということですね。 しかし、意図していた場合に強制的に戻されるのは困るわけです。 【検証パターンW-4: Retrofit(既存Web ACLの改修)モード】 そこで存在しているのが 「Retrofit existing web ACLs」 [9:1] パターンなのかなと考えています。 少しややこしくなってきたと思うので、検証内容をざっと説明しておくと、 W-3の状態(FMS管理のWeb ACLがALBに関連付けられている状態)から、一度FMS WAFポリシーの「Replace existing associated web ACLs」オプションをOFFに変更し、改めて独自のWeb ACLをALBに関連付けた状態で、自動修復有効・Retrofitモードも有効にした状態にWAFポリシーを更新します。 flowchart TD S["W-3の状態<br>FMS管理Web ACL → ALBに関連付け済み"] A["① 管理アカウントでWAFポリシーを更新<br>置換オプション(Replace)をOFF に変更"] B["② メンバーアカウントで手動操作<br>独自Web ACLをALBに関連付け<br>(FMS管理Web ACLは自動的に外れる)"] C["③ 管理アカウントでWAFポリシーを更新<br>自動修復: ON<br>Retrofit: ON"] D["④ FMSが独自Web ACLに対して<br>First / Last Rule Groupsを注入"] E["準拠(Compliant)状態<br>独自ルール + FMSルールが共存"] S --> A --> B --> C --> D --> E 項目 内容 FMS設定 自動修復: 有効 、既存Web ACLの置換: OFF 、Retrofit: ON 事前状態 ALBにFMS作成のWeb ACLが関連付け済み → 手動で独自Web ACLに変更 期待値 FMS管理のWeb ACLルールが、独自Web ACLの内容を保ちながらマージされ、Compliantになる 結果 : 想定通りの結果 Retrofitモードでは、FMSは新たなWeb ACLを作成するのではなく、 既存のカスタムWeb ACLの中に、FMS WAFポリシー定義時に設定したルールグループが注入 されました。 具体的には、独自のWeb ACLに定義されていたルールはそのまま維持され、FMS WAFポリシーで設定したFirst Rule Groups(優先ルールグループ)とLast Rule Groups(最終ルールグループ)(今回はFirst Rule Goupsのみ)を サンドイッチする形で追加 されます。 AWS Configのステータスも準拠(Compliant)となり、独自のWeb ACLの独自ルールとFMSのルールが共存している状態が確認できました。 以下検証時のコンソールの様子です。(※ 事前状態 から始めています。) メンバーアカウント上のコンソール メンバーアカウント上でのWeb ACLの様子 管理アカウント上のコンソール 管理アカウントでのWAFポリシー設定を「置換OFFの状態で、RetrofitをONにする」 RetrofitがONだと、自動で置換モードが選択できなくなる(以下の補足参照) メンバーアカウント上のコンソール FMS管理のWeb ACLはリソースへの紐づけがありませんが、 独自に作成したWeb ACLにはFMSのルールが注入されています。 --> 補足 置換ON設定とRetrofitモードは 排他的な設定 です。「置換ON設定で運用中にRetrofitに切り替える」場合は、一度置換OFFにしてからRetrofitを有効化する必要があります。 5. まとめ # 今回の検証を通じて、FMSの2つのセキュリティポリシーの挙動を実際に確かめることができました。検証結果をまとめると、以下のようになります。 Shield Advancedポリシーのポイント # 確認事項 結果 自動修復OFFでの検出 ✅ Non-Compliantとして正しく検出される 自動修復ONでの保護作成 ✅ Shield保護が自動作成される L7自動緩和によるWeb ACL ✅ ALBにはDDoS緩和用のWeb ACLが自動作成される ポリシー削除時のクリーンアップ ✅ FMS作成リソース(Config Rule、Web ACL)は自動削除される WAFポリシーのポイント # 確認事項 結果 自動修復OFFでの検出 ✅ Non-Compliantとして正しく検出される 自動修復ON・置換OFFでのWeb ACL適用 ✅ 未設定リソースにのみFMS Web ACLが適用される 置換OFF時の既存Web ACL保護 ✅ 既存Web ACLは書き戻されない(安全に運用可能) 置換ON時の強制置換 ✅ 既存Web ACLからFMS Web ACLに紐づけが切り替わる(カスタムWeb ACL自体は削除されない) Retrofitモード ✅ 既存Web ACLにFMSルールを注入。独自ルールは維持される 運用する際のポイント # 検証結果を踏まえ、FMSセキュリティポリシーを運用する際に意識しておくべきポイントを挙げます。 まずは自動修復OFFで始める : いきなり自動修復を有効にするのではなく、まずは「検出のみ」モードで対象を把握してから有効化するのが安全。 置換オプションは慎重に : 既にWAFを運用中のリソースがある場合、置換ONにすると既存の防御設定が外れるリスクがある。Retrofitモードの活用も検討。 反映までのタイムラグを考慮する : 地味に大事なのが、fmsポリシーの設定反映には数分〜十数分のラグがあるため、「設定したのに変わらない!」と慌てずに、しばらく待ってから確認したうえで次の設定をしたりするのがいいです。 ポリシー削除時のクリーンアップを信頼する : FMS作成のリソースはポリシー削除時にきちんと削除されます。ただし、こちらも完全削除までに若干の時間がかかります。 FMSは「設定して放っておけば組織全体を守ってくれる」便利なサービスですが、その裏側では多くのリソースが自動的に作成・管理されています。今回はFMSの2種類のポリシーに絞って紹介をしましたが、その挙動や背後で自動的に行われる設定などをざっくりとでも把握しておくことで、確証を持ったセキュリティ対策を実施できると思います。 --> ポリシー適用後のステップ 今回はボリューム感が大きくなってきたため扱えませんでしたが、FMSポリシーを設定したあとにさらに整備しておくと良い内容として、以下が挙げられます。 攻撃通知の整備 : Shield Advancedポリシーで検知した攻撃を管理者へ通知(SNSやCloudWatchアラームの活用) リクエストログの収集・分析 : 保護対象リソースへのリクエストをS3バケットに蓄積し、Athenaなどで分析できる体制を整える 補足: 関連サービスの概要 SNS(Simple Notification Service) : AWSのメッセージ通知サービス。Shield Advancedの攻撃検知イベントと組み合わせてアラート通知に利用する。 CloudWatch : AWSのモニタリングサービス。メトリクスやアラームを設定し、DDoS攻撃検知時に自動通知・自動対応のトリガーとして使える。 S3(Simple Storage Service) : WAFログの保管先。CloudFrontやALBのアクセスログ・WAFログを集約する。 Athena : S3上のデータをSQLでクエリできる分析サービス。S3に収集したWAFログをそのままSQL分析できるため、攻撃パターンの分析に有効。 私自身も今後のプロジェクトの中でこれらは実施していく予定ですので、また皆さんに共有すべき事項が出てきた場合にはまとめてみようと思います。 紹介した検証内容がSheld AdvancedとFMSの理解、そしてセキュリティ対策の一助となれば幸いです。 注釈 # AWS Firewall Manager(FMS) : AWS Organizationsと連携し、組織全体のWAF・Shield・セキュリティグループなどのセキュリティポリシーを一元管理するサービス。参考: AWS Firewall Manager Developer Guide ↩︎ Web ACL(Web Access Control List) : AWS WAFの中核リソース。リクエストの許可・拒否を判定するためのルールをまとめた「箱」。ALBやCloudFrontなどのリソースに関連付けて使用する。参考: AWS WAF Web ACLs ↩︎ ルール(Rule) : Web ACL内に定義する個々の検査条件。「IPアドレスの一致」「リクエストボディの検査」など、トラフィックを評価するための条件と、マッチした場合のアクション(Allow/Block/Count)をセットにしたもの。 ↩︎ 自動修復(Auto Remediation) : FMSポリシーの機能の1つ。ポリシーに違反しているリソースを検出した際に、自動的にリソースの設定(主にWeb ACLのルールに関するもの)を修正すること。また、対象リソース(ELB、EIPなど)を自動的に保護対象に加えること。参考: FMS Policy actions ↩︎ DDoS攻撃(Distributed Denial of Service attack) : 大量のコンピューターから一斉にリクエストを送り、サービスを利用不能にする攻撃手法。 ↩︎ DRT(DDoS Response Team) : AWSのDDoS対策専門チーム。Shield Advanced契約者が利用可能で、大規模攻撃時の対応支援を受けられる。参考: DRT support ↩︎ 委任管理者アカウント : AWS Organizationsの管理アカウントから、特定のサービス(FMSなど)の管理権限を委任されたメンバーアカウント。管理アカウントの権限をむやみに使わず、専用の管理アカウントに役割を分離するのがベストプラクティス。 FMSの場合、 Shield Advancedをサブスクライズしたアカウントを管理アカウントとみなし 、この管理アカウントから、FMSを管理するアカウント(メンバーアカウント)を権限を絞って複数追加することができる。 ↩︎ L7自動緩和(Automatic application layer DDoS mitigation) : Shield Advancedの機能の1つ。アプリケーション層(レイヤー7)のDDoS攻撃パターンを自動検知し、WAF Web ACLにルールを自動追加して緩和する。参考: Shield Advanced application layer DDoS mitigation ↩︎ Retrofit existing web ACLs : FMS WAFポリシーのWeb ACL管理モードの1つ。新しいWeb ACLを作成する代わりに、リソースに既に関連付けられている既存のWeb ACLをFMSの管理下に置き、FMSポリシーのルール(First/Last Rule Groups)を注入するアプローチ。参考: FMS WAF policy ↩︎ ↩︎
はじめに # アジャイルグループの石田です。 第1回:導入 、 第2回:透明性 に続く、3部作の最後となります。 第1回の導入ではスクラムガイド拡張パックとAIによる経験的プロセス制御強化の可能性について、第2回ではJira×GAS×AIによる可視化ツールの作成を通してAIによる透明性の強化について紹介しました。 今回は、スクラムの三本柱の残り2つ、検査と適応について、スクラムマスターとしてAIをどう活用するかについてです。 現在、ミーティングの録音や文字起こし、およびそれをまとめるのにAIを使うのは一般的になりつつあります。業務で生成AIを活用するにあたって、大体の会社で真っ先に使われるところでしょう。 しかし、それを単なる「議事録」として保存するだけでは非常にもったいないです。 AIを「客観的な評価者(コーチ)」として活用し、チームのプロセス改善に切り込む事例を紹介します。 なぜAIに「レトロスペクティブの評価」を任せるのか # 長い間同じチームで開発を行っていると、毎回同じような議論になってしまうマンネリ化を感じたことはないでしょうか。 毎回少しずつ改善点を見つけていく重要性は分かっていても、なんとなく良かったねという話で終わってしまい改善アクションの具体性が欠けたり、ステークホルダーに対する不満や愚痴に終始してネガティブな空気になってしまうこともありがちです。 レトロスペクティブ自体を改善するプラクティスとして「ふりかえりのふりかえり」を実施しているチームも多いかと思いますが、それもやはりマンネリ化の波にはなかなか逆らえません。 そこでAIの活用方法として、感情や人間関係に左右されない客観的な評価、すなわち検査をしてもらうという使い方が考えられます。 実践:AIによるレトロスペクティブの定量評価 # 実践方法は比較的シンプルです。ZoomやGoogle Meetなどで取得したレトロスペクティブの文字起こしデータを、そのまま生成AIに入力します。 その入力データをもとに、事前に定義した評価基準に従って、AIにチームの議論を採点してもらいます。 評価項目は、AIと壁打ちしながら優れたレトロスペクティブのための10項目を定め、各10点の100点満点でスコアリングします。実際に作成した評価項目の例は以下の通りです。 評価項目一覧: 話題の適切性 改善アクションの具体性 会議の進行とファシリテーションの質 活発な議論 参加度と発言分布 議論の論点の着地 議論の深さ 進化の持続性 ポジティブな視点 参加メンバー間の中立性 また、スコアだけでなく次回の改善点も具体的に3つ提案してもらいます。 そうすることで、特にスコアの低い評価項目に対して、ファシリテーター以外のメンバーの発言を促す、議論が長引かないようタイムボックスを意識する、手法を変えてみるといった、具体的なアクションを提示してくれます。 こうした評価とアクションを出力させるためのプロンプト自体も、生成AIに相談しながら構築します。AIと対話しながらプロンプトを作り上げる過程は、シンギュラリティの始まりを感じられて個人的にすごく好きです。 Geminiを使用する場合は、このプロンプトをシステムプロンプト(カスタム指示)としてGemを作成しておくと便利です。 フィードバックのループを回す # これまでにレトロスペクティブの定量評価を行う準備は整いましたが、評価して終わりでは意味がありません。この結果を次回のレトロスペクティブのアクションに繋げていきます。 具体的には、次回のレトロスペクティブの冒頭で、前回算出されたAIスコアと改善提案をチームに共有します。 これにより、前回の反省(例えば、アクションが曖昧だったなど)を意識した状態で新たなレトロスペクティブをスタートできます。 こうしたフィードバックは本来スクラムマスター自身がチームを観察したうえで行うべきものですが、生成AIというパートナーの存在により、さらに客観的で説得力のあるフィードバックが可能になります。 チームに起きた適応の実例 # AIによる客観的なスコアやアクションの提示によって、実際にチームに起きた適応(変化)の実例を2つ紹介します。 1つ目のケースは、ポジティブさの欠如による低スコアの例です。AIから不満や課題の指摘に終始しており、ポジティブな視点が不足しているという指摘を受けました。その適応策としてサンクスカードを導入し、意図的に感謝や良い点を伝え合う時間を設けることで、ポジティブな発言を促すようにしました。 2つ目のケースは、アクションの曖昧さによる低スコアの例です。AIからは改善案が出ているものの具体的な実行計画に落ちていないと指摘されました。これに対する適応策として、アクション決定時に「いつ、誰が、何をするか」という5W1Hの確認を徹底するルールを設けました。 応用:デイリースクラムへの展開 # 今回はレトロスペクティブでの検査や適応の例を紹介しましたが、もちろん他のイベントにも応用可能です。その一例として、一番効果が出やすいのはデイリースクラムかもしれません。 デイリースクラムには15分で終わるという厳格なルールがあります。もしそれより長引くようであれば、準備の不足や議論への深入り、あるいは不要な話題を話している可能性があります。 また、デイリースクラムは単なる作業報告ではなく、スプリントゴールを達成するための検査の場であるべきです。 このような毎日のイベントだからこそ、AIによる客観的な検査と適応の繰り返しが、大きな働き方の改善に繋がる可能性は高いでしょう。 まとめ:スクラムマスターのパートナーとしてのAI # 本連載では、第2回で透明性(データの可視化)を確保し、第3回で検査と適応(プロセスの評価と改善)を実践するという、スクラムの三本柱を生成AIで強化する方法について紹介しました。 スクラムマスターは、チームがスクラムを正しく実施できるようにチームを観察し、この三本柱の維持に努める必要があります。 AIは、人間が気づきにくい癖や傾向を客観的に指摘する検査が得意です。一方で、その指摘を受け止め、どうチームを導くか、どう文化を作るかといった適応の部分は、スクラムマスターとチームが考えて行う必要があります。 AIを良きパートナーとして活用することで、自分たちのスクラムをより強化していくことが、今後のアジャイル開発において重要になっていくのではないでしょうか。
はじめに # ビジネスソリューション事業部の塚野です。 本記事は「Vitestと統合可能!StorybookでNext.js v16のコンポーネントテストを行う」の後編です。 前編では Storybook の導入や基本的な使い方についてご紹介しました。本記事では Next.js 固有の設定やモジュールモックなどについてまとめていきます。 next/router、next/navigationのモック # Next.js でページ遷移や URL の参照・更新に関わるパッケージとして next/router 、 next/navigation パッケージがあります。 next/router は主に Page Router で、 next/navigation は App Router で使用されます。Storybook(@storybook/nextjs-vite)では next/router パッケージはデフォルトでスタブされ、ルーターオブジェクトはActions タブにイベントを出力するモックに置き換えられます。 next/navigation も自動的にスタブされるため、 Story 上でも usePathname、 useSearchParams、 useRouter などを呼び出せます。 ただし、App Routerを使用する場合 Storybook 側に「App Router を使う」ことを明示する必要があります。Story 単位で設定できますが、プロジェクト全体が App Router 前提であれば .storybook/preview.ts に書いて全 Story に適用するのが手軽です。 .storybook/preview.ts import type { Preview } from '@storybook/nextjs-vite'; const preview: Preview = { ... parameters: { ... nextjs: { appDirectory: true, // ← App Router を利用する場合 true とする }, }, }; export default preview; ここで、 next/navigation パッケージを使用したコンポーネントとその Story を作成してみます。 コンポーネントのコードは読み飛ばしてかまいません。このコンポーネントでは input に入力した値を searchParams として現在の URL を書き換えます。 コンポーネント内では next/navigation パッケージの useRouter、 useSearchParams を利用しています。 NavigationDemo.tsx 'use client'; import Link from 'next/link'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useState } from 'react'; export function NavigationDemo() { const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); const [query, setQuery] = useState(searchParams.get('query') ?? ''); const [currentQuery, setCurrentQuery] = useState(searchParams.get('query') ?? ''); const apply = () => { const next = new URLSearchParams(searchParams.toString()); query ? next.set('query', query) : next.delete('query'); const queryString = next.toString(); router.replace(queryString ? `?${queryString}` : '?'); setCurrentQuery(query); }; return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} className="p-2 border border-black" /> <button onClick={apply} className="p-2 border border-black">Apply</button> <Link href={`${pathname}/link?query=${query}`} className="ml-2 underline"> go to Link </Link> <div>current path: {pathname}</div> <div>current query: {currentQuery || '(empty)'}</div> </div> ); }; このコンポーネントの Story は以下のように作成しました。 NavigationDemo.stories.tsx import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { getRouter } from '@storybook/nextjs-vite/navigation.mock'; //useRouter()のMock import { expect, userEvent, within } from 'storybook/test'; import { NavigationDemo } from './NavigationDemo'; const meta = { component: NavigationDemo, parameters: { nextjs: { appDirectory: true, navigation: { pathname: '/demo/navigation', //Story上でURL Pathの初期値を設定可能 query: { query: 'initial' }, //Story上でクエリパラメータの初期値を設定可能 }, }, }, } satisfies Meta<typeof NavigationDemo>; export default meta; type Story = StoryObj<typeof meta>; export const ReplaceIsCalled: Story = { async play({ canvasElement }) { const c = within(canvasElement); getRouter().replace.mockClear(); await userEvent.clear(await c.findByRole('textbox')); await userEvent.type(await c.findByRole('textbox'), 'hello'); await expect(c.getByRole('link', { name: 'go to Link' })).toHaveAttribute( 'href', '/demo/navigation/link?query=hello', ); await userEvent.click(await c.findByRole('button', { name: 'Apply' })); //useRouter().replace呼び出しのアサートに相当 await expect(getRouter().replace).toHaveBeenCalledWith('?query=hello'); }, }; ここで、Story ごとに pathname や query などを変えたい場合は、meta オブジェクトの parameters.nextjs.navigation を上書きします。これにより、URL に依存するコンポーネント(アクティブ状態、検索条件の表示など)を Story 単位で再現できます。 parameters.nextjs.navigation は初期状態の再現に便利ですが、「クリックで router.push() が呼ばれた」など、呼び出しの検証をしたいケースでは不足します。 そこで使うのが @storybook/nextjs-vite/navigation.mock です。これは next/navigation のモック実装に加えて、 useRouter() 相当のルーターオブジェクトを getRouter() で取り出せるため、push、 replace、 back などの呼び出しを テストとして assert できます。 このコンポーネントの Story 上で Apply ボタンを押下すると、Actions タブに入力したクエリパラメータが出力され、ルーターオブジェクトがモックできていることが分かります。 @storybook/nextjs-vite/navigation.mock 以外のビルトインモックに関してはこちらを参照してください。( Built-in mocked modules | Storybook docs ) --> Information ページ遷移に関わるパッケージとして他に next/link パッケージがあります。このパッケージに含まれる Link コンポーネントは pre-fetch 機能を備えた <a> タグを拡張したコンポーネントとしてよく使われます。この Link は内部で next/navigation 、 next/router のルーターオブジェクトを使用しているため、これらパッケージのモックと同時に Link コンポーネントもモックされるはずです。 しかし、Next.js(15以降〜)+ App Router 設定の Storybook では、Link コンポーネントをクリックしたときに Storybook の iframe が存在しないページへ遷移しようとするケースが報告されています。( storybookjs/storybook | GitHub ) 実際、NavigationDemo 内の「go to Link」ボタンクリックでページ遷移が発生してしまいます(Storybook v10.2.7 執筆時点)。 修正されるまで、Link コンポーネントは後述するモジュールモックを用いて Storybook 上では <a> タグにモックするなどの対策が必要でしょう。 React Server Componentの利用とServer functionsのモック # App Router では、 use client ディレクティブを付与して明示的に Client Component としない限り、デフォルトとして React Server Components(RSC)としてコンポーネントは扱われます。 特に、async function としている RSC については そのままでは Storybook で使用できません 。 Storybook v10.2.7(@storybook/nextjs-vite)現在、RSC 対応は Experimental 扱いのため、RSC を Storybook 上でレンダリングする場合は明示的に機能を有効化する設定が必要です。 具体的には .storybook/main.ts で features.experimentalRSC: true を指定します。 main.ts import type { StorybookConfig } from '@storybook/nextjs-vite'; const config: StorybookConfig = { framework: '@storybook/nextjs-vite', features: { experimentalRSC: true, //RSCを利用するにはexperimentalRSC: trueとする }, }; export default config; この設定で RSC を Storybook で動作させることはできます。ただしコンポーネント内で "server actions" ディレクティブを付けた、 DB 接続やファイルアクセスなどのサーバー関数を呼び出す場合これも Storybook 上では実行ができません。 Next.js でのベストプラクティスとして、 RSC 側ではデータフェッチ関数を直接記述するのではなく、呼び出すサーバー関数を別モジュールに切り出すことが知られています。 Storybook ではコンポーネント内でimportするモジュールをモックできます( Mocking modules | Storybook docs )。そこでサーバー関数を利用する場合、Storybook ではモジュールごとモックをしてしまい UI 確認用の戻り値に差し替える、という形で運用します。 また、Storybook では、コンポーネント単体の表示確認や振る舞いの検証が目的であるため、実際のサーバー依存処理は実行しないようにモック化した方がよいです。 Storybook v10.2 では、Vite/webpack 環境での推奨手段として sb.mock() による モジュールモックが用意されています。 モジュールモックの例として、以下のようなサーバー関数 getGreeting.ts を用意しました。 actions/getGreeting.ts "server actions" export async function getGreeting(name: string) { // 実環境ではDBやAPIなどにアクセスする想定 return `Hello, ${name}!`; } この関数をモックする場合、 .storybook/preview.ts にモックを登録します。各 Story 内ではモックの登録はできません。 これにより、Story 実行前に対象モジュールが置き換えられ、Story 単位で戻り値だけを制御できます。 .storybook/preview.ts import type { Preview } from '@storybook/nextjs-vite'; import { sb } from 'storybook/test'; // モック登録は preview.ts で行う sb.mock(import('../src/server/getGreeting.ts')); const preview: Preview = { parameters: { nextjs: { appDirectory: true }, }, }; export default preview; モック登録の注意点として以下があります。 Typescript を使用する場合(モックする関数が .ts の場合)、 sb.mock() 内で import() を用いて記述すること @ のような alias の使用は不可。必ず preview.ts からの相対パスで記述すること 拡張子まで含めてパスは記述すること この設定で getGreeting.ts は Storybook 上でモック化ができます。 ただしこの場合、Storybook 上では getGreeting.ts の機能は完全に失われます。もし、機能はそのままにスパイ関数化をしたい場合は sb.mock() の第2引数に { spy: true } を含めます。 sb.mock(import('../src/server/getGreeting.ts'), { spy: true }); それではこの関数を利用するコンポーネントと、その Story ファイルを作成し、Storybook 上でこのモック化した関数をどのように使用するのか見ていきます。 components/GreetingPanel.tsx import { getGreeting } from '@/actions/getGreeting'; type Props = { name: string }; export async function GreetingPanel({ name }: Props) { const message = await getGreeting(name); return ( <div> <h3>Greeting</h3> <p>{message}</p> </div> ); } 簡単な、 getGreeting でメッセージを取得しそれを表示するだけのコンポーネントです。 components/GreetingPanel.stories.tsx import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { expect, mocked } from 'storybook/test'; import { within } from 'storybook/test'; import { GreetingPanel } from './GreetingPanel'; import { getGreeting } from '../server/getGreeting'; const meta = { component: GreetingPanel, args: { name: 'Taro' }, } satisfies Meta<typeof GreetingPanel>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { // beforeEach()でモック化した関数の戻り値などの設定を行う async beforeEach() { mocked(getGreeting).mockResolvedValue('Hello from mocked function!'); }, async play({ canvasElement }) { const canvas = within(canvasElement); await expect(getGreeting).toHaveBeenCalledWith('Taro'); await expect(canvas.getByText('Hello from mocked function!')).toBeTruthy(); }, }; GreetingPanel の Story を作成しました。 Story 内でモック化した関数を利用する場合、 beforeEach() 内でモック化関数の戻り値などの設定を行います。 beforeEach() は各 Story で実行してもよいですし、 meta 内 beforeEach 要素に記述することですべての Story に適用が可能です。 mocked() の引数に preview.ts で登録したモックしたい関数を渡し、その戻り値に対して、モックした関数が非同期関数である場合は mockResolvedValue() で戻り値を設定します。 モックした関数が同期関数である場合は mockReturnValue(value) 、モック関数に対して任意の実装を行いたい場合は mockImplementation(fn) を利用してください。 まとめ # ここまで Vitest アドオンを利用したコンポーネントテストやモジュールモックなどを利用した Next.js コンポーネントのテストをご紹介しました。 Storybook ではさらにアドオンを使うことで Visual Regression Test(VRT)やアクセシビリティのテストなども実行可能です。 学習コストは若干感じるものの、CI パイプラインへの統合が可能なことや、デプロイすることでデザイナーとイメージアップに利用できるため、使いこなせればフロントエンド開発において欠かせないツールになると感じました。 Storybook は Next.js だけでなく Vue.js や Angular など幅広いフレームワークに対応しています。ご興味持たれた方は是非導入検討してみてはいかがでしょうか。
はじめに # ビジネスソリューション事業部の塚野です。 皆さんはフロントエンド開発の際にコンポーネントのテストをどのように行っているでしょうか? 自分は最近になり、Storybook というオープンソースツールに入門しました。 https://storybook.js.org この Storybook は UI カタログを作成するサービスです。 コンポーネントをアプリ本体から切り離して単体で描画でき、Props や状態のパターンを「ストーリー」として整理ができます。 また、見た目の確認だけではなく、クリックなどのユーザーイベントを伴うコンポーネントの「ふるまい」のテストも Storybook 上で行えます。 このふるまいのテストはテストランナーに Vitest を使うことができ、 他の Vitest で作成した単体テストと一緒に一括実行が可能 です。 Storybook は様々なフロントエンドフレームワークに対応しています。その中で今回は人気のあるフレームワークとして Next.js でのコンポーネントテストの導入についてご紹介します。Storybook の2026年2月18日執筆時点での最新バージョンは v10.2.7 ですが、この構成を整理した情報はまだ多くないため、コンポーネントテストの作成だけでなくセットアップ手順も含めて具体例とともにまとめます。 書いているうちに長くなってしまったため、2回に分けました。 本記事では「Vitestと統合可能!StorybookでNext.js v16のコンポーネントテストを行う」の前編として、Storybook の導入と基本的な使い方、インタラクションテストの作成と Vitest テストランナーでの実行について記述します。 後編では Next.js 特有のビルトインパッケージのモックや、App Router での設定、モジュールモックなどについてご紹介します。 Storybookの導入と基本的な使い方 # まずは Storybook の導入です。以下のコマンドを実行します。 npm create storybook@latest 2026年2月18日執筆時点での Storybook の最新版は v10.2.7 です。Storybook では v10 以降から Next v16 に対応しています。(が、一部未対応の機能もあります。これについては後編で触れます。)Next の必須バージョンは v14 以上です。 上記のコマンド実行後、"New to Storybook?" と聞かれます。"Yes" を選んだ場合、簡単なチュートリアルとサンプルのストーリーファイルが作成されます。必要に応じて選択してください。 その後、"What configuration should we install?" と聞かれますがここは "Recommended" を選択し、オススメ設定で実行してもらいます。設定ファイルにアドオンの追加や Vitest の設定ファイルの作成などしてくれるのでこちらを選択しましょう。 ストーリー作成の前に設定ファイルをプロジェクトに合わせて変更します。 Storybook の設定ファイルはプロジェクトルートの .storybook 配下に作成されます。( Configure Storybook | Storybook docs ) Recommended 設定の場合 .storybook 配下は以下のようになっています。 / └── .storybook ├── main.ts #Storybookのメイン設定ファイル ├── preview.ts #グローバルなスタイル等の設定ファイル └── vitest.setup.ts #Storybookでのvitest設定ファイル .storybook/main.ts を以下のように変更します。 Recommended 設定の場合自動的に入っていますが、Minimum 設定の場合 "addons" に @storybook/addon-vitest と @storybook/addon-docs が追加されていることを確認してください。 main.ts import type { StorybookConfig } from '@storybook/nextjs-vite'; const config: StorybookConfig = { "stories": [ "../components/ui/**/*.stories.@(js|jsx|mjs|ts|tsx)" // ← プロジェクトに合わせて編集する ], "addons": [ "@chromatic-com/storybook", "@storybook/addon-vitest", // ← vitestとしての実行に必要 "@storybook/addon-a11y", "@storybook/addon-docs", // ← Document機能の利用に必要 "@storybook/addon-onboarding" // ← チュートリアル用のアドオン。必要ないなら削除してもOK ], "framework": "@storybook/nextjs-vite", "staticDirs": [ "../public" ] }; export default config; Storybook Config オブジェクトの "stories" 要素にストーリーファイルのパスを記述します。 ストーリーファイルは Button.stories.tsx のように .stories を付けて作成します。本記事でのデモプロジェクトでは components/ui 配下にコンポーネントファイルと共に作成します。プロジェクトに合わせて記述を変更してください。 Next.js プロジェクトでは tailwind CSS を利用している場合が多いかと思います。Storybookで tailwind CSS を有効化する場合は、 .storybook/preview.ts で globals.css を import します。 .storybook/preview.ts import type { Preview } from '@storybook/nextjs-vite' import '../app/globals.css'; // ← globals.cssをimport const preview: Preview = { parameters: { ... }, tags: ["autodocs"], // ← Document生成をすべてのStoryで有効化する }; export default preview; Storybook は各コンポーネントを Canvas と呼ばれる UI 上に表示させますが、内部では "preview" と呼ばれる iframe 内で動作させています。この preview に関する設定が preview.ts であり、ストーリーの表示に関するグローバルな設定が可能です。 後述する Document という機能が大変便利なので、ここですべてのストーリーで Document を生成する設定を追加します。Preview オブジェクトの tags 要素に ["autodocs"] を指定します。Document は各ストーリーファイル内で個別に有効化もできます。 これで準備ができました。 試しに以下のようなボタンコンポーネントを components/ui 配下に作成し、そのストーリーファイルを作って Storybook を実行してみます。 Props は size と variant を受け取り、variant でプリセットとして設定した primary と outline に見た目を切り替えられます。 ここでは tailwind-variants というライブラリを使い variant と size のプリセットを variants として定義しています。 コンポーネントのコードは軽く読み飛ばしていただいて大丈夫です。 components/ui/Button.tsx import React from "react"; import { tv, type VariantProps } from "tailwind-variants"; const buttonStyles = tv({ base: "inline-flex items-center justify-center rounded-md font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-60 disabled:cursor-not-allowed", variants: { size: { small: "px-3 py-1.5 text-sm", medium: "px-4 py-2 text-base", large: "px-5 py-3 text-lg", }, variant: { primary: "bg-blue-600 text-white border border-blue-600 hover:bg-blue-700 focus-visible:outline-blue-500", outline: "bg-white text-slate-900 border border-slate-300 hover:bg-slate-50 focus-visible:outline-slate-400", }, }, defaultVariants: { size: "medium", variant: "primary", }, }); type ButtonVariants = VariantProps<typeof buttonStyles>; export type ButtonProps = Omit< React.ButtonHTMLAttributes<HTMLButtonElement>, "className" > & ButtonVariants; export const Button = ({ size, variant, type = "button", children, ...props }: ButtonProps) => { return ( <button type={type} className={buttonStyles({ size, variant })} {...props} > {children} </button> ); }; export default Button; このボタンコンポーネントの Story ファイルはこのように作成しました。 Button.stories.tsx import type { Meta, StoryObj } from "@storybook/nextjs-vite"; import { fn } from "storybook/test"; import { Button } from "./Button"; const meta = { title: "UI/Button", component: Button, parameters: { layout: "centered" }, argTypes: { size: { control: { type: "inline-radio" }, options: ["small", "medium", "large"], description: "ボタンのサイズ", }, variant: { control: { type: "inline-radio" }, options: ["primary", "outline"], description: "ボタンのバリアント", }, }, args: { children: "送信", size: "medium", variant: "primary", onClick: fn(), }, } satisfies Meta<typeof Button>; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = {}; export const Outline: Story = { args: { variant: "outline", size: "large", children: "キャンセル" }, }; export const Disabled: Story = { args: { disabled: true, children: "無効" }, }; コンポーネントの指定やどのような種類の Props を渡せるのかといったストーリーのメタ情報を meta オブジェクトに記載し、これを default export します。 この meta オブジェクトから Story の型を生成し、Story オブジェクトを作成、export します。 Story オブジェクトがそのままストーリーとして Storybook 上で表示されます。オブジェクト名がストーリの表示名、 args でそのストーリーでコンポーネントに渡す Props を定義できます。 npm run storybook で Storybook を実行してみましょう。 Button コンポーネントが Canvas 内に表示されました。下の「Controls」タブでは children や Props の操作ができ、その場でコンポーネントの見た目やふるまいの確認ができます。 Controls に表示される Props は args で渡したものになります。 今回 meta オブジェクトでも args を記述しており、これがデフォルトで渡される args になります。 Props を args で記述するほかに、 argTypes で Props の詳細も記述できます。 args に記載されていない Props でも argTypes へ記載した場合、 Controls タブに表示されるようになります。 また、Controls タブでの表示方法も設定でき、例えば control: { type: "inline-radio" } と記述すればユニオン型などの場合横並びのラジオボタンで値の切り替えが可能となります。(デフォルトはセレクトボックス) Document の自動生成を有効化した場合、"Docs" というタブがサイドバーに表示されます。 ここでは作成したストーリーのメタ情報やストーリーの一覧表示などが可能で、コンポーネントの概要が一目でわかるようになっています。 ButtonコンポーネントのStory DocsでPropsなどの情報も含めたDocumentが参照できる Documentでは作成した全ストーリーを一覧で表示可能 この Document にはマークダウン形式で文章も記述可能です。 以下のように特定の場所に JSDoc 形式でコメントを記述した場合 Document 内に表示されます。JSDoc 内ではマークダウン記法がサポートされています。 Button.stories.tsx ... /** * Button コンポーネントの Storybook ストーリー * * | variant | スタイル | * |---------|----------| * | primary | メインアクション用の強調されたスタイル | * | outline | 補助的なアクション用のアウトラインスタイル | */ const meta = { title: "UI/Button", component: Button, ... } ... ここまでの基本的な使い方でコンポーネントの「見た目」についての確認はできました。 Storybook ではさらに、クリック時の挙動などユーザーインタラクションを含む「ふるまい」のテストが行えます。 コンポーネントテストの導入 # 各 Story ではふるまいに関するテスト(インタラクションテスト)を "play function" として記述ができます。( Interaction tests | Storybook docs ) 先ほど作った Button コンポーネントに play function を追加して「クリックすると onClick が1度だけ呼ばれること」を確認します。 Button.stories.tsx import type { Meta, StoryObj } from "@storybook/nextjs-vite"; import { expect, fn, userEvent, within } from "storybook/test"; // ← インタラクションテストに関するパッケージから import import { Button } from "./Button"; const meta = { ... , args: { children: "送信", size: "medium", variant: "primary", onClick: fn(), // ← onClick にはスパイ関数 fn() を渡す }, } satisfies Meta<typeof Button>; export default meta; type Story = StoryObj<typeof meta>; ... /** play functions の例: ボタンをクリックすると onClick が1回呼ばれることを確認 */ export const ClickTest: Story = { args: { children: "Click Me !" }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole("button")); await expect(args.onClick).toHaveBeenCalledTimes(1); }, }; インタラクションテスト用の Story "ClickTest" を追加しました。 インタラクションテストは Story の "play" 要素に非同期関数として記述します。 ユーザーイベントの模倣やアサーションには storybook/test パッケージのオブジェクト、関数を利用します。 play 内では順に、 Canvasを取得 Canvas内 "button" 要素を取得 [1] 、クリック args の onClick が1回呼ばれるかをアサート をしています。userEvent と expect は必ず await の内側で呼ぶ必要があります。 args の onClick では meta オブジェクトで定義されるように fn() を渡しています。 これは Vitest のスパイ関数ですが、 storyboo/test パッケージから利用可能です。実行されると Story の Actions タブにイベントが出力されます。( Via storybook/test fn spies ) それでは、ClickTest ストーリーを表示してテスト結果を確認してみましょう。 ストーリーを表示すると自動でテストが実行されます。結果は Interactions タブから確認ができます。 無事、テストを Pass していることが確認できました。 すべてのインタラクションテストは Storybook の UI 上から一括実行が可能です。 サイドバー下部の "Run tests" をクリックで一括実行が行われます。"Interaction" にチェックがついていることを確認してください。 サイドバー内 Run tests からplay functions の一括実行が可能 Storybook の起動には高速起動が人気の Vite が利用可能です [2] 。 とはいえテストのたびに起動して UI 上で結果を確認するのも手間です。また、コンポーネントのテストも CI パイプライン上で他の単体テストと一括で実行したくなります。 そこで、Storybook ではインタラクションテストを Vitest のテストとして CLI 上で実行可能とするアドオン "Vitest addon" が提供されています。( Vitest addon | Storybook docs ) このアドオンにより .stories ファイルをヘッドレスブラウザ上で実行可能なテストに変換し、既存の Vitest と一緒に vitest コマンドで実行可能とします。 Storybook セットアップ時に "Recommended" 設定を選択した場合、Vitest に関する設定ファイル( vitest.config.ts 、 .storybook/vitest.setup.ts )が自動的に作成されます。 vitest.config.ts import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; import { playwright } from '@vitest/browser-playwright'; const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ test: { projects: [ { extends: true, plugins: [ // ↓ Storybookの設定ファイルを取得、main.tsに記載したパスの.storiesファイルをテスト実行対象とする storybookTest({ configDir: path.join(dirname, '.storybook') }), ], test: { name: 'storybook', browser: { enabled: true, headless: true, provider: playwright({}), instances: [{ browser: 'chromium' }], }, setupFiles: ['.storybook/vitest.setup.ts'], }, }, ], }, }); .storybook/vitest.setup.ts import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import { setProjectAnnotations } from '@storybook/nextjs-vite'; import * as projectAnnotations from './preview'; // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); vitest.config.ts では .stories を対象とするテストプロジェクト「storybook」が追加されています。 .test 、 .spec を対象とする Vitest は別のプロジェクトとして作成します。こうすることで Storybook のテストのみを対象に Vitest を実行でき、一括実行の際にはタグを分けることで Storybook のテストと関数のテストをCLI 上で区別して表示ができます。 最後に package.json へスクリプトを追加しましょう。 package.json { "scripts": { "test": "vitest", "test-storybook": "vitest --project=storybook" } } "npm run test-storybook" で Stroybook のテストのみ実行可能です。 ここは既存のテストと一括実行を考えて npm run test を実行します。 $ npm run test > storybook-demo@0.1.0 test > vitest DEV v4.0.18 /home/tsukano/storybook-demo/ 3:02:47 PM [vite] (client) Re-optimizing dependencies because lockfile has changed ✓ storybook (chromium) components/ui/Button.stories.tsx (4 tests) 501ms ✓ Default 357ms ✓ Outline 57ms ✓ Disabled 28ms ✓ Click Test 58ms Test Files 1 passed (1) Tests 4 passed (4) Start at 15:02:46 Duration 3.84s (transform 0ms, setup 1.14s, import 49ms, tests 501ms, environment 0ms) 無事 Vitest から .stories が呼ばれテストに Pass することが確認できました。 テストの実行には Playwright を使用しています。そのため Storybook のテストは関数の UT と比べ若干実行に時間がかかります。 実際の CI パイプラインへの統合についてはこちらの公式ドキュメントを参考にしてください。( Testing in CI | Storybook docs ) おわりに # 本記事では Storybook の導入と基本的な使い方、Vitest の実行についてご紹介しました。 また、基本的にローカル実行のみについて取り上げています。デプロイについては公式ドキュメントを参照してください。( Publish Storybook | Storybook docs ) 次回は Next.js 固有の設定や、ルーターオブジェクトのモック、モジュールのモックなどについてご紹介します。 ちなみに、ボタン要素の取得は getByRole() で行っています。Storybook 公式ドキュメントでは要素の取得はなるべく実際の人が目で見て行う操作に近い方法で行うべきだとしています。内部の "id" などで要素を取得するのは最終手段です。( Querying the canvas ) ↩︎ Next.js の場合、 main.ts の "framework" 要素で Vite と webpack で利用するビルドツールを選択できます。 "@storybook/nextjs-vite" を渡した場合 Vite でビルドしますが、特段の理由がない限り Vite を選択していいと思います。また、本記事の肝である Vitest も Vite を選択した場合でしか利用できません。 ↩︎
こんな人におすすめ # 3Dスキャン、特に動画像を用いた3次元物体・空間の再構成技術に興味がある お気に入りのコレクションや景色をデジタルで保存したい お金をかけずにリアルな3Dモデルを作成したい はじめに # 弊社はモデリング技術に力を入れている会社です。 システム設計においては主にUMLを有効活用してシステムをモデル化し、全体を客観的に俯瞰することを得意としています。ここでは、弊社でよく使われている「システム全体を俯瞰する」目的とは異なり、「実世界をデジタル空間にそっくり再現する」ことを目的としたモデル化(3次元再構成技術)について解説します。 3次元再構成はどこで使われているか # 例としては以下のようなものが挙げられます。 現実世界に忠実なデジタル空間シミュレーション 例: 自動運転 VRデバイスを用いた没入体験型コンテンツ 例: VRchat with MetaQuest3 建築物や遺跡などを歴史的資料として保存(デジタルアーカイブ) 例: 首里城復元 使用するアプリケーション # COLMAP LichtFeldStudio(LFS) SuperSplat 動作環境・スペック # OS : Windows 11 Home 25H2 CPU : Intel Core i7-11700K RAM : 64GB (DDR4-3200, より少ないRAMでも動作可能) GPU : NVIDIA GeForce RTX 4060Ti(VRAM 16GB版を使用, LichtFeldStudioがVRAM 8GB以上推奨) CUDA Toolkit : 12.1 (LichtFeldStudioが12.8以上推奨だが、このバージョンでも動作することを確認) 3D Gaussian Splatting (3DGS) とは # 2023年に提案された3次元再構成技術 [1] で、大量の3次元ガウス分布で3Dモデルを構成します。 従来の3次元表現方法よりも透明物体や光沢(鏡面反射)のある物体の表現能力が高く、かつ描画が軽量です。この技術における3次元ガウス分布の概要について以下の図に示しています。一言で簡単に説明すると 「視点(どこから見るか)によって色が変わる半透明の楕円体」 です。 楕円体といえば、ラグビーボールやアーモンドチョコみたいな形を想像される方も多いでしょう。スケールの制約が特に無ければ、縦横の比率によっては針のようにも見えます。一般的には半透明であるため、靄(もや)のイメージが近いかもしれません。このように楕円体に「不透明度」を設けることで、ガラスなどの半透明物体や光の分布をリアルに再現できます。 また、視点によって色が変わることはまさに3DGSのキーとなる点で、今まで表現が難しかった光沢の再現をも可能としています。この表現技術には、球面調和関数(Spherical Harmonics, SH)という特殊関数が用いられています。 こうした性質を持った楕円体を空間に大量に配置することで、物体・空間を表現します。 どのように配置するかは、表現したい物体・空間を撮影した動画像をもとに決定されます。 ワークフロー # 撮影 :対象(物体・空間)の写真を撮影する 点群作成 :撮影した写真から、点で表現されたおおまかな3次元形状(点群)を計算する 3DGS作成 :作成した点群をもとに3DGSを計算する(点群を「骨」とすると「肉付け」のイメージ) 編集 :作成した3DGSを編集して仕上げる 1. 撮影 # 以下のような花束を対象物とします。 撮り方のコツは対象物の全周を上下のアングルで隈なく撮影することです。 最終的な3DGSの解像度を上げたければ近距離や光学ズームで撮影した画像を含めるのもよいです。 今回の撮影枚数は全部で204枚となりました。 撮影条件は以下としました。 カメラ:iPhone16 Pro 焦点距離:24mm(固定) 解像度:24MP(2400万画素) 露出:0.0(デフォルト設定) フラッシュ:なし(室内照明のみ) --> 豆知識 焦点距離や解像度が異なる画像を混ぜてもOKです。 2. 点群作成 # Structure from Motion(以下 SfM)という手法を用いて、撮影した画像から元の3次元物体・空間を再構成していきます。これは各画像の特徴点を抽出し、画像間でマッチングをすることで3次元空間内のどの位置に何があるかを推定する技術です。この処理のアウトプットとして、RGB情報を持つ3次元点群が出力されます。SfMを利用できるアプリケーションは様々ありますが、今回は簡単に実行できるOSSの COLMAP を使用します。またCOLMAPの詳細設定に詳しくは触れず、基本的にデフォルト値を用いるものとします。 COLMAP の最新版をダウンロードし、解凍(執筆時の最新版は3.13.0) 解凍したフォルダ内の"COLMAP.bat"をクリックするとCOLMAPのGUI画面が立ち上がる 左上メニューの"File" -> "New project"を選択し、プロジェクトを新規作成する データベースファイルのパスと点群の元となる画像が格納されたディレクトリのパスを設定し、保存する(①~③の順で実施) 左上メニューの"Processing" -> "Feature extraction"から以下の項目(①,②)を実施後、"Extract"(③)で各画像の特徴点を抽出する(今回はデフォルト設定) 処理完了("Extracting..."のダイアログが出なくなる)まで待機する 左上メニューの"Processing" -> "Feature matching"から設定を実施後、"Run"(②)で画像間の特徴点マッチングを実行する(今回はデフォルト設定) 処理完了("Matching..."のダイアログが出なくなる)まで待機する 左上メニューの"Reconstruction" -> "Start reconstruction"を選択し、特徴点マッチング結果からRGB情報を持つ3次元点群を生成する 左上メニューの"Extras" -> "Undistortion"を選択し、カメラレンズによる歪みを除去した画像を生成する "Select folder"から出力を保存するフォルダを作成・指定しておくこと ここでは入力画像と同じ階層に"dense"という名前のフォルダを作成する 処理完了("Undistorting..."のダイアログが出なくなる)まで待機する 作成・指定したフォルダ内に出力画像等が生成されていれば完了 3. 3DGS作成 # いよいよメイン工程です。COLMAPで作成した3次元点群や歪み補正した画像を用いて3DGSを作成していきます。本工程ではOSSの LichtFeldStudio を使用します。LichtFeldStudioについてもCOLMAPと同様、設定可能なパラメータは数多いですが、今回は詳細設定に詳しく触れず、基本的にデフォルト値を用いるものとします。 LichtFeldStudio の最新版をダウンロードし、解凍(執筆時の最新版は0.41) 解凍したフォルダ内の"bin -> LichtFeld-Studio.exe"をクリックするとGUI画面が立ち上がる 中央のプルダウンから言語を日本語などに変更可能 アプリケーションウィンドウ内の任意の場所をクリックすると、以下のように画面が切り替わる COLMAPの出力(今回は"dense")をフォルダごとドラッグ&ドロップすると、以下の画面が表示されるため、Outputの場所を確認し、"Load"ボタンを押す COLMAPで生成した3次元点群と画像の位置・向きを表現した視錐台(Frustum)が表示される ウインドウ右の"Training"タブをクリックし設定パラメータを確認後、"Start Training"を押す 以下に主要な設定パラメータを示しています。 Iterations:繰り返し計算の回数 Max Gaussians:3次元ガウス分布の最大個数 SH Degree:球面調和関数の次数(小さいほどパラメータ数が減る) いずれも数値が大きいほど高品質のものができやすい反面計算量が多くなるため、その他の設定パラメータも含め、試行錯誤が必要な場合があります。 以下はトレーニング中の様子。 COLMAPの点群を初期値として、点(=3次元ガウス分布の中心)の数を増やしたり、移動させたりしている(10倍速、点群表示モード) 各点を中心とした3次元ガウス分布を生成し、パラメータを調整している(10倍速) "Training Complete"のダイアログが表示されるまで待機する "File" -> "Export..."を選択し、作成した3DGSをファイルに保存できれば完了 4. 編集 # 作成した3DGSをキレイに仕上げていくフェーズです。 特に手を加えなくても3DGSの品質が十分と判断した場合は省略してもOKです。 ただ一般的には背景の解像度が低かったり、対象物の周囲などに意図していないモヤのようなもの(フローター)が浮かんでいることが多いため、それらを処理すると3DGSの見栄えがさらに良くなります。また、レンダリング速度の向上やファイルサイズの軽量化にもつながります。このような3DGSの編集に SuperSplat を使用します。 SuperSplat にアクセスする SuperSplatを表示したブラウザ画面に、作成した3DGSファイルをドラッグ&ドロップしてインポートする インポートした3DGSを編集する 原点位置と座標系の向きを変更する インポート時にはワールド座標系の原点が意図しない位置・向きになっていることが多く、編集の際に不便となることがあります。そこでまず並進移動と回転のツールを用いて対象物とワールド座標系の位置・向きを合わせます。 対象物の背景を領域選択して削除する 不要なガウス分布を個別に選択して削除する 編集した3DGSを保存する おわりに # 今回は3次元物体・空間をデジタルでリアルに再構成する技術である3DGSを、無料かつ高解像度で作成する手順に焦点を当てました。今後は技術的な深堀や3DGSの課題、最新研究を解説していきたいと思います。 おまけ # 本編におけるアプリケーションは今のところすべて無料ですが、環境構築の手間とGPUが必須のため、お手軽かと言われると微妙なところなのが正直なご感想かと思います。そこで他の選択肢も用意しました。 有料でもいいからもっと簡単に作りたいなら... Postshot スマホだけでお手軽にササッと作りたいなら... Scaniverse 手軽さ 表現できる解像度 パラメータ自由度 GPU 備考 COLMAP+LFS △ 〇~◎ ◎ 必須 無料、細かなチューニングができる Postshot 〇 〇~◎ 〇 必須 有料、高品質な3DGSが簡単に作成可能 Scaniverse ◎ △~〇 △ 不要 無料、スマホのみで作成可能 ただし今回ご紹介したLichtFeldStudioは、本記事を執筆した2026年1月現在開発が盛んに行われており、手軽さや機能の向上が今後見込まれます。 3D Gaussian Splatting for Real-Time Radiance Field Rendering ↩︎
はじめに # 豆蔵では太陽光発電パネルの清掃ロボットシステムの開発に取り組んでいます。 本システムは、太陽光発電パネルを清掃するロボットと、それを搬送するドローンで構成されています。本記事では、ドローン側の開発に用いる Payload SDK を使って、送信機にカスタムウィジェットを表示する方法を紹介します。 Payload SDK の概要は以下の記事でも紹介しています。あわせて参照してください。 https://developer.mamezou-tech.com/robotics/solar-panel-clean-robot/dji-drone-psdk-introduction/ カスタムウィジェットとは # ペイロードデバイスを機体に搭載して使う場合、ユーザーがペイロードに対して操作指示を出したり、状態を確認したりしたい場面は多いでしょう。 DJI のドローンシステムでは、UI として DJI 製の送信機(DJI Pilot 2 が動作)や Mobile SDK で開発したアプリケーションが使われます。 カスタムウィジェットは、これらの UI に独自のウィジェットを組み込むための仕組みです。 送信機を使う場合のシステム構成のイメージは以下のとおりです。 サードパーティ製のペイロードデバイスの SBC 内のアプリケーションが Payload SDK を介してウィジェットの定義を機体へ提供します。 送信機内で動作する DJI Pilot 2 は機体から自動でウィジェットの定義を取得し、UI へウィジェットを表示します。 本記事では、 DJI のチュートリアル をベースに、カスタムウィジェットでできることを紹介していきます。 カスタムウィジェットのサンプルコードによるデモ # Payload SDK のリポジトリ には、SDK の各機能ごとにサンプルコードが含まれています。 カスタムウィジェットのサンプルコードは次のパスにあります。 Payload-SDK/samples/sample_c/module_sample/widget/ 以下はサンプルコードを実行したときのデモ動画です。 DJI Pilot 2 のカメラビュー左側のメインメニューに「PSDK」が追加されています。 右上の設定ボタンを押すと、右側の設定メニューにも「PSDK」が表示されます。 サンプルではウィジェット操作時のコールバックがモック実装のため、ボタン押下時の実際の動作はありませんが、UI がどのように拡張されるかはイメージしやすいと思います。 カスタムウィジェットのファイル構成 # サンプルディレクトリ( Payload-SDK/samples/sample_c/module_sample/widget/ )のファイル構成は以下のとおりです。 ├── test_widget.c ├── test_widget.h └── widget_file ├── cn_big_screen │ ├── icon_button1.png │ ├── icon_button2.png │ ├── icon_list_item1.png │ ├── icon_list_item2.png │ ├── icon_scale.png │ ├── icon_switch_select.png │ ├── icon_switch_unselect.png │ └── widget_config.json └── en_big_screen ├── icon_button1.png ├── icon_button2.png ├── icon_list_item1.png ├── icon_list_item2.png ├── icon_scale.png ├── icon_switch_select.png ├── icon_switch_unselect.png └── widget_config.json widget_config.json がカスタムウィジェットの定義ファイルで、PNG ファイルがアイコンとして使われます。 UI の言語ごとにディレクトリが分かれており、中国語向けが cn_big_screen 、英語向けが en_big_screen です。送信機の言語設定に応じて、参照されるディレクトリが切り替わります。 すべての言語向けに定義を作る必要はなく、デフォルトのディレクトリを指定できます。サンプルでは en_big_screen をデフォルトとしており、前述のデモでは送信機が日本語設定だったため en_big_screen が参照されていました。 test_widget.c には、SDK から呼ばれるコールバック用のハンドラが実装されています。ハンドラ内で、ボタン押下時の処理や UI に表示する値を返す処理を記述します。 カスタムウィジェットの初期化処理の流れ # サンプルアプリケーションを起動すると、各機能用のコンソールメニューが表示されます。カスタムウィジェットは、起動後に自動で機体へアップロードされ、DJI Pilot 2 に表示されます。 0.016 core [Info] dji_core.c:113 Payload SDK Version : V3.15.0-beta.0-build.2318 Dec 10 2025 17:27:05 1.075 adapter [Info] dji_access_adapter.c:351 Identify mount position type is Extension Port Type 1.075 adapter [Info] dji_access_adapter.c:371 Identify aircraft series is Matrice 4 Series 1.578 adapter [Info] dji_access_adapter.c:493 Identity uart0 baudrate is 921600 bps 1.582 core [Info] dji_identity_verify.c:627 Updating dji sdk policy file... ...(omit) 12.455 core [Info] dji_core.c:328 Start dji sdk application 12.455 user [Info] application.cpp:372 Application start. | Available commands: | | [0] Fc subscribe sample - subscribe quaternion and gps data | | [1] Flight controller sample - you can control flying by PSDK | | [2] Hms info manager sample - get health manger system info by language | | [a] Gimbal manager sample - you can control gimbal by PSDK | | [c] Camera stream view sample - display the camera video stream | | [d] Stereo vision view sample - display the stereo image | | [e] Run camera manager sample - you can test camera's functions interactively | | [f] Start rtk positioning sample - you can receive rtk rtcm data when rtk signal is ok | | [g] Request Lidar data sample - Request Lidar data and store the point cloud data as pcd files | | [h] Request Radar data sample - Request radar data | | [l] Run widget states manager sample, control widget states on other payload | 以下は、SDK の初期化処理(抜粋)です。 Payload-SDK/samples/sample_c++/platform/linux/raspberry_pi/application/application.cpp void Application::DjiUser_ApplicationStart() { ...(omit) returnCode = DjiCore_SetAlias("PSDK_APPALIAS"); // ペイロード名(UI表示用) if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Set alias error."); } ...(omit) returnCode = DjiTest_WidgetStartService(); // ウィジェットサービス開始 if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("widget sample init error"); } DjiCore_SetAlias で指定した PSDK_APPALIAS は、ペイロードデバイス名として UI に表示されます。 DjiTest_WidgetStartService は、前述の test_widget.c で定義されている関数です。 以下は DjiTest_WidgetStartService の抜粋です。 widget_file のディレクトリパスを SDK に設定し、各ウィジェットの操作時コールバックと表示値を返すコールバックのハンドラを登録しています。 s_widgetHandlerList のインデックス 0〜8 は、 widget_config.json で定義した各ウィジェットの widget_index と対応します。 Payload-SDK/samples/sample_c/module_sample/widget/test_widget.c static const T_DjiWidgetHandlerListItem s_widgetHandlerList[] = { {0, DJI_WIDGET_TYPE_BUTTON, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {1, DJI_WIDGET_TYPE_LIST, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {2, DJI_WIDGET_TYPE_SWITCH, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {3, DJI_WIDGET_TYPE_SCALE, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {4, DJI_WIDGET_TYPE_BUTTON, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {5, DJI_WIDGET_TYPE_SCALE, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {6, DJI_WIDGET_TYPE_INT_INPUT_BOX, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {7, DJI_WIDGET_TYPE_SWITCH, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, {8, DJI_WIDGET_TYPE_LIST, DjiTestWidget_SetWidgetValue, DjiTestWidget_GetWidgetValue, NULL}, }; ...(omit) T_DjiReturnCode DjiTest_WidgetStartService(void) { T_DjiReturnCode djiStat; T_DjiOsalHandler *osalHandler = DjiPlatform_GetOsalHandler(); //Step 1 : Init DJI Widget djiStat = DjiWidget_Init(); if (djiStat != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("Dji test widget init error, stat = 0x%08llX", djiStat); return djiStat; } ...(omit) //set default ui config path djiStat = DjiWidget_RegDefaultUiConfigByDirPath(tempPath); if (djiStat != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("Add default widget ui config error, stat = 0x%08llX", djiStat); return djiStat; } //set ui config for English language djiStat = DjiWidget_RegUiConfigByDirPath(DJI_MOBILE_APP_LANGUAGE_ENGLISH, DJI_MOBILE_APP_SCREEN_TYPE_BIG_SCREEN, tempPath); if (djiStat != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("Add widget ui config error, stat = 0x%08llX", djiStat); return djiStat; } //set ui config for Chinese language djiStat = DjiWidget_RegUiConfigByDirPath(DJI_MOBILE_APP_LANGUAGE_CHINESE, DJI_MOBILE_APP_SCREEN_TYPE_BIG_SCREEN, tempPath); if (djiStat != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("Add widget ui config error, stat = 0x%08llX", djiStat); return djiStat; } //Step 3 : Set widget handler list djiStat = DjiWidget_RegHandlerList(s_widgetHandlerList, s_widgetHandlerListCount); if (djiStat != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { USER_LOG_ERROR("Set widget handler list error, stat = 0x%08llX", djiStat); return djiStat; } 言語設定の識別子( DJI_MOBILE_APP_LANGUAGE_ENGLISH など)は dji_typedef.h に定義されています。 /** * @brief Mobile APP system language. */ typedef enum { DJI_MOBILE_APP_LANGUAGE_UNKNOWN = 255, /*!< The system language of the mobile app is unknown */ DJI_MOBILE_APP_LANGUAGE_ENGLISH = 0, /*!< The system language of the mobile app is English */ DJI_MOBILE_APP_LANGUAGE_CHINESE = 1, /*!< The system language of the mobile app is Chinese */ DJI_MOBILE_APP_LANGUAGE_JAPANESE = 2, /*!< The system language of the mobile app is Japanese */ DJI_MOBILE_APP_LANGUAGE_FRENCH = 3, /*!< The system language of the mobile app is French */ } E_DjiMobileAppLanguage; サンプルでは日本語( DJI_MOBILE_APP_LANGUAGE_JAPANESE )用の UI 設定を登録していません。そのため、送信機が日本語のときは DjiWidget_RegDefaultUiConfigByDirPath で登録したデフォルト( en_big_screen )が参照されます。これが前節で述べた「デフォルトのディレクトリ」の挙動です。 次に、ハンドラの実装を見てみましょう。 static T_DjiReturnCode DjiTestWidget_SetWidgetValue(E_DjiWidgetType widgetType, uint32_t index, int32_t value, void *userData) { s_widgetValueList[index] = value; return DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS; } static T_DjiReturnCode DjiTestWidget_GetWidgetValue(E_DjiWidgetType widgetType, uint32_t index, int32_t *value, void *userData) { *value = s_widgetValueList[index]; return DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS; } s_widgetValueList は、ウィジェットのインデックスを添字とする配列です。UI でウィジェットを操作すると DjiTestWidget_SetWidgetValue が呼ばれ、操作に対応する値が value として渡されます。 DjiTestWidget_GetWidgetValue は周期的に呼ばれ、ウィジェットの状態を value で返します。 サンプルでは SetWidgetValue で受け取った値を保持し、 GetWidgetValue でそのまま返しているだけです。そのため、スイッチの ON/OFF 操作で表示が即座に切り替わります。 たとえば、スイッチの ON 操作で外部機器を制御する場合、機器の状態が変わるまでは表示を OFF にしておきたいときがあります。そのときは GetWidgetValue で実際の機器状態に応じた値を返すとよいでしょう。 DjiCore へのウィジェットファイル・ハンドラの登録、およびウィジェット操作時・状態取得(周期呼び出し)時のコールバックの流れは次のとおりです。アプリケーション側の開発対象は、エントリポイントとウィジェットハンドラです。 sequenceDiagram participant App as エントリポイント participant Core as DjiCore participant Handler as ウィジェットハンドラ Note over App,Handler: 初期化時 App->>Core: DjiWidget_Init() App->>Core: DjiWidget_RegUiConfigByDirPath()<br/>(widget ファイル登録) App->>Core: DjiWidget_RegHandlerList()<br/>(ハンドラ登録) Note over Core: 登録完了 Note over Core,Handler: 実行時(ウィジェット操作時) Core->>Handler: SetWidgetValue コールバック<br/>(例: DjiTestWidget_SetWidgetValue) Handler-->>Core: 結果 Note over Core,Handler: 実行時(状態取得・周期的) Core->>Handler: GetWidgetValue コールバック<br/>(例: DjiTestWidget_GetWidgetValue) Handler-->>Core: 状態値(value) widget_config.json の解説 # 本節では、サンプルアプリの widget_config.json を例に、定義ファイルの構成を説明します。詳細は Payload SDK チュートリアル(Custom Widget) を参照してください。 トップレベルの構成 version … 設定フォーマットのバージョン(major / minor)。 ar_config … チュートリアルおよび API 仕様に記載がないため、本記事では説明を省略する。 main_interface … メインメニューに表示するウィジェットの定義。 config_interface … 設定メニューに表示するウィジェットの定義。 main_interface メインメニュー用の設定です。 floating_window (フローティング画面の表示有無)と speaker (TTS/音声)のほか、 widget_list でウィジェットを並べます。サンプルでは widget_index 0〜3 の 4 つ(Button, List, Switch, Scale)を定義しています。 メニュー上部に表示されるペイロードデバイス名は、前述の DjiCore_SetAlias で設定した文字列です。 config_interface 設定メニュー用の設定です。 text_input_box (テキスト入力の有無やプレースホルダー)と widget_list でウィジェットを定義します。サンプルでは widget_index 4〜8 の 5 つ(Button/Scale/Integer Input Box/Switch/List)を定義しています。 widget_list の各要素 各ウィジェットは少なくとも次のプロパティを持ちます。 プロパティ 説明 widget_index ウィジェットのインデックス。ハンドラ登録時の s_widgetHandlerList のインデックスと対応し、コールバックでどのウィジェットかを識別するために使います。 widget_type ウィジェット種別。 button ・ list ・ switch ・ scale ・ int_input_box の 5 種類があります。 widget_name UI に表示する名前。 このほか、種別に応じて次のプロパティを指定できます。 ウィジェット種別 追加プロパティ 説明 button icon_file_set icon_file_name_selected と icon_file_name_unselected に PNG ファイル名を指定。GetWidgetValue が返す value が 1 のとき選択時、0 のとき非選択時のアイコンが表示される。同一ディレクトリを参照。 list list_item 配列で各項目の item_name を指定。各項目に icon_file_set を指定可能。 switch icon_file_set button と同様。 scale icon_file_set 、 customize_rc_buttons_config アイコンは button と同様。 button_value_step_length で送信機ボタン操作時のステップ幅を指定。 int_input_box int_input_box_hint 単位などのヒント文字列(例: "unit:s" )を指定。 アイコン仕様 # DJI Pilot 用のカスタムウィジェットでは、アイコンに次のデザイン仕様が推奨されています。 サイズ・フレーム 項目 推奨値 アイコン標準サイズ 96px ボトムフレーム(サイズ) 80px ボトムフレーム(背景色) #000000(黒) ボトムフレーム(不透明度) 0.6(アルファ。0=完全透明、1=完全不透明。約 60% の不透明度で半透明) ボトムフレーム(ブラー) 4(枠のぼかし強さ。エッジが柔らかく表示される) 円形要素のストローク(線の太さ) 4px 程度 ボトムフレームは、仕様で示されているアイコン表示用の枠です。 表示状態と色 アイコンは表示状態に応じて次の色で表現します。 icon_file_name_selected / icon_file_name_unselected および GetWidgetValue コールバックが返す value に対応します。 状態 色(HEX) 説明 Normal(非選択) #4E4E4E 非選択時(value が 0 のとき) Active(選択) #1FA3F6 選択時(value が 1 のとき) Disable #BCBCBC 無効時 --> 補足 上記の表は UI アイコン仕様全般の記載と考えられます。カスタムウィジェットでは widget_config.json で指定できるのは 2 種類のみです。 icon_file_name_selected と icon_file_name_unselected です。Disable 用を SDK に渡す仕組みはありません。 Normal と Active に従い、2 種類のアイコンを用意すれば十分です。 形式・レイアウト アイコン形式は PNG を使用する。 表示領域サイズの例は 48×48、56×56、40×56、56×40 など。実コンテンツを中央に収め、余白を適切にすることが推奨されている。 Sketch 用テンプレート( psdk_widget.sketch )が DJI から提供されています。アイコン作成時は、これをベースにすることを推奨します。 まとめ # 本記事では、送信機の UI をペイロード用に拡張するカスタムウィジェットの仕組みと、定義ファイル( widget_config.json )およびハンドラ登録による実装の流れを紹介しました。 Payload SDK を用いたカスタムペイロードの開発事例はまだ多くなく、チュートリアルだけでは情報が限られるため、「どのような UI が作れるか」が分かりにくい状況にあります。本記事が、ペイロードデバイスを開発しようとしている方の参考になれば幸いです。
前回の記事では、Action Definitionと Action Usageを作成しました。 /blogs/2026/02/05/sysmlv2-tool-syson-action/ 本記事ではそれらを用いて ActionFlowを作成します。 SysMLv2には標準で Action間の接続を表示するための ActionFlowViewが用意されています。 ActionFlowを作成するにはこの ActionFlowViewを使うのが順当でしょう。 しかし、SysONのドキュメントにある Action Flow Viewのページには「開発中(under development)」とあります。 この連載で使用してきた v2025.8.0はもちろん、執筆時点の最新版である mainでも同様でした。 そこで今回は、要素の Graphical Compartmentに ActionFlowを作成します。 Graphical Compartmentは、Partや Actionの枠内にグラフィカルなビューを表示する区画のことです。 本記事では、主に作成の流れをご紹介します。 要素の追加方法といった操作方法については、本連載のこれまでの記事を参照してください。 Action Flowを作成する(その1) # " Introduction to the SysML v2 Language Textual Notation " スライド30の図を作成してみましょう。 General Viewを開き、Action Definitionを3つ作成します。 作成した Action Definitionの名前をそれぞれ、"Focus", "Shoot", "TakePicture"に変更します。 "TakePicture"に入力と出力の Itemを1つずつ追加します。 入力 Itemの名前を"scene : Scene"に、出力 Itemの名前を"picture : Picture"に変更します。 "TakePicture"のManage Visibilityコンテキストメニューを表示し、"action flow"のチェックをONにします。 これにより、"TakePicture"に Action Flow Viewを表示する Graphical Compartmentが表示されます。 "TakePicture"にある action flowの区画を右クリックし、コンテキストメニューから Action Usageを2つ作成します。 作成した Action Usageの名前をそれぞれ"focus : Focus"と"shoot : Shoot"に変更します。 Action Usageの"focus"に入力 Itemと出力 Itemを1つずつ追加します。 入力 Itemの名前を"scene"、出力 Itemの名前を"image"に変更します。 同じように、Action Usageの"shoot"に入力と出力の Itemを1つずつ追加します。 入力 Itemと出力 Itemの名前をそれぞれ"image"と"picture"に変更します。 "TakePicture"の入力 Itemである"scene"を選択します。 その外側に表示される">"をドラッグし"focus"の入力 Itemでドロップすると、接続の種別を選択するメニューが表示されます。 メニューで"New Binding Connector As Usage (bind)"を選択してください。 2つの Item間を結ぶ線(コネクタ)が追加されます。 また、このコネクタの近傍に"="が表示されます。 これが Binding Connectionです。 Action Usage"shoot"の出力 Itemと"TakePicture"の出力 Itemである"picture"も同様にコネクタでつなぎましょう。 "focus"の出力 Itemと"shoot"の入力 Itemをつなぎます。 この場合もドラッグ&ドロップで接続の種別を選択するメニューを出しますが、今度はメニューから"New Flow (flow)"を選択します。 片側に矢印の付いた線が追加されます。 これが flow connectionです。 不要な要素を非表示にすれば作図終了です。 Action Flowを作成する(その2) # 今回は、もう1つ作図してみます。 " Intro to the SysML v2 Language-Graphical Notation.pdf " スライド58の図です。 分岐やマージなど、いくつかの Control Nodeが使われています。 General Viewを開き、Action Usageを追加します。 追加した Action Usageの名前を"transportPassenger"に変更します。 "transportPassenger"の Manage Visibilityで"action flow"にチェックを付けます。 表示された action flowの区画に Action Usageを追加していきます。 題材にあわせて11個の Action Usageを追加し、それぞれの名前を変更します。 action flowの区画を右クリックし、表示されたコンテキストメニューの"Behavior"から必要な Control Nodeを追加します。 Decision Nodeや Marge Nodeのサイズを変更したり、Fork Nodeや Join Nodeを縦長に変更できないようです。 Control Nodeや Action Usageを選択した際、外側に表示される">"をドラッグ&ドロップして、Control Nodeや Action Usageをフローで接続します。 接続する際は、コンテキストメニューから”New Transition”を選択します。 action flowの要素を配置しなおして、フローを記述するところまでは出来ました。 あとはガードを付ければ作図終了なのですが、この手順が見つかりませんでした。 テキスト記法で Action Flowを作成する # テキスト記法を用いれば、ガード条件も追加できます。 SysMLv2仕様書 p.92の表に記載されたテキスト記法を参考に以下を作成しました。 ガードはBooleanでなければならないため、attributeとして追加しました。 terminateは表示されないため、doneに変更しました。 action act { attribute guard1 : ScalarValues::Boolean; attribute guard2 : ScalarValues::Boolean; first start; then fork fork1; then action1; then action2; action action1; then join1; action action2; then join1; join join1; then decide decision1; if guard2 then action3; if guard1 then action4; action action3; then merge1; action action4; then merge1; merge merge1; then done; } これを SysONに読ませてオブジェクトを生成し、General Viewで表示、整形すると下図のようになります。 まとめと次回予告 # SysMLv2の仕様書には他にも Action Flowの例が載っています。 また、本連載の題材にしているドキュメントにも上記の他に Action Flowが記載されています。 しかし本連載で使用した SysONでは、これらすべての Action Flowをグラフィカル記法で表現することは出来ません。 その一方、GitHubのコミットログをみると、日々 SysONの開発が進められていることがわかります。 Action Flow Viewを含め、今後のリリースに期待しましょう。 次回は、State Definitionと State Usageを作成します。 Stateもまだまだ開発中だと思いますが、どこまで出来るのか試してみましょう。
はじめに # 豆蔵では太陽光発電パネルの清掃ロボットシステムの開発に取り組んでいます。 本システムでは太陽光発電パネルを清掃するロボットとロボットを搬送するドローンで構成されています。本記事では、ドローン側の開発技術である Payload SDK における Application Binding について紹介します。 Payload SDK については以下の記事でもご紹介していますので併せて参照して下さい。 https://developer.mamezou-tech.com/robotics/solar-panel-clean-robot/dji-drone-psdk-introduction/ Application Binding について # 一部の機体ではペイロードデバイスを使用する前に Application Binding という以下の手順が必要となります。 機体とペイロードデバイスを接続し Payload SDK で開発されたアプリケーションを起動する Payload SDK の初期化シーケンスで機体とのバインド待ちとなる 機体とPCを接続し DJI Assistant 2 を起動する バインド待ちとなっているペイロードデバイスの一覧が表示される DJI Assistant 2 で機体とペイロードデバイスをバインドする SDK の初期化シーケンスで機体が応答を返すようになり SDK の API を利用可能となる バインドしたペイロードデバイスの情報は機体内に永続化され、以降は対象のペイロードデバイスを使用可能となります。 Application Binding が必要な機体 # 以下は、現行機体が提供している拡張ポートの一覧です。 Standard Hardware Port Introduction より抜粋。 Aircraft Port Name Supports App Binding FlyCart 100 E-Port Lite – FlyCart 30 E-Port Lite – Matrice 4D/4TD E-Port, E-Port Lite ✓ Matrice 4E/4T E-Port, E-Port Lite ✓ Matrice 3D/3TD E-Port, E-Port Lite – Matrice 30/30T E-Port – Mavic 3E/3T E-Port – M400 E-Port V2 ✓ M350 RTK E-Port – M350 RTK Gimbal Port ✓ M300 RTK OSDK Port – M300 RTK Gimbal Port ✓ Supports App Binding にチェックが入っている機体の拡張ポートに対してペイロードデバイスを接続する場合はバインドが必要です。 E-Port、E-Port V2、Gimbal Portで接続するペイロードデバイスが対象ですが、Matrice 系では Matrice 4E/4T 以降のモデルから必要となっています。今後発売される機体では(E-Port Lite を除けば)基本的にはペイロードデバイスに対するバインドが必要になってくるものと思われます。 SDK 認証チップ # バインドするサードパーティ製のペイロードデバイスには DJI SDK 認証チップ(略称 DJI SDK CC)を取り付ける必要があります。 DJIストア から50個セットのものを購入できます。 以下の写真のパッケージングされた細長いシート状のものが購入した認証チップです。 袋の上に置いてあるのは認証チップを取り付けるためのアダプタです(後述)。 認証チップは機体とサードパーティ製ペイロードデバイス間の通信を認証・暗号化するハードウェアセキュリティモジュールです。 この認証チップにより機体側が各ペイロードデバイスを識別可能となり、バインド済のペイロードデバイスの情報(認証チップの情報)が機体内に永続化されます。 このチップはサードパーティ向けに提供されているものですが DJI製のペイロードデバイスにも同様の認証チップ或いはこれに準ずる仕組みが組み込まれているものと思います。 SDK 認証チップの接続 # SDK Certified Chip Quick Start に Raspberry Pi 4B を対象とした接続例が記載されていますので、これをベースに解説致します。 SDK 認証チップのインターフェイス # 認証チップは I²C インターフェースでホスト( Raspberry Pi )と通信します。 下図は認証チップのピン配置です。 VCC: 電源入力ピン(動作電圧範囲: 1.62 V - 5.5 V) GND: グランドピン NRST: 外部リセットピン I2C_SCL: I²Cバスインターフェースピン(Serial Clock Line) I2C_SDA: I²Cバスインターフェースピン(Serial Data Line) チップのパッケージタイプは DFN8 2x3 です。 外径サイズが 2mm x 3mm と非常に小型であるため、これに直接配線することは困難です。 そのため、以下の写真のような DIP8 ソケットにつなげる変換アダプタを使用します。 SDK 認証チップと Raspberry Pi の接続 # Raspberry Pi の 40-pin GPIO header のピンと認証チップを接続します。 認証チップと GPIO のピン対応は以下のとおりです。 認証チップ GPIO 1pin(7816IO) (NC) 2pin(Vcc) 1pin(3.3V power) 3pin(7816CLK) (NC) 4pin(GND) 9pin(Ground) 5pin(I2C_SDA) 3pin(GPIO2:SDA) 6pin(NC) (NC) 7pin(I2C_SCL) 5pin(GPIO3:SCL) 8pin(NRST) 7pin(GPIO4:GPCLK0) 9pin(GND) 9pin(Ground) デバイスツリーで I²C を有効化した後に i2cdetect コマンドなどで I²C のアドレスが表示されればOKです。 以下の例だと認証チップに割り当たっているデバイスは /dev/i2c-1 です。 $ ls /dev/i2c-* /dev/i2c-1 /dev/i2c-20 /dev/i2c-21 認証チップの Vcc に 3.3V が給電されただけでは反応しません。 $ sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- 以下のように GPIO4(認証チップの NRST と接続)を LOW にして HIGH にすると認証チップがリセットされ、I²C アドレス 0x2a が検出されます。 $ sudo gpioset gpiochip0 4=0 $ sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- $ sudo gpioset gpiochip0 4=1 $ sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- 2a -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- DJIのデベロッパーセンターで Payload SDK アプリケーションを登録する # バインドする前に Payload SDK アプリケーションを DJIのデベロッパーセンター で登録する必要があります。 アプリケーションの情報を入力します。 アプリケーションの登録後に Send Email を押下するとアクティベーションのインビテーションメールが送信されます。 メールのリンク先を開くとアクティベーションが完了し ID や KEY が表示されます。 ページに記載されている通り、登録したアプリケーションに対してバインドできるペイロードデバイスは最大で20台までです。 Application Verification のページで会社の説明やペイロードデバイスのテストレポートなど様々な書類を用意して審査が完了すると、台数の制約が解除されます。開発初期にはこの台数の制約は問題になりませんが、ペイロードデバイスを量産するフェーズではテストレポートを用意して申請しましょう。 Payload SDK アプリケーションの設定 # ここでは Payload SDK の Raspberry Pi 向けのサンプルアプリケーションを例にして SDK への設定内容を説明します。 DJIのデベロッパーセンターで登録したアプリケーション情報を以下のファイルへ設定します。 Payload-SDK/samples/sample_c++/platform/linux/raspberry_pi/application/dji_sdk_app_info.h /* Exported constants --------------------------------------------------------*/ // ATTENTION: User must goto https://developer.dji.com/user/apps/#all to create your own dji sdk application, get dji sdk application // information then fill in the application information here. #define USER_APP_NAME "your_app_name" #define USER_APP_ID "your_app_id" #define USER_APP_KEY "your_app_key" #define USER_APP_LICENSE "your_app_license" #define USER_DEVELOPER_ACCOUNT "your_developer_account" #define USER_BAUD_RATE "460800" 定数名 説明 例 USER_APP_NAME DJIのデベロッパーセンターの登録情報の App Name が対応します DockingControl USER_APP_ID DJIのデベロッパーセンターの登録情報の App ID が対応します (省略) USER_APP_KEY DJIのデベロッパーセンターの登録情報の App Key が対応します (省略) USER_APP_LICENSE DJIのデベロッパーセンターの登録情報の App Basic License が対応します (省略) USER_DEVELOPER_ACCOUNT DJIのデベロッパーセンターのアカウント名です masayuki-kono サンプルアプリケーションを起動して以下のログが延々と出力されれば OK です(バインド待ちの状態です)。 [Error] dji_auth_sha256_rsa_verify.c:137 The DJI SDK CC has not binded. Please check the bind state of the DJI SDK CC and bind it. --> Information Raspberry Pi 向けのサンプルコードはメンテナンスされていないようで、そのままでは以下のようなエラーが出力されて認証チップと通信に失敗します。 Connect DJI SDK CC device failed, errno: 0x30000002 アドレス 0x2A への書き込みで ioctl(I2C_RDWR) が -1 を返し、スレーブが ACK を返していないのが原因です。 HalI2c_ResetDevice() で GPIO4 を LOW→25ms→HIGH とリセットした直後に、即座にデバイスを開いて書き込みしています。チップがリセットから復帰しきる前に初回トランザクションが走っているのが原因と考えられます。リセット解放後、チップが I²C に応答できるまでに必要な待ち時間が不足しているようなので、リセット解放後 50ms 待ってから I²C アクセスを行うように変更して改善しました。 Payload SDKをフォークしたリポジトリ に修正したコードをアップしていますので参考にして下さい。デバッグログの出力も追加しているのでどのようなデータをチップと送受信しているか観測すると理解が深まると思います。 機体とペイロードデバイスを接続する # 今回は Matrice 4E を使用しました。 各機器の接続イメージは以下の通りです。 Matrice 4E Application Binding で使用する機体 PC DJI Assistant 2 の動作環境 DJI Assistant 2 は機体によってバリエーションがあり Matrice 4E の場合は Enterprise Series を使用する DJI Assistant 2 は DJI のクラウドサービスと通信するためインターネットに接続する必要がある E-Port Development Kit 機体とペイロードデバイスを接続するためのアダプタボード UART-USB Adapter 今回は FTDI のUART-USB変換アダプタを中継 Raspberry Pi のGPIO(UART ピン)へ直接接続する場合は不要 Raspberry Pi Payload SDK アプリケーションの動作環境 DFN8 Breakout Adapter DFN8(2×3 mm)パッケージの表面実装ICを DIP8 互換ピン配置に変換するためのブレークアウトアダプタ SDK Certified Chip E-Port Development Kit # Development Kit の基盤上に E-Port switch というディップスイッチがあり、これを ON にして UART の出力を有効にします。 USB ID switch(Device|Host) のディップスイッチは、USB で RNDIS や Bulk 転送する場合に Host を設定する必要があります。今回は UART のみを使用するため、設定不要(どちらでも良い)です。 E-Port のコネクタはHW的にはリバーシブルですが、機体の E-Port コネクタと Development Kit を接続する際に機体側と開発キット側のコネクタの向きに指定があります。 Connect Development Board to E-Port からの抜粋です。 Note: The E-Port coaxial USB-C cable doesn't have a foolproof design, allowing A/B side to be reversibly connected. Due to pin layout differences in the aircraft's USB-C, if the coaxial cable is reversed, the other end also needs to be flipped correspondingly. If not flipped correspondingly, the E-Port Development Kit can not power up and communicate. 以下の写真のようにコネクタに A/B が印字されており、機体側が A なら開発キット側は B 、機体側が B なら開発キット側は A のようにフリップする必要があります。 DJI のページの記載では、どの向きが正しいのか判断できないため、結局、どちらも試して動作する向きを特定しました(写真は動作した時の組み合わせです)。 Application Binding を行う # Payload SDK アプリケーションから以下のログが延々と出力される状態(バインド待ち)にします。 [Error] dji_auth_sha256_rsa_verify.c:137 The DJI SDK CC has not binded. Please check the bind state of the DJI SDK CC and bind it. この状態で 機体と E-Port Lite で接続した PC 上で DJI Assistant 2 を開くと Payload SDK メニューに以下が表示されます。 Bind ボタンを押下すると、バインドが完了します。 バインドが完了すると、サンプルアプリケーションの起動時のログは以下のようになります。 0.016 core [Info] dji_core.c:113 Payload SDK Version : V3.15.0-beta.0-build.2318 Dec 10 2025 17:27:05 1.075 adapter [Info] dji_access_adapter.c:351 Identify mount position type is Extension Port Type 1.075 adapter [Info] dji_access_adapter.c:371 Identify aircraft series is Matrice 4 Series 1.578 adapter [Info] dji_access_adapter.c:493 Identity uart0 baudrate is 921600 bps 1.582 core [Info] dji_identity_verify.c:627 Updating dji sdk policy file... 2.582 core [Info] dji_identity_verify.c:635 Update dji sdk policy file successfully 2.627 core [Info] dji_core.c:261 Identify AircraftType = Matrice 4E, MountPosition = Extension Port, SdkAdapterType = None 2.748 auth [Info] dji_sdk_cc_auth.c:86 Get DJI SDK CC serial num: 99PDN73EUB13J3 4.812 linker [Warn] dji_command.c:1025 <0xd5d0>Command async send retry: index = 0, retryTimes = 1, 0x0A06->0x0F01(0x002F) 0x3C13 5.945 linker [Warn] dji_command.c:910 Received invalid ack,<0xd5d0> 0x0F01(0x002F)->0x0A06(0x00CA) 0x3C13 6.322 adapter [Info] dji_identity_verify.c:257 the license level is basic 6.322 core [Info] dji_product_info.c:187 Set alias: PSDK_APPALIAS 6.942 user [Info] test_widget.c:141 widget file: /home/dev/DockingController/third_party/Payload-SDK/samples/sample_c/module_sample/widget/widget_file/en_big_screen 6.952 user [Info] test_widget_speaker.c:594 Set widget speaker volume: 60 6.952 user [Warn] test_widget_speaker.c:613 No audio device found, please add audio device and init speaker volume here!!! 12.455 core [Info] dji_core.c:328 Start dji sdk application 12.455 user [Info] application.cpp:372 Application start. | Available commands: | | [0] Fc subscribe sample - subscribe quaternion and gps data | | [1] Flight controller sample - you can control flying by PSDK | | [2] Hms info manager sample - get health manger system info by language | | [a] Gimbal manager sample - you can control gimbal by PSDK | | [c] Camera stream view sample - display the camera video stream | | [d] Stereo vision view sample - display the stereo image | | [e] Run camera manager sample - you can test camera's functions interactively | | [f] Start rtk positioning sample - you can receive rtk rtcm data when rtk signal is ok | | [g] Request Lidar data sample - Request Lidar data and store the point cloud data as pcd files | | [h] Request Radar data sample - Request radar data | | [l] Run widget states manager sample, control widget states on other payload | --> Information I²C への読み書きする hal_i2c.c で通信データをログ出力すると分かりますが、SDK は初期化完了後も周期的に認証チップと通信しており、毎回異なるデータを送受信しています。 公式のプロトコル仕様は公開されていないため以下は推測ですが、チャレンジ・レスポンス型の認証として次のような流れと考えられます。 機体 → 認証チップ: チャレンジデータ送信(ランダム値やタイムスタンプを含む) 認証チップ → 機体: 署名済みレスポンス返送(認証チップ固有の秘密鍵を使用) 機体側が認証チップの公開鍵で署名を検証 これによりサードパーティが販売したペイロードデバイスを機体が正規のものとして識別でき、バインド済みのデバイスのみが Payload SDK を利用可能になっているようです。 バインド完了後に、DJIのデベロッパーセンターを開くと 1 Payloads が表示されカウントが増えていることが確認できるはずです。 まとめ # Application Binding は Matrice 4E/4T(2025年1月発売)以降で登場した比較的新しい仕様です。 そのため DJI の公式サイトを見ても全体像の把握が難しく、具体的な手順が分かりづらい状況にあります。 本記事では、Application Binding が必要な機体の一覧、SDK 認証チップの接続方法(Raspberry Pi を例に)、デベロッパーセンターでのアプリケーション登録を説明しました。 さらに、機体・ペイロード・PC を接続したうえで DJI Assistant 2 からバインドするまでの流れを一通り紹介しました。 Application Binding は今後発売される機体では標準となる可能性が高いです。 カスタムペイロードの開発に取り組まれる方は、本記事を手がかりにぜひ試してみてください。
はじめに # 豆蔵では太陽光発電パネルの清掃ロボットシステムの開発に取り組んでいます。 本システムでは太陽光発電パネルを清掃するロボットとロボットを搬送するドローンで構成されており、本記事では、ドローン側の開発技術である Payload SDK を紹介します。 プロジェクトの概要 # 太陽光発電パネルの発電効率を最大限に保つためには、表面に堆積する埃や汚れの定期的な除去が不可欠です。 一般家庭用の小規模なパネルであれば手作業での清掃も可能ですが、 メガソーラーなどの大規模な発電施設では、人力による清掃作業は効率面・コスト面で現実的ではありません。 当社では、このような発電施設向けの自律型清掃ロボットを開発しています。 清掃ロボットのシステム構成 # 発電所には連結された太陽光発電パネルがそれぞれ離れた位置に設置されています。 太陽光発電パネルは地表から2m以上の高い位置に設置されており、人手でロボットをパネル間で搬送するのは困難です。 そのため、本システムではドローンを使用してロボットを搬送する手法を採用しています。 主な構成要素は以下のとおりです。 清掃ロボット # 太陽光発電パネル上をブラシで清掃しながら自律走行する自社開発の AMR です。ドローンの搬送対象(ペイロード)であり、総重量がドローンの可搬重量以内になるように設計しています。 2025 国際ロボット展 でも展示しました。 ドローン # DJI FlyCart 30 を使用しています。可搬重量は、デュアルバッテリーモードで 30 kg(最大飛行時間 18 分)、シングルバッテリーモードで 40 kg(最大飛行時間 9 分)です。 送信機(操作端末)は DJI RC Plus でDJI Pilot 2 というアプリケーションが動作しています。 ペイロードデバイス # FlyCart 30 に標準で付属している以下の貨物ケースは内寸 573×416×305 mm のため、ロボットを格納できません。そのため、ドローンにロボットを固定するデバイスを開発しています。 ペイロードデバイスの構成 # 本システムにおけるペイロードデバイスの主な役割はロボットをドローンに固定することです。 ドローンの送信機の操作でロック機構を制御し、ロボットを固定します。 ロック機構の制御や送信機へのウィジェットの提供はペイロードデバイス内のSBC(シングルボードコンピュータ)が担います。 DJIはペイロードデバイスの開発用に Payload SDK というSDKを提供しており、ペイロードデバイスではこのSDKを使用して開発したアプリケーションをSBC上で動作させます。 FlyCart 30の場合は以下のインターフェイスがペイロードデバイス向けに提供されています。 E-Port Lite USB Type-C のメンテナンス用ポート DJI Assistant 2 がインストールされた PC と USB Type-C ケーブルで直接接続し、機体のファームウェア更新やログ収集が可能 FlyCart 30 のように E-Port を提供していない機体では、E-Port Lite と SBC を USB to TTL シリアルモジュールで接続し、拡張ポートとして使用が可能 Payload Port ペイロードデバイス向けの電源供給ポート 定格電圧は 51.2 V 本システムのロック機構は開発中のため詳細は割愛します。以下は CAN 対応のサーボを使う場合の構成イメージです。この場合、サーボへの電源供給は Payload Port から行い、SBC が E-Port Lite を介して機体と連携し、サーボを制御します。 機体が提供するさまざまな拡張ポート # 前述した構成は FlyCart 30 の例です。機体によっては E-Port Lite 以外の拡張ポートを提供しているものもあります。 E-Port # 多くの機体がサポートしている拡張ポートで、電源、UART、USB を提供します。 E-Port Development Kit を中継してカスタムペイロードと接続し、UART や USB の通信が可能になります。 E-Port Lite ではカメラ画像の取得などに制約がありますが、E-Port では Development Kit を中継することで USB が拡張され、多くの機能を利用できます。 E-Port V2 # M400(Matrice 400)で提供される拡張ポートです(M400 は 2025年6月発表であり、E-Port V2 は比較的、最近登場したポートです)。E-Port が機体あたり 1 ポートであることが多いのに対し、E-Port V2 は M400 の機体下部に 4 ポートを備え、1 ポートあたり 120 W の電源供給が可能です。電源出力は 13.6 V / 17 V / 24 V の 3 段階で調整できます。USB 3.0 をサポートしており、4K ストリームやレーダーポイントクラウドデータなどを同時に取得可能です。 E-Port V2 Development Kit を中継してカスタムペイロードと接続します。 Gimbal Port # M300 RTK(2020年5月発表)と M350 RTK(2023年5月発表)のジンバル部に装備される標準インターフェースで、PSDK Port とも呼ばれます。Zenmuse シリーズに代表される DJI 製ジンバルペイロード(カメラ・センサー等)を接続するためのインターフェースです。 サードパーティ向けには Payload SDK Development Board Kit 2.0 が提供されており、これを中継すればカスタムペイロードと接続できます。 OSDK Port # 現行機体ではM300 RTK のみが提供する旧来のインターフェースです。E-Port 登場以前の方式であり、 Onboard SDK (OSDK) を利用しますが、OSDK の最終リリースは 2021-02-02(OSDK 4.1.0) で、新機能の追加は終了しています。 OSDK Port は OSDK Expansion Module 以外に E-Port Development Kit との接続もサポートしており、Payload SDK(PSDK)を使用できます。 OSDK Version Support Information (2023年5月9日付)では、 OSDK 4.x の機能はすべて PSDK V3 へ移行済みである とされています。新規開発では PSDK V3 への移行が推奨されています。 機体別の拡張ポート # 以下は、現行機体が提供している拡張ポートの一覧です。 Standard Hardware Port Introduction より抜粋。 Aircraft Port Name Supports App Binding FlyCart 100 E-Port Lite – FlyCart 30 E-Port Lite – Matrice 4D/4TD E-Port, E-Port Lite ✓ Matrice 4E/4T E-Port, E-Port Lite ✓ Matrice 3D/3TD E-Port, E-Port Lite – Matrice 30/30T E-Port – Mavic 3E/3T E-Port – M400 E-Port V2 ✓ M350 RTK E-Port – M350 RTK Gimbal Port ✓ M300 RTK OSDK Port – M300 RTK Gimbal Port ✓ Supports App Binding にチェックが入っている機体は Application Binding の手順が必要な機体です。 この手順は別の記事で紹介する予定です。 ポート種別ごとの機能対応表 # 下表は、 Aircraft Type Function Difference の一覧から、一部の機体について、ポートごとの対応機能を抜粋したものです。 同じポート種別でも機体によって対応機能は異なるため、「E-Port ならこの機能が使える」のようにポート種別だけでは判断できません。例えば Hoisting Control は、FlyCart 100 の E-Port Lite でのみサポートされています。 Function Name Function Level FlyCart 30 E-Port Lite Matrice 4E/4T E-Port Matrice 400 E-Port V2 Matrice 350 RTK Gimbal Port Log Management basic ✓ ✓ ✓ ✓ Data Subscription basic ✓ ✓ ✓ ✓ Basic Camera Function basic - ✓ ✓ ✓ Basic Camera Management advanced - - ✓ - Gimbal Function basic - ✓ ✓ ✓ Gimbal Management advanced - - ✓ - Power Management basic ✓ - ✓ ✓ Flight Control advanced ✓ - ✓ ✓ Custom Widget basic ✓ ✓ ✓ ✓ Custom HMS basic ✓ ✓ ✓ ✓ HMS Manager advanced ✓ - ✓ ✓ Time Synchronization basic - ✓ ✓ ✓ Low-speed Data Transmission basic ✓ ✓ ✓ ✓ Camera Video Stream basic - ✓ ✓ ✓ Playback Download basic - - ✓ ✓ X-Port Function basic - - ✓ ✓ Camera Stream Liveview advanced - - ✓ - Local Upgrade basic - ✓ - ✓ High-speed Data Transmission basic - - ✓ - Positioning basic - - ✓ ✓ SDK Interconnection basic - - ✓ ✓ Waypoint Mission advanced ✓ - ✓ - Speaker basic ✓ ✓ ✓ ✓ Hoisting Control basic - - - - Access Internet advanced - - ✓ - Network RTK advanced - - ✓ - シリアル通信仕様 # Payload SDKはUARTやUSBのシリアル通信を使用します。 USB 通信では USB Gadget(Linux デバイスを USB 機器のデバイス側として振る舞わせる仕組み)を用い、次の 2 種類の通信方式を使い分けます。 Bulk(USB Bulk Transfer) デバイスとホスト間の双方向の生データ通信 RNDIS(Remote Network Driver Interface Spec) USB上で Ethernet をエミュレートする規格 デバイスとホスト間のIP通信 各ポートのサポート状況は次の表のとおりです。 Port Only UART UART+Bulk UART+RNDIS Only Bulk Only RNDIS E-Port Lite ✓ – – – – E-Port ✓ ✓ ✓ – – E-Port V2 ✓ – – ✓ ✓ Gimbal Port ✓ – ✓ – – 方式 説明 Only UART UART のみで機体と通信 UART+Bulk UART と Bulk を併用して機体と通信 UART+RNDIS UART と RNDIS を併用して機体と通信 Only Bulk Bulk のみで機体と通信 Only RNDIS RNDIS のみで機体と通信 Bulk のみまたは RNDIS のみで通信できるのは E-Port V2 だけで、それ以外のポートでは UART が必須です。 詳細は以下の DJI Developer Support のページ(中国語)を参照してください。いずれも閲覧には DJI Developer Center でのアカウント登録が必要です。 PSDK 各机型硬件连接介绍 树莓派4B配置USB device RNDIS 和 BULK どのポートでも UART のみで通信できるため、開発初期は UART だけを接続し、Payload SDK で開発したアプリケーションの動作検証から始めることを推奨します。 Payload SDK の API 仕様 # Payload SDK の API リファレンス には、SDKのソースコードのヘッダから自動生成されたと思われる API 仕様が掲載されています。ただし説明文はほとんどなく、関数や型の一覧が中心です。そのため、API仕様を理解するには、サンプルコードを参照しつつ実機で動作を確認する必要があります。 Payload SDK のサンプルアプリケーション # DJI の Payload-SDK リポジトリには、Payload SDK のライブラリとそれを使ったサンプルアプリケーションが公開されています。 ディレクトリ構成 # リポジトリのディレクトリ構成は以下のとおりです。 ├── psdk_lib │ ├── include │ └── lib │ ├── aarch64-linux-gnu-gcc │ ├── arm-linux-gnueabi-gcc │ ├── arm-linux-gnueabihf-gcc │ ├── armcc_cortex-m4 │ └── x86_64-linux-gnu-gcc ├── samples │ ├── sample_c │ │ ├── module_sample │ │ │ ├── camera_emu │ │ │ ├── camera_manager │ │ │ ├── cloud_api │ │ │ ├── data_transmission │ │ │ ├── fc_subscription │ │ │ ├── flight_control │ │ │ ├── gimbal_emu │ │ │ ├── gimbal_manager │ │ │ ├── hms │ │ │ ├── interest_point │ │ │ ├── liveview │ │ │ ├── mop_channel │ │ │ ├── payload_collaboration │ │ │ ├── perception │ │ │ ├── positioning │ │ │ ├── power_management │ │ │ ├── tethered_battery │ │ │ ├── time_sync │ │ │ ├── upgrade │ │ │ ├── utils │ │ │ ├── waypoint_v2 │ │ │ ├── waypoint_v3 │ │ │ ├── widget │ │ │ ├── widget_interaction_test │ │ │ └── xport │ │ └── platform │ │ ├── linux │ │ │ ├── common │ │ │ │ ├── 3rdparty │ │ │ │ ├── monitor │ │ │ │ ├── osal │ │ │ │ └── upgrade_platform_opt │ │ │ ├── manifold2 │ │ │ │ ├── application │ │ │ │ └── hal │ │ │ ├── manifold3 │ │ │ │ ├── app_json │ │ │ │ ├── application │ │ │ │ └── hal │ │ │ ├── nvidia_jetson │ │ │ │ ├── application │ │ │ │ └── hal │ │ │ └── raspberry_pi │ │ │ ├── application │ │ │ └── hal │ │ └── rtos_freertos │ │ ├── common │ │ │ └── osal │ │ ├── gd32f527_development_board │ │ │ ├── application │ │ │ ├── bootloader │ │ │ ├── drivers │ │ │ ├── hal │ │ │ ├── middlewares │ │ │ └── project │ │ └── stm32f4_discovery │ │ ├── application │ │ ├── bootloader │ │ ├── drivers │ │ ├── hal │ │ ├── middlewares │ │ └── project │ └── sample_c++ │ ├── module_sample │ │ ├── camera_manager │ │ ├── flight_controller │ │ ├── gimbal │ │ ├── hms_manager │ │ ├── liveview │ │ ├── perception │ │ ├── positioning │ │ └── widget_manager │ └── platform │ └── linux │ ├── common │ │ ├── 3rdparty │ │ └── osal │ ├── manifold2 │ │ ├── application │ │ └── hal │ ├── manifold3 │ │ ├── application │ │ └── hal │ ├── nvidia_jetson │ │ ├── application │ │ └── hal │ └── raspberry_pi │ ├── application │ └── hal psdk_lib # プラットフォームごとの静的ライブラリが配置されています。各ツールチェーンについては Using third-party development platforms を参照してください。Raspberry Pi や Jetson の場合はこのディレクトリ内の aarch64-linux-gnu-gcc/libpayloadsdk.a を使用します。ここにないツールチェーンを使う場合は、SDK テクニカルサポート(dev@dji.com)に依頼すれば、そのツールチェーン用の静的ライブラリを用意してもらえるようです。 PSDK platform static library link も参照してください。 samples # module_sample SDK が提供する各機能のサンプルコードが配置されています。カメラ管理・飛行制御・ジンバル・ライブビュー・HMS・位置情報・ウィジェットなど、機能ごとにサンプルが用意されています。 platform プラットフォーム依存のコードがまとめられています。 hal/ に配置されているソースコードは、ハードウェア抽象化レイヤー(HAL)の実装(ネットワーク・UART・USB Bulk・I2C など)です。 サンプルアプリケーションのエントリポイントは application/main.cpp です。ここから application/application.cpp が呼ばれ、HAL ハンドラの登録や DjiCore_Init による SDK のセットアップが行われます。UART・Bulk・RNDIS のいずれを使うかは、登録する HAL ハンドラの組み合わせで決まります。 上記の HAL ハンドラ登録部分( CONFIG_HARDWARE_CONNECTION による分岐)の抜粋です。 Payload-SDK/samples/sample_c++/platform/linux/raspberry_pi/application/application.cpp returnCode = DjiPlatform_RegHalI2cHandler(&i2CHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("register hal i2c handler error"); } #if (CONFIG_HARDWARE_CONNECTION == DJI_USE_UART_AND_USB_BULK_DEVICE) returnCode = DjiPlatform_RegHalUartHandler(&uartHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal uart handler error."); } returnCode = DjiPlatform_RegHalUsbBulkHandler(&usbBulkHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal usb bulk handler error."); } #elif (CONFIG_HARDWARE_CONNECTION == DJI_USE_UART_AND_NETWORK_DEVICE) returnCode = DjiPlatform_RegHalUartHandler(&uartHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal uart handler error."); } returnCode = DjiPlatform_RegHalNetworkHandler(&networkHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal network handler error"); } #elif (CONFIG_HARDWARE_CONNECTION == DJI_USE_ONLY_USB_BULK_DEVICE) returnCode = DjiPlatform_RegHalUsbBulkHandler(&usbBulkHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal usb bulk handler error."); } #elif (CONFIG_HARDWARE_CONNECTION == DJI_USE_ONLY_NETWORK_DEVICE) returnCode = DjiPlatform_RegHalNetworkHandler(&networkHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal network handler error"); } //Attention: if you want to use camera stream view function, please uncomment it. returnCode = DjiPlatform_RegSocketHandler(&socketHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("register osal socket handler error"); } #elif (CONFIG_HARDWARE_CONNECTION == DJI_USE_ONLY_UART) /*!< Attention: Only use uart hardware connection. */ returnCode = DjiPlatform_RegHalUartHandler(&uartHandler); if (returnCode != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) { throw std::runtime_error("Register hal uart handler error."); } #endif 以下はアプリケーションのレイヤー構成のイメージです。 flowchart LR subgraph App["エントリポイント"] A["application.cpp<br/>HAL 登録・DjiCore_Init"] end subgraph SDK["SDK コア"] B["DjiCore_Init 等"] end subgraph HAL["HAL ハンドラ"] H["Network / UART /<br/>USB Bulk / I2C"] end A --> B --> H 実行時には、DjiCore が UART への書き込みなどを行う際に、事前に登録した HAL ハンドラがコールバックとして呼ばれます。その流れは次のとおりです。 sequenceDiagram participant App as application.cpp participant Core as DjiCore participant HAL as HALハンドラ App->>Core: HAL ハンドラを登録 App->>Core: DjiCore_Init() Note over Core: 初期化完了 Note over Core,HAL: 実行時(UART への書き込みが必要なとき) Core->>HAL: コールバック呼び出し<br/>(例: HalUart_WriteData) HAL-->>Core: 結果 図中の application.cpp と HALハンドラがサンプルコードに含まれており、DjiCore は psdk_lib の静的ライブラリとして提供されています。 初見ではサンプルコードの割にコード量が多く感じられるかもしれませんが、プラットフォーム依存のコードは基本的にそのまま利用できます。ただし、同じプラットフォームでも、最新の OS や付随するライブラリでは動作しない場合があるため、開発者側での保守が必要です。 Payload SDK の検証環境 # 残念ながら、Payload SDK のアプリケーションの動作を確認するには実際の機体が必要です。 DJI Assistant 2 にはフライトをシミュレートする機能がありますが、送信機とペアリングされた機体と接続した状態でしか利用できません。また、Payload SDK の通信先も実際の機体が前提となります。 本番運用で使用する機体と同じポート(E-Port Lite や E-Port)を備えた機種を調達またはレンタルする必要があります。 弊社では本番運用に FlyCart 30 を使用しますが、高価なため開発・デバッグには E-Port Lite を備えた別機種(Matrice 4E など)でアプリケーションを開発しています。 まとめ # 本記事では、太陽光発電パネルの清掃ロボットをドローンで搬送するシステムのうち、ドローン側の開発に用いる Payload SDK を紹介しました。 インターネット上で検索しても Payload SDK に関する資料は DJI 提供のものに限られ、実用的な手順も DJI Developer Support の中国語ページが中心です。弊社でも、アプリケーションを動作するところまで持っていくのに手間がかかりました。同じようにカスタムペイロードの開発に取り組まれる方にとって、本記事がスタート地点の一助となれば幸いです。 飛行状態の取得やウィジェット連携、Application Binding の手順など、Payload SDK の実践的なトピックは続く記事で詳しくお届けする予定です。引き続きお付き合いいただければと存じます。
これまでの記事では、Part Definitionと Part Usage、Packageの作成をご紹介しました。 /blogs/2026/01/29/sysmlv2-tool-syson-partusage/ 本記事から振る舞いのモデリングを行います。 執筆時点における SysONの安定版は v2025.12.0が最新ですが、本記事では引き続き v2025.8.0を使用します。 ざっとドキュメントを見る限りでは、v2025.8.0と v2025.12.0の間に大きな機能追加はなさそうです。 ただし、今後を含めた最新リリースの挙動は一部異なる可能性がありますのでご了承ください。 Parameterを持つAction Definitionを作成する # " Intro to the SysML v2 Language-Graphical Notation.pdf " スライド50の図を作成してみましょう。 General Viewを開き、エディタ画面を右クリックでしてンテキストメニューから"Behavior" > "New Action Definition"を選択します。 作成した Action Definitionの名前を"ProvidePower"に変更しましょう。 変更方法は以前の記事を参照してください。 次にParameterを追加します。 "ProvidePower"を右クリックしてコンテキストメニューから"Structure" > "New Item In"を選択します。 追加された Itemを"pwrCmd : PwrCmd"に変更します。 このとき、左サイドバーのツリーに"PwrCmd"が追加されたことに着目してください。 これは、"pwrCmd"のItem Definitionです。 ツリーにある"PwrCmd"をエディタ画面にドラッグ&ドロップしたら"pwrCmd"との間に definitionが表示されます。 再び "ProvidePower"を選択し、名前の右にマウスカーソルを移動すると表示される目のアイコンをクリックします。 表示された"Manage Visibility"のコンテキストメニューで、"parameters"のチェックをON、"pwrCmd : PwrCmd"のチェックをOFFにします。 pwrCmdのitemを(モデルではなく)図から削除します。 同様の方法で、"torque : Torque"を追加しましょう。 ”pwrCmd : PwrCmd”の場合と同様、左サイドバーのツリーに"Torque"が追加されます。 Parameterのin, out, inout, noneは、右サイドバーのDetailsにある"Direction"のラジオボタンで変更できます。 題材は"torque"が配列になっています。 "torque : Torque"を"torque[*] : Torque"に変更してください。 右サイドバーのツリーの"torque"に"LiteralInfinity"の入った"MultiplicityRange"が追加されます。 題材では"torque : Torque [*]"となっていますが、v2025.8.0のSysONではこの表記だと多重度が無視されてしまいました。 Action DefinitionからAction Usageを作成する # エディタ画面を右クリックでコンテキストメニューから"Behavior" > "New Action"を選択します。 作成した Action Usageの名前を"providePower"に変更しましょう。 "providePower"を選択し、要素4辺の外側に表示された">"を"ProvidePower"までドラッグ&ドロップします。 表示されたコンテキストメニューから"New Feature Typing"を選択します。 Action DefinitionにParameterを追加したのと同様にして、Action Usageである"providePower"にもItemを追加します。 "Manage Visibility"で"item1In"を非表示にし、"item1In"を"fuelCmd : FuelCmd :>> pwrCmd"に変更します。 見た目にはいくつか差異がありますが、意味的には同じものが出来ました。 Action UsageのDecomposition # " Intro to the SysML v2 Language-Graphical Notation.pdf " スライド51の図を作成してみましょう。 4つのAction Usage("generateTorque", "amplifyTorque", "distributeTorque", "transferTorque")を作成します。 Action Usageの Decompositionはエディタ画面で作図できませんでした。 (今後はできるようになるかもしれません) 左サイドバーのツリーで先程作成した4つのAction Usageを選択し、"providePower"にドラッグ&ドロップします。 すると、ドラッグ&ドロップした4つのAction Usageと"providePower"間にDecompositionが表示されます。 referenceにしたい場合は、対象のAction Usageを選択します。 右サイドバーで"Advance"タグを選択し、"Is Composite"のチェックをOFFにします。 対象のAction Usageのステレオタイプが"action"から"ref action"に変わり、"providePower"側の黒塗りひし形が白塗りに変わります。 Action DefinitionとAction UsageのDecompsition # SysMLv2仕様では、Action UsageをAction Definitionの部品とすることも出来ます。 先程のスライド51の図の、"providePower"をAction Definitionである"ProvidePower"に変更してみましょう。 左サイドバーのツリーにある"ProvidePower"をエディタ画面にドラッグ&ドロップします。 次に、左サイドバーのツリーで"provodePower"内にあった4つのAction Usageを"ProvidePower"に移動します。 "Manage Visibility"でポートをダイアグラムから削除すると下図のようになります。 SysON起動時にエラーした場合の対応 # これまで何度か SysONの起動と終了を繰り返してきました。 その中で、SysON起動時にエラーが発生して起動しないケースが偶に発生します。 こんな時は以下のコマンドでDockerの使われていないリソースを削除してみてください。 docker system prune 削除後に再度 Dockerで SysONを起動します。 次回予告 # 本記事では、Action Definitionと Action Usageを作成しました。 また、Decompositionで Actionを分割することをモデルで表現しました。 次回は、Action Usageをつなげて Action Flowを作成します。
はじめに # AIエージェントによる自律開発は魅力的ですが、長時間の処理でコンテキストの劣化により精度が落ちる問題があります。この課題に対するアプローチとして注目されているのは Ralphループ (コンテキストを都度破棄して新しいセッションで処理を継続する自律開発手法)です。本記事では、Kiro CLI [1] (AIエージェントによる自律開発を支援するCLIツール)を使ったRalphループの検証結果と、実践で得た教訓を共有します。 背景:コンテキスト管理の課題とRalphループ # 従来のAIチャットにおける課題は、長時間の会話でコンテキストが圧縮されたり劣化したりして精度が落ちることです [2] 。 Ralphループの核となる原則は コンテキスト腐敗の回避 です [3] 。1つのタスクが終わるごとにコンテキストを破棄し、新しいセッションで次のタスクを開始するというループ構造を回します。一見シンプルなテクニックですが、これにより精度を安定させながら長時間のタスク実行が可能になると考えられます。 今回の検証では、定番構成のClaude Code + PRD.md(Product Requirements Document: 製品要求仕様書)ではなく、 Kiro IDE [4] (仕様作成からタスク管理まで対話的に支援するIDE)の仕様成果物3種(requirements.md、design.md、tasks.md)に置き換え ました。構造化された指示により精度向上を狙っています。 検証題材:スプレッドシートアプリ # 今回の検証では、Webブラウザ上で動作する軽量スプレッドシートアプリを開発しました。 アプリケーション仕様 # 10列×20行のグリッド、セル参照、四則演算 SUM/AVG関数、循環参照エラー検知 技術スタック:React + TypeScript + Vite + Vitest 選定のポイントは、 比較的複雑度が高く、コンテキストがひっ迫して処理が迷走しそうなアプリケーション であることです。数式パーサー、依存関係グラフ、循環参照検知など、複数の概念が絡み合う題材で、Ralphループの実用性を試しました。 完成したアプリケーション # 今回作成したスプレッドシートアプリを先に説明します。 初期画面では、以下のように10列×20行のグリッドと数式バーが表示されます。 セル参照、四則演算、SUM/AVG関数などが実装されており、数式による自動演算が可能です。入力例として、簡単な数値演算をSUM関数を用いて行いました。 テスト品質 # 自律実行により、以下のテストが自動生成・実装されました。 テストケース総数 : 126テスト ユニットテスト : 101テスト プロパティベーステスト (ランダム入力により仕様の性質を検証するテスト手法): 25テスト Kiro CLIはテスト駆動開発のアプローチに従い、プロパティベーステストによるランダム入力検証を含むテストスイートを自律構築しました。人間では予測困難な入力パターンに対しても、循環参照検知や数式評価の正確性を効率的に検証するテストが生成され、品質確保に寄与しています。 実装ステップ # Ralphループの実装は、以下の2ステップで進めました。 ステップ1:Kiro IDEによる準備フェーズ # プロジェクト構成 # まず、プロジェクトのディレクトリ構造を以下のように準備します。 project/ ├── .kiro/specs/spreadsheet-sample/ │ ├── requirements.md # EARS記法による要件定義 │ ├── design.md # システム設計書 │ └── tasks.md # 実装タスクリスト ├── progress.txt # 実装進捗を記録(イテレーション間で引き継ぎ) ├── ralph-once.sh # 単発実行用スクリプト └── afk-ralph.sh # Ralphループ制御スクリプト 1-1. 仕様成果物の作成 # Kiro IDEを使ってスプレッドシートアプリの仕様を定義します。 Specモードで、 .kiro/specs/spreadsheet-sample/ ディレクトリに以下3つの仕様成果物を生成します。 requirements.md : EARS記法(要件定義の構文ルール)による要件定義。受入基準が明確に記述される design.md : システム設計書。アーキテクチャやコンポーネント設計が含まれる tasks.md : 実装タスクリスト。Kiro CLIがこれを読み取り、未完了タスクを実装する Kiro IDEとの対話を通じて、アプリケーションの要件を伝え、これらの仕様成果物を完成させます。この段階では、まだコードは生成されません。 1-2. シェルスクリプトの作成 # 次に、Ralphループを制御するシェルスクリプト afk-ralph.sh を作成します。シェルスクリプトの実装は、AIHero.devのガイド [5] を参考にしました。 メインループ # afk-ralph.sh(メインループ部分) for ((i=1; i<=${1}; i++)); do echo "loop iteration $i" # 仕様3種とprogress.txtを読み込み req="$(cat "${SPEC_DIR}/requirements.md")" des="$(cat "${SPEC_DIR}/design.md")" tasks="$(cat "${SPEC_DIR}/tasks.md")" progress="$(cat progress.txt 2>/dev/null || echo 'まだ進捗なし')" # プレースホルダーを実際の内容に置換 prompt="$(build_prompt)" prompt="${prompt/__REQ__/$req}" prompt="${prompt/__DES__/$des}" prompt="${prompt/__TASKS__/$tasks}" prompt="${prompt/__PROGRESS__/$progress}" logfile="/tmp/kiro-iteration-${i}.log" kiro-cli chat --no-interactive --trust-all-tools "$prompt" 2>&1 | tee "$logfile" # tasks.mdの未完了タスク数とCOMPLETE出力で終了判定 uncompleted=$(grep -cE '^\- \[ \]' "${SPEC_DIR}/tasks.md" 2>/dev/null || echo "0") has_promise=$(grep -q "<promise>COMPLETE</promise>" "$logfile" && echo "yes" || echo "no") if [ "$uncompleted" -eq 0 ] && [ "$has_promise" = "yes" ]; then echo "All tasks verified complete after $i iterations." exit 0 fi done メインループでは、毎イテレーションで仕様ファイルを読み込み、プロンプトに埋め込んでKiro CLIを実行します。終了条件は、 tasks.mdの未完了タスクがゼロ かつ AIによる <promise>COMPLETE</promise> の出力 の両方を満たす場合です。 実行オプションとリスク # Kiro CLIの実行には、以下の2つの重要なオプションを指定しています。 --no-interactive : 対話モードを無効化し、ユーザー入力を待たずに自動実行する --trust-all-tools : すべてのツール実行を自動承認し、コマンド実行の確認を求めない これらのオプションにより完全自律実行が可能になりますが、意図しないコマンドの実行リスクがあります。そのため、devcontainerなどの隔離環境での実行が必須です。後述の「気づきと教訓」でも述べるように、環境分離なしでの実行は推奨しません。 AIエージェントへのプロンプト # afk-ralph.sh(プロンプトテンプレート部分) build_prompt() { cat <<'PROMPT' 【要件】__REQ__ 【設計】__DES__ 【タスク一覧】__TASKS__ 【進捗】__PROGRESS__ 1. 要件と設計を理解する 2. タスク一覧と進捗を確認し、次の未完了タスクを見つける 3. そのタスクを実行する 4. 変更をコミットする 5. 完了後、tasks.md のチェックボックスを [ ] から [x] に更新する(必須) 6. progress.txt に完了した内容を追記する(必須) 1回の実行で1タスクのみ実装すること npm run test は禁止。必ず npm run test:unit または npm run test -- --run を使う 常駐プロセスは禁止、必ず一回で終了するコマンドのみ実行すること (中略) 全タスク完了時のみ <promise>COMPLETE</promise> を出力すること tasks.mdに未完了タスク [ ] が残っている場合は絶対に <promise>COMPLETE</promise> を出力しないこと PROMPT } プロンプトには、仕様3種と進捗を埋め込むプレースホルダーと、AIエージェントへの詳細な実行制約を含めています。特に、常駐プロセスの禁止と終了条件の明確化が重要でした。 ステップ2:Kiro CLIでRalphループの実行 # devcontainer(VS Codeのコンテナベース開発)環境でシェルスクリプトを実行し、Ralphループを開始します。 $ ./afk-ralph.sh 10 START afk-ralph.sh loop iteration 1 # ... kiro-cliがタスク1を実装、コミット ... loop iteration 2 # ... kiro-cliがタスク2を実装、コミット ... ... All tasks verified complete after 7 iterations. 引数の 10 は最大イテレーション数です。各イテレーションでは、新しいコンテキストでKiro CLIが起動し、タスクを実行します。 上図は、タスク2.2と2.3を完了した後、イテレーション2に移行する様子です。 各イテレーションで以下を実行します。 仕様成果物3種と進捗を読み込み 次の未完了タスクを特定 タスクを実装し、テストを実行 変更をコミット tasks.mdのチェックボックスを更新 progress.txtに進捗を記録 気づきと教訓 # 環境分離は必須 # 自律実行は実行コマンドの全自動承認を前提とするので、何が起こるかわかりません。今回はdevcontainer環境で実行しました。進捗ファイルが複数箇所にできるなど、AIの行動を予測することの難しさを感じました。 待機モード・対話確認を消す # 自律実行のためには中断を挟まないようにする必要があります。今回はプロンプトの中で対話確認や待機を伴うコマンドの実行禁止を指示しました。 トークンを大量に消費する # 処理の都度、新しくセッションを立ち上げてゼロからインプットする行為を繰り返すので、消費トークンが従来よりも増えます。余裕のある環境で実行する必要があります。 まとめ # 完成したアプリケーションの基本機能は問題なく動作しましたが、商用製品と比較すると機能面での差は歴然です。それでも、夜中にスクリプトを起動して朝起きたら動くアプリケーションが完成していた体験は、AIエージェントの可能性を実感させるものでした。 今回は数10回のイテレーションで完了するシンプルな題材でしたが、数100回のイテレーションを要する複雑なアプリケーション開発にも挑戦してみたいと考えています。 今回開発したリポジトリは以下で公開しています。(予告なく公開停止する場合があります) https://github.com/hironori-maruoka/kiro-ralph AWS. Kiro CLI の紹介 . ↩︎ 16x Engineer. LLM Context Management Guide: Performance degrades with more context . ↩︎ The Ralph Wiggum Loop from 1st principles (by the creator of Ralph). YouTube . ↩︎ AWS. Kiro の紹介 . ↩︎ AIHero.dev. Getting Started with Ralph: Create your script . ↩︎