コンテンツにスキップ

DUST Go との統合

DUST Go は組み込みモバイルブラウザーです。WebView にウェブアプリを読み込み、小さな JavaScript ブリッジである @dustid/dust-go-connect を介して、デバイスの DUST スキャンハードウェアをページに公開します。通常のウェブアプリを構築してホストすれば、DUST Go がカメラ、光学アクセサリー、撮影パイプラインを提供します。ネイティブツールチェーンは必要ありません。

このページでは、1 回のスキャンを解決するところまで説明します。それ以降の内容(サインイン、位置情報、互換性、デスクトップハードウェア)については、以下の最初のスキャンの後を参照してください。

始める前に

ロール
バックエンドで保持する Service Account 認証情報
デバイス
DUST Go と、DUST 撮影に対応した Loupe アクセサリーを備えた iPhone
  1. ユーザーがアプリリンクを使用して、DUST Go 内でウェブアプリを開きます。
  2. ページが @dustid/dust-go-connect をインポートします。ライブラリは DUST Go ブリッジを検出し、connector を公開します。
  3. ページが scanAsync() を呼び出します。DUST Go はページ上にネイティブスキャナーを開きます。
  4. 撮影時に、DUST Go がスキャンイベントをページへ返します。ペイロードには撮影データとメタデータが含まれます。
  5. ページが撮影データを独自のバックエンドへ送信し、バックエンドが APID(/api/v1/tags/identify、/bind、または /verify)を呼び出して解決します。

DUST スキャンがページへ渡すのは、生の撮影データ(base64 エンコードされた JPEG)と撮影メタデータであり、解決済みの識別子ではありません。識別、検証、バインドはすべてサーバー側で行われます。

Terminal window
npm install @dustid/dust-go-connect

このパッケージには依存関係がなく、MIT ライセンスで提供され、TypeScript の型が同梱されています。

ページが DUST Go 内で実行されていない場合、connector エクスポートは undefined になります。そのため、同じアプリビルドを通常のブラウザーと DUST Go の両方に配信できます。

dust-go.ts — runs in the BROWSER
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 は誰でも偽装できます。

最も簡単なのは Promise API を使用する方法です。スキャナーを表示し、1 回の撮影を待機します。

scan.ts — runs in the BROWSER
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 を確認してください。

payload.typepayload.data備考
DUSTDUST 撮影の base64 エンコードされた JPEGサイズが大きいため、APID を介してサーバー側で解決してください
QR, BARCODE, DATA_MATRIXデコードされたシンボルの内容
NFCNFC チップから読み取った 16 進数 ID

payload.metadata(型は ScanMetadata)には、デバイス識別子(deviceId、modelName、osName、osVersion、appVersion)、レンズおよびカメラの選択情報に加え、必要に応じて光学情報(ズーム、フォーカス、露出、ISO)、位置情報(latitude/longitude/accuracy)、外部スキャンアクセサリーの撮影元情報(captureSource、usbVendorId、usbProductId、dragonBackend)が記述されます。バインド時には内容を解釈せず、そのまま転送してください。APID は識別子とともにこの情報を保存します。

解決手順に進む前に、このセクションを読んでください。統合全体の構成がここで決まります。

したがって、解決手順は 2 つの場所にある 2 つのファイルで構成されます。

ファイル実行場所DUST 認証情報を保持するか
スキャンページDUST Go 内のブラウザーいいえ
/api/dust-scan ハンドラー独自のサーバーはい

APID に対してスキャンを解決する

Section titled “APID に対してスキャンを解決する”

DUST 撮影は、APID が一致対象を見つけて初めて利用できます。ページは base64 の撮影データをバイナリーへデコードし、独自のエンドポイントへ送信します。

identify.ts — runs in the BROWSER
// 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() };
}

バックエンドは認証情報とコンテキストを追加し、検索範囲自体を指定します。

