Przejdź do głównej zawartości

Integracja z DUST Go

DUST Go to wbudowana przeglądarka mobilna: wczytuje aplikację internetową w komponencie WebView i udostępnia stronie sprzęt urządzenia do skanowania DUST za pośrednictwem niewielkiego mostu JavaScript, @dustid/dust-go-connect. Tworzysz i hostujesz zwykłą aplikację internetową, a DUST Go zapewnia kamerę, akcesoria optyczne i potok wykonywania ujęć — bez potrzeby korzystania z natywnych narzędzi programistycznych.

Ta strona prowadzi do jednego rozpoznanego skanu. Wszystko, co wykracza poza ten etap — logowanie, geolokalizację, zgodność i sprzęt na komputery stacjonarne — opisano w sekcji Poza pierwszym skanem poniżej.

Zanim zaczniesz

Rola
Poświadczenie konta usługi przechowywane przez backend
Urządzenie
iPhone z aplikacją DUST Go i obsługiwanym akcesorium Loupe do wykonywania ujęć DUST
  1. Użytkownik otwiera aplikację internetową w DUST Go (za pomocą linku aplikacji).
  2. Strona importuje @dustid/dust-go-connect; biblioteka wykrywa most DUST Go i udostępnia obiekt connector.
  3. Strona wywołuje scanAsync(). DUST Go otwiera natywny skaner nad stroną.
  4. Po wykonaniu ujęcia DUST Go przekazuje stronie zdarzenie skanowania: ładunek zawiera dane i metadane ujęcia.
  5. Strona wysyła ujęcie do twojego backendu, który wywołuje APID (/api/v1/tags/identify, /bind lub /verify), aby je rozpoznać.

Skan DUST przekazuje stronie surowe ujęcie (obraz JPEG zakodowany w base64) wraz z metadanymi ujęcia — a nie rozpoznany identyfikator. Identyfikacja, weryfikacja i powiązanie odbywają się w całości po stronie serwera.

Okno terminala
npm install @dustid/dust-go-connect

Pakiet nie ma żadnych zależności, jest udostępniany na licencji MIT i zawiera definicje typów TypeScript.

Eksport connector ma wartość undefined, gdy strona nie działa w DUST Go, więc ta sama kompilacja aplikacji może obsługiwać zarówno zwykłe przeglądarki, jak i DUST Go:

dust-go.ts — runs in the BROWSER
import { connector } from "@dustid/dust-go-connect";
export const insideDustGo = Boolean(connector);

Warto pamiętać o dwóch rzeczach:

  • Moment importu. Wykrywanie następuje w chwili importu modułu, w przeglądarce. Jeśli korzystasz z renderowania po stronie serwera, uzależnij każde użycie obiektu connector od zakończenia hydratacji.
  • Wykrywanie po stronie serwera. Nowsze kompilacje DUST Go dodają też do nagłówka User-Agent komponentu WebView oznaczenie DustGo/<version> (<app id>), dzięki czemu serwer może wykryć aplikację, zanim zostanie uruchomiony jakikolwiek kod JavaScript. Traktuj to jako wskazówkę, a nie granicę bezpieczeństwa — każdy może sfałszować nagłówek User-Agent.

Najprostszą drogą jest API oparte na obietnicach (promise) — wyświetl skaner i zaczekaj na jedno ujęcie:

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() zwraca Promise<ScanPayload>. Obietnica zostaje odrzucona, jeśli użytkownik zamknie skaner bez wykonania ujęcia, a także natychmiast po wywołaniu poza DUST Go (gdy obiekt connector nie istnieje) — dlatego najpierw sprawdź connector, jeśli ta sama ścieżka kodu działa w zwykłych przeglądarkach.

payload.typepayload.dataUwagi
DUSTObraz JPEG ujęcia DUST zakodowany w base64Duży; rozpoznaj go po stronie serwera za pomocą APID
QR, BARCODE, DATA_MATRIXZdekodowana zawartość symbolu
NFCIdentyfikator chipu NFC (UID): wielkie litery szesnastkowe bez separatorów, np. 04A1B2C3D4E5F6Taki sam na iPhonie i na Androidzie

payload.metadata (typ ScanMetadata) opisuje ujęcie: identyfikatory urządzenia (deviceId, modelName, osName, osVersion, appVersion), wybór obiektywu i kamery oraz opcjonalnie pola optyki (zoom, ostrość, ekspozycja, ISO), geolokalizację (latitude/longitude/accuracy) i szczegóły źródła ujęcia w przypadku zewnętrznych akcesoriów skanujących (captureSource, usbVendorId, usbProductId, dragonBackend). Podczas powiązania przekazuj je bez zmian — APID zapisuje je razem z identyfikatorem.

