11 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) reportsType: Laser 1223(some units1222). - 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 usesWL2. - Units seen in the ROM:
WL2:AandWL2: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*.ldddecoders (BAREAN,BARC39, …) +BAR:device. These are software decoders for the external wand modules. LoadingBAREAN.LDDand openingBAR:A/BAR:Breturns-41(E_FILE_DEVICE, "no interface found in slot") — the integral laser is not aBAR:expansion interface.BAR:D/BAR:Ereturn-38(E_FILE_NAME, invalid unit).TTY:Dserial reads. OpeningTTY:Dpowers the laser (it fires briefly), but no decoded bytes ever arrive on the serial channel — not with default config, not afterP_FSETto 9600/8/1, not withP_OBEY_DSRcleared, 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 onTTY: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)returns0.- 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 in the ROM its
category id, class, and the message ordinals (O_...) for
init / set-params / trigger / read, plus their parameter structs. With those, a C
client can create the scanner object and message it — the documented OO way.
Validation still requires the physical device (MAME cannot inject a scan).
To close it, one of:
- The Workabout MX C SDK
SCANNER.DYLheader / OO category definition (whatSCANAPPwas built against) — the clean answer, and likely the only practical one, because the interface is object-oriented. - A deep RE effort under MAME (
psionwamx): automate the UI to launchSCANAPP, 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 |