Files
sibo-playground/code/inventory/SCANNER-API.md

12 KiB

Workabout MX barcode scanner — API notes (reverse-engineered)

There is no official SDK documentation for the Workabout MX integral laser scanner. The Psion SIBO C SDK and I/O Devices Reference predate the MX and describe only the older external barcode modules (wands / wand-emulation), not the integral laser. These notes are reverse-engineered from device behaviour and from the Workabout MX ROM w2mx_v7.20f_eng.bin (strings), so treat unconfirmed items as such.

Hardware

  • Integral laser scanner, Symbol engine. The demo (DEMMAN.APP) reports Type: Laser 1223 (some units 1222).
  • It is a decoded scanner: it decodes in hardware and lights a green good-read LED. It does not emit an undecoded (HHLC) signal.
  • Trigger: the keyboard scan key, which the Window Server reports as key code 368. The key alone does not fire the laser from an arbitrary app; the scanner software arms/reads the engine.

Device driver and ports

LLDEV on the device lists a logical driver wl2 (units=1). The scanner is reached through this driver. The demo's "Select Scanner" screen lets you pick both a decoder type and a port (device driver + unit letter):

  • Port device drivers offered: TTY, WLS, WL2. The MX integral laser uses WL2.
  • Units seen in the ROM: WL2:A and WL2:D.

Decoder types (from the demo, "chosen from those used in standard Workabout products"):

Decoder Meaning
Generic External non-configurable reader
Symbol1 Workabout standard scanner
Datalogic Workabout CCD
HP Workabout Wand
Symbol2 Workabout MX scanner (the integral laser)

Opening the device

VOID *h;
INT  err = p_open(&h, "WL2:D", (UINT)-1);

Observed results:

Call Result Meaning
p_open("WL2:D", -1) 0 opens — this is the integral-laser unit
p_open("WL2:A", -1) -9 (E_GEN_INUSE) in use — held by the resident scanner software
p_open("WL2:B" / ":C") (untested)

WL2 has a single unit, so only one process may hold it. WL2:A is permanently -9 even with the demo closed, indicating a resident holder; WL2:D is the unit an application opens.

The 11-byte parameter block

The scanner is configured by an eleven-byte parameter block. The application interprets these bytes and converts them into commands for the selected decoder (so the same block means different wire commands for Symbol2 vs HP, etc.):

Byte Parameter
Param0 Decode security
Param1 Code type (symbology select)
Param2 Decode options A
Param3 Decode options B
Param4 General parameters
Param5 ITF length 1
Param6 ITF length 2
Param7 Preamble
Param8 Postamble byte 1
Param9 Postamble byte 2
Param10 General decode options

The preamble/postamble bytes mean decoded output may carry configurable leading / trailing characters — parsing code must account for them.

Access model

Applications do not drive WL2 byte-by-byte themselves; they use the ROM library SCANNER.DYL (used by SCANAPP.APP and DEMMAN.APP). That library opens the port, converts the 11-byte block to decoder commands, configures and enables the engine, and returns decoded scans.

Paths that do NOT work for the integral laser

Recorded so they are not retried:

  • bar*.ldd decoders (BAREAN, BARC39, …) + BAR: device. These are software decoders for the external wand modules. Loading BAREAN.LDD and opening BAR:A/BAR:B returns -41 (E_FILE_DEVICE, "no interface found in slot") — the integral laser is not a BAR: expansion interface. BAR:D/BAR:E return -38 (E_FILE_NAME, invalid unit).
  • TTY:D serial reads. Opening TTY:D powers the laser (it fires briefly), but no decoded bytes ever arrive on the serial channel — not with default config, not after P_FSET to 9600/8/1, not with P_OBEY_DSR cleared, not with the intelligent-reader escape commands (<Esc>-y1J / <Esc>-y1K), not reading one byte at a time. The integral laser's decoded data is not on TTY:D.

Initialisation sequence (reverse-engineered from the ROM)

Opening WL2:D is not enough — the scanner must be configured and enabled first. The sequence was recovered by disassembling SCANNER.DYL in the v7.20f ROM (around file offset 0xD7AA4). It uses the SIBO I/O executive int 0xCF, whose convention here is: CL = I/O function code, BX = channel handle, DX = argument (by value), result in AX. From C this is the p_iow(chan, func, ...) layer.

Function codes observed on the WL2 channel (from p_file.h, plus WL2-specific ones above the standard range):

CL Meaning
6 (P_FDETACH slot) used as a scanner enable/control op
7 (P_FSET) used as a scanner enable/control op
8 P_FSENSE
9 P_FFLUSH
0x0C (12) WL2-specific: write a config/command byte (byte passed in DX)

The driver keeps a per-channel structure; the param block sits at channel+4, with the decoder type at offset +6 (dispatch values 1..4) and the 11-byte parameter block at +8..+0x12. Configuration is pushed to the engine as a series of cl=0x0C writes (one command byte per call in DX), then enable ops (cl=6, cl=7).

Default Symbol2 parameter block (from the ROM)

The routine at 0xD7AB3 fills the 11-byte block with these defaults for the Workabout MX (Symbol2) scanner:

Byte Param Value
+8 Param0 Decode security 0x04
+9 Param1 Code type 0x3F (all symbologies)
+0xA Param2 Decode options A 0x01
+0xB Param3 Decode options B 0x15
+0xC Param4 General params 0x06
+0xD Param5 ITF length 1 0x04
+0xE Param6 ITF length 2 0x1E
+0xF Param7 Preamble 0x80
+0x10 Param8 Postamble 1 0x0D (CR)
+0x11 Param9 Postamble 2 0x0A (LF)
+0x12 Param10 General decode 0x06