Przeczytaj tę sekcję przed krokiem rozpoznawania: od niej zależy kształt całej integracji.

Dlatego krok rozpoznawania to dwa pliki w dwóch miejscach:

PlikGdzie działaCzy przechowuje poświadczenie DUST?
Strona skanowaniaPrzeglądarka w DUST GoNie
Procedura obsługi /api/dust-scanTwój serwerTak

Ujęcie DUST staje się przydatne dopiero wtedy, gdy APID je dopasuje. Strona dekoduje ujęcie z base64 do postaci binarnej i wysyła je do własnego punktu końcowego:

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() };
}

Backend dodaje poświadczenie i kontekst oraz sam określa zakres skanowania:

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" },
});
}

Pole nazywa się searchTeamIds. Ładunki identyfikacji odrzucają właściwości, których nie deklarują, więc starsza pisownia searchGroupIds powoduje niepowodzenie całego żądania z kodem 400 INVALID_REQUEST, zamiast zostać zignorowana. Jedyną zachowaną starszą nazwą z group jest nagłówek Dust-Ctx-Grp-Id, nadal akceptowany jako alias Dust-Ctx-Team-Id.

Ten sam kształt multipart obsługuje dwie pozostałe operacje:

  • /api/v1/tags/bind — dodaj threadId. Pole options.enrollmentSessionId jest opcjonalne: podaj UUID wygenerowany po stronie klienta i używany ponownie w ramach całej serii, gdy kilka ujęć należy do siebie (stanowisko rejestracji przetwarzające partię); pomiń je przy jednorazowym powiązaniu.
  • /api/v1/tags/verify — dodaj threadId i tags, czyli tablicę obiektów ([{ "tagId": "…", "tagType": "DUST" }], zakodowaną jako JSON w multipart), a nie ciągów z identyfikatorami. Pole tags jest wymagane.

Pełne kontrakty żądań i odpowiedzi opisano w sekcji Identyfikatory, a w sekcji Skaner React znajduje się gotowy do skopiowania komponent, który implementuje wszystkie trzy operacje względem procedury obsługi w backendzie, takiej jak powyższa.

  1. Zainstaluj DUST Go z App Store lub Google Play (wymagania sprzętowe opisano w sekcji Obsługiwane urządzenia — ujęcie DUST wymaga obsługiwanego akcesorium optycznego).
  2. Udostępnij aplikację przez HTTPS pod adresem URL osiągalnym z urządzenia (podczas programowania wystarczy adres w sieci LAN).
  3. W DUST Go dodaj swój adres URL jako niestandardowy link aplikacji i otwórz go.
  4. Upewnij się, że obiekt connector został wykryty, a następnie wykonaj skan.
  5. Potwierdź, że ujęcie dociera do backendu, a wywołanie DUST wykonywane przez backend zwraca wynik, który możesz wyświetlić — również w przypadku „brak dopasowania”.

Aby zobaczyć działanie całego mostu (wykrywanie, scanAsync, możliwości, dziennik zdarzeń), otwórz w DUST Go demonstrację DUST Go.


Wszystko poniżej jest opcjonalne. Wróć do tej części, gdy pojedynczy skan będzie rozpoznawany od początku do końca.

W przepływach typu „zeskanuj wiele, a potem wyślij” użyj API nasłuchiwania — skaner pozostaje otwarty między kolejnymi ujęciami:

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 działa tak samo, ale bez zarządzania nasłuchiwaczami — otrzymuje każdy skan i zwraca własną funkcję anulowania subskrypcji:

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

Jedna sesja skanera może dostarczyć serię ujęć, dlatego wysyłaj je pojedynczo. Ładunek DUST to duży obraz JPEG w base64; wysyłanie kilku naraz przeciąża połączenie i nie daje operatorowi użytecznej informacji o postępie. Umieszczaj ładunki w kolejce i czekaj na zakończenie każdego wysyłania przed rozpoczęciem następnego.

Hosty się różnią — telefon może regulować zoom i ekspozycję, akcesorium w postaci mikroskopu USB udostępnia inny zestaw ustawień, a starsza kompilacja może nie udostępniać niczego. Zapytaj o możliwości, zamiast wnioskować o nich z nagłówka User-Agent:

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

Brak ogłoszenia lub puste ogłoszenie traktuj jako tylko pojedyncze ujęcia, bez regulowanych ustawień. Nigdy nie traktuj braku odpowiedzi jako dostępności danej możliwości.