server/dust-scan.ts — runs on YOUR SERVER
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 を参照してください。

  1. App Store または Google Play から DUST Go をインストールします(ハードウェア要件については対応デバイスを参照してください。DUST 撮影には対応する光学アクセサリーが必要です)。
  2. デバイスからアクセスできる URL で、HTTPS を使用してアプリを配信します(開発時には LAN アドレスも使用できます)。
  3. DUST Go で URL をカスタムアプリリンクとして追加し、開きます。
  4. connector が検出されたことを確認してから、スキャンを実行します。
  5. 撮影データがバックエンドに到達し、バックエンドの DUST 呼び出しから、「一致なし」の場合を含め、表示可能な結果が返されることを確認します。

ブリッジ全体(検出、scanAsync、イベントログ)を試せる最小限の診断ページが、パッケージリポジトリーに用意されています。


以下の内容はすべて任意です。1 回のスキャンをエンドツーエンドで解決できるようになってから参照してください。

複数回スキャンしてから送信するフローでは、リスナー API を使用します。撮影を繰り返してもスキャナーは開いたままになります。

scan-many.ts — runs in the BROWSER
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 は、リスナー管理が不要であることを除けば同じものです。すべてのスキャンを受け取り、専用の購読解除関数を返します。

scan-many-simple.ts — runs in the BROWSER
import { addScanListener } from "@dustid/dust-go-connect";
const stop = addScanListener((payload) => handleScan(payload));
// later: stop();

1 回のスキャナーセッションで一連の撮影データが届く場合があるため、1 件ずつ送信してください。DUST ペイロードはサイズの大きい base64 JPEG です。複数を一度にアップロードすると接続帯域を圧迫し、オペレーターが進捗を把握できなくなります。ペイロードをキューに入れ、各送信が完了してから次の送信を開始してください。

ホストによって機能は異なります。スマートフォンではズームと露出を調整できますが、USB 顕微鏡アクセサリーでは別の機能セットが公開され、古いビルドでは何も公開されないことがあります。User-Agent から推測せず、問い合わせてください。

capabilities.ts — runs in the BROWSER
const capabilities = connector?.getCapabilities?.();

通知が存在しない、または空の場合は、1 回ずつ撮影できるだけで、調整可能な項目はないものとして扱ってください。応答がないことを機能の存在として解釈しないでください。

標準の navigator.geolocation 呼び出しは DUST Go 内でも機能します。アプリがネイティブ OS の権限プロンプトを介して透過的にプロキシします。connector のコードは必要ありません。

アプリで OAuth/OIDC を使用する場合は、DUST Go が外部 ID プロバイダーへのナビゲーションをシステムブラウザーに引き渡し、コールバックがカスタム URL スキームを介してページへ戻ることに注意してください。

ページで OAuth の redirect_uri を構築する場合は、必ず次のようにラップしてください。

redirect.ts — runs in the BROWSER
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 として登録しておく必要があります。

ブリッジは番号付きのプロトコルを使用します。ページはインポート時の自動 hello ハンドシェイクで、理解できるイベントを通知します。ホストはページが通知したイベントのみを送信するため、古いホストと新しいページは、失敗するのではなく機能を縮退させることで相互運用できます。

番号はこのページではなく、インストールしたパッケージから確認してください。SDK がエクスポートしています。

import { CONNECT_PROTOCOL_VERSION } from "@dustid/dust-go-connect";
プロトコル追加された機能
1暗黙的な従来の仕様:scan/hide/show。ハンドシェイクなし。
2hello ハンドシェイクと calibrationResult イベント。
3ホスト→ページの capabilities 通知、ページ→ホストの runCapture コマンド、およびその応答となる captureStatus イベント。
4CaptureRequirements.app(ホストが適用するアプリバージョンポリシー)と CaptureCapabilities.enforcedRequirements。これにより、ページは要件を確認するホストと、要件を無視するホストを区別できます。
5ScanPayload.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 は自動的に再接続します。ブラウザーの権限に対応が必要な場合は、接続またはカメラを開始を選択して接続を完了してください。

dragon.ts — runs in the BROWSER
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() の仕様は、これとは独立して引き続き利用できます。