Scanning
A warehouse operator holds a device in one hand and a carton in the other. They
point and pull a trigger; nothing on screen is tapped first. @onerp/react/scan
is the pipeline behind that trigger: a scan arrives from wherever it arrives,
is read into the things it names, and reaches whichever screen is on top.
flowchart LR D[device] -->|ScanSource| R[RawScan] R -->|interpret| E["ScanElement[]"] E -->|resolve| M[ScanMatch] M -->|useScan| S[innermost screen]
Four seams, each replaceable without the others noticing. The reason for four
rather than one is a single fact about labels: one scan can name several
things. A plain code names one product; a GS1-128 label names a product, a
lot and a quantity in one trigger pull. Anything shaped onScan(kind, id)
cannot say that, so the pipeline carries a set of facts from end to end.
Mounting the provider
Section titled “Mounting the provider”One <ScanProvider> in the application’s layout — resolution reads through
the workspace’s business-object manager, which is loaded by then.
const SCAN_RESOLVERS: ScanResolver[] = [ { businessObject: "inventory/Location", field: "code", kind: "code" }, { businessObject: "master/Product", field: "sku", kind: "code" },];
<ScanProvider resolvers={SCAN_RESOLVERS}>{children}</ScanProvider>;The resolvers are the whole vocabulary of the application: what a label can name, which field carries it, and which element kind that field answers. The package itself names no business object — an application that scans pallets and work orders passes different rows and changes nothing else.
Receiving a scan
Section titled “Receiving a scan”useScan({ expect: ["inventory/Location", "master/Product"], onScan: (match) => { const location = match.records.get("inventory/Location"); if (location) { navigate(`/stock/location/${location}`); } }, onUnresolved: (match) => setMissed(match.raw.data),});match.records maps a business object name to the single record its resolver
found. expect orders the attempts — this is how one code lands on a location
from the bin screen and on a product from the product screen. Resolvers that
were not expected still run afterwards, so a scan is never dropped for being
off-topic; a screen that truly accepts one kind ignores the rest in its handler.
Handlers form a stack and the innermost mounted one wins, so a sheet takes the scans while it is open and the screen beneath gets them back when it closes.
An unresolved scan never reaches onScan. The provider vibrates the device
where the browser supports it, and onUnresolved gets the match so the screen
can word its own message. Two records answering one code is not a match either:
those business object names arrive in match.ambiguous, and nothing is picked
for the operator. master/Product.sku carries no uniqueness constraint, so this
is reachable with real master data, not a theoretical branch.
Submitting a scan
Section titled “Submitting a scan”No device implementation ships. Codes reach the pipeline from a typed field:
const submitScan = useSubmitScan();
<form onSubmit={() => submitScan(code)}>…</form>;This field is not a stopgap — it is the fallback every deployment needs for the
device that cannot scan, the label that will not read, and the code read off a
packing slip by eye. Put it in a <form>: a keyboard-wedge scanner ends its
transmission with a terminator key, so the same Enter that submits the field for
a human submits it for the scanner.
Widening the vocabulary
Section titled “Widening the vocabulary”ScanElementKinds is an open interface. A domain adds its kinds by declaration
merging, and the package learns nothing:
declare module "@onerp/react/scan" { interface ScanElementKinds { gtin: string; lotCode: string; netWeight: string; }}An interpreter then produces them. Interpreters are tried in order and the first
one that recognises the scan owns it; returning null passes it along.
const gs1: ScanInterpreter = (raw) => raw.symbology === "]C1" ? parseApplicationIdentifiers(raw.data) : null;
<ScanProvider interpreters={[gs1, plainCode]} resolvers={SCAN_RESOLVERS} />;Every label format is one function added beside the others: GS1-128, an
ISO/IEC 15434 transport label, a supplier’s own pallet tag. Elements resolve
independently, so the GS1 scan above fills the product and the lot from one
trigger pull, while an element that names no record — a net weight — stays on
match.elements for a screen that wants it.
Where a kind is looked up is one more resolver row:
{ businessObject: "master/Product", field: "barcode", kind: "gtin" }Adding a device
Section titled “Adding a device”ScanSource is the device seam: subscribe, push raw scans, return the
unsubscribe.
type ScanSource = (emit: (raw: RawScan) => void) => () => void;
<ScanProvider resolvers={SCAN_RESOLVERS} source={keyboardWedge} />;A keyboard wedge, a camera decoder, or a vendor SDK bridge is one file and one prop. No screen, resolver or interpreter changes when one arrives — and the typed field keeps working beside it.
Two facts shape a keyboard-wedge implementation on Android. A hardware scanner
in keystroke mode is usually an input method, and an input method commits text
rather than pressing keys — so the source listens for input on a field it
owns and keeps focused, not for keydown. Only the terminator key reliably
arrives as a genuine key event. On a desktop with a USB gun in HID mode,
keydown works and the accumulator distinguishes a scan from typing by speed.
RawScan.symbology carries the AIM identifier when the device sends one — the
interpreter’s cheapest way to know what it is holding before it parses anything.