TECH PLAY

株式会社豆蔵

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

100

はじめに # アジャイルグループの石田です。 第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を良きパートナーとして活用することで、自分たちのスクラムをより強化していくことが、今後のアジャイル開発において重要になっていくのではないでしょうか。
はじめに # ビジネスソリューション事業部の塚野です。 皆さんはフロントエンド開発の際にコンポーネントのテストをどのように行っているでしょうか? 自分は最近になり、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 を選択した場合でしか利用できません。 ↩︎
はじめに # ビジネスソリューション事業部の塚野です。 本記事は「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 など幅広いフレームワークに対応しています。ご興味持たれた方は是非導入検討してみてはいかがでしょうか。
こんな人におすすめ # 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を作成します。
はじめに # 本記事では、GitHub Copilotのエージェント(Agents)およびインストラクション(Instructions)の設定方法について説明します。 Agents(エージェント)とは 特定のタスクや分野に特化した専門家としてCopilotをカスタマイズする機能です。 たとえば、バックエンド開発用、フロントエンド開発用など、異なる専門性を持つ複数のエージェントを定義し、状況に応じて使い分けることができます。 Issueにアサインしたり、VS Code上で選択して利用します。 Instructions(インストラクション)とは Copilotに対する共通のルールや制約を定義する機能です。 コーディング規約、命名規則、プロジェクト固有のベストプラクティスなどを記載し、すべての開発者が一貫したコード生成の支援を受けられるようにします。 エージェントと併用され、VS CodeでのコーディングやPull Requestのレビューなど、あらゆる場面で適用されます。 適用順序 # 以下の順序でルールが適用されます。競合するルールがある場合、数字の小さいルールが優先されます。 選択したエージェント(例: agents/backend.agent.md ) マッチするインストラクション(例: instructions/typescript.instructions.md ← applyToパターンに一致) 全体のインストラクション( copilot-instructions.md ) ファイルの配置 # GitHub Copilotが認識できるよう、下記のように配置します。 .github/ agents/ xxx.agent.md : 特定の分野(ロールなど)に合わせて定義するエージェントファイル(e.g. backend, frontend, test) copilot-instructions.md : 全体に適用されるルールや制約を定義するファイル instructions/ xxx.instructions.md : 特定の分野(テクノロジーなど)に合わせて定義するファイル。(e.g. typescript, python, react)※サブフォルダーで分類したくなりますが、フォルダー分けすると読み込まれません。 --> Caution AGENTS.mdについて .github/AGENTS.md (ディレクトリ直下)は GitHub CLI用 のファイルです。 VS Codeでは読み込まれませんので注意してください。 VS Codeでは agents/ ディレクトリ内の *.agent.md ファイルのみが有効です。 --> Information ワークスペースでの配置 VS Codeでマルチリポジトリ(複数のリポジトリを同時に開いて作業)する場合、 設定ファイルは ワークスペースのルートディレクトリ の .github/ に配置する必要があります。 各リポジトリに個別の設定を使いたい場合は、リポジトリごとに別のVS Codeウィンドウで開いてください。 .github/agents/sample.agent.md : ワークスペースルートに配置すると選択できます repo-A/.github/agents/sample.agent.md : 各リポジトリ配下のエージェントは読み込まれません repo-B/.github/agents/sample.agent.md ヘッダー部の用途 # 定義ファイルのヘッダー部に設定できるプロパティの一部を紹介します。 エージェント --- name: Backend Agents(TypeScript) description: This custom agent implements backend features using TypeScript. model: GPT-5.2 --- ※エージェント選択画面(VS Code) プロパティ 設定時 未設定時 name エージェント名として使用 拡張子を除いたファイル名をエージェント名として使用 description エージェントの説明として使用 空欄 model 使用するAIモデルを指定 デフォルトモデル インストラクション --- applyTo: "src/**/*.ts" # e.g. src配下のtsファイルを対象 --- プロパティ 設定時 未設定時 applyTo 指示を適用するファイルのパターンを指定(globパターン) すべてのファイルに適用 --> Information applyToの指定例 **/*.ts - すべてのTypeScriptファイル src/** - srcディレクトリ配下のすべてのファイル **/*.{js,ts} - JavaScriptとTypeScriptファイル 定義例 # エージェントおよびインストラクションの定義例を以下に示します。 これらを組み合わせることで、プロジェクトやタスクに最適化されたCopilotの動作を実現できます。 エージェント:バックエンド開発者 # .github/agents/backend-specialist.agent.md --- name: Backend Developer Agent description: NestJSを使用したバックエンド開発の専門家 --- # 役割 あなたはNestJSとTypeScriptを使用したバックエンド開発の専門家です。 # 技術スタック - **フレームワーク**: NestJS 11.x - **言語**: TypeScript 5.x - **データベース**: PostgreSQL - **ORM**: TypeORM - **テスト**: Jest # コーディング規約 - ヘキサゴナルアーキテクチャを遵守してください - DTOには必ずバリデーションデコレータを付与してください - 例外処理は適切なHTTPステータスコードを返すカスタム例外を使用してください # テスト方針 - 単体テストはすべてのServiceクラスに対して作成してください - テストカバレッジは80%以上を目標としてください 全体インストラクション:プロジェクト共通規約 # .github/copilot-instructions.md # コーディング規約 ## 共通ルール - **言語**: 日本語でコメントとドキュメントを記載してください - **命名規則**: - クラス名: PascalCase - 関数名・変数名: camelCase - 定数: UPPER_SNAKE_CASE - **インデント**: スペース2文字 - **文字列**: シングルクォートを使用 ## 禁止事項 - `any`型の使用は原則禁止(型定義を適切に行うこと) - `console.log`のコミットは禁止(ロガーを使用すること) - 機密情報のハードコードは厳禁 ## セキュリティ - 外部入力は必ずバリデーションを行うこと - SQLインジェクション対策を実施すること - 認証・認可が必要なエンドポイントにはガードを設定すること 分野別インストラクション:TypeScript専用ルール # .github/instructions/typescript.instructions.md --- applyTo: "**/*.ts" --- # TypeScript固有のルール ## 命名規則 - ファイル名: kebab-case ## 型定義 - 明示的な型注釈を優先してください - Utility Typesを活用してください(`Partial`, `Pick`, `Omit`など) - 複雑な型は`type`エイリアスで定義してください ```typescript // Good type UserProfile = { id: string; name: string; email: string; }; type UserProfileUpdate = Partial<Pick<UserProfile, 'name' | 'email'>>; // Bad const updateUser = (data: any) => { ... }; ``` ## 非同期処理 - `async/await`を使用してください(Promiseチェーンは避ける) - エラーハンドリングは`try-catch`で行ってください ## インポート順序 1. 外部ライブラリ 2. 内部モジュール(絶対パス) 3. 相対パス ```typescript // 外部ライブラリ import { Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; // 内部モジュール import { UserEntity } from '@/entities/user.entity'; import { CreateUserDto } from '@/dto/create-user.dto'; // 相対パス import { UserService } from './user.service'; ``` 運用上の注意事項 # VS Codeでのキャッシュ管理 # エージェントやインストラクションのファイルを変更した場合、初回ロードした内容がキャッシュされます。 変更を反映するには、以下のいずれかの操作が必要です。 チャットで変更したファイルを明記して再読み込みを促す(例: sample.agent.mdを変更したので再読み込みしてください ) 新しいチャットを開始する VS Codeを再起動する GitHubでのファイルサイズ制限 # エージェントファイルの文字数が30,000文字(バイト数ではなく、ヘッダー部は含まない)を超えると選択できなくなります。 適切な粒度でファイルを分割してください。 GitHub IssueでCopilotをアサイン後に表示されるダイアログ
はじめに # 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 . ↩︎
前回の記事「無料のOSSツールSysONで始めるSysMLv2モデリング(3)〜 Part Definitionの作成」では、Part Definitionを作成しました。 /blogs/2026/01/22/sysmlv2-tool-syson-partdef/ 本記事では、Part Usageを作成します。 本記事で使用する SysONは前回同様、v2025.8.0です。 SysONは現在も進化中ですので最新リリースの挙動とは異なる可能性があります。 ご了承ください。 モデリングの題材は、SysMLv2の仕様書 A Annex: Example Modelから拝借します。 "Figure 63. Variant engine4Cyl"を作成してみましょう。 新規のPart Usageを作成する # General Viewを右クリックして表示されるコンテキストメニューから "Structure" > "New Part"を選択すると、General Viewに "part1"が追加されます。 要素を追加する方法はもう1つあります。 左サイドバーのツリーでパッケージなどの要素の右にあるケバブアイコン(︙)をクリックします。 表示されたダイアログで Partを選択し、"CREATE"ボタンを押下することで指定した要素の中に新たな要素を追加できます。 追加した要素を General Viewにドラッグ&ドロップすれば、グラフィカル記法で表示できます。 Part DefinitionとPart Usageの間にdefinitionを作成する # 前回の記事の手順でPart Definitionを作成し、"Engine"に名前を変更します。 次に、Part Usageを作成し、"engine"に名前を変更します。 "engine"を選択し、4つの辺の外に表示された三角(>)を"Engine"までドラッグ&ドロップすると、コンテキストメニューが表示されます。 コンテキストメニューで"New Feature Typing"を選択すると、Part DefinitionとPart Usageの間にdefinitionを作成できます。 definitionは白抜き三角と2つの点が付いた線であらわします。 もう1セット、Part Definitionの"Cylinder"と Part Usageの"cylinders"も作成しましょう。 Part Usage間にcomposite-feature-membershipを作成する # "engine"と"cylinders"間にcomposite-feature-membershipを作成します。 "engine"を選択し、4つの辺の外に表示された三角(>)を"cylinder"までドラッグ&ドロップし、コンテキストメニューを表示します。 コンテキストメニューで"Add as nested Part"を選択すると、Part Usage間にcomposite-feature-membershipを作成できます。 "cylinders"から"engine"にドラッグ&ドロップの操作をした場合、コンテキストメニューで"Become nested Part"を選択すると同様に作成できます。 左サイドバーのツリー上で、”cylinders”を"engine"にドラッグ&ドロップすることによっても作成できます。 composite-feature-membershipは黒塗りひし形の付いた線であらわします。 Part Usage間にnoncomposite-feature-membershipを作成する # "engine"と"cylinders"間のrelationshipをnoncomposite-feature-membershipにします。 noncomposite-feature-membershipはusageが参照的であることをあらわします。 したがって、参照されている側のusageの設定を変更します。 参照されている"cylinders"を選択して、右サイドバーのDetailsにあるAdvancedタグを選択します。 ここにある Is Compositeのチェックを外してください。 すると Is Referenceにチェックが付き、エディタ上の"cylinders"のステレオタイプが"«ref part»"に変わります。 これと共に、"engine"と"cylinders"の間のrelationshipがnoncomposite-feature-membrship(白抜きのひし形)に変わります。 戻す場合は、Is Compositeにチェックを付けます。 UsageにMultiplicityを設定する # ”cylinders”のMultiplicityを"4..8"に設定します。 "cylinders"を選択して、F2キーやEditで直接編集できるようにします。 次に、"cylinders"を"cylinders[4..8]"に変更します。 すると、左サイドバーのツリーの"cylinders"内にMultiplicityRangeが追加されます。 MultiplicityRangeの中にはLiteralIntegerが2つあり、1つのValueは”4”、もう1つは"8"が設定されます。 "cylinders"を編集した際、"cylinders[4..8] : ShapeItems::Cylinder"に変わることがあります。 このとき ShapeItems Libraryの"Cylinder"がdefinitionとして選択されている状態になっています。 この場合は以下の手順で修正できます。 右サイドバーのDetailにある Typed byで設定されている"Cylinder"を削除 左サイドバーのツリーにある"cylinders"内の"FeatureTyping"をモデルから削除 "cylinders[4..8]"と"Cylinder"の間にdefinitionを再作成 Part Usage間にsubsettingを作成する # "engine"と"engine4Cyl"の間にsubsettingを作成します。 Part Usageを作成し、名前を"engine4Cyl"に変更します。 ”engine4Cyl”を選択して外側に表示された三角(>)を"engine : Engine"までドラッグ&ドロップします。 表示されたコンテキストメニューで”New Subsetting”を選択します。 subsettingは白抜き三角が付いた線であらわします。 Part Usage間にredefinitionを作成する # "cylinders[4]"のPart Usageを作成し、"cylinders[4..8]"との間にredefinitionを作成します。 "cylinders[4]"を選択し、外側に表示された三角(>)を”cylinders[4..8]”までドラッグ&ドロップします。 表示されたコンテキストメニューで"New Redifinition"を選択します。 redefinitionは白抜き三角と1本線が付いた線であらわします。 Viewを完成させる # "cylinder1[1]", "cylinder2[1]", "cylinder3[1]", "cylinder4[1]"の4つのPart Usageを作成します。 作成した4つのPart Usageと"cylinders[4]"の間にsubsettingを作成します。 また、先の4つのPart Usageに加えて、"cylinders[4]"と"engine4Cyl"の間にcomposite-feature-membershipを作成します。 "engine4Cyl"と"cylinders[4]"の間のcomposite-feature-membershipを選択します。 右クリックでコンテキストメニューを表示し、”Show/Hide” > "Hide"を選択します。 ”Engine”と"Cylinder"の2つのPart Definitionを(モデルから削除ではなく)ダイアグラムから削除すると、下図のようになります。 題材と「全く同じ」とはいきませんが、等価なモデルが作成できました。 次回予告 # 本記事では、Part Usage要素を作成し、構造をモデル化しました。 次回は、振る舞いのモデル要素である Action Definitionと Action Usageを作成します。
はじめに # 弊社では様々なメーカのロボットを使用してシステムを構築しています。ロボットコントローラとの通信部分は「出来て当たり前」の機能です。ここのインテグレーションコストを抑え、ビジョンやハンドといったシステム固有の機能開発にフォーカスしたいという課題がありました。 一方で、産業用ロボットコントローラのプロトコル仕様はPDFで配布されていることが多く、LLMへの入力にはマークダウン化が必要です。マークダウン化しても理解にはドメイン知識を要したり、Webに活用事例のような情報が少なくLLMの学習データが不足していたりと、別途コンテキストの入力が必要なケースも多いです。 そこで今回、コントローラ通信プロトコルとクライアントの使用方法を Agent Skills として整備し、LLMにコントローラ通信コードを書かせる取り組みを行いました。 今回は安川ロボットのHSES(High-Speed Ethernet Server)プロトコル向けにスキルを作成し、Rust製クライアント moto-hses と組み合わせて検証しました。 通信仕様やクライアントの使用方法をAgent Skillsの形式で提供することで、Webに活用事例がなくてもLLMが適切なコードを生成してくれます。まだまだ内容は成熟していませんが、スキルを活用・改善してゆくことでコントローラとの通信コードはLLMが自動で実装してくれつつあります。また、通信の障害が発生した際にパケットデータと通信プロトコルを照合してデバッグするといった使い方も可能であり、コード生成から保守までLLMへ任せられるようになってきました。 安川電機が提供する標準SDK # 安川電機のロボットコントローラと通信する手段としてメーカからは以下の3つのSDKが提供されています。 項目 MotoCom32 / MotoComES MotoPlus YMConnect 概要 PCからEthernet経由でロボットコントローラへアクセスするための従来型通信SDK。外部PC上で実行。 コントローラ内部で動作するユーザアプリをC言語で開発するための組込みSDK。 MotoComの後継。クロスプラットフォーム対応の新世代通信SDK。外部PC上で実行。 対応OS Windows(32bit/64bit) 専用RTOS(ロボットコントローラ内で動作) Windows 10+ / Ubuntu 22.04+ 対応言語 C / C++ / VB6 / .NET C C++17 / C# (.NET 10) 動作場所 外部PC(ホスト側) コントローラ内(組込み側) 外部PC(ホスト側) 通信方式 Ethernet(TCP/IP) 内部API(コントローラOSと直接連携) Ethernet(TCP/IP) 主な用途 監視・I/O制御・ジョブ起動など外部制御 高速制御・カスタム動作・外部通信タスク 監視・I/O制御・ジョブ起動など外部制御 有償 / 無償 有償(USBドングルによるHWライセンス。実行環境ごとに必要) 有償(開発ライセンスのみ。実行環境は不要) 無償(Apache License 2.0) 特徴 Windows専用、歴史が長く安定だが新機能は更新停止傾向。 最も自由度が高く、リアルタイム処理・外部通信も可能。 マルチプラットフォーム・モダンAPI設計。 配布元 Yaskawa Electric(販売契約が必要) Yaskawa Electric(契約した開発者のみ) GitHub MotoPlusの場合は、コントローラ内部で動作するアプリとPC側の通信クライアントをそれぞれ自身で開発する必要があります。そのため、提供されている通信クライアントとしてはMotoComとYMConnectの2択となります。 YMConnectは比較的最近(2024年)に公開されたSDKです。C++17以降や.NET 10以降を使用可能なモダンなプロジェクトならYMConnectが良さそうですが、既存のレガシーシステムではMotoComを使用し続けているケースも多いのではないでしょうか。YMConnectの活用事例はまだほとんど見かけません。しかし、 YMConnectのDiscussions を見ると少しずつ不具合報告も挙がってきているので、徐々に採用実績も増えてくるのではないかと思います。 一方で安川ロボットのコントローラは High-Speed Ethernet Server (HSES) というサーバー機能を提供しており、通信プロトコルも公開されています( FS100 HSES Manual (PDF) )。 MotoCom(恐らくYMConnectも)はHSESの通信クライアントとして安川から提供されたSDKであり、同等のクライアントは内製することができます。 今回は上位アプリケーションがRustであったこと、レガシーシステムでも使用したいこと、LLM駆動の開発に必要なモックサーバー機能が欲しかったことから、自作したRustクライアントを使用しています。 moto-hses: Rust製HSESクライアント # moto-hses は、安川ロボットコントローラのHSES (High-Speed Ethernet Server) プロトコルに対応したRust製の非同期通信クライアントライブラリです。 --> moto-hses自体もLLMで開発 実はこのクライアント自体もLLMで開発しました。プロトコル仕様PDFをマークダウン化したドキュメントと、リファレンスとなる別言語のクライアントコードをコンテキストとして入力しています。LLMに対するガードレールや自動フィードバックの仕組みを整備しながら開発を進めました。同様のアプローチでC#向けクライアントなども作成できそうです。この開発プロセスについては、機会があれば別の記事で紹介したいと思います。 特徴 # 型安全 : Rustの型システムを活用した安全なAPI設計 非同期処理 : Tokioランタイムを使用した非同期UDP通信 スレッドセーフ : SharedHsesClient による複数タスクからの並行アクセスに対応 テスト容易性 : モックサーバー ( moto-hses-mock ) による統合テストが可能 --> モックサーバーの重要性 安川が提供するロボットシミュレータ(MotoSim EG-VRC)はHSESサーバー機能を有していません。そのため、これまでは実機のロボットコントローラを使用して通信検証する必要がありました。moto-hsesのモックサーバーを使えばローカル環境やCIで通信コードのテストが可能です。ローカルで完結して通信検証できることは、LLMへ自動でフィードバックする仕組みを構築する上でも非常に重要な要素となります。 クレート構成 # クレート 説明 moto-hses-proto プロトコル定義とシリアライゼーション moto-hses-client Tokioベースの非同期UDPクライアント moto-hses-mock テスト用のローカルモックHSESサーバー 対応コマンド # 現在、以下のロボット制御コマンドに対応しており、逐次追加中です。 コマンドNo コマンド名 0x70 アラームデータ読み出し 0x71 アラーム履歴読み出し 0x72 ステータス情報読み出し 0x73 実行中ジョブ情報読み出し 0x75 ロボット位置データ読み出し 0x78 I/Oデータ読み書き 0x79 レジスタデータ読み書き 0x7A〜0x7E 各種変数(B/I/D/R/S型)読み書き 0x82 アラームリセット / エラーキャンセル 0x83 ホールド / サーボON/OFF 0x84 ステップ / サイクル / 連続切替 0x86 スタートアップ(ジョブ起動) 0x87 ジョブ選択 その他、ファイル操作コマンド(削除、保存、一覧取得)や複数データの一括読み書きコマンドにも対応しています。 基本的な使い方 # use moto_hses_client::HsesClient; use moto_hses_proto::AlarmAttribute; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // クライアント作成 let client = HsesClient::new("192.168.0.3:10040").await?; // アラームデータ読み出し let alarm = client.read_alarm_data(1, AlarmAttribute::All).await?; println!("Alarm Code: {}", alarm.code); println!("Alarm Name: {}", alarm.name); // アラームリセット client.reset_alarm().await?; println!("Alarm reset completed"); Ok(()) } Agent Skills によるLLM支援 # Agent Skills は、AIコーディングエージェントに特定のドメイン知識や使用方法を教えるためのフォーマットです。スキルは、SKILL.md(エージェントへの指示)、references/(参考ドキュメント)、scripts/(自動化スクリプト)で構成されます。 今回、moto-hsesを活用するために以下の3つのスキルを作成しました。 スキル 説明 hses-protocol HSESプロトコル仕様。メッセージ構造、コマンドフォーマット、エラーコードなど moto-hses-usage moto-hsesクレートの使用ガイド。クライアント操作、コマンドリファレンスなど hses-packet-analysis HSESパケットの解析ガイド。通信障害時のデバッグに活用 スキルのインストール # 作成したスキルは GitHubリポジトリ で公開しています。Vercelが提供するスキルインストーラー add-skill を使用して、プロジェクトにスキルを導入できます。 # Cursorの場合 npx add-skill masayuki-kono/agent-skills -s hses-protocol moto-hses-usage hses-packet-analysis -a cursor -y # Claude Codeの場合 npx add-skill masayuki-kono/agent-skills -s hses-protocol moto-hses-usage hses-packet-analysis -a claude-code -y インストールすると、プロジェクトに以下のようなディレクトリ構造でスキルが配置されます。 .agents/ └── skills ├── hses-packet-analysis │ └── SKILL.md ├── hses-protocol │ ├── references │ │ ├── data-types.md │ │ ├── error-codes.md │ │ ├── protocol-overview.md │ │ └── ... │ └── SKILL.md └── moto-hses-usage ├── references │ ├── examples │ │ ├── alarm_operations.rs │ │ ├── job_start.rs │ │ ├── read_status.rs │ │ └── ... │ └── protocol-commands.md └── SKILL.md Cursorの場合は .cursor/skills/ 配下にシンボリックリンクが作成され、AIエージェントがスキルを参照できるようになります。add-skillの詳しい使い方については 公式リポジトリ を参照してください。 スキルをインストールすると、AIエージェントがHSESプロトコルを理解し、moto-hsesを使った適切なコードを生成できるようになります。 Agent Skillsを使ったコード生成デモ # スキルの効果を検証するため、Cursor Agentにコードを生成させました。生成したコードは moto-hses-examples リポジトリで公開しています。 生成プロンプト # 以下のシンプルなプロンプトを入力しました。 moto-hsesを使用したRustサンプルアプリケーションを開発してください。アプリ起動時にサーボをONにして、指定したジョブを起動してください。ロボットコントローラのIPアドレスはコマンドライン引数で指定できるようにしてください。 生成されたアプリケーション # 上記プロンプトから、Cursor Agentが以下の機能を持つアプリケーションを自動生成しました。 コマンドライン引数でロボットコントローラのIPアドレスとジョブ名を指定 ロボットコントローラへ接続 サーボをONに設定 指定されたジョブを選択して起動 起動状態を確認して結果を表示 実行例 # # ロボットコントローラ(192.168.0.18)に接続し、ジョブ "TEST" を起動 cargo run -- 192.168.0.18 TEST 実行すると以下のような出力が得られます。 [2026-01-26T21:50:24Z INFO moto_hses_examples] Connecting to robot controller: 192.168.0.18:10040 [2026-01-26T21:50:24Z INFO moto_hses_examples] ✓ Successfully connected to controller [2026-01-26T21:50:24Z INFO moto_hses_examples] Reading initial status... [2026-01-26T21:50:24Z INFO moto_hses_examples] ✓ Status read successfully [2026-01-26T21:50:24Z INFO moto_hses_examples] - Running: false [2026-01-26T21:50:24Z INFO moto_hses_examples] - Servo ON: true [2026-01-26T21:50:24Z INFO moto_hses_examples] - Alarm: false [2026-01-26T21:50:24Z INFO moto_hses_examples] - Error: false [2026-01-26T21:50:24Z INFO moto_hses_examples] Turning servo ON... [2026-01-26T21:50:24Z INFO moto_hses_examples] ✓ Servo ON command sent successfully [2026-01-26T21:50:25Z INFO moto_hses_examples] ✓ Servo is now ON [2026-01-26T21:50:25Z INFO moto_hses_examples] Selecting job 'TEST'... [2026-01-26T21:50:25Z INFO moto_hses_examples] ✓ Job 'TEST' selected successfully [2026-01-26T21:50:25Z INFO moto_hses_examples] Starting job 'TEST'... [2026-01-26T21:50:25Z INFO moto_hses_examples] ✓ Job start command sent successfully [2026-01-26T21:50:25Z INFO moto_hses_examples] ✓ Job 'TEST' started successfully 自作のクライアントライブラリであり、Web上に活用事例がほとんど存在しない状況でも、Agent Skillsによってドメイン知識を補完することで、LLMが適切なコードを生成できることが確認できました。 Agent Skillsを使ったパケット解析デモ # 次に、通信障害時のデバッグにスキルを活用する例を紹介します。hses-packet-analysis スキルは、tsharkでパケットをキャプチャし、hses-protocol スキルのプロトコル仕様と照合してレポートを出力します。このようにスキル間で連携することで、複雑な解析タスクにも対応できます。 障害シナリオの作成 # 検証のため、Status Reading(0x72)コマンドの応答パケットをモックサーバー側で意図的に不正なデータに書き換えて返信してみます。 Status Reading の Data 1 フィールドは 4バイト(32ビット)ですが、有効なステータスビットは下位8ビットのみ使用されます。 ビット 内容 Bit 0 Step モード Bit 1 One Cycle モード Bit 2 Continuous モード Bit 3 Running(動作中) Bit 4 Speed Limited Bit 5 Teach モード Bit 6 Play モード Bit 7 Remote モード Bit 8-31 未使用(常に0であるべき) 仕様違反の内容 Data 1 の上位バイト(Bit 16-23)に値 0x01 を設定し、定義された値域を超過させます。 期待値: [0x00][0x00][0x00][0x00] (上位3バイトは常に0) 実際: [0x00][0x00][0x01][0x00] (3バイト目に0x01) ↓ ↓ ↓ ↓ Bit Bit Bit Bit 0-7 8-15 16-23 24-31 ↑ 不正な値 この状態で先ほど生成したアプリケーションを実行すると、以下のエラーログが出力されます。 [2026-01-27T21:18:54Z INFO moto_hses_examples] Connecting to robot controller: 192.168.0.18:10040 [2026-01-27T21:18:54Z INFO moto_hses_examples] ✓ Successfully connected to controller [2026-01-27T21:18:54Z INFO moto_hses_examples] Reading initial status... [2026-01-27T21:18:54Z ERROR moto_hses_examples] ✗ Failed to read status: Protocol error: deserialization error: Invalid status word value Error: ProtocolError(Deserialization("Invalid status word value")) Invalid status word value エラーが発生しました。ここで hses-packet-analysis スキルを使ってLLMにパケット解析をしてもらいましょう。 解析プロンプト # 以下のプロンプトを入力しました。 cargo run -- 192.168.0.18 TEST が失敗します。パケット解析してマークダウンに結果を出力して。 解析結果 # 以下の解析レポートが出力されました。スキルはパケットキャプチャを実行し、プロトコル仕様と照合して問題箇所を特定しています。 --> レポートの構成 出力されるレポートには、プロトコル検証結果、シーケンス図、パケット詳細(バイナリ解析)、推奨対応を含みます。今回は Status Data 1 の bit 16 に不正な値がセットされているという仕様違反を原因として特定しました。 HSES Protocol Analysis Report # Generated: 2026-01-28 06:19:36 Capture File: hses_capture_20260128_061936.pcap Export File: hses_export_20260128_061936.txt 1. Summary # Metric Value Total Packets 2 Requests 1 Responses 1 Errors 0 (protocol level) Duration 0.000136 sec Issue Detected: Application error "Invalid status word value" - the robot controller returned a status value with an unexpected bit set (bit 16) that is not defined in the protocol specification. 2. Protocol Validation # Check Result Details Magic bytes ✅ Pass Both packets start with "YERC" (0x59455243) Header size ✅ Pass All headers are 32 bytes (0x0020) Reserved (byte 8) ✅ Pass Value is 0x03 Reserved (bytes 16-23) ✅ Pass Value is "99999999" Division ✅ Pass 0x01 (Robot) ACK field ✅ Pass Request=0x00, Response=0x01 Block number ✅ Pass Request=0, Response=0x80000000 (single) Request/Response pairs ✅ Pass 1 request matched with 1 response Service code match ✅ Pass Response service (0x81) = Request service (0x01) + 0x80 Command ID ✅ Pass 0x72 (Status reading) is valid Response status ✅ Pass 0x00 (Normal reply) Payload validity ⚠️ Warning Status word contains undefined bit 16 Payload Analysis Issue # The response payload contains status data with an unexpected value: Expected: Status Data 1 should only use bits 0-7 (bits 8-15 are reserved) Received: 0x00010000 (bit 16 is set) Consequence: moto-hses library rejects this value as "Invalid status word value" 3. Sequence Diagram # sequenceDiagram participant C as Client participant R as Robot Controller C->>R: Status Reading (0x72) Instance=1 R-->>C: Status=0x00 (OK), Data=0x00010000 Note over C: Deserialization failed:<br/>Invalid status word value 4. Communication Sequence # # Time (sec) Direction Command Instance Status Description 1 0.000000 Request Status Reading (0x72) 1 - Read Status Data 1 2 0.000136 Response Status Reading - 0x00 (OK) Returned status=0x00010000 5. Protocol Errors # No protocol-level errors detected. However, an application-level deserialization error occurred. # Time Command Issue Meaning 2 0.000136 Status Reading Status word bit 16 set Robot returned status data with undefined bit, causing deserialization failure Root Cause Analysis # The robot controller returned Status Data 1 with value 0x00010000 , which has bit 16 set. According to the HSES protocol specification: Status Data 1 (Command 0x72, Instance 1) bit definitions: Bit Meaning 0 Step mode 1 One-cycle mode 2 Continuous mode 3 Running 4 Speed limited 5 Teach mode 6 Play mode 7 Remote mode 8-15 Reserved Bit 16 is not defined in the specification. The moto-hses library strictly validates status values and rejects undefined bits. Possible causes: Newer firmware version with extended status bits not yet documented Controller-specific extension to the protocol Memory/data corruption on the controller 6. Packet Details # Packet 1 - Request # Field Value Description Time 0.000000 Start of capture Type Request ACK=0x00 Magic YERC 0x59455243 Header Size 32 0x0020 Payload Size 0 No payload Division Robot 0x01 Request ID 1 Session ID Block Number 0 Request block Command ID 0x0072 Status reading Instance 1 Status Data 1 Attribute 0 Default Service 0x01 Get_Attribute_All Raw Hex: 59455243 2000 0000 03 01 00 01 00000000 3939393939393939 7200 0100 00 01 0000 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Padding │ │ │ │ │ │ │ │ │ │ │ │ └─ Service (Get_Attribute_All) │ │ │ │ │ │ │ │ │ │ │ └─ Attribute │ │ │ │ │ │ │ │ │ │ └─ Instance (1) │ │ │ │ │ │ │ │ │ └─ Command ID (Status reading) │ │ │ │ │ │ │ │ └─ Reserved "99999999" │ │ │ │ │ │ │ └─ Block Number (0) │ │ │ │ │ │ └─ Request ID (1) │ │ │ │ │ └─ ACK (Request) │ │ │ │ └─ Division (Robot) │ │ │ └─ Reserved (0x03) │ │ └─ Payload Size (0) │ └─ Header Size (32) └─ Magic "YERC" Packet 2 - Response # Field Value Description Time 0.000136 136μs after request Type Response ACK=0x01 Magic YERC 0x59455243 Header Size 32 0x0020 Payload Size 8 Status data Division Robot 0x01 Request ID 1 Matches request Block Number 0x80000000 Single response Service 0x81 Get_Attribute_All + 0x80 Status 0x00 Normal reply Added Status Size 2 Added Status 0x0000 No error Payload Data: 00000100 00000000 │ └─ Status Data 1 part 2: 0x00000000 └─ Status Data 1 part 1: 0x00010000 (bit 16 set - UNEXPECTED) Raw Hex: 59455243 2000 0800 03 01 01 01 00000080 3939393939393939 81 00 02 00 0000 0000 | 00000100 00000000 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Payload (8 bytes) │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Padding │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Added Status (0x0000) │ │ │ │ │ │ │ │ │ │ │ │ └─ Padding │ │ │ │ │ │ │ │ │ │ │ └─ Added Status Size (2) │ │ │ │ │ │ │ │ │ │ └─ Status (0x00 = OK) │ │ │ │ │ │ │ │ │ └─ Service (0x81 = 0x01 + 0x80) │ │ │ │ │ │ │ │ └─ Reserved "99999999" │ │ │ │ │ │ │ └─ Block Number (0x80000000 = single) │ │ │ │ │ │ └─ Request ID (1) │ │ │ │ │ └─ ACK (Response) │ │ │ │ └─ Division (Robot) │ │ │ └─ Reserved (0x03) │ │ └─ Payload Size (8) │ └─ Header Size (32) └─ Magic "YERC" 7. Recommendations # Investigate controller firmware - Check if the robot controller has a newer firmware that uses extended status bits Update moto-hses library - Consider relaxing status validation to ignore unknown bits (mask with 0x00FF for Status Data 1) Contact Yaskawa - If the issue persists, consult Yaskawa support about the meaning of bit 16 in Status Data 1 シンプルなプロンプトから、パケットのバイナリデータをプロトコル仕様と照合し、原因を特定してくれました。このように、Agent Skillsを活用することで通信障害のデバッグ作業もLLMに任せることができます。 まとめ # 本記事では、安川ロボットコントローラのHSES通信クライアント(moto-hses)とAgent Skillsを組み合わせた取り組みを紹介しました。 コード生成 : moto-hses-usage スキルにより、LLMがmoto-hsesを使った適切な通信コードを自動生成 パケット解析 : hses-packet-analysis スキルにより、通信障害時のデバッグをLLMに委任 産業用ロボットのプロトコル仕様はPDFとして配布されていたり、ドメイン知識が必要だったりとLLMには扱いにくい情報ですが、Agent Skillsの形式に整備すればこの課題を解決できます。コード生成から保守・デバッグまで、一貫してLLMに任せられる環境が整いつつあります。 今後の展望 # 各社ロボットコントローラがROS2のようなフレームワークに対応し、共通I/Fで利用できるようになる未来も想定されますが、コントローラ側の歩み寄りが必要であり現実的には難しいと考えています。また、各社ロボットには様々な独自仕様(溶接のような用途別の機能など)があり、共通I/Fでは吸収しきれない部分も存在します。 コントローラのI/Fが異なっていてもスキルが提供されれば、必要とするアプリケーションの開発をLLMが行うことは可能です。各社ロボットコントローラに対する様々なスキルを作成してゆき、ロボットシステム開発においてLLMが担える部位を増やしてゆきたいと考えています。
前回の記事では、新しいプロジェクトとPackage要素を作成しました。 /blogs/2026/01/15/sysmlv2-tool-syson-pkg/ 本記事では、構造定義の要の1つである Part Definitionを作成します。 執筆時点における SysONの安定版は v2025.12.0が最新ですが、本記事では前回同様 v2025.8.0を使用します。 最新リリースの挙動は一部異なる可能性がありますのでご了承ください。 モデリングの題材は、SysMLv2の仕様書 A Annex: Example Modelから拝借します。 "Figure 59. Axle and its Subclass FrontA"を作成してみましょう。 新規のPart Definitionを作成する # 前回と同じように General Viewを表示し、右クリックで表示されるコンテキストメニューから "Structure" > "New Part Definition"を選択します。 すると、General Viewに Part Definitionが表示されます。 Part Definitionの名前を変更する # 作成した Part Definitionの名前を"Axle"に変更しましょう。 名前を変更する方法は2つあります。 後述しますが、この2つの方法はくわしくみると動作が異なります。 変更方法その1 # 1つは右サイドバーで変更する方法です。 対象の要素(PartDefinition1)を選択すると右サイドバーに Detailsが表示されます。 その Declared Nameの欄に表示されている名前を直接編集します。 変更方法その2 # もう1つは要素のコンテキストメニューやファンクションキーで変更する方法です。 コンテキストメニューを表示して上部に並んでいるアイコンの中で最も左にあるペンアイコンをクリックします。 ペンアイコンをクリックすると、要素内に表示された名前を直接編集できるようになります。 コンテキストメニューで、ペンアイコンではなく "Edit" > "Edit"を選択しても同様に名前を直接編集できるようになります。 要素を選択して F2キーを押下しても同様に名前を直接編集できます。 Part DefinitionにAttributeを追加する # 次は、"Axle"の attributesに"mass"を追加しましょう。 "Axle"を選択してコンテキストメニューを表示し、"Structure" > "New Attribute"を選択します。 "Axle"にattributes区画が表示され、その区画に"attribute1"が追加されます。 追加された"attribute1"を選択し、その名前を"mass :> ISQBase::mass"に変更します。 コンテキストメニューかファンクションキーを用いた方法で変更した場合、右サイドバーの Detailsに Subsetsの項目があらわれ、そこに massが表示されます。 "attribute1"の名前を左サイドバーの Detailsにある Declared Nameで変更した場合、図の見た目は同じですが Detailsに Subsetsの項目はあらわれません。 これはどういうことでしょうか。 前者(コンテキストメニューなどで変更)の場合は、subsetsの記号(:>)が解釈されて massが ISQBase::massの subsetsになります。 一方後者(Declared Nameで名前を変更)の場合は、単純に名前が"mass :> ISQBase::mass"に変更されます。 この違いは図の見た目だけではわからないため注意してください。 Part Definition間にSubclassificationを設定する # Axleと同様の手順でもう1つ Part Definitionを作成して名前を"FrontAxle"にします。 "FrontAxle"を選択した時に要素の上下左右に表示される三角(>)の位置にマウスをあわせるとマウスのポインタが十字(+)に変わります。 この状態で"Axle"にドラッグ&ドロップすると、relationshipを選択するコンテキストメニューが表示されます。 ここで、"New Subclassification"を選択すると、Subclassificationをあらわす白抜き矢印の線が表示されます。 加えて、"FrontAxle"の名前が"FrontAxle :> Axle"に変更されます。 Subclassificationを設定するもう1つの方法があります。 それは、"FrontAxle"をコンテキストメニューもしくはファンクションキーで"FrontAxle :> Axle"に変更する方法です。 変更すると、"FrontAxle"の表示が変わるとともに、Axleとの間に Subclassificationをあらわす線が表示されます。 "FrontAxle"に"steering"の attributeを追加します。 SysMLv2仕様書の"Figure 59. Axle and its Subclass FrontA"と等価な図ができました。 テキスト記法を用いたPart Definitionの追加 # ここまで作図でモデルを作成してきましたが、Packageと同様テキスト記述を用いたモデル作成もできます。 part def Axle { attribute mass:>ISQ::mass; } part def FrontAxle :> Axle { attribute steeringAngle :> ISQ::angularMeasure; } 作成された Axleと FrontAxleを General Viewにドラッグ&ドロップします。 attribute区画を表示するにはまず、Part Definitionの名前の右にマウスカーソルをあわせた際に表示される目のアイコンをクリックします。 表示された"Manage Visibility"コンテキストメニューの"attribute"にチェックを入れます。 次回予告 # 本記事では、Part Definition要素を作成し要素間に Subclassificationを設定しました。 次回は、Part Usageを作成し、Part Definitionとの関連付けを行います。
1. はじめに # 先日、業務日誌について書いた投稿がこちらになります。 /blogs/2026/01/06/businessdiary/ ただ、 ここは技術ブログなので、記事だけだと片手落ちな気がする。 自分は技術者なんだから、せっかくだから何か作りたい。 と思いましたので、業務日誌を題材にアプリを作りました。 (あくまで学習用のサンプルアプリになりますが) また、最近、社内研修で要件定義、設計、文章の書き方などを 学びましたので、練習を兼ねてこれらも実施しました。 そこで、本記事では、業務日誌アプリを題材に 要件定義〜設計をどのように整理したかを中心に紹介します。 私と同じようにアプリを作成するエンジニアの方向けに、設計の良し悪しではなく、作業の過程について共有できればいいなと思っています。 なお、実装したアプリそのものについては、本記事では割愛し、別記事で公開したいと思います。 2. アプリを開発する # まず、今回のゴールとしては、業務日誌を作成するアプリの必要最低限の機能のみを開発します。 前回のブログでSlackを使用して業務日誌を作成していることに触れていることも踏まえ、 今回はSlackのアプリを作成してみます(技術要素は別途、以下で整理します)。 開発は以下の手順で進めます。 要件定義 設計 製造・テスト(ブログ上では詳細割愛) 2-1. 要件定義 # 本記事で中心となる内容で、少し記述量が多くなってしまいましたが、 よろしければお付き合いいただければと思います😓 要件定義では、要望→要求→要件→仕様の順に作成していきます。 2-1-1. 要望を書いてみる。 # まず、要望ですが、要望とは「システム(今回はアプリ)で実現したいこと」となります。 とりあえず、業務日誌アプリでできそうなことを思いつくまま書き出してみます。 --> Information 業務日誌を自動で作成してほしい。 手書きは面倒なので、アプリで自動生成してほしい。 業務日誌を自動で報告してほしい。 Slackのチャネル投稿するなど、報告を自動化したい。 決められた時間に業務日誌を作成・報告してほしい。 定時前などスケジュールに基づいて業務日誌を作成・報告してほしい。 業務日誌の体裁を調整したい。 業務日誌のテンプレートを作成して、日誌の体裁を整えたい。 作業は随時登録したい。 業務日誌に記載する作業は、業務中につど登録したい。 (日誌を作成時に考えるのは、効率的ではないので、事前に登録しておきたい) 作業の状況を管理したい。 作業状況が分かるようにしたい。 登録した作業は保管しておきたい。 アプリを停止、再開した後も作業が参照できるようにしたい。 作成した業務日誌は保管しておきたい。 アプリを停止、再開した後も業務日誌が参照できるようにしたい。 業務日誌の要約したい。 作成済みの複数日の業務日誌を要約して、週報、月報、あるいは、評価面談用の資料としたい。 フィードバックがほしい。 作成した業務日誌の内容についてフィードバックがほしい。 (AIとかにレビューさせるとかできるかも) 2-1-2. 要求を書いてみる。 # 次に、要求ですが、要求とは「要望の内、実現するものとして採用されたもの」となります。 今回は、業務日誌アプリとして必要な最低限な要望のみを要求として選択します。 よって、要求は以下のとおりとします。 --> Information 作業を登録したい。 業務中に実施する作業を登録できるようにします。 登録した作業を確認したい。 登録した作業が確認できるようにしたい。 作業のステータスを設定したい。 登録済みの作業に対して、作業ステータスが設定できるようにします。 作業ステータスは、以下のとおりとします。 未着手 作業中 完了 業務日誌を作成したい。 登録済みの作業から業務日誌を作成します。 業務日誌には「当日実施した作業一覧」と「翌日実施する作業予定一覧」が登録されています。 登録した作業を保存しておきたい。 登録した作業が消失しないようにします。 作成した業務日誌を保存しておきたい。 作成した業務日誌が消失しないようにします。 1.-4.はいいかと思います。5.6.はちょっと違うような気もしますが、いったんよしとします。 登録と永続化って大抵はセットなので、どこかで1つにしたほうがいいとは思うのですが。 2-1-3. 要望のうち、要求に含めないもの。 # 要望として挙げたもののうち、要求としないものについても、一応明記するようにします。 --> Information 業務日誌の自動生成 業務日誌の生成はアプリの機能としますが、生成処理の起動は手動で行うこととします。 業務日誌の自動報告 報告は手動で行うこととします。 業務日誌のスケジュール生成 必須機能ではないので、対象外とします。 業務日誌のテンプレート 今回は業務日誌のテンプレートは決め打ちとします。 業務日誌の要約 必須機能ではないので、対象外とします。 業務日誌へのフィードバック 必須機能ではないので、対象外とします。 2-1-4. 要件を書いてみる。 # 続けて、上記の要求を基に要件を定義してみます。 要件とは「完成したシステムが実現することを定義したもの」となります。 --> Information ユーザーは、アプリの画面から作業が登録できなければならない。 ユーザーは、アプリの画面上で、登録した作業の一覧を確認できなければならない。 ユーザーは、アプリの画面から、登録済みの作業に対して、定義された作業のステータスを設定できなければならない。 ユーザーは、アプリに業務日誌の作成を指示できなければならない。 アプリは、登録済みの作業を基に業務日誌を生成し、その内容をユーザーに提示しなければならない。 ユーザーがアプリを停止、再開しても、停止前に登録した作業が参照できなければならない。 ユーザーがアプリを停止、再開しても、停止前に作成した業務日誌が参照できなければならない。 2-1-5. 仕様を書いてみる。 # 最後に、仕様を定義してみます。 仕様とは「網羅的に記述された要件」となります。 --> Information 作業の登録 ユーザーは、画面から作業内容を入力する。 アプリは、ユーザーが入力した作業内容に加え、以下の内容を合わせて登録する。 作業ID 登録日 作業ステータス 作業IDは、作業を一意に特定できる識別子とする。 登録時点の現在日を登録日として登録する。登録日の書式はyyyy/MM/dd とする。 登録日は、ユーザーが作業登録を実行した時点の日本標準時(JST)に基づく日付とする。 また、作業のステータスを未着手として設定する。 作業内容は必須入力とし、前後の空白文字を除去した結果が空文字列となる場合は無効とする。 作業内容が未入力の場合、アプリは登録が失敗した旨のメッセージを表示して、処理を終了する。 作業内容として入力可能な文字数の上限は100文字とする。この時、全半角は考慮せず、どちらも1文字として扱うこととする。 作業の一覧確認 ユーザーはアプリへ登録済みの作業の一覧を表示するように指示する。 アプリは画面上に登録済みの作業の一覧を表示する。 一覧は登録した順序(作業ID)の昇順で表示する。 一覧には作業内容、登録日、作業ステータスを表示する。 作業ステータスの変更 ユーザーは、対象となる作業を指定し、作業ステータスを変更できる。 作業ステータスは、未着手、作業中、完了の3つとする。 ユーザーが存在しない作業(作業ID)を更新しようとした場合、アプリは設定が失敗した旨のメッセージを表示して処理を終了する。 ユーザーが規定のステータス以外に更新しようとした場合、アプリは設定が失敗した旨のメッセージを表示して処理を終了する。 アプリはユーザーが入力した作業IDと作業ステータスで指定された作業のステータスを更新する。 ステータス間の遷移に制約は設けないこととする。ユーザーが指定したステータスを必ず設定するようにする。 業務日誌の生成 ユーザーは画面上から、アプリに対して業務日誌の生成を指示する。 アプリはユーザーの指示を受け、登録済みの作業から業務日誌を作成する。 業務日誌には以下の項目で構成される。 生成日 当日実施した作業の一覧 翌日実施する作業予定の一覧 生成日は、業務日誌を生成時の現在日を表示する。 生成日の表示形式は yyyy年MM月dd日(WeekDay)とする ※WeekDayは曜日で、月、火、水..といった値となる。 生成日は、日誌作成時点の日本標準時(JST)に基づく日付とする。 当日実施した作業の一覧には、次の条件をすべて満たす作業を含める。 当日中に作業ステータスを更新している。 作業ステータスが作業中、あるいは、完了である。 翌日実施する作業予定の一覧には、次の条件をすべて満たす作業を含める。 作業ステータスが未着手、あるいは、作業中のどちらかである。 作業の保存 ユーザーが作業を登録した際に、アプリは作業を永続化する。 ユーザーが作業の一覧を確認する際に、アプリは作業の一覧を永続化された作業から生成する。 ユーザーが作業のステータスを設定した際に、アプリは永続化された作業のステータスを更新する。 業務日誌の保存 アプリは業務日誌を生成した時に、生成した業務日誌を永続化する。 2-2. 設計 # 2-2-1. 方式設計 アプリ全体の設計方針について記載します。 設計フェーズなので、具体的な技術要素についても記載します。 --> Information アプリケーションはSlackアプリとして実装します。 アプリにUIが必要ですが、デスクトップアプリを作るのはちょっと大げさだと感じました。 業務では、Slackを利用していますので、Slackで業務日誌を作成できると、 作成→報告が一箇所でできるのでベターかと思いました Bolt for JavaScriptフレームワークを使用します。 データを永続化するために、DBを利用します。DBはSQLiteを採用します。 今回はローカルで動作するサンプルですので、 軽量なインメモリデータベースで十分だと思います。 永続化層にO/RマッパーとしてDrizzle ORMを採用します。 https://orm.drizzle.team/ これは個人的な好みです。 開発言語はTypeScript(JavaScript)を使用します。 前述のBoltフレームワークではPython、JavaScriptが利用できますが、 あいにく、Pythonは不得手ですので、得意なJavaScriptを使用します。 Denoは使用しないこととします。 Bolt for JavaScriptではDenoが利用でき、個人的にもDenoは嫌いではないのですが、 Denoがそこまで浸透しているかが不明ですので、今回はいったん見送ります。 2-2-2. 機能設計 仕様の5と6、登録した作業、業務日誌の保存(永続化)は それぞれ、1と4にマージします。 結果として機能が4つとなりますので、それぞれについて記載します。 --> Information 作業を登録する 作業内容の入力 作業内容はアプリのDMにメッセージとして入力します。 作業内容の検証 作業内容が未入力の場合、エラーメッセージを表示します。 エラーメッセージはアプリからの返信として表示します。 作業内容が100文字を超過する場合、エラーメッセージを表示します。 エラーメッセージはアプリからの返信として表示します。 作業内容の登録 作業内容の登録はSlackアプリのコマンドを実行することで起動します。 作業内容をDBに保存します。 保存する内容は、入力した作業内容、登録日、初期作業ステータス(未着手)になります。 作業を確認する 作業一覧を表示する。 コマンドを実行することで作業の一覧を表示するようにします。 作業の一覧の各項目は、作業内容、ステータス、設定ボタンの4項目とします。 ステータスは変更できるように、セレクトボックスで表示します。 ステータスの初期値は登録済みの作業ステータスの値となります。 作業の一覧は登録順(ID順)の昇順とします。 作業が未登録の場合は、エラーメッセージを表示します。 エラーメッセージはアプリからの返信として表示します。 作業ステータスを設定する 作業ステータスの更新 作業一覧に表示されている、設定ボタンを押下することで、 作業ステータスの値を選択している値に設定します。 業務日誌を生成する 業務日誌を生成する。 業務日誌は、文章(テキスト)とします。 業務日誌には、当日実施した作業の一覧、および、翌日実施した作業の一覧を含めます。 当日実施した作業の一覧は、作業ステータスが作業中、あるいは、完了となっている作業となります。 翌日実施した作業の一覧は、作業ステータスが未着手、あるいは、作業中となっている作業とします。 該当する作業が存在しない場合は、作業の一覧ではなく、テキスト”作業が存在しません”を業務日誌に含めるようにします。 コマンドを実行することで業務日誌を生成します。 生成した業務日誌は、アプリからの返信として表示します。 業務日誌の保存 生成した業務日誌はDBへ保存します。 登録内容は、業務日誌ID、登録日、業務日誌の3項目とします。 2-3. 図 # 設計の練習を兼ねて、作図も行います。 2-3-1. ユースケース図 2-3-2. ドメインモデル 2-3-4. ロバストネス図 3. 作成したアプリについて # アプリを作成したので、ここでは動作結果のみを記載します。 ※アプリ自体については別の記事で言及できればと考えています。 3-1. 機能の動作確認 # 3-1-1. 作業を登録する # まずは、作業を登録します。 /task_add コマンドに、引数として作業内容を設定して送信。 登録に成功するとメッセージが返信されます。 3-1-2. 作業を確認する # 次に、登録した作業を/task_listコマンドで表示します。 一覧表示は引数なしのコマンドとなります。 一覧が登録順に表示されます。 また、ステータスが更新できるようにステータスをセレクトボックスで表示し、更新処理を実行するためのボタンを配置しました。 なお、動作確認用に3つのタスクを登録しています。 3-1-3. 作業ステータスを設定する # さらにステータスを変更してみます。 3つの作業のうち、1つを作業中に、もう1つを完了に変更します。 アプリ上でステータスを変更して、更新ボタンをクリックすると、 ステータスが更新され、メッセージが返信されます。 3-1-4. 業務日誌を作成する # 最後に業務日誌を生成してみます。 こちらは/businessDiary_createコマンドを実行します。 コマンドの受け付けに対して、応答を返した後、 生成された業務日誌が表示されます。 当日の作業一覧に作業中、あるいは、完了の作業が表示されます。 また、翌日の作業予定一覧に未着手、あるいは、作業中の作業が表示されます。 4. まとめ # 本記事では、業務日誌を題材に、アプリケーションの要件定義、設計、実装を行ってみました。個人の学習の一環として取り組んだ内容の整理になりますが、 同じように設計に悩みながらアプリを作っている方の参考になれば幸いです。 実際に取り組んでみた所感としては、以下のように感じました。 業務外の作業なので、所々手抜きもあるのですが、 それを別にしても、まだまだ至らない点があるので、こういった作業を通じて、能力向上ができればなと思いました。 また、こういったサンプル程度のアプリでも、実際に要件定義・設計すると、 結構なボリュームになったなと感じています。 ただ、アプリの開発自体は(体感としては)スムーズに進んだようにも思えましたので、こういった要件定義、設計のプロセスというのは無駄ではないと思いました。 以前、ローコードツールを使用した開発を行っていましたが、ツールの「生成した成果物(コード)= 設計書」という思想の影響で、設計を行わずに実装することが多かったです。 ですが、今回のように設計 → 実装の流れで作業すると、ローコードでなくても、事前に設計を実施してから実装に着手したほうが作業が捗るように感じました。理由としては、実装中にトライ・アンド・エラーで作業するよりも、事前に設計を文書化するほうが、仕様をよりよく理解できるためだと思います。 業務日誌アプリについては、今後、技術習得も兼ねて、機能拡張していければと考えています。また、採用した技術要素についても、別途記事として公開できればと考えています。
はじめに # アジャイルグループの石田です。 第1回:導入 の続編です。前回は、 スクラムガイド拡張パック を参考に、AIがスクラムを強化する可能性の一つとして「経験的プロセス制御」の強化について触れました。 スクラムマスターとして、スクラムというプロセスにAIを活用することで、チームが実践するスクラムの三本柱「透明性・検査・適応」をより強化することができます。 透明性へのアプローチ # 本記事では、三本柱の第一歩である「透明性」に着目します。ここでいう透明性とは、単に見えるようにすることではなく、チームが正しい判断(検査・適応)を行うための材料を揃えることに他なりません。 今回はAIにデータの分析そのものを依頼するのではなく、「可視化のためのツール作成(コーディング)をAIに任せる」というアプローチを紹介します。 具体的には、JiraのデータをGoogle Apps Script (GAS)で取得・加工し、可視化するプロセスをAIと共に実装した事例をご紹介します。 なぜAIに「可視化ツール」を作らせるのか # スクラムマスターがチームの状態を正しく観測するために、またチームメンバー自身が自分たちを客観視するために、定量的なデータは不可欠です。 Jiraなどのプロジェクト管理ツールには標準的なレポート機能が備わっていますが、現場では「もう少しこの切り口で見たい」「外部スプレッドシートの情報と突き合わせたい」といった、標準機能ではカバーしきれないニーズが頻繁に発生します。 Jiraには強力なAPIが用意されており、データを自由に扱うことが可能ですが、開発専任ではないスクラムマスターにとって、ゼロからツールを開発するのは学習コストや時間の面でハードルが高いのが現実です。 そこで活用したいのが、生成AIにコードを書かせ、「スクラムマスター専属のエンジニア」として振る舞わせる方法です。これにより、技術的な壁を取り払い、スクラムマスターとしての機動力を劇的に高めることができます。 実際のプロダクトコードにAI生成コードをそのまま使うことには保守性の観点から慎重な議論が必要ですが、チーム内部で使う一時的なツールやダッシュボードを作るという観点では、積極的に活用してこそ価値が発揮される領域だと考えます。 ここから先は、実際に業務で行った生成AIでのチーム可視化の実例をご紹介します。 実践例1:Four Keysによる開発スピードと安定性の可視化 # Four Keysとは # まずは、今回可視化の対象とした「Four Keys」について簡単に触れておきます。 Four Keysは、GoogleのDevOps Research and Assessment(DORA)チームが提唱した、ソフトウェア開発チームのパフォーマンス(デリバリー能力)を測定するための4つの指標です。 デプロイの頻度 (Deployment Frequency): 本番環境へのリリースの頻度。 変更のリードタイム (Lead Time for Changes): コードがコミットされてから本番環境で稼働するまでの時間。 変更障害率 (Change Failure Rate): デプロイが原因で障害が発生した割合。 サービス復元時間 (Time to Restore Service): 障害発生から復旧までの時間。 これらは、開発の「スピード(1と2)」と「安定性(3と4)」を示す指標です。 これらの指標を計測することで、チームが速く、かつ安定して価値を届けられているかを客観的に把握できます。 指標の定義は明確ですが、いざ自分たちの環境で計測しようとすると、いくつかの壁にぶつかりました。 まず、変更のリードタイム(開発着手からリリースまでの時間)はJiraのステータス遷移履歴から計算可能ですが、標準のレポート機能だけで算出するのは難しく、APIによるデータ取得と計算処理が必要になります。 また変更障害率については、今回のプロジェクトでは商用障害をGoogleスプレッドシートで管理しているため、そのデータとJiraのリリース情報を突き合わせて計算する必要がありました。 このように散在するデータを集計し、Four Keysダッシュボードを構築するためにGoogle Apps Script (GAS)を採用しました。そして前述の通り、そのコード作成は生成AIに任せました。 1. Jiraから「スピード」を抽出する まず、Jira APIを通じてチケットデータを取得するスクリプトをAIに作成させました。 プロンプトのポイントは、「ステータスの遷移履歴」に着目させたことです。 プロンプトのイメージ: 「Jiraの特定のプロジェクトから、完了したチケット情報を取得したい。各チケットが『In Progress』になった日時と、そのチケットを含む リリースが完了になった日時 を抽出し、その差分をリードタイムとして計算するGASの関数を書いてください。」 これにより、チケットごとのリードタイムと、月ごとのリリース回数(デプロイ頻度)が自動集計できるようになりました。 2. スプレッドシートから「安定性」を計算する 次に、障害管理をしているスプレッドシートへのアクセスです。 こちらは、月ごとの障害発生件数をカウントし、先ほど算出したデプロイ回数と突き合わせることで「変更障害率」を算出させます。 これらを最終的に一つのスプレッドシート上のグラフとして描画することで、チームの「Four Keys」が日次で更新されるダッシュボードが完成しました。 こうして最新のFour Keysをチーム全員がいつでも見られる状態にしたことで、自分たちのリリース頻度や、それに伴う品質への影響(障害)がひと目でわかるようになり、チームの「スピード」と「安定性」が透明化されました。 実践例2:リリース必達案件における「加速」の可視化 # 2つ目は、少しスクラムの枠を超えた対応についてです。 プロジェクトの特性上、リリース日が固定されている「必達案件」があり、その対応に追われていました。本来のスクラムであれば、ベロシティの実績に基づいてスコープを調整すべきですが、この案件ではスコープも固定されており、従来のベロシティのままではリリースに間に合わないことが判明しました。 そこで現実的な対応として、開発者の稼働(人数や時間)を一時的に増やし、ペースアップを図るという決断をしました。 しかし、単に稼働を増やしたといっても、それによって開発スピードがどれだけ上がったのか、今のペースで本当に期日に着地できるのかは、従来のバーンダウンチャートだけでは直感的に分かりづらい状態でした。 「頑張ってはいるが、間に合うかどうかわからない」という不安な状態は、チームの士気を下げてしまいます。 そこで、ここでもAIを活用しました。 Jira APIから取得した「日々の消化ポイント数(実績)」をベースに、残作業を消化するのにあと何日かかるかを計算し、「予測完了日」の推移をプロットするスクリプトをAIに作成させました。 これにより、稼働を上げたあとにグラフの傾きが急になり(消化ペースが上がり)、予測完了日がリリース日手前まで近づいてくる様子が可視化されました(下の図はイメージです)。チームとステークホルダーに対し「現在の加速具合なら○日に終わる」という明確な見通しを提示できたことで、漠然とした不安を払拭し、開発メンバーが自信をもって仕事を進める手助けをすることができました。 AIがもたらす「スクラムマスターの機動力」 # これまで「こういうデータがあったらいいな」と思っても、技術的なスキル不足や時間の制約で諦めていた可視化が、AIペアプログラミングによって数時間で実現可能になりました。 透明性は、鮮度が命です。 問題が起きそうだと感じた時、すぐにその状況を可視化するツールを自分自身の手で作れることは、チームを守るスクラムマスターにとって強力な武器になります。 「見えない不安」を即座に「見える課題」に変えられる機動力こそが、AIを活用する最大のメリットかもしれません。 まとめと次回予告 # 本記事では、AIを活用してGASを書くことで、Jiraやスプレッドシートに眠るデータを掘り起こし、チームに必要な透明性を素早く、低コストで実現した事例を紹介しました。 これは、スクラムガイド拡張パックで示唆されている「AIによる経験的プロセス制御の強化」を実践するための第一歩です。 しかし、透明性はゴールではありません。見えたデータを元にどう判断し、行動を変えるかが重要です。 次回の第3回「検査・適応」では、透明性とはまた違った視点、あるいは可視化された情報を元にした意思決定の場面でのAI活用についてご紹介します。
前回の記事「無料のOSSツールSysONで始めるSysML v2モデリング(1) 〜 はじめてのSysON」では、SysONをインストールして Webブラウザでホーム画面を表示しました。 /blogs/2026/01/08/sysmlv2-tool-syson-intro/ 本記事では、新しいプロジェクトとパッケージを作成してみましょう。 本記事では Release 2025.8.0を使用しています。 最新版ではUIや挙動が異なる可能性がありますのでご了承ください。 ホーム画面はプロジェクトブラウザ画面とも呼びます。 プロジェクトブラウザ画面の上側にはプロジェクトを作成するアイコンが並びます。 その下側には既存のプロジェクトのリストが表示されます。 プロジェクトを作成する # 新しい SysMLv2プロジェクトを作るには、"Create a new project"にある左から2番目の"SysMLv2"と記載されたアイコン(SysMLv2テンプレート)を選択します。 選択すると、プロジェクトエディタ画面が表示されます。 エディタ画面は、画面上部の「ツールバー」、左側の「左サイドバー」、右側の「右サイドバー」、左右サイドバーの間にある「エディタ」の4つから構成されます。 プロジェクト名を変更する # プロジェクト名は、ツールバーの中央にある"SysMLv2"です。 まずはプロジェクトの名前を変更しましょう。 ツールバーにあるプロジェクト名横のケバブボタン(︙)をクリックし"Rename"を選択すると、"Rename the project"のダイアログが表示されます。 今回は"SysMLv2.trial"と入力して"RENAME"ボタンを押下します。 ビューを表示する # 左サイドバーのツリーで"General View"を選択すると、エディタに図を描く画面が表示されます。 グラフィカル記法でモデルを作成する場合は、ここに図を描いていきます。 ビューを追加する # 新たにビューを追加したい場合は、左サイドバーのツリーにあるパッケージなどの要素に付いているケバブアイコン(︙)をクリックします。 表示されたコンテキストメニューから"New representation"を選択するとビューを追加するためのダイアログが表示されます。 このダイアログで名前を入力しビューの種類を指定して"CREATE"ボタンを押すとビューを追加できます。 プロジェクトブラウザ画面に戻る # プロジェクトブラウザ画面に戻る場合は、画面上部ツールバーの右端にあるハンバーガーボタン(≡)をクリックして"Projects"を選択します。 ツールバーの左端にある立方体のアイコンをクリックしても、プロジェクトブラウザ画面に戻ります。 新規のPackage要素を作成する # 最初に Packageを作成してみます。 中央のエディタで空きスペースを右クリックすると、図に配置できる要素のカテゴリ一覧がコンテキストメニューに表示されます。 "Structure"を選択すると、コンテキストメニューの表示が図に配置できる"Structure"の要素の一覧に切り替わるので、"New Package"を選択します。 すると、エディタに Package要素が表示され、左サイドバーのツリーに Package要素が追加されます。 このとき、左サイドバーのツリーをみると、新しく追加した"Package1"は"General View"がある"Package1"の中に追加されていることがわかります。 右サイドツリーの"Details"にある"Declared Name"にも"Package1"と記載されています。 ここでもうひとつ、”Package1”を作成したのと同じ手順で"Package2"を追加します。 次は、左サイドバーのツリーにある"Package2"をツリーにある"Package1"の中にドラッグ&ドロップで移動します。 その後、"Package1"を右クリックしてコンテキストメニューを表示します。 表示されたコンテキストメニューから "Related Elements" > "Add existing nested elements"を選択します。 すると、"Package1"の中に"Package2"が表示されます。 このとき、"Package1"の外に表示されている"Package2"に変化はありません。 SysMLv2仕様をみると、"Package1"と"Package2"の間に owned-membershipを表示するのがよさそうです。 今後のリリースで owned-membershipを表示するように変更されるかもしれません。 Packageをビューから削除する # "General View"には"Package2"が2つ表示されています。 このうち"Package1"の外にある"Package2"をビューから削除します。 "Package2"を選択した状態で右クリックしてコンテキストメニューを表示します。 コンテキストメニューの上部に並んでいるアイコンの中で左から4つ目にある四角に斜線が入ったアイコンをクリックします。 "Package2"はビューから削除されますが、左サイドバーのツリーには"Package2"が残っています。 Packageをビューに配置する # 左サイドバーのツリーの"Package2"をエディタにドラッグ&ドロップしてください。 ふたたび、ビューに"Package2"が表示されます。 PackageをImportする # 左サイドバーのツリーでPackage右側のケバブアイコン(︙)をクリックします。 表示されたコンテキストメニューの"New object"を選択すると"Create a new object"のダイアログが表示されます。 ダイアログの"Object type"で"Namespace Import"を選択します。 すると、ツリーで先に選択したPackageの中に”Namespace Import”が追加されます。 追加された”Namespace Import”を選択すると右サイドバーの"Details"にその詳細が表示されます。 左サイドバーに表示される"Datails"の"Imported Namespace"でImportするPackageを選択するとImportの設定は完了です。 図のPackageを右クリックし、コンテキストメニューから "Related Elements" > "Add existing nested elements"を選択します。 Packageの中に表示された点線のPackageがImportをあらわします。 左サイドバーの"Details"にある"Visibility"でImportされたPackageの可視性を変更できます。 Packageをモデルから削除する # "Package1"をモデルから削除します。 "Package2"をビューから削除したのと同様の手順でコンテキストメニューを表示します。 表示したコンテキストメニュー上部のアイコンの中で、今度は左から2つ目にあるゴミ箱アイコンをクリックします。 すると、削除してよいかを確認するダイアログが表示されます。 ”DELETE”ボタンを押下すると"Package1"の中にあった"Package2"ごと左サイドバーのツリーから削除され、"Package1"と"Package2"がビューから消えます。 テキスト記法を用いたPackageの追加 # SysONは SysML v2の特徴であるテキスト記法を扱うこともできます。 左サイドバーのツリーで、"Package1"の右にあるケバブアイコン(︙)をクリックして、表示されたメニューから"New objects from text"を選択します。 すると、上部に"Enter or paste SysMLv2 text to create new objects in the model"と記載されたダイアログが表示されます。 ダイアログに以下を入力して"CREATE OBJECTS"のボタンを押下します。 package Package2 { package 'Package 3' { } } package 'パッケージ' { } 左サイドバーのツリーに"Package2", "Package3", "パッケージ"の3つのパッケージが追加されます。 続けて追加しない場合は"CLOSE"ボタンでダイアログを閉じます。 テキスト記法では、パッケージ名をシングルクオート(')で囲むと日本語や半角スペースを入れた文字列を使うことができます。 次回予告 # 本記事では、新しいプロジェクトを作成し、パッケージを追加しました。 次回は Part Definition要素を作成します。
昨年の 2025 年 1 月に Kubestronaut の称号を得たのに続き、同年 12 月に GOLDEN Kubestronaut の称号を得たので、その記録をまとめます。 流れは以下です。 GOLDEN Kubestronaut とは: GOLDEN Kubestronaut について簡単に記述 本記事のスコープ: 本記事で記載する範囲について記述 試験の特徴と関係性: GOLDEN Kubestronaut の取得に必要な試験の特徴と関係性について記述 学習方法: 私の学習方法について記述 終えてみて: GOLDEN Kubestronaut の取得を終えてみての所感を記述 各試験への取り組み: それぞれの試験ごとに、取り組みや所感を記述 GOLDEN Kubestronaut とは # GOLDEN Kubestronaut とは、すべての CNCF 認定資格と LFCS に合格すると得られる称号です。GOLDEN Kubestronaut の前段階に Kubestronaut があり、それはその中の 5 つに合格すると称号が得られます。 以下が、Kubestronaut の要件と、GOLDEN Kubestronaut に追加で必要なものです(私が受験した順に記載しています)。 Kubestronaut 要件 Certified Kubernetes Application Developer (CKAD-JP) Certified Kubernetes Administrator (CKA-JP) Certified Kubernetes Security Specialist (CKS-JP) Kubernetes and Cloud Native Associate (KCNA-JP) Kubernetes and Cloud Native Security Associate (KCSA) GOLDEN Kubestronaut 追加要件 OpenTelemetry Certified Associate (OTCA) Istio Certified Associate (ICA) Cilium Certified Associate (CCA) Certified Argo Project Associate (CAPA) GitOps Certified Associate (CGOA) Prometheus Certified Associate (PCA) Certified Backstage Associate (CBA) Kyverno Certified Associate (KCA) Certified Cloud Native Platform Engineering Associate (CNPA) Linux Foundation Certified System Administrator (LFCS-JP) この様に、Kubestronaut だけで 5 個、GOLDEN Kubestronaut には合計 15 個の認定が必要になります。Kubestronaut の称号は 5 つの認定のいずれかを失効すると消失するのですが、GOLDEN Kubestronaut の称号は生涯有効です。 なお、上記の要件は、CNCF に新たな認定試験が追加になるとアップデートされます。具体的には、2026/03/01 に GOLDEN Kubestronaut の要件に以下の試験が追加になるとアナウンスされています。ただ、一度 GOLDEN Kubestronaut に到達すれば、その後に要件が追加されたとしても、それに新たに合格する必要はありません。 Certified Cloud Native Platform Engineer (CNPE) 余談ですが、各試験の料金は、頻繁に行われるセールやクーポンを使うことで、概ね正規料金の 40%〜50%オフにできます。 本記事のスコープ # 本記事では、主に、Kubestronaut から GOLDEN Kubestronaut へのステップアップに追加で必要な認定試験について取り上げます。Kubestronaut までに必要な認定試験については、すでに多くの情報が公開されているので、このレポートの中では割愛します。 試験の特徴と関係性 # 試験の特徴 # 追加要件の試験を総合的に見ると、その特徴は、以下のようであると思います。 クラウドネイティブ関連が広くカバーされている: それぞれの試験を個別に見ると、その殆どは、特定のプロダクトに関する知識やスキルが問われる(CGOA と CNPA は例外)。しかし全体をまとめてみると、セキュリティを始め、オブザーバビリティやガバナンス。また、GitOps とデリバリー、そして IDP など、クラウドネイティブに関する主要な要素が広くカバーされている。 ほとんどは選択式試験(ただし英語): 現在のところ、ICA と LFCS、そして今後追加になる CNPE 以外は、選択式試験。そのため各試験は、英語がそれなりに読めれば、比較的容易。ただ CGOA と CNPA は、特定のプロダクトを対象としない反面、その問題文が理念や考え方を問われるややこしいものになるため、若干、英語のリーディング能力が問われる。 試験の関係性 # それぞれの試験で扱うプロダクトや技術領域に類似や関連があり、ある試験で得た情報が他の試験で役立ちます。各試験が対象とする技術領域と、私が受験した順番の中で感じた各試験の関連は以下のような感じです。 ちなみに、"()"の数字が私の受けた順番で、以下のようにしました。 興味のあるものを先に受ける: オブザーバビリティには興味があったので OTCA は先に受けました。PCA も同じ領域ですが、Prometheus 固有のクエリ言語(PromQL)を覚えるのが面倒そうだったので、後に回しました。 難易度が高そうな実技試験を先に受ける: 実技試験は選択式よりも難易度が高いので、実技試験の ICA は先にしました。LFCS も実技試験ですが、ほかと趣が違うので、後にしました。 関連が強い試験は続けて受ける: 同じ技術領域である ICA と CCA、同じく GitOps を扱う CAPA と CGOA は続けて受けました。 知識や情報の少ない試験は後にする: 知識が薄かった PCA, CBA, KCA や、後で要件に加わって情報が少なかった CNPA は後回しにしました。 全体を通してみて、私が感じた中では、CKS の学習で得た知識が最も多く他の試験で役立ちました。 その一方で、CNPA は他の多くの試験で得られる知識が広く必要でした。これは裏を返すと、「CNPA を先に受験すれば、そこで得た知識は他の多くの試験で役に立つ」ということになります。CNPA を先に受けるか後にするかは人それぞれかと思いますが、CNPA は試験問題の英文が難しいので、私のように英語が苦手な人は後にしたほうが良いかもしれません。 学習方法 # 学習方法は、基本は Kubestronaut 要件の認定資格を受験したときと同じで、以下のパターンでした。 e-learning: KodeKloud や Udemy で提供されているそれぞれの試験対策のコースを受講。 模擬試験: KodeKloud や Udemy の試験対策コースに付帯されている模擬試験を受けるともに、Udemy で追加の模擬試験コンテンツを購入して実施。 ハンズオン: 手持ちの Linux PC で k8s 環境を作って試験カリキュラムに含まれる内容を実践。また、一部の試験については、Killercoda でハンズオン環境が提供されているので、そちらも活用。 リファレンス参照: 試験対象のプロダクトのリファレンスを参照し、試験カリキュラムに含まれる部分をチェック。 e-learning # 基本的に、KodeKloud や Udemy で提供されているそれぞれの試験対策のコースを受講しました。それ以外にも、Linux Foundation や Tetrate Academy の e-learning など、試験に関係しそうな無料コンテンツがあれば見ておきました。 難点として、日本語字幕付きのものはほとんど無かったです。Kubestronaut 要件までの認定資格関連の e-learning の中には日本語字幕つきものもありましたが、今回はほぼ英語のみでした。序盤に受けた試験はある程度知っている技術領域のものだったので、受けた e-learning の内容の理解も追いつくことができました。しかし徐々に、知識の少ない領域の試験になってきて、学習効率が落ちてきました。 そこで、追加 10 個の半分の 5 つ取ったところで、 動画の音声を文字起こししてまとめる という学習法をとりました。音声の文字起こしは、Vibe コーディングで作った、Whisper を使った文字起こしアプリを使いました。 文字起こしアプリを使って、まず動画の各レッスンを視聴直後にテキスト化し、その後で AI を使って、その英文を補正してから日本語に翻訳するとともに要約。それをセルフホストしている Wiki に転記してさっと読み直しました。この方法は、レッスン直後に復習できるのが効果的でした。難点は動画を視聴するのに比べて 1.5 倍くらい時間がかかることですが、それよりも理解を深める方を重要視しました。 この「動画視聴」→「文字起こし」→「自然な英語に整形」→「翻訳&要約」→「Wiki に転記して復習」の流れができたことで、学習のパターンが固まりました。 アプリを作り始めた当初は、同時通訳して e-learning の動画と同時に見るのを理想としたのですが、以下の理由でそれは断念し、アプリの用途は文字起こしのみに限定しました。 翻訳が若干遅れる 標準英単語ではない専門用語の文字起こしに難がある 翻訳したターミナルと動画を同時に見てられない 若干面倒だったのは、起こしたテキストの整形につかった ChatGPT が、テキストが多いと雑に返すようになって使えなくなることでした。目安として、動画の長さが 15 分を超えるとそれに陥ることが多かったです。その場合は、いったん課金している Claude Code に自然な英語への整形をやらせて、その結果を小分けにして ChatGPT に翻訳させました。 模擬試験 # KodeKloud や Udemy の試験対策コースにはだいたい模擬試験が含まれているので、まずそれは実施しました。その他に、Udemy に模擬試験のみのコンテンツもあるので、それもいくつか購入して受験しました。模擬試験は、基本的に、本試験よりも難易度の低いものがほとんどでしたが、心の準備にはなりました。 最近は、Golden Kubestronaut ができたことで、関連する模擬試験のコースが充実してきたように思います。しかし、学習を始めた初期は、適当な模擬試験のコンテンツが無かったり、あったはずのコンテンツが公開終了してしまったりしたものがありました。その対策用に、Vibe コーディングで、AI に試験問題を作成させるアプリを作りました。 そのアプリは受験期間の序盤こそ使っていたのですが、AI が生成する試験問題が簡単すぎるのと、次第に Udemy の模擬試験のコンテンツが充実してきたので、後半は使う必要がなくなりました。 ハンズオン # OTCA と、試験の直接的な対象となるプロダクトが存在しない CGOA と CNPA 以外は、手持ちの Linux PC で環境を作って、カリキュラムの範囲を確認しました。 Kubestronaut 要件を受験したときには、CKAD や CKA のカリキュラムにクラスタのインストールやアップデートがあったので、確認のための k8s 環境そのものを構築していて手間でした。しかし今回はその必要がなく、minikube や kind のような簡易環境を使って各試験ごとに簡単に環境を作れて楽でした。 LFCS については、クラウドネイティブ関連のプロダクトではなく、Linux 環境の操作を実践することになります。LFCS のカリキュラムに、仮想マシンを作る libvirt が含まれていたので、libvirt で仮想マシンを作って、その上で全体を実践しました。 リファレンス参照 # これは、他の 3 つと比べて、かけた時間は少ないです。文字起こしを導入する以前の ICA, CCA, CAPA あたりまでは、リファレンスを参照して e-learning で捉えきれなかった内容を補完していました。 文字起こしの導入以降は e-learning で理解が十分になったので、それほど、リファレンスまで参照する必要はなくなりました。e-learning の中では説明されない設定項目や、デフォルト値などの仕様が問われることも多少あるので、タイミング的に受験まで日が空くときには、関係しそうなところをチェックしたりしました。 終えてみて # GOLDEN Kubestronaut の称号を得るには、追加要件の試験数が多く、その殆どが英語の試験ということで、到達は難しいだろうと思っていました。しかし、試験の難易度自体はそれほど高くなく、根を詰めれば、それぞれの試験を 1〜2 週間ほどでクリアできると思います。また、試験問題の英文についても、さすがに 10 個受けたら慣れました。 Kubestronaut に到達した時は、k8s のスキルはもとより、特に CKS の学習によってクラウドセキュリティの知識を得られたと感じました。そして今回、GOLDEN Kubestronaut の追加要件に関する学習を通じて、GitOps や、プログレッシブデリバリー。または、オブザーバビリティや IDP など、クラウドネイティブのプラットホームに関する知識を広げることができたと感じています。 この先、GOLDEN Kubestronaut の要件には CNPE が追加になることがすでに明らかになっています。カリキュラムを見るに、おそらく、今回の追加要件の中で学んだプロダクトに関する実技が問われるものになるのではないかと予想します。私はすでに GOLDEN Kubestronaut の称号を得たので、追加でそれに合格する必要はないのですが、是非、チャレンジしてみたいです(情報が集まるまで待って)。 各試験への取り組み # 2025 年 1 月に、Kubestronaut の取得に必要な 5 つの認定資格をクリアしました。その後少しして、GOLDEN Kubestronaut のプログラムが追加になりました。そこで再び、2025 年 7 月から 12 月までかけて、GOLDEN Kubestronaut に必要な 10 個の認定資格をクリアしました。 当初は、2025 年度いっぱいかけてゆっくり取り組むつもりだったのですが、以下の理由により、後半は計画を前倒しして 2025 年の年内に終わらせました。 あまり時間をかけると逆に忘れてしまう 2026/03/01 に CNPE が要件へ加わると難易度が結構高くなりそう 年末年始をゆっくり休みたい OpenTelemetry Certified Associate (OTCA) # 受験日: 2025/07/06 学習日数: 5 日 使った教材: OpenTelemetry Foundations: Hands-On Guide to Observability e-learning OpenTelemetry Certified Associate (OTCA) Practice Exams 模擬試験 公開終了 GOLDEN Kubestronaut を目指すかどうかは決めてはいませんでした。ただ、Kubestronaut の特典で 50%オフのクーポンがあったこともあり、試しに、追加要件の中で興味があった OpenTelemetry を受験しました。 e-learning は量も短めで、CKS の Cilium の学習の中で Hubble に触っていたので、理解は容易でした。学習内容は、メトリクスと OpenTelemetory の基礎。平日の間に e-learning と合わせて模擬試験を実施して、そのまま週末に受験して合格しました。 ざっと学習した程度で合格はできたので、難易度はあまり高くないように感じました。他の試験もこれくらいの難易度なら、すべての認定を取ることはできるだろうと思い、ここで GOLDEN Kubestronaut を目指すことにしました。 Istio Certified Associate (ICA) # 受験日: 2025/08/23 学習期間: 48 日 使った教材: Istio Hands-On for Kubernetes e-learning (日本語字幕あり) Learn Istio Fundamentals e-learning 無料 https://killercoda.com/ica ハンズオン OTCA は試しにさらりと受けたのですが、ICA は、実技試験のため難易度が高いと予想し、時間をかけて準備しました。e-learning やハンズオンに加えて、理解の薄いところはリファレンスを翻訳して理解を深めました。 Istio は、以前に受けた CKS のカリキュラムに含まれてはいるものの、CKS ではどちらかというと Cilium の方が使われるので、Istio そのものについてはあまり学習していませんでした。とは言え、同じくサービスメシュ関連のプロダクトである Cilium の理解があったので、スムーズに学習できました。また、先に OTCA を受験していたので、Kiali や Jaeger あたりのオブザーバビリティに関連するプロダクトの理解も容易でした。 その一方で分かり難かったのは、Istio のトラフィック制御の基本である VirtualService や DestinationRule です。名前と振る舞いがうまく繋がらず、理解に苦しみました。その辺りは Istio 用語として割り切りました。 試験については、ICA には、同じ実技試験の CKA/CKAD/CKS の試験に付帯する Killer.sh のような試験ミュレータはありません。模擬試験としては、Killercoda のハンズオンが助けになりました。 いざ受験の申し込みをしようという段階で、ICA のカリキュラムが更新になるというハプニングがありました(ICA の申込ページをよく見たらそのアナウンスがされてました)。夏季休暇の直前に受験を予定していたのですが、その日がちょうどカリキュラムの切り替え期間の中に当たってしまって、受験ができなくなりました。そのため、受験を延期して夏季休暇後に受験しましたが、アップデート内容のチェックと、覚えたことを忘れないようにするのがちょっと面倒でした。 試験自体は、大筋に変更はなかったので、問題はなかったです。試験の難易度は、CKA や CKAD の同等かすこし低目と感じました。 Cilium Certified Associate (CCA) # 受験日: 2025/09/23 学習期間: 31 日 使った教材: Introduction to Cilium (LFS146) e-learning 無料 Prep Course - Cilium Certified Associate (CCA) Certification e-learning CCA は、CKS の学習のときの Cilium に触れた印象で、ややこしいイメージを持っていました。その印象の主な原因は、Cilium のリファレンスの構成の煩雑さかなと思います。 それはそれとして、e-learning での学習において、CKS と ICA で得た知識が助けになりました。CKS で、CiliumNetworkPolicy や mTLS、Hubble などについては学習済みで、さらに、Falco の学習で得た eBPF の知識が助けになりました。また、ICA については、Istio の Ambient モードの方式が Cilium の方式に近そうなので、Istio の Ambient モードを多少理解していたことが役立ちました。 なお、購入予定だった Udemy の模擬試験コンテンツが公開終了になってしまったのが CCA での誤算でした。心の準備をしたかったので、ICA の学習期間の途中の夏季休暇の間に模擬試験アプリを作っておいて、ここで活用しました。また、ICA の時のように e-learning の後にリファレンスを見て理解を深めようと思ったのですが、冒頭に書いたように、リファレンスの構成がややこしくて思うように整理できませんでした。 試験問題の中には結構細かい内容を問われるものもありましたが、そういった問題の数はそれほど多くなかったかと思います。 Certified Argo Project Associate (CAPA) # 受験日: 2025/10/12 学習期間: 19 日 使った教材: Argo Workflows: The Complete Practical Guide : Unlock DevOps e-learning Argo CD Essential Guide for End Users with Practice e-learning Mastering Argo Rollouts: Progressive Delivery in Kubernetes e-learning https://killercoda.com/argo ハンズオン CAPA は、Argo の 4 つのプロダクト(Workflows, CD, Rollouts, Events)をまとめて題材とする試験です。これについては、試験対策用の e-learning のコースは見つかりませんでした。ただ、Workflows, CD, Rollouts のプロダクトそのものを扱う e-learning が Udemy にあったので、その中の試験カリキュラムに該当するレッスンを視聴して学習しました。Events については e-learning が無かったので、試験カリキュラムの範囲をリファレンスを見て学習しました。もともと Events は全体の中での割合が低いので、問題はないと判断しました。 個人的に、4 つのプロダクトの中で馴染みがあるのは Argo CD と Argo Workflows で、その 2 つ学習は容易でした。とは言え、普段はプライベートでの表面的な利用のみなので、Argo CD の Projects や RBAC など、突っ込んだテーマをここで学びました。Rollouts は初見ですが、プログレッシブデリバリーの知見が多少はあったので、理解は難しくなかったです。ICA で学んだトラフィックシフトの知識が、Rolleouts のトラフィック制御をイメージするのに役立ちました。 CAPA の厄介っどころとしては、「覚えたことがどのプロダクトのものだったかわからなくなる。」という点でした。4 つのプロダクトを同時に学ぶので、仕様の細かい点などは、どのプロダクトのものだったのか曖昧になってしまいました。 e-learning 後、ハンズオンは Argo CD と Workflows については killercoda のコンテンツが充実しているので、まずはそれでざっと実施しました。Rollouts と Event については、手元に環境を作ってハンズオンをしました。模擬試験のコンテンツは見当たらなかったので、CAPA でも念の為、自作の模擬試験アプリで心の準備をしました。 なお Argo Workflows については、 こちらの記事 もご覧いただけます。 GitOps Certified Associate (CGOA) # 受験日: 2025/10/19 学習期間: 7 日 使った教材: Prep Course - GitOps Certified Associate (CGOA) e-learning Introduction to GitOps (LFS169) e-learning https://www.udemy.com/course/certified-gitops-associate-cgoa/ 模擬試験 公開終了 CGOA は、特定のプロダクトを対象とするものではなく、GitOps というテーマを題材とするものです。直前の CAPA で GitOps のプロダクトである Argo CD を学んだ時点で、GitOps に関する知識はおおよそ習得しました。そのため油断して、e-learning と模擬試験をサラリとやって、そのまま試験に望みました。 その結果、合格はしたものの、正答率は 15 個の試験の中で一番低かったです。その原因は、GitOps の理解というより、英語力だと思われます。前述した通り、特定のプロダクトを対象としない CGOA では、プロダクトの特徴や仕様を問われる様な単純な問題が無い代わりに問われる内容が比較的複雑で、問題の英文の理解が難しかったです。CGOA で、「特定のプロダクトを対象としない試験は意外と厄介」と気づきました。 Prometheus Certified Associate (PCA) # 受験日: 2025/11/09 学習期間: 21 日 使った教材: Prep Course - Prometheus Certified Associate (PCA) Certification e-learning Prometheus Certified Associate Practice Exams 模擬試験 PCA が題材とする Prometheus は、これまで何度か、触ってみようとしたことはありました。その度に、「PromQL のややこしさ」がどうにも面倒そうで、深く追求することは避けてきました。しかし、ここに至っては避けることができないので取り組みました。 GCOA の受験で自分の英語力のなさを痛感したので、GCOA をパスしたその日に文字起こしアプリを作成して、この PCA の学習から使い始めました。その結果、e-learning での理解度がかなり向上しました。 PCA の受験では、先に受験した OTCA で得たメトリクスやエクスポーターの知識が役立ちました。Prometheus にはメトリクスの収集の他にもアラートのトリガーや通知の特徴がありますが、その辺りの仕組みは理解しやすかったです。ただ、Histgram と Summary の違い、Relabel Config の振る舞いや演算子など、細かい点がちらほらあるので、その辺はリファレンスを参照して頭に入れておきました。 そして肝心の、PCA の特徴である独自言語の PromQL については、ローカルに作った Prometheus の環境でのハンズオンを通じて理解を深めました。Prometheus の Expression Browser を使って実践し、何度もクエリの記述エラーを吐き出しながら、言語仕様を理解しました。 Certified Backstage Associate (CBA) # 受験日: 2025/11/23 学習期間: 14 日 使った教材: Prep Course - Certified Backstage Associate (CBA) Certification e-learning Introduction to Backstage: Developer Portals Made Easy (LFS142) e-learning 無料 Certified Backstage Associate (CBA): Tests December 2025 模擬試験 CBA は、Backstage という IDP( Internal Developer Portal)を構築するためのプロダクトを題材とするもので、他の試験とはちょっと趣が違っていました。CBA の学習で得た Platform as a Product の考え方は、この後の、CNPA で大きく役立ちました。 Backstage 自体は monorepo 構成の React アプリであり、デザインシステムに Material UI が使われています。そのため、CBA では、IDP の理念や Backstage での IDP 機能とともに、Material UI を使った React アプリの実装に関する知識が問われます。私は Material UI や React アプリの知見があったので、それは問題がなかったです。e-learning に中にもアプリ構築のセッションがあり、他の試験対策講座とはちょっと雰囲気が違ってました。 React アプリの Backstage はローカルでそのまま起動できるので、比較的、実践が容易でした。プラグインの実装やデザインのカスタマイズなどを試して、実装方法を理解しました。 なお Backstage については、 こちらの記事 もご覧いただけます。 Kyverno Certified Associate (KCA) # 受験日: 2025/12/07 学習期間: 14 日 使った教材: Prep Course - Kyverno Certified Associate (KCA) Certification e-learning KCA - Kyverno Certified Associate - Mock Exams 模擬試験 KCA は、確か、学習を始めた当初は e-learning や模擬試験のコンテンツが無かったと記憶しています。そのため、受験の順番を後ろにしていました。その時点では、Kyverno のプロダクトサイトを見て学習するつもりでいたのですが、GOLDEN Kubestronaut ができたからかいつの間にかコンテンツができていたので、それらを利用しました。 KCA の学習では、CKS の学習で得たポリシーの知識が役に立ちました。CKS で扱われたのは OPA/Gatekeeper ですが、考え方や仕組みは類似しているので、理解の助けになりました。OPA/Gatekeeper ではポリシーの記述に Rego が使われていて理解が難しいのですが、Kyverno のポリシーの記述は基本的に yaml と json で、理解しやすかったです。e-learning の講師の口調もゆっくりで、理解しやすかったです。 試験前には手元に k8s の環境を作ってハンズオンをしました。ポリシーの仕様の確認とともに、各コントローラの振る舞いをチェックしました。バックグラウンドスキャンやレポート生成の動きが若干ややこしいので、実際に動かしてその様子を確認しました。 Certified Cloud Native Platform Engineering Associate (CNPA) # 受験日: 2025/12/13 学習期間: 6 日 使った教材: Prep Course - Certified Cloud Native Platform Engineering Associate (CNPA) e-learning CNPA- Cloud Native Platform Associate - Mock Exams 模擬試験 CNPA は、学習を始めた時点では要件になく、後から追加になりました。KCA と同様、当初は e-learning や模擬試験のコンテンツが無く、LFCS よりも後の最後に受験する予定でした。その後、コンテンツができたので、予定を変更して、LFCS よりも先に受験しました。ここに来て、なんとか年内に終わらせたい気持ちが強まり、KCA から一週間で臨みました。 CNPA は、CGOA 同様、特定のプロダクトを対象としない試験です。対象の範囲は結構広くて、GitOps や DevSecOps のような開発・運用に関する理解や、Platform as a Product の理解など、様々な領域を知っておく必要がありました。ここまでに受けた試験の中では、CKS, CGOA, CBA で得た知識が役立ちました。 ちなみに、e-learning の講師がとても早口で理解がしにくかったです。AI を使って翻訳と要約をして、やっとその内容を理解できる感じでした。 そして、CNPA の試験受験で苦労したのは、 問題数の多さ と 問題の英文の難しさ でした。まず、「問題数の多さ」については、他の選択式の試験が 90 分で 60 問であるのに対し、CNPA は 120 分で 85 問でした。e-learning の中で「問題数は 60 問」と説明されていたので、それは表示のバグで実際には 60 問で終わるのかと思ったら、それ以降も終わることなく続いて結局 85 問フルに回答しました。その結果、集中力が乱れ、また、回答を見直す時間はあまり取れませんでした。次に、「問題の英文の難しさ」ですが、特定のプロダクトを対象としない試験であるために、その分、英文が長く難しいです。他の試験と比べて、問題と選択肢の文章の長さが 1.5〜2 倍くらいあったと思います。CNPA の難易度は、英語の得手不得手でだいぶ違うと感じました。 CNPA に至るまで半年勉強してきたので余裕だろうと思っていたのですが、e-learnig と試験の双方で、思いの外手こずりました(主に英語で)。 Linux Foundation Certified System Administrator (LFCS-JP) # 受験日: 2025/12/28 学習期間: 15 日 使った教材: Linux Foundation Certified Systems Administrator - LFCS e-learning https://killercoda.com/lfcs ハンズオン 最後に受験したのは LFCS です。これは Linux そのものの実技を問う試験です。普段から Linux(Ubuntu)は使っていますが、個人利用の範囲ではあまり深い使い方はしないので、足りない部分を e-learning と手持ちの Linux PC でのハンズオンで学習しました。 受験時には、CKAD,CKA,CKS と同様に、killer.sh の試験シミュレータを事前に 2 回使えます。本番対策に非常に有効でした。1 回目は受験の一週間前に使いました。過去の経験で「killer.sh の試験シミュレータの難易度は本番より若干難しめ」と感じるところなので、出題範囲とともに、本番の難易度を予想しました。実際そのとおりだったかと思います。そして受験の前日に 2 回目を使って、(心の準備は)万端で試験に臨みました。 LFCS の試験の特徴としては、他の実技試験(CKAD,CKA,CKS,ICA)と違って、試験中にネット上のマニュアルを参照できません。そのため参照できるのはターミナル上の man と help です。試験対策に、man の操作に慣れておくのと、想定される問題に関する man や help へのたどり着き方を覚えておくと良いと思います。 また、問われている対象が明確に分かるなら、man と help で調べられるのですが、「あれはなんだったか」というようなおぼろげな状況では man と help にたどりつけません。そんな状況に備え、例えば以下のような方法で、対象にたどりつく術を身に着けておくと良いと思います。 Tab 補完の候補であたりをつける 例えば日付に関する問題なら、time や date などと打って Tab 補完すると候補が列挙されるので、そこからあたりをつける。 man の SEE ALSO であたりをつける とりあえず関連しそうなものの man を表示して、man の最後の"SEE ALSO"の候補であたりをつける。 etc 配下を grep で総当り検索 etc 配下を grep の R オプションで階層的に関連しそうなキーワードでテキスト検索して、それらしいファイルを探す。 試験にはそれなりの手応えで全問回答できたので、試験終了の時点で合格を確信しました。24 時間以内にくるはずの通知がなかなか来ずに変だなと思いましたが、ちょっと遅れて合格通知が来ました。その後すぐ年内には GOLDEN Kubestronaut の通知も来て、心置きなく年越しを迎えられました。
はじめに # こんにちは。 最近、個人開発でNuxt.jsを使ったWebアプリ開発をしています。サーバーサイドをどうしようかと検討したところ、Supabaseというフルスタックバックエンドサービスが話題になっていることを知りました。 公式サイト: https://supabase.com/ どうやらFirebaseの代替として注目を集めているバックエンドサービスで、モダンなアプリケーション開発に必要な機能を包括的に提供しているようです。 Nuxt.jsにSupabaseを導入して認証まで意外と簡単にできたので、自分のメモがてら紹介しようと思います。 本記事ではNuxt.jsにSupabaseを導入する方法、そしてメールアドレスによる認証機能の実装方法を紹介します。 Supabaseとは # Supabaseは、PostgreSQLをベースにしたオープンソースのBaaS(Backend as a Service)プラットフォームです。このプラットフォームはリアルタイムデータベース、認証、ストレージなど多様な機能を提供しています。 Supabaseを使えば、バックエンドを自分で開発することなく、すぐにデータベースやユーザー認証を導入でき、フロントエンドの開発だけに集中できます。 SQLが使えるのでデータ管理がしやすく、オープンソースで自由度が高いです。 何より(制限はありますが)無料プランで利用可能なところが魅力的です。 Supabaseの各機能の詳細については、公式ドキュメントや多くの解説記事がありますのでぜひ併せて参照してみてください。個人的には、これほど多機能でありながら導入が驚くほどスムーズで、開発者にとって非常に使い勝手の良いサービスだと感じました。 Supabase導入方法 # それではNuxt.jsにSupabaseを導入してみましょう。まず前提として、下記は既に行っているとします。 公式サイト でユーザー登録をしていること プロジェクトを立ち上げて使用するデータベースがSupabase内にあること まだの方はSupabaseと調べれば色々と出てきますので試してみてください。思ったよりも簡単に登録できると思います。 supabaseを導入前の開発環境が下記になっているとします。 package.json "dependencies": { "@nuxt/scripts": "0.12.1", "@nuxt/ui": "4.0.1", "@tailwindcss/vite": "^4.1.18", "nuxt": "^4.2.2", "tailwindcss": "^4.1.18", "vue": "^3.5.26", "vue-router": "^4.6.4" }, "devDependencies": { "nuxt-icon": "1.0.0-beta.7", "typescript": "^5.9.3" } この状況で、下記コマンドを実行してsupabaseをインストールします。 (※パッケージ管理ツールとしてはpnpmを使用していますが、npmやyarnでも問題ありません) pnpm install @nuxtjs/supabase @supabase/supabase-js インストールしたら、nuxt.config.tsに @nuxtjs/supabase を追加します。 nuxt.config.ts import tailwindcss from "@tailwindcss/vite" export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: true }, css: ['./app/assets/css/main.css'], vite: { plugins: [tailwindcss()], }, modules: [ '@nuxtjs/supabase', // これを新しく入れる '@nuxt/ui', 'nuxt-icon', ] }) 次に、Supabaseで作成したプロジェクトのProject URLやAPI Keyを取得します。「Project Settings > Data API」や「Project Settings > API Keys」を確認してみましょう。 Project Settings > Data API Project Settings > API Keys .envファイルを新規作成して、確認したProject URLやAPI KEYを追加します。 .env SUPABASE_URL=<Project URL> SUPABASE_KEY=<Publishable key> --> Information envで設定しているプロパティ名はデフォルトの名称を使用しています。 https://supabase.nuxtjs.org/getting-started/introduction#options もしプロパティ名を別の名前にしたい場合は、nuxt.config.tsにて下記のようにsupabaseのオプションを設定してください。 nuxt.config.ts export default defineNuxtConfig({ // ... supabase: { // Options } } これでSupabaseの導入は完了です。それではNuxt.jsでログインページを作成していきましょう。 メールアドレスによる認証の実装 # これから認証方法の実装に入りたいと思います。 Supabaseの認証方法はGoogleログイン認証やGitHubログイン認証など数多くありますが、今回はサクッと簡単に作りたいのでメールアドレス認証で作ろうと思います。 ページの作りとしては、 こちらのソースコード が非常に参考になりましたので、こちらをベースに解説したいと思います。 Nuxt.js × Supabaseでは、(ログインしていない場合)デフォルトで /login にリダイレクトします。そのためpagesディレクトリには下記を作成する必要があります。 vueファイル 説明 login.vue ログインや新規登録をするページ index.vue ログインした後に遷移するページ またログアウトする機能も必要ですが、そちらはcomponentsディレクトリにAppHeaderコンポーネントを用意してヘッダーにログアウト機能を追加します。 vueファイル 説明 AppHeader.vue ヘッダーコンポーネント。ログアウト機能を追加します。 app.vue # pagesディレクトリに色々作成しないといけないので、まずapp.vueを下記のようにします。 app.vue <template> <UApp> <NuxtLayout> <NuxtPage></NuxtPage> </NuxtLayout> </UApp> </template> layouts/default.vue <template> <div> <AppHeader /> <UMain> <slot /> </UMain> </div> </template> --> Information <UApp> や <NuxtLayout> などはNuxtUIというライブラリのUIコンポーネントです。 下記では特に断りなくNuxtUIのコンポーネント群を使用しています。 ログインページ # 次にログインページを作成します。下記がlogin.vueの全体像です。 login.vue <script setup lang="ts"> import type { AuthError } from '@supabase/supabase-js'; /** Supabaseクライアントのインスタンス */ const supabase = useSupabaseClient(); /** ログインユーザー情報 */ const user = useSupabaseUser(); /** 通知(トースト)機能の利用 */ const toast = useToast(); /** 表示モードの切り替え(in: ログイン、up: 新規登録)*/ const sign = ref<'in' | 'up'>('in'); watchEffect(() => { // ユーザーが認証済み(ログイン中)の場合、トップページへリダイレクト if (user.value) { return navigateTo('/'); } }); // フォームの入力項目定義 const fields = [ { name: 'email', label: 'Email', type: 'text' as const, placeholder: 'メールアドレスを入力してください', required: true, }, { name: 'password', label: 'Password', type: 'password' as const, placeholder: 'パスワードを入力してください', }, ]; /** * メールアドレスとパスワードによるログイン処理 * * @param email メールアドレス * @param password パスワード */ const signIn = async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { displayError(error); } }; /** * 新規ユーザー登録処理 * * @param email メールアドレス * @param password パスワード */ const signUp = async (email: string, password: string) => { const { error } = await supabase.auth.signUp({ email, password, }); if (error) { displayError(error); } else { toast.add({ title: 'Sign up successful', icon: 'i-lucide-check-circle', color: 'success', }); await signIn(email, password); } }; /** * 認証エラーをトースト通知として表示 * * @param error Supabaseから返却される認証エラーオブジェクト */ const displayError = (error: AuthError) => { toast.add({ title: 'Error', description: error.message, icon: 'i-lucide-alert-circle', color: 'error', }); }; /** * フォーム送信時のハンドラー * * @param payload フォームから渡される入力データ(emailやpassword) */ async function onSubmit(payload: any) { const email = payload.data.email; const password = payload.data.password; if (sign.value === 'in') { // ログインの場合 await signIn(email, password); } else { // 新規登録の場合 await signUp(email, password); } } </script> <template> <UContainer class="h-[calc(100vh-var(--ui-header-height))] flex items-center justify-center px-4" > <UPageCard class="max-w-sm w-full"> <UAuthForm :title="sign === 'in' ? 'ログイン' : '新規登録'" icon="i-lucide-user" :fields="fields" @submit="onSubmit" > <template #description> {{ sign === 'up' ? '既にアカウントをお持ちの方は' : '新規登録の場合は' }} <UButton variant="link" class="p-0" @click="sign = sign === 'up' ? 'in' : 'up'"> こちら </UButton> </template> <template #submit> <div class="flex items-center justify-center"> <UButton type="submit" class="justify-center cursor-pointer w-80"> {{ sign === 'up' ? '新規登録' : 'ログイン' }} </UButton> </div> </template> </UAuthForm> </UPageCard> </UContainer> </template> コードを上から順を追って解説していきます。 最初にsupabaseクライアントの準備とログイン状態に応じたリダイレクト処理を行います。 /** Supabaseクライアントのインスタンス */ const supabase = useSupabaseClient(); /** ログインユーザー情報 */ const user = useSupabaseUser(); /** 通知(トースト)機能の利用 */ const toast = useToast(); /** 表示モードの切り替え(in: ログイン、up: 新規登録)*/ const sign = ref<'in' | 'up'>('in'); watchEffect(() => { // ユーザーが認証済み(ログイン中)の場合、トップページへリダイレクト if (user.value) { return navigateTo('/'); } }); またログインフォーム(UIコンポーネント UAuthForm )に渡すための入力項目を定義します。 // フォームの入力項目定義 const fields = [ { name: 'email', label: 'Email', type: 'text' as const, placeholder: 'メールアドレスを入力してください', required: true, }, { name: 'password', label: 'Password', type: 'password' as const, placeholder: 'パスワードを入力してください', }, ]; 次にSupabaseのauthライブラリを用いた認証部分の実装をします。 ログイン処理では signInWithPassword メソッド、新規登録には signUp メソッドを使用して、引数にはemailとpasswordを指定します。 /** * メールアドレスとパスワードによるログイン処理 * * @param email メールアドレス * @param password パスワード */ const signIn = async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { displayError(error); } }; /** * 新規ユーザー登録処理 * * @param email メールアドレス * @param password パスワード */ const signUp = async (email: string, password: string) => { const { error } = await supabase.auth.signUp({ email, password, }); if (error) { displayError(error); } else { toast.add({ title: 'Sign up successful', icon: 'i-lucide-check-circle', color: 'success', }); await signIn(email, password); } }; エラー時にはトースト通知として表示するようにメソッドを作っています。 /** * 認証エラーをトースト通知として表示 * * @param error Supabaseから返却される認証エラーオブジェクト */ const displayError = (error: AuthError) => { toast.add({ title: 'Error', description: error.message, icon: 'i-lucide-alert-circle', color: 'error', }); }; フォームの送信ボタンが呼ばれたら、表示モードの状態に合わせて上記の signIn メソッドと signUp メソッドが呼ばれるようにします。 /** * フォーム送信時のハンドラー * * @param payload フォームから渡される入力データ(emailやpassword) */ async function onSubmit(payload: any) { const email = payload.data.email; const password = payload.data.password; if (sign.value === 'in') { // ログインの場合 await signIn(email, password); } else { // 新規登録の場合 await signUp(email, password); } } 最後にNuxtUIを利用してログイン用のテンプレートを作成します。表示モードに合わせてログインか新規登録を切り替えるようにしました。 <template> <UContainer class="h-[calc(100vh-var(--ui-header-height))] flex items-center justify-center px-4" > <UPageCard class="max-w-sm w-full"> <UAuthForm :title="sign === 'in' ? 'ログイン' : '新規登録'" icon="i-lucide-user" :fields="fields" @submit="onSubmit" > <template #description> {{ sign === 'up' ? '既にアカウントをお持ちの方は' : '新規登録の場合は' }} <UButton variant="link" class="p-0" @click="sign = sign === 'up' ? 'in' : 'up'"> こちら </UButton> </template> <template #submit> <div class="flex items-center justify-center"> <UButton type="submit" class="justify-center cursor-pointer w-80"> {{ sign === 'up' ? '新規登録' : 'ログイン' }} </UButton> </div> </template> </UAuthForm> </UPageCard> </UContainer> </template> 最終的に出来上がったテンプレート部分が下記になります。 メインページ # 次にログイン後に遷移するメインページを作成します。今回は例としてユーザーが書いたブログ記事の一覧を表示するページを作成しています。 index.vue <script setup lang="ts"> import type { Database } from '#build/types/supabase-database'; import type { TableColumn } from '@nuxt/ui'; /** Supabaseクライアントのインスタンス */ const client = useSupabaseClient<Database>(); /** ログインユーザー情報 */ const user = useSupabaseUser(); /** * 記事一覧の取得 */ const { data: articles } = await useAsyncData( 'articles', async () => { const { data } = await client .from('article') .select('*') .eq('uuid', user.value!.sub) .order('regist_date'); return data ?? []; }, { default: () => [] } ); /** * テーブルのカラム定義 */ const columns: TableColumn<any, any>[] = [ { accessorKey: 'id', header: 'ID' }, { accessorKey: 'regist_date', header: '日付' }, { accessorKey: 'title', header: 'タイトル' }, { accessorKey: 'abstract', header: '概要' }, ]; </script> <template> <UContainer> <UPageSection title="記事一覧" description="最新記事を表示します" headline="ブログ"> <div class="flex justify-center items-center"> <div v-if="articles.length > 0"> <UCard variant="subtle"> <UTable :data="articles" :columns="columns"> </UTable> </UCard> </div> </div> </UPageSection> </UContainer> </template> コードを上から順を追って解説していきます。 最初にログインページと同じようにsupabaseクライアントを行います。 /** Supabaseクライアントのインスタンス */ const client = useSupabaseClient<Database>(); /** ログインユーザー情報 */ const user = useSupabaseUser(); 今回はSupabaseで作成したarticleテーブルのデータを表示する機能を実装します。そのため、Supabaseクライアントを生成する際に自動生成した型定義ファイルを適用して「Database型」を指定しています。こうすることでテーブル名やカラム名に入力補完が効くようになり、開発効率が格段にアップします。 --> Information 型定義ファイルはSupabase CLIを利用することによって生成できます。 まず下記コマンドを実行してSupabaseのログインと初期化をします。 npx supabase login npx supabase init その後、下記コマンドを実行すると、型定義ファイルが生成されます。 npx supabase gen types typescript --project-id "<project_id>" --schema public > .\app\types\database.types.ts 次に記事一覧を取得する機能を実装します。ログインユーザー情報 user にあるuuidを user.value!.sub で取得して、下記のようにユーザーに紐づいている記事を取得するようにしました。 また記事一覧をテーブル形式で表示するために、テーブルのカラムを定義しています。 accessorKey はarticleテーブルのカラムと一致するように設定して、 header はテーブルのヘッダーに表示する名称を設定します。 /** * 記事一覧の取得 */ const { data: articles } = await useAsyncData( 'articles', async () => { const { data } = await client .from('article') .select('*') .eq('uuid', user.value!.sub) .order('regist_date'); return data ?? []; }, { default: () => [] } ); /** * テーブルのカラム定義 */ const columns: TableColumn<any, any>[] = [ { accessorKey: 'id', header: 'ID' }, { accessorKey: 'regist_date', header: '日付' }, { accessorKey: 'title', header: 'タイトル' }, { accessorKey: 'abstract', header: '概要' }, ]; 最後にテンプレート部分を作成します。 <template> <UContainer> <UPageSection title="記事一覧" description="最新記事を表示します" headline="ブログ"> <div class="flex justify-center items-center"> <div v-if="articles.length > 0"> <UCard variant="subtle"> <UTable :data="articles" :columns="columns"> </UTable> </UCard> </div> </div> </UPageSection> </UContainer> </template> ヘッダーコンポーネント # 最後にログアウト機能を実装します。こちらの機能はヘッダー部分(components > AppHeader.vue)に実装しました。 AppHeader.vue <script setup lang="ts"> /** Supabaseクライアントのインスタンス */ const client = useSupabaseClient(); /** ログインユーザー情報 */ const user = useSupabaseUser(); /** * ログアウト処理 */ const logout = async () => { await client.auth.signOut(); navigateTo('/login'); }; </script> <template> <UHeader :toggle="false"> <template #left> <span class="font-bold text-lg">Demo</span> </template> <template #right> <UButton v-if="user" variant="link" class="cursor-pointer" @click="logout"> ログアウト </UButton> <UButton v-if="!user" variant="link" to="/login"> ログイン </UButton> </template> </UHeader> </template> ログアウト処理はかなり単純で、ただsupabaseクライアントで signOut メソッドを使って実装するだけです。あとはテンプレート部分にログアウトボタンを追加することで、ログアウトできてしまいます。 メインページと合わせて実際に出来たページがこちらになります。 以上でメールアドレスによる認証が実装できました。 メールアドレスによる認証の検証 # それでは実際に画面上で新規登録してみましょう。 ログインページでメールアドレスとパスワードを入力して新規登録ボタンを押すと、認証メールが届きます。 こちらの「Confirm your mail」のリンクを押すと、ユーザー登録が完了し、アプリのメインページにリダイレクトします。 またユーザ登録が完了しているかは、Supabaseで作成したプロジェクトの「Authentication > Users」で確認できます。データが列として入っている場合は登録が完了しています。まだリンクでの認証が済んでいない場合は、Last Sign Inの列でWaiting for verificationと表示されます。 まとめ # 今回はNuxt.jsとSupabaseを組み合わせたauth認証の実装を解説しました。特に意識したところもなく、簡単にサクッと実装できたのが今回の驚きでした。 バックエンドの開発工数を最小限に抑えつつ、安全な認証を簡単に実装できるのはかなり魅力的ですね。 今回はメールアドレスによる認証のみ解説しましたが、他にもGoogleやGitHubなどの認証も簡単に導入できます。 よかったらそちらも試してみてください。 補足:ログインしないで閲覧できるページがほしい場合 # 基本的に、紹介した方法でNuxt.jsとSupabaseを利用した認証できます。しかし、こちらの方法だとログインをしていない場合、必ずログインページにリダイレクトされてしまいます。ですが、たまにログインしないでも閲覧できるページもほしい場合があると思います。 その時の設定方法も、実は非常に簡単でnuxt.config.tsに下記を付け足せばよいです。 export default defineNuxtConfig({ // ~~~省略~~~ supabase: { redirectOptions: { login: '/login', callback: '/confirm', include: [], exclude: ['/'], // ここの部分がログインしなくても閲覧できるページ cookieRedirect: false, }, }, // ~~~省略~~~ }) こちらを設定すると、 exclude の部分で設定したページについてはログインする必要はありません。 ぜひお試しください! 参考文献 # Supabase公式サイト Nuxt Supabase導入公式サイト ログインフォームを作る際にこのサイトで紹介しているDemoのソースコードが非常に参考になりました。 Todo list example using Supabase and Nuxt 3 Supabaseを布教したい Supabaseとは?初心者向けに分かりやすく解説! Supabase + Nuxt 3でチャットアプリを作ってみた Nuxt + Supabase で Googleログイン機能を作ってみる 話題のSupabaseでサクッと認証機能をつくってみた! こちらはNext.jsによる実装ですが、参考になりました。
2025年9月、SysML Version 2.0(SysML v2)が正式リリースされました。 「SysML v2を試してみたい」と思っても対応しているツールは高価だったり、汎用の描画ツールで SysML v2のモデルを作成してみてもいまいちピンとこなかったりといった経験はないでしょうか。 本記事では「SysML v2のグラフィカル記法がどんなものか試してみたい」という時におすすめのツール SysONをご紹介します。 SysONとは # SysON(読みは シスオン または スィスオン )は、SysML v2の主にグラフィカル記法を作成、編集するためのツールです。 この名前は、「システムにオンする」と「システムモデリングの新しいシーズン(seasonとsysonはやや音が似ている)」というのが由来だそうです。 SysONのソースコードは GitHub で公開されています。 ライセンスは EPL-2.0です。 GitHubのリポジトリ名(eclipse-syson / syson)からわかるとおり、このツールは Eclipse財団の SysONプロジェクトで開発・保守されています。 この SysONプロジェクトはフランスの OBEO社と CEA(フランス原子力・代替エネルギー庁)が主導し、実開発は OBEO社が担っています。 ちなみに、「OSSツール、フランス、Eclipse財団」といえば、UML2モデリングツールである Papyrus を思い浮かべる方もいるかもしれません。 日本では認知度の低いツールですので知らない方も結構いらっしゃるのではないかと思います。 実はこの Papyrusの開発も SysONと同じ OBEO社が担っています。 Papyrusは SysML v1をサポートしているので、SysML v1を使いたいなら Papyrus、SysML v2を使いたいなら SysONという棲み分けになっているのかなと思います。 SysONの構成 # SysONは Webアプリケーションです。 ユーザーはクライアントPCの Webブラウザで SysONサーバーにアクセスします。 ユーザーが Webブラウザで行ったモデルに対する操作は SysONサーバーで実行されます。 複数のユーザーによるモデリングが可能ですし SysML v2仕様には REST APIの要件もありますので、Webアプリケーションは妥当だと思います。 しかしその一方、ネットワーク環境によっては動作が遅くすぐに表示が更新されないといったデメリットもあるため、モデリングの操作に慣れてくるとストレスを感じることもあるかもしれません。 マニュアルに記載されているサポートする Webブラウザは Google Chromeと Firefoxの最新安定版です。 Safari、Microsoft Edge、Operaなど他のブラウザを用いる場合は使えるかどうか検証してからがよいでしょう。 英語ですが、SysONの ユーザーマニュアルに該当するドキュメント もあります。 インストール # 事前準備 # まずはどのリリースをインストールするかを決めましょう。 リリースは Eclipse SysONの Webサイト に記載されています。 GitHubの Tags を確認するといくつもの Tagがありますが、末尾に ".0" が付いているものが安定版の位置づけになります。 本記事では、安定版である v2025.8.0をインストールします。 インストール方法は マニュアル(v2025.8.0) に記載されています。 マニュアルにはインストール方法が4つ記載されていますが、大きく分けるとローカルテスト用と本番用の2タイプです。 セキュリティを気にしないならばローカルテスト用、セキュリティを考慮すべき環境ならば本番用の方法でインストールしましょう。 本記事は SysML v2を試しに使ってみることを想定していますので、 Basic Local Test Setup のインストールを行います。 SysONのローカルテスト用インストールには Docker Engineを使用します。 Docker Desktopは有償ですが、Docker Engineは Apache License 2.0ですので無料で利用できます。 ここでは Docker Engineのインストール方法は割愛します。 筆者は Windows11とその WSL2(Debian/Linux)に Docker Engineをインストールしました。 Docker Engineのインストールが完了したら SysONのインストールを開始します。 docker-compose.ymlを取得する # Webブラウザで GitHubにある SysONの Webページ にアクセスして、docker-compose.ymlをダウンロードします。 curlコマンドを用いて docker-compose.ymlをダウンロードする場合は以下の通りです。 curl -OL https://raw.githubusercontent.com/eclipse-syson/syson/refs/tags/v2025.8.0/docker-compose.yml dockerを起動する # Docker Engineのサービスを起動するにあたって、現状の確認をしましょう。 serviceコマンドで dockerサービスの状態を確認します。 sudo service docker status dockerサービスが起動していない場合は以下のメッセージが表示されます。 Docker is not running ... failed! Docker Engineのサービスを起動します。 sudo service docker start 再び、サービスの状態を確認してみましょう。 Docker is running. dockerサービスが起動しました。 先程ダウンロードした docker-compose.ymlファイルのあるフォルダで以下のコマンドを実行します。 docker compose up SysONサーバーが bootすると、コンソールログの一部に以下のロゴが出力されます。 app-1 | _____ ____ _ __ app-1 | / ___/ __ __ _____ / __ \ / | / / app-1 | \__ \ / / / // ___// / / // |/ / app-1 | ___/ // /_/ /(__ )/ /_/ // /| / app-1 | /____/ \__, //____/ \____//_/ |_/ app-1 | /____/ app-1 | app-1 | :: Spring Boot :: (v3.5.0) app-1 | 起動が正常に完了すると、以下のメッセージが表示されます。 app-1 | 2025-12-01T06:45:59.914Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' app-1 | 2025-12-01T06:45:59.937Z INFO 1 --- [ main] org.eclipse.syson.SysONApplication : Started SysONApplication in 18.896 seconds (process running for 19.808) Tomcat started on port 8080 (http) は Webサーバーである Apache Tomcatが起動したことをあらわします。 SysONサーバーが起動したら、いよいよ Webブラウザから SysONサーバーアクセスしてみましょう。 最初の画面 # Webブラウザを起動し、 http://localhost:8080 にアクセスします。 以下のホーム画面が表示されれば準備完了です。 ちなみにこの画面の Existing Projectsのリストにある "Batmobile"は、あのアメコミヒーローが使っている車を題材にしたサンプルです。 終了する # SysONサーバーを起動したシェルで Ctrl + Cすると SysONサーバーが終了します。 dockerサービスを停止する場合は、以下のコマンドで停止します。 sudo service docker stop 次回予告 # ここまでで SysONを使ってモデリングする準備が整いました。 次回からはいよいよ、SysONを使った SysML v2のモデリング操作をみていきましょう。