Standardowe wywołania navigator.geolocation działają w DUST Go — aplikacja w sposób przezroczysty przekazuje je przez natywny monit systemu operacyjnego o uprawnienia. Nie jest potrzebny żaden kod korzystający z obiektu connector.

Jeśli aplikacja korzysta z OAuth/OIDC, pamiętaj, że DUST Go przekazuje nawigację do zewnętrznych dostawców tożsamości do przeglądarki systemowej, a wywołanie zwrotne wraca do strony przez niestandardowy schemat URL.

Gdy tworzysz na stronie parametr OAuth redirect_uri, zawsze go opakowuj:

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;

Poza DUST Go (oraz w hostach, w których przepisanie nie jest potrzebne) ten kod niczego nie zmienia. W DUST Go zamienia protokół adresu URL na niestandardowy schemat aplikacji, tworząc URI w rodzaju com.dustidentity.dustgo://your-host/auth/callback (dokładny schemat pochodzi z kompilacji DUST Go; wartością zapasową jest dustgo), który dostawca tożsamości musi mieć zarejestrowany jako dozwolony URI przekierowania.

Most korzysta z numerowanego protokołu. Strona ogłasza, które zdarzenia rozumie, w automatycznym uzgadnianiu hello w chwili importu, a host wysyła wyłącznie zdarzenia zadeklarowane przez stronę — dzięki temu starszy host i nowsza strona współpracują z ograniczoną funkcjonalnością, zamiast przestać działać.

Numer odczytaj z zainstalowanego pakietu, a nie z tej strony: SDK go eksportuje.

import { CONNECT_PROTOCOL_VERSION } from "@dustid/dust-go-connect";
ProtokółCo dodano
1Niejawny starszy kontrakt: scan / hide / show, bez uzgadniania.
2Uzgadnianie hello i zdarzenie calibrationResult.
3Ogłoszenie capabilities wysyłane przez host do strony, polecenie runCapture wysyłane przez stronę do hosta oraz odpowiadające na nie zdarzenie captureStatus.
4CaptureRequirements.app (wymuszana przez host zasada wersji aplikacji) i CaptureCapabilities.enforcedRequirements, dzięki czemu strona może odróżnić host, który egzekwuje wymaganie, od takiego, który by je zignorował.
5ScanPayload.exif — fotograficzne dane EXIF ujęcia wraz z informacją o ich pochodzeniu. Wyłącznie rozszerzenie, bez zmian w istniejącym działaniu.
6Polecenie capturePhoto wysyłane przez stronę do hosta i odpowiadające na nie zdarzenie photo: zwykłe zdjęcie (na przykład załącznik rekordu), celowo niebędące skanem.

SDK w drzewie źródłowym tej dokumentacji to @dustid/dust-go-connect 0.2.0, który obsługuje protokół 6. Ta strona nie określa, co obecnie oferuje publiczny rejestr npm — sprawdź CONNECT_PROTOCOL_VERSION i pole version faktycznie zainstalowanego pakietu, a jeśli potrzebujesz konkretnego wydania, zwróć się do DUST Identity.

Zasady, które obowiązują niezależnie od numeru wersji:

  • Rozstrzygające jest wykrywanie możliwości w czasie działania. Numer protokołu określa, co potrafi wyrazić SDK strony; nic nie mówi o hoście po drugiej stronie ani o podłączonym sprzęcie. Zapytaj za pomocą getCapabilities() i traktuj brak ogłoszenia jako „tylko pojedyncze ujęcia, bez regulowanych ustawień”.
  • Rozgałęziaj kod według event.type i ignoruj to, czego nie rozpoznajesz. Dochodzą nowe typy zdarzeń; strona, która zgłasza wyjątek przy nieznanym typie, przestanie działać po aktualizacji hosta, która w ogóle nie powinna jej dotyczyć.
  • Ręczna kontrolka w interfejsie hosta nie jest możliwością protokołu, a pomyślnie wykonane polecenie sprzętowe nie dowodzi, że wartość została zastosowana w chwili wykonania ujęcia.
  • Zdarzenia calibrationResult i ackCalibrationResults() to wewnętrzne mechanizmy własnych przepływów kalibracji DUST — integracje zewnętrzne mogą je ignorować.

Ta sekcja dotyczy urządzenia Dragon podłączonego do komputera stacjonarnego i sterowanego ze zwykłej przeglądarki za pośrednictwem aplikacji towarzyszącej. Nie jest to ścieżka dla Androida: w DUST Go na Androidzie aplikacja sama steruje podłączonym urządzeniem Dragon, a jego ujęcia trafiają do strony przez zwykły przepływ runCapture opisany powyżej — createDragonClient() nie bierze w tym udziału, a na telefonie nie instaluje się żadnej aplikacji towarzyszącej. Zobacz Obsługiwane urządzenia.

