DUST Go との統合
DUST Go は組み込みモバイルブラウザーです。WebView にウェブアプリを読み込み、小さな JavaScript ブリッジである @dustid/dust-go-connect を介して、デバイスの DUST スキャンハードウェアをページに公開します。通常のウェブアプリを構築してホストすれば、DUST Go がカメラ、光学アクセサリー、撮影パイプラインを提供します。ネイティブツールチェーンは必要ありません。
このページでは、1 回のスキャンを解決するところまで説明します。それ以降の内容(サインイン、位置情報、互換性、デスクトップハードウェア)については、以下の最初のスキャンの後を参照してください。
始める前に
- ロール
- バックエンドで保持する Service Account 認証情報
- デバイス
- DUST Go と、DUST 撮影に対応した Loupe アクセサリーを備えた iPhone
全体の仕組み
Section titled “全体の仕組み”- ユーザーがアプリリンクを使用して、DUST Go 内でウェブアプリを開きます。
- ページが
@dustid/dust-go-connectをインポートします。ライブラリは DUST Go ブリッジを検出し、connectorを公開します。 - ページが
scanAsync()を呼び出します。DUST Go はページ上にネイティブスキャナーを開きます。 - 撮影時に、DUST Go がスキャンイベントをページへ返します。ペイロードには撮影データとメタデータが含まれます。
- ページが撮影データを独自のバックエンドへ送信し、バックエンドが APID(
/api/v1/tags/identify、/bind、または/verify)を呼び出して解決します。
DUST スキャンがページへ渡すのは、生の撮影データ(base64 エンコードされた JPEG)と撮影メタデータであり、解決済みの識別子ではありません。識別、検証、バインドはすべてサーバー側で行われます。
インストール
Section titled “インストール”npm install @dustid/dust-go-connectこのパッケージには依存関係がなく、MIT ライセンスで提供され、TypeScript の型が同梱されています。
DUST Go を検出する
Section titled “DUST Go を検出する”ページが DUST Go 内で実行されていない場合、connector エクスポートは undefined になります。そのため、同じアプリビルドを通常のブラウザーと DUST Go の両方に配信できます。
import { connector } from "@dustid/dust-go-connect";
export const insideDustGo = Boolean(connector);知っておくべき点が 2 つあります。
- インポートのタイミング。 検出はブラウザーでのモジュールインポート時に行われます。サーバーレンダリングを行う場合は、connector の使用をハイドレーション後に限定してください。
- サーバー側での検出。 最近の DUST Go ビルドでは、WebView の User-Agent に
DustGo/<version> (<app id>)も付加されるため、JavaScript が実行される前にサーバーでアプリを検出できます。これはヒントとして扱い、セキュリティ境界として扱わないでください。User-Agent は誰でも偽装できます。
スキャンを撮影する
Section titled “スキャンを撮影する”最も簡単なのは Promise API を使用する方法です。スキャナーを表示し、1 回の撮影を待機します。
import { scanAsync } from "@dustid/dust-go-connect";
const payload = await scanAsync();// payload: { type: 'DUST' | 'QR' | 'BARCODE' | 'DATA_MATRIX' | 'NFC',// data: string, metadata?: ScanMetadata }scanAsync() は Promise<ScanPayload> を返します。ユーザーが撮影せずにスキャナーを閉じた場合は reject されます。また、DUST Go の外部で呼び出した場合(connector が存在しない場合)も即座に reject されます。そのため、通常のブラウザーでも同じコードパスを実行する場合は、最初に connector を確認してください。
ペイロードの内容
Section titled “ペイロードの内容”payload.type | payload.data | 備考 |
|---|---|---|
DUST | DUST 撮影の base64 エンコードされた JPEG | サイズが大きいため、APID を介してサーバー側で解決してください |
QR, BARCODE, DATA_MATRIX | デコードされたシンボルの内容 | |
NFC | NFC チップから読み取った 16 進数 ID |
payload.metadata(型は ScanMetadata)には、デバイス識別子(deviceId、modelName、osName、osVersion、appVersion)、レンズおよびカメラの選択情報に加え、必要に応じて光学情報(ズーム、フォーカス、露出、ISO)、位置情報(latitude/longitude/accuracy)、外部スキャンアクセサリーの撮影元情報(captureSource、usbVendorId、usbProductId、dragonBackend)が記述されます。バインド時には内容を解釈せず、そのまま転送してください。APID は識別子とともにこの情報を保存します。
認証情報を置く場所
Section titled “認証情報を置く場所”解決手順に進む前に、このセクションを読んでください。統合全体の構成がここで決まります。
したがって、解決手順は 2 つの場所にある 2 つのファイルで構成されます。
| ファイル | 実行場所 | DUST 認証情報を保持するか |
|---|---|---|
| スキャンページ | DUST Go 内のブラウザー | いいえ |
/api/dust-scan ハンドラー | 独自のサーバー | はい |
APID に対してスキャンを解決する
Section titled “APID に対してスキャンを解決する”DUST 撮影は、APID が一致対象を見つけて初めて利用できます。ページは base64 の撮影データをバイナリーへデコードし、独自のエンドポイントへ送信します。
// No DUST credential in this file.async function identifyDustScan(base64Jpeg: string) { // The scan arrives base64-encoded; APID expects binary multipart data. const bytes = Uint8Array.from(atob(base64Jpeg), (c) => c.charCodeAt(0)); const form = new FormData(); form.set("operation", "identify"); form.set("tagType", "DUST"); form.set("data", new Blob([bytes], { type: "image/jpeg" }));
const response = await fetch("/api/dust-scan", { method: "POST", body: form, credentials: "same-origin", }); return { status: response.status, body: await response.json() };}バックエンドは認証情報とコンテキストを追加し、検索範囲自体を指定します。
export async function handleIdentify(form: FormData, session: Session) { // `searchTeamIds` is a JSON array of Team UUIDs. Set it on the server — a // browser-supplied scope is a request, never an authorization. form.set("searchTeamIds", JSON.stringify(session.allowedTeamIds));
const response = await fetch("https://apid.dustid.io/api/v1/tags/identify", { method: "POST", headers: { Authorization: `Bearer ${await getDustToken()}`, "Dust-Ctx-Org-Id": session.organizationId, }, body: form, });
// Pass the status and body through unchanged: the page needs to tell // "nothing matched" (404 IDENTIFIER_NOT_FOUND) from "try again" // (503 SCAN_SEARCH_INCOMPLETE). return new Response(await response.text(), { status: response.status, headers: { "Content-Type": "application/json" }, });}フィールド名は searchTeamIds です。Identify のペイロードは宣言されていないプロパティを拒否するため、従来の searchGroupIds という表記を使用すると、無視されるのではなくリクエスト全体が 400 INVALID_REQUEST で失敗します。現在も残っている唯一の従来の group 名は、Dust-Ctx-Team-Id の別名として引き続き受け入れられる Dust-Ctx-Grp-Id ヘッダーです。
同じ multipart 形式を、ほかの 2 つの操作にも使用できます。
/api/v1/tags/bind—threadIdを追加します。options.enrollmentSessionIdは任意です。複数の撮影をひとまとまりとして扱う場合(登録ステーションでバッチを処理する場合)は、クライアントで生成した UUID を指定し、実行全体で再利用します。1 回限りのバインドでは省略してください。/api/v1/tags/verify—threadIdとtagsを追加します。tagsは ID 文字列ではなく、オブジェクトの配列([{ "tagId": "…", "tagType": "DUST" }]を multipart 内で JSON エンコードしたもの)です。tagsは必須です。
完全なリクエスト/レスポンスの仕様については識別子を、上記のようなバックエンドハンドラーを介して 3 つの操作すべてを実装する、そのまま利用可能なコンポーネントについては React Scanner を参照してください。
統合をテストする
Section titled “統合をテストする”- App Store または Google Play から DUST Go をインストールします(ハードウェア要件については対応デバイスを参照してください。DUST 撮影には対応する光学アクセサリーが必要です)。
- デバイスからアクセスできる URL で、HTTPS を使用してアプリを配信します(開発時には LAN アドレスも使用できます)。
- DUST Go で URL をカスタムアプリリンクとして追加し、開きます。
- connector が検出されたことを確認してから、スキャンを実行します。
- 撮影データがバックエンドに到達し、バックエンドの DUST 呼び出しから、「一致なし」の場合を含め、表示可能な結果が返されることを確認します。
ブリッジ全体(検出、scanAsync、イベントログ)を試せる最小限の診断ページが、パッケージリポジトリーに用意されています。
最初のスキャンの後
Section titled “最初のスキャンの後”以下の内容はすべて任意です。1 回のスキャンをエンドツーエンドで解決できるようになってから参照してください。
複数スキャンのワークフロー
Section titled “複数スキャンのワークフロー”複数回スキャンしてから送信するフローでは、リスナー API を使用します。撮影を繰り返してもスキャナーは開いたままになります。
import { connector } from "@dustid/dust-go-connect";
connector?.add("my-listener", (event) => { switch (event.type) { case "scan": handleScan(event.payload); break; case "hide": // scanner closed case "show": // scanner opened break; default: // Ignore unknown event types — the protocol may grow. break; }});
connector?.showScanner();// later: connector?.hideScanner(); connector?.remove("my-listener");addScanListener は、リスナー管理が不要であることを除けば同じものです。すべてのスキャンを受け取り、専用の購読解除関数を返します。
import { addScanListener } from "@dustid/dust-go-connect";
const stop = addScanListener((payload) => handleScan(payload));// later: stop();1 回のスキャナーセッションで一連の撮影データが届く場合があるため、1 件ずつ送信してください。DUST ペイロードはサイズの大きい base64 JPEG です。複数を一度にアップロードすると接続帯域を圧迫し、オペレーターが進捗を把握できなくなります。ペイロードをキューに入れ、各送信が完了してから次の送信を開始してください。
スキャナーでできること
Section titled “スキャナーでできること”ホストによって機能は異なります。スマートフォンではズームと露出を調整できますが、USB 顕微鏡アクセサリーでは別の機能セットが公開され、古いビルドでは何も公開されないことがあります。User-Agent から推測せず、問い合わせてください。
const capabilities = connector?.getCapabilities?.();通知が存在しない、または空の場合は、1 回ずつ撮影できるだけで、調整可能な項目はないものとして扱ってください。応答がないことを機能の存在として解釈しないでください。
標準の navigator.geolocation 呼び出しは DUST Go 内でも機能します。アプリがネイティブ OS の権限プロンプトを介して透過的にプロキシします。connector のコードは必要ありません。
DUST Go 内のサインインフロー
Section titled “DUST Go 内のサインインフロー”アプリで OAuth/OIDC を使用する場合は、DUST Go が外部 ID プロバイダーへのナビゲーションをシステムブラウザーに引き渡し、コールバックがカスタム URL スキームを介してページへ戻ることに注意してください。
ページで OAuth の redirect_uri を構築する場合は、必ず次のようにラップしてください。
import { connector } from "@dustid/dust-go-connect";
const origin = window.location.origin;const redirectUri = ( connector?.rewriteRedirect(new URL(`${origin}/auth/callback`)) ?? new URL(`${origin}/auth/callback`)).href;DUST Go の外部(および書き換えが不要なホスト)では、何も変更されません。DUST Go 内では、URL のプロトコルをアプリのカスタムスキームへ置き換え、com.dustidentity.dustgo://your-host/auth/callback のような URI を生成します(正確なスキームは DUST Go のビルドによって決まり、dustgo がフォールバックです)。ID プロバイダーでは、この URI を許可されたリダイレクト URI として登録しておく必要があります。
バージョン管理と互換性
Section titled “バージョン管理と互換性”ブリッジは番号付きのプロトコルを使用します。ページはインポート時の自動 hello ハンドシェイクで、理解できるイベントを通知します。ホストはページが通知したイベントのみを送信するため、古いホストと新しいページは、失敗するのではなく機能を縮退させることで相互運用できます。
番号はこのページではなく、インストールしたパッケージから確認してください。SDK がエクスポートしています。
import { CONNECT_PROTOCOL_VERSION } from "@dustid/dust-go-connect";| プロトコル | 追加された機能 |
|---|---|
| 1 | 暗黙的な従来の仕様:scan/hide/show。ハンドシェイクなし。 |
| 2 | hello ハンドシェイクと calibrationResult イベント。 |
| 3 | ホスト→ページの capabilities 通知、ページ→ホストの runCapture コマンド、およびその応答となる captureStatus イベント。 |
| 4 | CaptureRequirements.app(ホストが適用するアプリバージョンポリシー)と CaptureCapabilities.enforcedRequirements。これにより、ページは要件を確認するホストと、要件を無視するホストを区別できます。 |
| 5 | ScanPayload.exif — 撮影画像の写真 EXIF とその来歴。純粋な追加機能です。 |
| 6 | ページ→ホストの capturePhoto コマンドと、その応答となる photo イベント。通常の写真(たとえばスレッドへの添付)であり、意図的にスキャンとは区別されています。 |
このドキュメントのソースツリーに含まれる SDK は @dustid/dust-go-connect 0.2.0 で、プロトコル 6 を使用します。公開 npm レジストリーで現在提供されているバージョンについて、ここでは断定しません。実際にインストールしたパッケージの CONNECT_PROTOCOL_VERSION と version フィールドを確認し、特定のリリースが必要な場合は DUST Identity にお問い合わせください。
どのバージョンにも共通するルールは次のとおりです。
- 実行時の機能検出が正式な判断基準です。 プロトコル番号が示すのは、ページ側の SDK が表現できる機能だけです。相手側のホストや接続されているハードウェアについては何も示しません。
getCapabilities()で問い合わせ、通知がない場合は「1 回ずつ撮影できるだけで、調整可能な項目はない」と扱ってください。 event.typeで分岐し、認識できないものは無視してください。 新しいイベントタイプが追加されることがあります。未知のタイプで例外を発生させるページは、本来対応する必要のなかったホストのアップグレードによって動作しなくなります。- ホストのインターフェイスに手動コントロールがあることは、プロトコル上の機能を意味しません。また、ハードウェアコマンドが成功しても、撮影時にその値が適用された証拠にはなりません。
calibrationResultイベントとackCalibrationResults()は、DUST の自社キャリブレーションワークフロー用の内部機構です。サードパーティー統合では無視できます。
デスクトップの Dragon コントロール
Section titled “デスクトップの Dragon コントロール”このセクションでは、通常のブラウザーから companion アプリを介して操作する、デスクトップコンピューターに接続された Dragon について説明します。これは Android の経路ではありません。Android の DUST Go 内では、アプリ自体が接続された Dragon を操作し、撮影データは上記の通常の runCapture フローを通じて届きます。createDragonClient() は使用せず、スマートフォンに companion アプリをインストールすることもありません。対応デバイスを参照してください。
ウェブサイト側で Dragon のプレビューとフォーカスコントロールを管理できます。インストール済みの companion がバックグラウンドで USB コマンドを処理します。初回使用時に、ユーザーは DUST Camera Controls でウェブサイトを承認し、プロンプトが表示されたらブラウザーによるカメラおよびローカルデバイスへのアクセスを許可します。承認は、そのウェブサイトとブラウザーについて記憶されます。ウェブサイトでは HTTPS を使用する必要があります。Camera Controls はウィンドウを閉じた状態でもメニューバーで動作し続けるため、接続用のポップアップは必要ありません。DUST がホストするウェブサイトも必要ありません。
DUST Camera Controls を Applications にインストールし、一度開いてください。アプリには Dragon の接続状態が表示され、メニューバーから引き続き利用できます。ログイン時の起動はデフォルトで有効になっており、アプリ内で無効にできます。
各プラットフォーム用のインストーラーは、ダウンロードページに掲載されています。更新機能が有効なディストリビューションは更新を自動的に確認し、メニューバーに**アップデートを確認…**を表示します。更新をインストールするタイミングはユーザーが選択します。アプリを再起動する前に、作業を保存してスキャンを完了してください。更新フィードのない評価用ビルドでは、DUST から代替インストーラーを入手する必要があります。
DICE でスキャンを開き、DUST を選択して、スキャナーの下にある Dragon フォーカスコントロールをオンにし、初回使用時に接続を選択します。承認後、ビューポートでオートフォーカス、フォーカス設定、および選択した操作によるスキャンを利用できます。このモードを有効にしたままスキャンへ戻ると、Camera Controls が実行中で、ブラウザーのカメラアクセスが引き続き許可されている場合、DICE は自動的に再接続します。ブラウザーの権限に対応が必要な場合は、接続またはカメラを開始を選択して接続を完了してください。
import { createDragonClient } from "@dustid/dust-go-connect";
const dragon = createDragonClient();const video = document.querySelector("video")!;
// First use: request native approval and browser permissions from a user action.connectButton.onclick = async () => { try { await dragon.connect(); await dragon.openVideo(video); const controls = await dragon.getControls(); const focus = controls.find((control) => control.name === "focus"); // Use focus.min / focus.max for your manual slider when focus is available. } catch (error) { showConnectionError(error); }};
// On a later visit, reuse approval without creating a native approval prompt.// If this rejects, present Connect Dragon; do not repeatedly request approval.// await dragon.connect({ interactive: false });// Reopen video only after browser camera permission has already been granted.
manualSlider.onchange = async () => { await dragon.setControl("focus", Number(manualSlider.value));};
autofocusButton.onclick = async () => { try { const result = await dragon.autofocus(video, { radius: Number(autofocusRangeSlider.value), onProgress: ({ position, sampled }) => showProgress(position, sampled), }); manualSlider.value = String(result.position); // "low-contrast" means autofocus retained the starting focus. showFocusResult(result.outcome); } catch (error) { showFocusError(error); }};cancelButton.onclick = () => dragon.cancelAutofocus();
captureButton.onclick = async () => { const payload = await dragon.capture(video); // Send the raw DUST JPEG to your server, which calls the Identifier API. await sendToYourServer(payload);};
const unsubscribe = dragon.onState(({ connected, controls }) => { updateDeviceUI(connected, controls);});// On page/component teardown: unsubscribe(); dragon.dispose();一度に Dragon を制御できるウェブサイトセッションは 1 つだけです。ユーザーは Camera Controls の**ウェブサイトアクセスを管理…**からアクセスを取り消せます。ネイティブウィンドウを閉じても接続は利用可能なままですが、アプリを終了すると接続が停止します。承認済みのセッションは、Dragon が切断または再接続されたときにデバイス状態の更新を受信します。再接続後は映像を開き直してください。ハードウェアを接続しても、ウェブサイトに暗黙的なアクセス権が与えられることはありません。
オートフォーカスは、最後のフォーカスコマンドの位置を中心に、デバイスのキャリブレーション済み範囲内を検索します。範囲スライダーは、その開始位置から両側への距離を表します。ページを表示したままにし、試料を動かさず、目的の細部を画像の中央に配置してください。特徴がない、または照明が不十分な試料では、フォーカスを選択するのに十分なコントラストが得られない場合があります。撮影前に結果画像を確認してください。
フォーカス値はコマンドであり、測定されたレンズ位置ではありません。これらの手動制御メソッドはシャッター作動時の設定を保証せず、所定の撮影や union 撮影を有効にするものでもありません。モバイルスキャナーと既存の scanAsync() の仕様は、これとは独立して引き続き利用できます。
- 識別子 — この統合で撮影データの解決に使用する API。
- エラーとスキャン結果 — スキャン UI で分岐に使用する結果表。
- 対応デバイス — DUST 撮影に必要なハードウェア。
- React Scanner — DUST Go を使用せず、デバイスのカメラで実現する同じフロー。
- AI エージェントによる構築 — コーディングエージェント向けに、この統合を扱うエージェントスキル。