So decoded output is terminated by CR LF (0x0D 0x0A), and Code type 0x3F enables all symbologies (UPC included). A reader should assemble bytes until CR/LF and strip the preamble/postamble.

Confirmed on-device

  • p_open(&h, "WL2:D", -1) returns 0.
  • Issuing control ops 6 then 7 on the channel (p_iow(h, 6), p_iow(h, 7)) triggers a scan: the laser fires and decodes (green good-read LED). This is reproducible. The trigger is one-shot — it must be re-issued for each scan.

NOT yet solved: retrieving the decoded data

After a confirmed good read, the decoded barcode could not be retrieved from the WL2:D channel by any tried method:

  • p_iow(h, P_FREAD, buf) (count-in-buf[0] style) — no data.
  • p_read(h, buf, len) (synchronous, with length) — no data.
  • p_ioc(h, P_FREAD, &stat, buf, &len) + trigger + p_iowait() (async) — no data.

The retrieval logic in SCANNER.DYL/SCANAPP fetches the decoded data on a separate handle (a driver global, e.g. [0x13b5]) obtained through OS-service stubs of the form mov ah,N; int 0x84/0x85/0x86/0x87; ret — not a plain P_FREAD on the WL2:D control channel. This resolves an earlier puzzle: the SIBO executive uses different register conventions per interrupt vector — the int 0x84 service family passes the function in AH, whereas the int 0xCF I/O executive uses CL.

SCANNER.DYL is an object-oriented ROM library (a DYL). This strongly suggests the scanner is driven by creating a scanner object and sending it messages (the OLIB/OO model), not by raw device I/O on WL2:D. That would explain why every raw WL2:D read returned no data — wrong paradigm.

Confirmed working on device: p_open("WL2:D") + control ops 6 then 7 trigger the laser and a good decode (green LED). Only retrieving the decoded bytes is unsolved.

Decompilation (Ghidra) — the read path

SCANNER.DYL/SCANAPP decompile cleanly in Ghidra (16-bit real-mode x86). The app-level read routine (FUN_858d) is:

obj->flags |= 8;
obj->[0x190] = 0;
FUN_8ce8(); FUN_8cd3(); FUN_8cd3();       // pre-read setup
obj->[0x194] = FUN_8d8e();                // acquire a read handle  (int 0x84)
obj->[0x192] = FUN_8c8b(0,2,1,obj);       // set up the read buffer (int 0x84; int 0xd2)
swi(0xcf)(); swi(0xcf)();                 // config / enable ops on the channel
if (obj->[0x196] == 0)
    obj->[0x196] = swi(0xcf)();           // P_FREAD -> decoded result stored at +0x196
else
    swi(0xcf)();                          // P_FCANCEL

The decoder-side helpers also decompile clearly: FUN_7ae3 is the per-decoder init (dispatch on [channel+10]; case 4 = the Symbol path, which calls FUN_6dc4 to build the 0xff 0xee … command frame and writes the 11-byte param block); FUN_7aa4 is the trigger (two int 0xCF ops = the 6/7 we use).

The scanner is an OO library object (paradigm correction)

The System Services vector table names the services: int 0x84 = LIBMANAGER (the Library Manager) and int 0x83 = MESSMANAGER (object messaging). LIBMANAGER sub-functions (in AH) are NMLIBLOAD=0, UNLOAD=1, LINK, FIND, HANDLE, CREATE(=5), CREATEBYHANDLE(=6), …. So the read's FUN_8d8e/FUN_8c8b call NMLIBCREATE / NMLIBCREATEBYHANDLE — they create a scanner library object.

Therefore the scanner is driven as an object-oriented library object, not by device I/O. WL2:D is what SCANNER.DYL opens and reads internally; an application never p_open/p_reads it. Every earlier raw-device attempt was the wrong paradigm — which is why they returned nothing.

The correct C approach uses the standard OO API (the same one the SDK's growbar.c demo uses):

  • p_getlibh(category) — get the library handle for a category.
  • p_newsend / f_newsend(category, class, O_..._INIT, &args) — create the scanner object and send its init message.
  • p_send2 / p_send3(obj, O_...) — send it messages (configure, trigger, read).

These C wrappers sit over LIBMANAGER (create/handle) and MESSMANAGER (send).

Next RE target (well-defined): extract from SCANNER.DYL its category id (candidate token in ROM: oscanner), class, and the message ordinals (O_...) for init / set-params / trigger / read, plus their parameter structs. OLIB assigns ordinals dynamically from the whole class hierarchy (base classes in olib/hwim), so they resolve at runtime rather than in a static dump — capture them with the SIBO Debugger, which supports remote debugging (PC↔device over serial) and breakpoints inside dynamic libraries. With those, a C client can create the scanner object and message it. Validation requires the physical device (MAME cannot inject a scan).

See CONTINUATION.md for the full on-device debugging procedure (capture the category/ordinals/params via LIBMANAGER/MESSMANAGER breakpoints), a C test-harness template, and how to turn the captures into the shipped reader.

To close it, one of:

  1. The Workabout MX C SDK SCANNER.DYL header / OO category definition (what SCANAPP was built against) — the clean answer, and likely the only practical one, because the interface is object-oriented.
  2. A deep RE effort under MAME (psionwamx): automate the UI to launch SCANAPP, breakpoint the read routine, and trace it. Caveats: MAME cannot inject a real barcode (no laser input), so only the read setup is observable in emulation; the reconstructed sequence must still be validated on the physical device.

Error codes seen (from epocdefs.h)

Value Name Meaning
-9 E_GEN_INUSE device already open / in use
-32 E_FILE_EXIST (LDD) already loaded
-38 E_FILE_NAME invalid device name / unit
-41 E_FILE_DEVICE device / interface not present