Witryna może przejąć podgląd z urządzenia Dragon i sterowanie jego ostrością. Zainstalowana aplikacja towarzysząca obsługuje polecenia USB w tle. Przy pierwszym użyciu użytkownicy zatwierdzają witrynę w DUST Camera Controls i, gdy pojawi się monit, przyznają przeglądarce dostęp do kamery i do urządzeń lokalnych. Zatwierdzenie jest zapamiętywane dla danej witryny i przeglądarki. Witryna musi korzystać z HTTPS. Camera Controls może pozostawać na pasku menu z zamkniętym oknem; żadne wyskakujące okno połączenia nie jest wymagane. Nie jest też wymagana żadna witryna hostowana przez DUST.

Zainstaluj DUST Camera Controls w folderze Aplikacje i uruchom ją raz. Aplikacja pokazuje, czy urządzenie Dragon jest podłączone, i pozostaje dostępna na pasku menu. Uruchamianie przy logowaniu jest domyślnie włączone; możesz je wyłączyć w aplikacji.

Instalatory dla poszczególnych platform są wymienione na stronie Pliki do pobrania. Dystrybucje z obsługą aktualizacji automatycznie wyszukują aktualizacje i udostępniają polecenie Wyszukaj aktualizacje… na pasku menu. To ty decydujesz, kiedy zainstalować aktualizację; przed ponownym uruchomieniem aplikacji zapisz swoją pracę i zakończ skanowanie. Wersje ewaluacyjne bez kanału aktualizacji wymagają zastępczego instalatora od DUST.

W DICE otwórz Skanowanie, wybierz DUST, włącz Sterowanie ostrością Dragon pod skanerem i przy pierwszym użyciu wybierz Połącz. Po zatwierdzeniu obszar podglądu udostępnia autofokus, ustawienia ostrości oraz przycisk Zeskanuj, który wykonuje wybraną operację. Gdy wrócisz do skanowania z włączonym trybem, DICE połączy się automatycznie, jeśli Camera Controls działa, a dostęp przeglądarki do kamery jest nadal przyznany. Jeśli uprawnienia przeglądarki wymagają uwagi, wybierz Połącz lub Uruchom kamerę, aby dokończyć łączenie.

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();

W danej chwili urządzeniem Dragon może sterować tylko jedna sesja witryny. Użytkownik może cofnąć dostęp za pomocą polecenia Zarządzaj dostępem witryn… w Camera Controls. Zamknięcie natywnego okna pozostawia połączenie dostępne; zamknięcie aplikacji je przerywa. Zatwierdzona sesja otrzymuje aktualizacje stanu urządzenia, gdy urządzenie Dragon zostanie odłączone lub ponownie podłączone. Po ponownym podłączeniu otwórz wideo jeszcze raz. Podłączenie sprzętu nie daje witrynom dostępu do niego bez wiedzy użytkownika.

Autofokus przeszukuje zakres wokół ostatniego polecenia ostrości, ograniczony skalibrowanym zakresem urządzenia. Suwak zakresu określa odległość po każdej stronie tej pozycji początkowej. Pozostaw stronę widoczną, a próbkę nieruchomą, z interesującym szczegółem na środku obrazu. Próbka pozbawiona wyraźnych cech lub słabo oświetlona może nie zapewniać kontrastu wystarczającego do ustawienia ostrości. Przed wykonaniem ujęcia obejrzyj uzyskany obraz.

Wartości ostrości są poleceniami, a nie zmierzonymi pozycjami obiektywu. Te metody ręcznego sterowania nie gwarantują ustawień w chwili wykonania zdjęcia i nie umożliwiają ujęć zgodnych z planem ani skanów scalonych. Skaner mobilny i jego dotychczasowy kontrakt scanAsync() pozostają dostępne niezależnie.

Kamery Dragon z obsługą AMR raportują skalibrowane powiększenie optyczne obok ostrości w DICE i DUST Camera Controls. Subskrybuj onState, aby otrzymywać state.magnification, lub wywołaj await dragon.getMagnification(), aby uzyskać bieżący odczyt. Opcjonalny odczyt zawiera value, unit: "x" i raw (pozycję czujnika w ćwiartkach jednostki). Starsze aplikacje towarzyszące, nieobsługiwana kalibracja i nieudane odpytania nie dają odczytu; nigdy nie zastępuj go zerem. Powiększenie jest tylko do odczytu i jest niezależne od ostatniego polecenia ostrości wydanego silnikowi.