2026-07-06 21:35:08 +01:00
|
|
|
# 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 (`<Esc>-y1J` / `<Esc>-y1K`), not reading
|
|
|
|
|
one byte at a time. The integral laser's decoded data is not on `TTY:D`.
|
|
|
|
|
|
2026-07-06 21:42:36 +01:00
|
|
|
## Initialisation sequence (reverse-engineered from the ROM)
|
2026-07-06 21:35:08 +01:00
|
|
|
|
2026-07-06 21:42:36 +01:00
|
|
|
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.
|
2026-07-06 21:35:08 +01:00
|
|
|
|
2026-07-06 21:42:36 +01:00
|
|
|
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.
|
|
|
|
|
|
2026-07-06 22:08:40 +01:00
|
|
|
### Confirmed on-device
|
2026-07-06 21:42:36 +01:00
|
|
|
|
2026-07-06 22:08:40 +01:00
|
|
|
- `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.
|
|
|
|
|
|
2026-07-06 22:49:49 +01:00
|
|
|
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`.
|
2026-07-06 22:08:40 +01:00
|
|
|
|
2026-07-06 22:49:49 +01:00
|
|
|
`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.
|
|
|
|
|
|
2026-07-06 23:20:56 +01:00
|
|
|
### 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).
|
|
|
|
|
|
2026-07-06 22:49:49 +01:00
|
|
|
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.
|
2026-07-06 21:35:08 +01:00
|
|
|
|
|
|
|
|
## 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 |
|