# 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 ```c 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 (`-y1J` / `-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: ```c 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). **Remaining dependency:** the read acquires its handle and buffer through two further OS services — **`int 0x84`** (handle/manager service, sub-function in `AH`) and **`int 0xd2`** — and is woven into the app's object layout (`obj+0x190..0x196`). Reproducing it in C needs those two services mapped (their C wrappers / semantics) and the object fields understood. That is the next RE target; Ghidra makes it tractable but it is still several layers, and any result must be validated on the physical device (MAME cannot inject a scan). 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 |