# Psion SIBO / Workabout MX — I/O Device Model and Drivers

Reference for the EPOC (SIBO) I/O system as documented in the Psion SIBO 'C' SDK.
Everything here is drawn from two manuals:

- **PLIB Reference** — I/O System chapter (Ch. 9), Asynchronous Requests and
  Semaphores (Ch. 8), Memory Allocation (Ch. 7). Cited as *PLIB*.
- **I/O Devices Reference** v2.30 (March 1999). Cited as *IODEV*.

Where a fact is specific to the Workabout / Workabout MX, or is uncertain from the
manuals, it is flagged. The MX integral laser scanner is **not** covered by these
manuals; see the cross-reference in the Barcode section.

> Notation used by the manuals: an I/O service is identified by a function code of
> the form `P_Fxxx` (defined in `p_file.h`). The manuals write `p_iow(P_FWRITE)`,
> `p_ioc(P_FWRITE)` or just `P_FWRITE` to refer to that service. (*IODEV* Ch.1;
> *PLIB* Ch.9.)

---

## 1. The device model

### 1.1 Device drivers: LDDs and PDDs

The purpose of a device driver is to hide the underlying hardware behind a stable
software interface. EPOC defines two driver types (*PLIB* Ch.9, "LDDs and PDDs"):

- **PDD — physical device driver**: hardware dependent (the lower layer).
- **LDD — logical device driver**: hardware independent (the upper layer).

Applications normally interface to **LDDs only**. An LDD may use one or more PDDs.
The serial driver is the canonical example: an upper hardware-independent LDD over
a hardware-dependent PDD; incoming data is buffered at the LDD level. Some drivers
touch no hardware at all — e.g. the C floating-point library is implemented as a
device driver (the 8087 emulator LDD `sys$8087.ldd`). (*PLIB* Ch.9; Ch.5.)

Some drivers are built into the ROM (e.g. the RS232 LDD/PDD); others are external
and must be loaded (e.g. a bar-code reader). (*PLIB* Ch.9.)

The interface between the OS and an LDD does **not** follow a C calling convention;
an LDD may be written in C but requires some 8086 assembly for the LDD vector
interface. (*PLIB* Ch.9; System Services reference.)

### 1.2 Opening a channel: `p_open`

```c
INT p_open(VOID **ppfcb, TEXT *name, UINT mode);
INT f_open(VOID **ppfcb, TEXT *name, UINT mode);   /* p_leave()s on error */
```

`name` is a **3-character device name terminated by a `':'`**, optionally followed
by further text depending on the device. Examples (*PLIB* Ch.9):

| Name  | Device |
| ----- | ------ |
| `FIL:` | a file |
| `PAR:` | a parallel port |
| `TTY:` | an RS232 serial port |
| `TIM:` | an asynchronous timer |

Rules (*PLIB* Ch.9, "Opening a channel to a device"):

- The device name of an external LDD bears **no relation** to the file it was
  loaded from.
- Where a driver supports more than one **unit**, the `':'` is followed by a unit
  letter: `TTY:A` = serial port A, `PAR:B` = parallel port B.
- For `FIL:`, the qualifying text is a file name or full path.
- A loaded device driver **supersedes** any existing device of the same name.

On success the channel control block address is written to `*ppfcb`. On failure
`*ppfcb` is left untouched — pre-set it to zero so `p_close(0)` (a no-op) is safe
in clean-up. For an **attached** driver, the control block is attached to `*ppfcb`
and the value is not changed. (*PLIB* Ch.9.)

Errors: `E_FILE_ALLOC` (no memory for control block), `E_FILE_DEVICE` (device does
not exist), `E_GEN_ARG` (bad `mode`, possibly from falling through to `FIL:`).

### 1.3 The `FIL:` fallback

If `p_open` fails to match a device name, the name is **passed to the `FIL:`
driver** (the process must be connected to the file server). This makes the leading
`FIL:` optional when opening files:

```
p_open(..., "C:\\NOTES\\NEW.TXT", ...)  ==  p_open(..., "FIL:C:\\NOTES\\NEW.TXT", ...)
```

Side effect: opening a **non-existent device** does not give the expected
`E_FILE_DEVICE`, because the name is handed to `FIL:` and the result depends on
`mode` (it might even succeed). Passing `mode = -1` guarantees the `FIL:` open
fails — albeit with the misleading `E_GEN_ARG`. Keeping an explicit `FIL:` prefix
avoids mistaking a file spec for a device name (e.g. a file literally named
`TTY:`). (*PLIB* Ch.9.)

### 1.4 Modes

The interpretation of `mode` **depends on the device**; many devices ignore it.
When a device ignores `mode`, pass `-1`. (*PLIB* Ch.9.) The character/hardware
devices in *IODEV* (`TTY:`, `PAR:`, `SND:`, `FRC:`, `MCR:`, `BAR:`) are all opened
with `mode = -1`. File-open mode flags such as `P_FOPEN`, `P_FREPLACE`,
`P_FUPDATE`, `P_FUNIQUE`, `P_FSTREAM`, `P_FSHARE`, `P_FTEXT` are `FIL:`-specific and
are described in the Files chapter of *PLIB*.

### 1.5 Attached drivers

An **attached driver** is an LDD layered over another LDD, replacing or augmenting
its services. Opening it *attaches* to an already-open underlying channel rather
than allocating a new one. Example: the printer driver `PRO:` is opened over a
print-output device (`PAR:`, `TTY:` or `FIL:`) and thereafter replaces that
device's `P_FWRITE`, `P_FCANCEL` and `P_FCLOSE`. Attached drivers that work
asynchronously install a **device wait handler** at open, called from within the
opening process's `p_iowait`. (*PLIB* Ch.9, "Attached drivers".)

### 1.6 The file server

`FIL:` operations are serviced by the high-priority system process **`SYS$FSRV`**,
which also loads executables and **loads external device drivers**. It serialises
access to shared storage (SSDs) and uses PDDs to reach the many FLASH/RAM/ROM
configurations. Applications are "clients" of the file server; the normal start-up
code connects to it. (*PLIB* Ch.9, "The file server".)

---

## 2. Operations on an open channel

All I/O requests are **asynchronous in principle** — the process I/O semaphore is
always signalled on completion — but many are implemented **synchronously**
(completing before the call returns). A typical device offers zero to three truly
asynchronous functions (commonly `P_FREAD` / `P_FWRITE`); the rest are synchronous.
`P_FCLOSE` is always synchronous. (*PLIB* Ch.9; *IODEV* Ch.1.)

### 2.1 The I/O primitives

```c
/* asynchronous, CDECL + register-calling variants */
INT  p_ioa (VOID *pcb, INT func, WORD *pstat, ...);
INT  p_ioa3(VOID *pcb, INT func, WORD *pstat);
INT  p_ioa4(VOID *pcb, INT func, WORD *pstat, VOID *a1);
INT  p_ioa5(VOID *pcb, INT func, WORD *pstat, VOID *a1, VOID *a2);

/* asynchronous, error folded into *pstat (preferred over p_ioa) */
VOID p_ioc (VOID *pcb, INT func, WORD *pstat, ...);
VOID p_ioc3(VOID *pcb, INT func, WORD *pstat);
VOID p_ioc4(VOID *pcb, INT func, WORD *pstat, VOID *a1);
VOID p_ioc5(VOID *pcb, INT func, WORD *pstat, VOID *a1, VOID *a2);

/* synchronous: start then wait (calls p_waitstat) */
INT  p_iow (VOID *pcb, INT func, ...);
INT  p_iow2(VOID *pcb, INT func);
INT  p_iow3(VOID *pcb, INT func, VOID *a1);
INT  p_iow4(VOID *pcb, INT func, VOID *a1, VOID *a2);
```

- **`p_ioa`** starts the operation and returns immediately. On success, `*pstat`
  holds `E_FILE_PENDING` until the I/O semaphore is signalled, then zero or a
  negative error. A cancelled operation completes with `E_FILE_CANCEL`. Returns
  `E_FILE_INV` if `func` is invalid for the device. **The status word must outlive
  the operation** — never put it on a stack frame you return from. (*PLIB* Ch.9.)
- **`p_ioc`** behaves like `p_ioa` except a failure to *start* is reported exactly
  as if the request had started and then completed with that error (only `*pstat`
  to check). Preferred over `p_ioa`. (*PLIB* Ch.9.)
- **`p_iow`** starts the request and waits (`p_waitstat`) for completion, returning
  the status. Preferred unless you actually need asynchrony. (*PLIB* Ch.9.)

The register-calling variants (`p_iow2/3/4`, `p_ioc3/4/5`, `p_ioa3/4/5`) generate
smaller code and are preferred where applicable; e.g. `p_ioc3` over `p_ioa3`.
(*IODEV* Ch.1; *PLIB* Ch.9.)

Driver rule: normally **one pending request per I/O operation per channel**. A
second `P_FWRITE` while one is pending will `p_panic` the caller; but one read and
one write may be pending simultaneously. (*PLIB* Ch.9; *ADDSYS* driver chapter.)

### 2.2 Convenience functions

| Function | Wraps | Notes |
| -------- | ----- | ----- |
| `INT p_close(VOID *pcb)` | `p_iow(P_FCLOSE)` | `p_close(NULL)` returns 0. Always closes even if it returns an error. |
| `INT p_read(VOID *pcb, VOID *buf, UINT len)` | `p_iow(P_FREAD)` | returns bytes read, or negative error. `f_read` `p_leave`s on error. |
| `INT p_write(VOID *pcb, VOID *buf, UINT len)` | `p_iow(P_FWRITE)` | returns 0 or negative error. `f_write` `p_leave`s on error. |
| `INT p_seek(...)` | `p_iow(P_FSEEK)` | `FIL:` only (Files chapter). |

(*PLIB* Ch.9.)

### 2.3 Waiting and cancelling

```c
VOID p_iowait(VOID);            /* wait on the process I/O semaphore */
VOID p_waitstat(WORD *pstat);   /* wait until *pstat leaves E_FILE_PENDING */
```

To wait on **one specific** request, use `p_waitstat`; `p_iowait` waits for *any*
completion and is used in the central dispatch loop. Every `p_iosignal` must be
matched by a `p_iowait` (or a function that calls it). (*PLIB* Ch.8.)

Cancelling — `p_iow(pcb, P_FCANCEL)` — cancels outstanding async requests on the
channel and returns zero (harmless if none pending). Principles (*PLIB* Ch.9):

- the cancel **precipitates** completion; it does not stop the request completing;
- it may or may not be effective (the request may complete naturally first);
- **you must still consume the completion signal** — typically an immediate
  `p_waitstat` to "use up" the signal.

`p_waitstat` is safer than `p_iowait` for using up a cancelled request's signal.
General-purpose synchronous wrappers must use `p_waitstat`, not `p_iowait`.

---

## 3. I/O function codes (`p_file.h`)

Services are named `P_Fxxx` and defined in `p_file.h`. The generic codes that apply
across many devices are (*PLIB* Ch.9; *IODEV* Ch.1):

| Code | Meaning |
| ---- | ------- |
| `P_FREAD`   | read data from a channel |
| `P_FWRITE`  | write data to a channel |
| `P_FCLOSE`  | close a channel |
| `P_FCANCEL` | cancel outstanding async requests |
| `P_FSENSE`  | sense channel characteristics |
| `P_FSET`    | set channel characteristics |
| `P_FFLUSH`  | flush buffered data |
| `P_FCTRL`   | test/set control lines (serial) |
| `P_FINQ`    | inquire supported characteristics |

Numeric values are assigned in `p_file.h`. The canonical numbering is:

| Constant | Value |
| -------- | ----- |
| `P_FREAD`   | 1 |
| `P_FWRITE`  | 2 |
| `P_FCLOSE`  | 3 |
| `P_FCANCEL` | 4 |
| `P_FSET`    | 7 |
| `P_FSENSE`  | 8 |
| `P_FFLUSH`  | 9 |
| `P_FCTRL`   | 11 |
| `P_FINQ`    | 12 |

> Confirmation from the manuals: the AccessIr chapter tabulates its own function
> numbers and gives `P_FREAD = 1`, `P_FWRITE = 2` (*IODEV* Ch.16, "Constants").
> The System Services driver model (*ADDSYS*) documents the OS-defined common set
> `IoFuncRead / IoFuncWrite / IoFuncClose / IoFuncCancel / IoFuncSet /
> IoFuncSense / IoFuncFlush`, which map to `P_FREAD … P_FFLUSH`; `p_read`,
> `p_write` and `p_close` call the driver with `IoFuncRead`, `IoFuncWrite` and
> `IoFuncClose` respectively. Drivers are urged to keep these conventional meanings
> so that attached drivers work.

Device-specific codes also exist, e.g. `P_FSEEK`, `P_FSETEOF` (files); `P_FTEST`
(test for input); `P_FEDIT` (console edit); `P_FRELATIVE` / `P_FABSOLUTE` (timers);
`P_FSTART` (FRC); `P_FCONNECT` / `P_FDISCONNECT` / `P_FRSUPER` (NCP, Xmodem);
`P_FIR*` (IR); `E_FALARM` / `E_FDIAL` / `E_FSSOUNDCHANNELn` (sound). These are
listed with the individual devices below and in *IODEV*.

Error numbers `-32..-63` are reserved for I/O device errors of the form
`E_FILE_xxx` (also in `p_file.h`). (*PLIB* Ch.6.)

---

## 4. Loading external device drivers

### 4.1 Load / delete

```c
INT p_loadldd(TEXT *pName);           /* default extension .LDD */
INT p_loadpdd(TEXT *pName);           /* default extension .PDD */
INT p_devdel (TEXT *pName, INT devType);   /* devType: E_LDD or E_PDD */
```

- `p_loadldd` / `p_loadpdd` load a driver from a file; if `pName` has no extension,
  `.LDD` / `.PDD` is assumed, and a relative name uses the current path. After
  loading, a channel is opened with `p_open`. Errors: `E_FILE_EXIST` (same file
  already loaded), `E_FILE_NXIST` (file missing), `E_GEN_IMAGE` (bad/corrupt
  format), `E_GEN_NOMEMORY`, `E_GEN_NOSEGMENTS`. Apps that rely on an external LDD
  should call `p_loadldd` and ignore `E_FILE_EXIST`. (*PLIB* Ch.9.)
- `p_devdel` deletes a **RAM-loaded** driver by device name (no trailing `':'`).
  Only loaded (not ROM) drivers can be deleted. Errors: `E_FILE_DEVICE` (not
  loaded), `E_GEN_NSUP` (ROM driver), `E_GEN_INUSE` (currently open), or a
  device-dependent error. **Warning:** a null `pName` deletes the first unloadable
  driver of that type. Good practice: attempt `p_devdel` when finished and ignore
  the result (harmless if in use elsewhere or in ROM). (*PLIB* Ch.9.)

Related helpers: `p_devqu(pName)` returns the number of units an LDD supports (e.g.
`p_devqu("TTY")` → 2 if two serial boards are fitted; `E_GEN_FAIL` means unlimited,
as `FIL:` returns); `p_devfnd(...)` iterates device names matching a wildcard.
(*PLIB* Ch.9.)

### 4.2 The `.LDD` / `.PDD` model and device memory segments

External drivers are loaded from a driver file into a **device memory segment**,
allocated by the segment allocator. The ability to load and remove drivers **without
a system reset** is a key EPOC feature: you can physically attach a peripheral and
load its driver without exiting any process. The I/O system also notifies drivers
on power off/on so they can save/restore device state. (*PLIB* Ch.9.)

Memory segments (*PLIB* Ch.7, "Memory Allocation"):

- **Device segments** are created when an external device is installed and deleted
  when removed; once created a device segment does not normally change size. The
  first two device segments are special — the data segments of `SYS$NULL` and the
  supervisor `SYS$SMANG`.
- Device segments are allocated at **lower addresses** than the volatile dynamic
  segments so that dynamic-segment activity does not move them (a driver normally
  has to stop working while its segment moves — risking data loss on, say, a serial
  receive).
- Segment name extension conventions: **`.LDD`** = LDD device segment, **`.PDD`** =
  PDD device segment; `.$SC` primary shared code, `.DYL` dynamic library, `.$nn`
  process data segment.
- The segment index table has fixed sub-capacities — typically **96 total (32
  device + 64 dynamic)**. Addresses/sizes are in 16-byte paragraphs.

---

## 5. Per-device reference

### 5.1 Serial port — `TTY:` (*IODEV* Ch.4)

Fully interrupt-driven RS-232. Two cooperating layers: a hardware PDD and a
hardware-independent LDD (which buffers incoming data).

**Device names.** First port `TTY:A`; second `TTY:B`. Availability varies: a Series 3
with a 3-Link recognises only `TTY:A`; an MC with two serial/parallel modules has
`TTY:A` (left) and `TTY:B` (right); HC has a third, cradle port `TTY:C`. HC also
exposes TTL-level ports `TTY:D/E/F` (direct) and `TTY:G/H/I` (inverted) — not
documented in that chapter.

**Parameters — `P_SRCHAR` (`p_serial.h`), all fields:**

```c
typedef struct {
    UBYTE tbaud;    /* transmit baud rate */
    UBYTE rbaud;    /* receive baud rate  */
    UBYTE frame;    /* data bits + stop + parity-present */
    UBYTE parity;   /* parity type */
    UBYTE hand;     /* handshake flags */
    UBYTE xon;      /* XON character  (default DC1 0x11) */
    UBYTE xoff;     /* XOFF character (default DC3 0x13) */
    UBYTE flags;    /* control flags  */
    ULONG tmask;    /* terminator mask */
} P_SRCHAR;
```

**Baud (`tbaud`, `rbaud`)** — one of `P_BAUD_50, _75, _110, _134, _150, _300, _600,
_1200, _1800, _2000, _2400, _3600, _4800, _7200, _9600, _19200, _38400, _56000`.
Default `P_BAUD_9600`. All SIBO machines support `_50`..`_9600`; MC 200/400 and
Series 3a also support `_19200`; HC/Series 3 can be set to `_19200` but their clock
is slightly too slow (frequent overruns). No SIBO hardware supports split
transmit/receive rates.

**Frame (`frame`)** — one of `P_DATA_5 / P_DATA_6 / P_DATA_7 / P_DATA_8`, optionally
OR'd with `P_TWOSTOP` (2 stop bits, else 1) and `P_PARITY` (parity bit present).
Default `P_DATA_8` (8 data, 1 stop, no parity). All settings supported on all SIBO
machines.

**Parity (`parity`)** — used only if `P_PARITY` is set: `P_PAR_EVEN`, `P_PAR_ODD`,
`P_PAR_MARK`, `P_PAR_SPACE`. Default 0. **No SIBO machine supports mark or space
parity.**

**Handshaking (`hand`, plus `xon`/`xoff`)** — combination of:

| Flag | Effect |
| ---- | ------ |
| `P_OBEY_XOFF` | obey received XON/XOFF (input flow control) |
| `P_SEND_XOFF` | transmit XON/XOFF to remote (output flow control) |
| `P_IGN_CTS`   | if set, RTS held permanently active and incoming CTS ignored; if clear, RTS/CTS flow control |
| `P_OBEY_DSR`  | if set, suspend TX when incoming DSR inactive (DTR/DSR); DTR held active while port is open |
| `P_FAIL_DSR`  | (only if `P_OBEY_DSR`) complete outstanding `P_FREAD`/`P_FWRITE` with `E_FILE_LINE` if DSR goes inactive |
| `P_OBEY_DCD`  | if set, suspend TX when incoming DCD inactive |
| `P_FAIL_DCD`  | (only if `P_OBEY_DCD`) complete requests with `E_FILE_LINE` if DCD goes inactive |

Default `hand = 0` → RTS/CTS handshaking. DCD is an input-only carrier indicator,
not a flow-control line. `xon`/`xoff` default to DC1 (0x11) / DC3 (0x13).

**Control flags (`flags`)** — only `P_IGNORE_PARITY` is defined (discard parity
errors; the errored character is still delivered). Other bits reserved zero. Default 0.

**Terminator mask (`tmask`)** — 32 bit flags, bit *n* (n = 0..31) selecting control
code `0x00`..`0x1F` as a terminating character. E.g. bit 13 = CR, bit 10 = LF (to
read a line at a time); bit 26 = Ctrl-Z. A read completes when a terminator is
received (included in the returned length). Default 0 (none).

**Errors:** `E_FILE_PARITY`, `E_FILE_FRAME`, `E_FILE_OVERRUN`, `E_FILE_LINE`
(inactive required control line), plus `E_GEN_OVER` (driver receive buffer full)
and `E_FILE_RECORD` (buffer filled with no terminator seen).

**Services:**

| Service | Prototype / notes |
| ------- | ----------------- |
| `p_open("TTY:x", -1)` | Powers up port, drives DTR active. Defaults as above; RTS not driven active until first `P_FREAD` or a `P_FSET` (so buffered-modem data is not lost). Errors: `E_GEN_NOMEMORY`, `E_GEN_INUSE`, `E_FILE_DEVICE`, `E_FILE_LOCKED`. |
| `P_FREAD`  | `p_iow(pcb, P_FREAD, buf, &len)` — read up to `*len` bytes. Completes on: full length; receive error (partial data + negative status); a `tmask` terminator (length includes it); or cancel (`E_FILE_CANCEL`). After a cancel, use `P_FTEST` — more buffered chars may remain. |
| `P_FWRITE` | `p_iow(pcb, P_FWRITE, buf, &len)` — obeys current handshaking. A **zero-length** write with `P_OBEY_DSR` set completes only when DSR goes active (used to detect a connection). Cancel → `E_FILE_CANCEL`; the driver does not report bytes sent before cancel. |
| `P_FCANCEL` | cancel outstanding read and write. |
| `P_FSENSE` | `p_iow(pcb, P_FSENSE, P_SRCHAR*)` — read current characteristics (cannot fail). |
| `P_FSET`   | `p_iow(pcb, P_FSET, P_SRCHAR*)` — set characteristics (sense-modify-set idiom). Errors `E_GEN_ARG`, `E_GEN_NSUP`, `E_FILE_LINE`. Panics if a read/write is outstanding. |
| `P_FFLUSH` | discard the LDD read buffer, clear error status, release paused remote. |
| `P_FTEST`  | `p_iow(pcb, P_FTEST, &len)` — bytes currently buffered (at least that many can be read synchronously). |
| `P_FCTRL`  | `p_iow(pcb, P_FCTRL, UBYTE *pctrl)` — read CTS/DSR/DCD input line state into `*pctrl` as a bit mask, and optionally set DTR (see below). |
| `P_FINQ`   | `p_iow(pcb, P_FINQ, UWORD *pmask)` — write three words of supported-characteristic bit flags. |

**Control-line constants (`p_serial.h`):**

- Input line state returned by `P_FCTRL` in `*pctrl`:
  `P_SRCTRL_CTS`, `P_SRCTRL_DSR`, `P_SRCTRL_DCD` (bit set ⇒ line active).
- DTR output: if `*(pctrl+1)` is non-zero it sets DTR to `P_SRDTR_ON` (active) or
  `P_SRDTR_OFF` (inactive). `E_GEN_NSUP` if the driver can't set DTR (all current
  SIBO machines can).
- `P_FINQ` bit flags: word 0 = `P_SRINQ_50 … P_SRINQ_19200`; word 1 =
  `P_SRINQ_38400`, `P_SRINQ_56000` (not supported on SIBO); word 2 =
  `P_SRINQ_DATA5/6/7/8`, `P_SRINQ_STOP2`, `P_SRINQ_PAREVEN/PARODD/PARMARK/PARSPACE`,
  `P_SRINQ_SETDTR`, `P_SRINQ_SPLIT`. SIBO supports all except `PARMARK`, `PARSPACE`
  and `SPLIT`.

`p_close` flushes the receive buffer, waits for any in-flight TX char, cancels
outstanding read/write, drops RTS and DTR, and powers the port down.

### 5.2 Parallel port — `PAR:` (*IODEV* Ch.3)

Standard Centronics, **output-only** (no read service). Available via serial/parallel
or parallel expansion modules; HC cradle provides a third. Names `PAR:A`, `PAR:B`,
`PAR:C` (e.g. Series 3 = `PAR:A` only; MC with two modules = `PAR:A` left, `PAR:B`
right).

- `p_open("PAR:x", -1)` — powers up the port lines (all control lines cleared low);
  it draws power until closed. Errors: `E_FILE_ALLOC`, `E_FILE_DEVICE`,
  `E_FILE_LOCKED` / `E_GEN_INUSE`.
- `P_FWRITE` — `p_iow(pcb, P_FWRITE, buf, &len)`. **Never completes if no
  functioning receiver is connected** — always write asynchronously with a timer
  timeout. Errors `E_FILE_WRITE`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the write (an indeterminate amount will already have been
  written).
- `P_FSENSE` — `p_iow(pcb, P_FSENSE, UWORD *port)` reads input control lines
  (`p_par.h`): `S_BUSY` (pin 11), `S_ACK` (10), `S_ERROR` (15), `S_PE` (12). *Only
  on HC/MC ranges with EPOC ≥ 2.30; not on Series 3/3a.*
- `P_FSET` — `p_iow(pcb, P_FSET, UWORD *type, UWORD *port)` sets/clears output lines:
  `*type = 1` sets high, `0` clears low. Lines: `S_INIT` (16), `S_AUTOFD` (14),
  `S_SELECT` (17), plus `S_SPARE` (custom hardware only — ASIC5 pin 42, no effect on
  the standard module). *Same HC/MC ≥ 2.30 restriction.*

### 5.3 Timers — `TIM:` (async) and `FRC:` (free-running counter)

**`TIM:` — asynchronous timer.** Documented in the *PLIB* "Time, Timers and Dates"
chapter (not in *IODEV*). Opened `p_open(&tcb, "TIM:", -1)`. Requests are placed on
a delta queue via `p_ioc(P_FRELATIVE, ..., &ticks)` (relative) or
`p_ioc(P_FABSOLUTE, ...)`; cancel with `P_FCANCEL`. Relative units are 1/10 s (the
examples use `tval = 10L * secs`). `TIM:` is the standard tool for I/O timeouts (see
the `PAR:` and `TTY:` examples in *IODEV*). (*PLIB* Ch.8/9; Time chapter.)

**`FRC:` — free-running counter** (*IODEV* Ch.7). **Specific to Series 3a and
Workabout.** Accuracy ±2 ppm, resolution 1/1024 s. **Supports only one process at a
time.** For resolutions of 1/32 s or coarser, use `TIM:` instead.

- `p_open("FRC:", -1)` — errors `E_FILE_OPEN` (in use), `E_GEN_NOMEMORY`.
- `P_FSTART` — `p_iow(pcb, P_FSTART, UWORD *pmode, UWORD *pint)`:
  - `E_FRC_COUNTING`: counter increments every 1/1024 s from 0; `pint` ignored.
  - `E_FRC_REPEATING`: increments every `*pint` × 1/1024 s (`*pint` in 10..65535,
    ≈0.01–64 s). Errors `E_GEN_NSUP` (bad mode), `E_GEN_RANGE` (interval 0..9).
- `P_FREAD` (COUNTING mode): `p_iow(pcb, P_FREAD, ULONG *arg1)` — elapsed 1/1024 s
  since `P_FSTART`, not reset by the read.
- `P_FREAD` (REPEATING mode): `p_iow(pcb, P_FREAD)` — completion status = number of
  whole intervals elapsed since the last `P_FSTART`/`P_FREAD` (waits for ≥1
  interval; time is not lost between reads).
- Read errors: `E_GEN_OVER` (>32767 intervals / too large for `arg1`),
  `E_FILE_CANCEL`, `E_FILE_READ` (counter not running, or machine was switched off
  mid-count).

### 5.4 Sound — `SND:` (*IODEV* Ch.5)

Speaker driven by the `SND:` driver; a separate piezo **buzzer** (all machines
except Series 3a, which emulates it via the speaker) is driven by the PLIB
`p_sound` routine, not `SND:`. Output is disabled if the `E_SOUND_DEVICE` sound-flag
bit is clear or `E_SOUND_DISABLE` is set (managed via `p_getsnd`/`p_setsnd`). The
speaker is a **two-voice** device (chords / DTMF). Series 3 `SND:` is limited to
DTMF and simple alarms — richer sound needs an extra LDD (e.g. `SVDFRC.LDD` on the
SDK disk). Series 3a can also play/record `.WVE` files.

- `p_open("SND:", -1)` — errors `E_FILE_ALLOC`, `E_GEN_FAIL` (disabled),
  `E_FILE_LOCKED`/`E_GEN_INUSE`.
- `P_FCANCEL` — cancel outstanding write.
- `P_FSENSE` / `P_FSET` — `E_SOUND` struct (`epoc.h`):
  ```c
  typedef struct { UBYTE beatsPerMinute; UBYTE volume; } E_SOUND;
  ```
  `beatsPerMinute` (HC/MC/3a) `E_SOUND_MIN_BPM` 2 .. `E_SOUND_MAX_BPM` 240, default
  120 (no effect on Series 3). `volume` 0 (`E_SOUND_MAX_VOLUME`, loudest) .. 5
  (`E_SOUND_MIN_VOLUME`); Series 3 range 1..4; Series 3a has four distinct volumes;
  smaller = louder; default 1. Always sense-then-set immediately after open, before
  playing — even at defaults.
- `E_FALARM` — `p_iow(pcb, E_FALARM, UWORD *palarm)`: `*palarm` 0 = "rings", 1 =
  "chimes".
- `E_FSSOUNDCHANNELn` *(HC/MC/3a only)* — `p_ioc(pcb, E_FSSOUNDCHANNELn, pstat,
  WORD *pnotes, WORD *plen)`, n = 1 or 2. `*plen ≤ 16384` notes; each note is two
  words (frequency Hz — middle A = 440; duration in beats). Output starts only once
  **both** channels have been called (for sync); use `*plen = 0` for an unused
  voice.
- `E_FDIAL` *(Series 3 / 3a only)* — `p_iow(pcb, E_FDIAL, TEXT *pstr, E_DIAL
  *pdial)` emits DTMF for `*pstr`.
  ```c
  typedef struct { UBYTE toneLengthTicks; UBYTE delayLengthTicks;
                   UWORD pauseLengthTicks; } E_DIAL;   /* ticks = 1/32 s */
  ```
  Valid chars 0-9, A-F (`#`→F, `*`→E); space/comma = pause; others ignored; max 26
  tone/pause chars. Errors `E_FILE_CANCEL`, `E_GEN_ARG` (too many chars).

### 5.5 Magnetic card reader — `MCR:` (*IODEV* Ch.12)

HC MCR driver, built into the HC OS. Interface fits top (`MCR:A`), bottom (`MCR:B`)
or cradle (`MCR:C`).

- `p_open("MCR:x", -1)` — errors `E_GEN_NOMEMORY`, `E_FILE_DEVICE` (no interface in
  slot), `E_FILE_NAME`, `E_FILE_LOCKED`/`E_GEN_INUSE`.
- `P_FREAD` — `p_iow(pcb, P_FREAD, UBYTE *buf1, UBYTE *buf2)`: track 1 → `*buf1`,
  track 2 → `*buf2` (each buffer ≥ 256 bytes). Data is **leading-byte-count ASCII**;
  count 0 = unsuccessful. Pass either buffer as `NULL` to skip that track. Track 1 =
  alphanumeric (name/account), track 2 = numeric (most readers do track 2 only).
  Errors `E_FILE_READ` (decode error), `E_GEN_OVER`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the read.
- `P_FSET` *(EPOC ≥ 2.32)* — `p_iow(pcb, P_FSET, UWORD *mask)` programs the 100 kΩ
  pull-up/down resistors on the five reader lines (bit set = pull-up, clear =
  pull-down; default all pull-down): `M_DATA1PU` 0x01, `M_CLK1PU` 0x02, `M_DATA2PU`
  0x04, `M_CLK2PU` 0x08, `M_CLSPU` 0x10 (card-present). Other bits ignored.

### 5.6 Infrared (*IODEV* Ch.15–17)

Psion IR is **IrDA-compliant only** (S3a/Siena ports won't talk to non-IrDA
devices). The protocol stack, bottom to top: **SIR** (Serial Infrared physical
layer device driver, drives the hardware) → **IrLAP** (link access) → **IrLMP**
(link management) → the **IrMUX API** (server side) and **AccessIr API** (client
side). Two application-facing device channels are exposed:

- **`AIR:`** — the AccessIr API device. `p_open(&pcb, "AIR:", 0)` (mode 0) powers up
  the port; errors `E_GEN_INUSE`, `E_GEN_NOMEMORY`. Services use `P_FIR*` codes
  (numeric values from *IODEV* Ch.16): `P_FREAD` 1, `P_FWRITE` 2,
  `P_FIRDISCONNECT` 4, `P_FIRDISCOVER` 5, `P_FIRSELECT` 6, `P_FIRAWAITCONNECT` 7,
  `P_FIRMAKECONNECT` 8. Flow: `P_FIRDISCOVER` (log in-range machines) →
  `P_FIRSELECT` → `P_FIRMAKECONNECT` (primary) / `P_FIRAWAITCONNECT` (secondary),
  exchanging up to 56 bytes of connect data → `P_FREAD`/`P_FWRITE` →
  `P_FIRDISCONNECT`.
- **`IRP:`** — the IR **printer-port** device driver (the "IRP" API), used for IR
  printing. (*IODEV* Ch.15, protocol-layer diagram.)

The **IrMUX API** (*IODEV* Ch.17) is the server-side interface (LM-IAS
registration, connection-oriented and connectionless reads/writes) exposed through
`LM_*` messages rather than `P_Fxxx` codes.

> MX note: these chapters describe the S3a/Siena/S3c-era IrDA stack; the manuals do
> not document Workabout-MX-specific IR behaviour.

---

## 6. Barcode scanning

### 6.1 Workabout MX integral laser — see `SCANNER-API.md`

The **Workabout MX integral laser scanner is not covered by these manuals.** It is
documented separately in **`SCANNER-API.md`** (sibling of this file, at
`/tmp/claude/inv/SCANNER-API.md`). In brief, per that document: the MX uses the
logical driver **`WL2`** (units `WL2:A`, `WL2:D`) with decoder type **`Symbol2`**
(the integral Symbol laser engine); it is a decoded scanner, triggered by the
keyboard scan key (Window Server key code 368). **Do not duplicate that content
here — consult `SCANNER-API.md` for the MX.**

The remainder of this section summarises only the **older external** wand/RS232
barcode devices as documented in *IODEV*.

### 6.2 External wand decoders — `BAR:` (*IODEV* Ch.13)

The HC has **no** built-in barcode driver/decoder; readers are supported by external
combined **decoder + device-driver LDDs** that the app must load (`p_loadldd` or OPL
`DevLoadLdd`). Wand interface module fits the top (`BAR:A`) or bottom (`BAR:B`) of
the HC. Available drivers (device name `BAR` in all cases):

| File | Symbologies |
| ---- | ----------- |
| `BAREAN.LDD`  | EAN8, EAN13, UPC, UPCE |
| `BARC39.LDD`  | Code 39 |
| `BARITF.LDD`  | Interleaved 2 of 5 (ITF) |
| `BAR128.LDD`  | Code 128 |
| `BARMPLES.LDD`| Modified Plessey |
| `BARRAW.LDD`  | raw |

None auto-discriminates. Load e.g. `p_loadldd("BARC39.LDD")`; remove with
`p_devdel("BAR")` (or OPL `DevDelete`).

- `p_open("BAR:", -1)` — opens via the previously loaded LDD. Errors as for `MCR:`.
- `P_FREAD` — `p_iow(pcb, P_FREAD, UBYTE *buf)` (buf ≥ 256). Leading-byte-count
  ASCII; count 0 = failed read. The **first character encodes the symbology** (e.g.
  EAN8/13, UPC, Code 39, ITF, Code 128, Modified Plessey, UPCE); the rest is the
  decoded data. Errors `E_GEN_OVER`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the read.

### 6.3 Intelligent reader / RS232 module — `TTY:` escape commands (*IODEV* Ch.14)

The **intelligent** RS232/barcode expansion module contains a decoding
micro-controller and shares one serial port between an RS232 interface and the
barcode interface (not usable simultaneously). Port mapping by slot: top slot →
RS232 `TTY:A`, barcode `TTY:D`; bottom slot → RS232 `TTY:B`, barcode `TTY:E`. The
barcode interface talks to the host at the standard HC comms settings (XON/XOFF,
no hardware handshaking). It auto-discriminates EAN/JAN 8/13, UPCA, UPCE, Codabar,
Code 128, Interleaved 2 of 5, and Code 39 (standard/extended), transmitting decoded
data left-to-right as ASCII, terminated by default with a single CR.

Because it is reached through `TTY:`, it uses the normal serial services
(`p_open`, `P_FSENSE`, `P_FSET`, `P_FREAD`, `P_FWRITE`, `p_close`). It is
**programmed by writing escape sequences** to it via the serial channel:

```
<Esc>-y<code><command>            general form
<Esc>-y<code><command><text>      set-termination-string form
<Esc>E                            hard reset (special form)
```

`<Esc>` = 0x1B, then `-` (0x2D), then `y`/`Y`, a decimal `<code>` of 1–3 digits
(0–255), and an uppercase command letter; no embedded spaces. Multiple option bits
are combined by **summing** their `<code>` values. Multiple commands may be
concatenated in one sequence — intermediate command letters lowercase, the final
one uppercase (e.g. `<Esc>-y13f2h1D`).

Command letters (each `<Esc>-y<code><letter>`):

| Cmd | Function |
| --- | -------- |
| `D` | serial inter-character delay (0 = none, 1 = 10 ms) |
| `E` | hard reset + self-test (special form `<Esc>E`; reverts all options to default; self-test failures reported as `... SELF TEST FAILED<CR><LF>`) |
| `F` | select symbology bitmask: 1 Code39, 2 ITF, 4 UPC/EAN, 8 Codabar, 16 Code128 (default `31`) |
| `G` | check-character options (verify / transmit check digits; per-symbology) |
| `H` | decoding options (extended Code39, Codabar start/stop, UPC-vs-EAN, 2/5-digit supplements, UPC E→A expansion, auto-discriminate supplements) |
| `J` | single-read mode enable/disable |
| `K` | single-read control (fire one read when in single-read mode) |
| `M` | set Interleaved 2 of 5 length |
| `O` | set termination string (`<Esc>-y<code>O<string>`, up to 4 chars) |
| `Q` | Code ID characters |
| `S` | status request |
| `W` | scanner enable |

Per-symbology output formats (ID chars, check digits, supplement digits for
UPC E/A, EAN 8/13 and their +2/+5 variants; Codabar start/stop; ITF and Code 39
check handling) are tabulated in *IODEV* Ch.14.

---

## 7. Other channel devices (pointers)

*IODEV* also documents the **Console** (`CON:`, Ch.2 — screen/keyboard, `P_SCR_*`
function codes, `P_CON_KBREC`), **Alarm** (Ch.6, `A_FTIMED`/`A_FUNTIMED`), **World
database** (Ch.8), **Xmodem/Ymodem** (Ch.9, `P_FCONNECT`/`P_FREAD`/`P_FWRITE`),
**NCP / Link** (Ch.10, `SYS$NCP`), **Cradle/Docking Station** (Ch.11), and the
**Fast Charger** (Ch.18, `FCHG_*`). The **`FIL:`** file driver and the **`TIM:`**
timer are documented in the *PLIB* Files and Time chapters. These are outside the
scope of this reference but follow the same `p_open` + `P_Fxxx` model described in
Sections 1–3.

---

### Source map

| Topic | Source |
| ----- | ------ |
| Device model, `p_open`, `FIL:` fallback, modes, attached drivers, file server | *PLIB* Ch.9 |
| I/O primitives (`p_ioa/ioc/iow` + variants), `p_read/write/close`, cancel | *PLIB* Ch.9 |
| `p_iowait` / `p_waitstat`, semaphore rules | *PLIB* Ch.8 |
| Function codes, `IoFunc*` mapping | *PLIB* Ch.9; *IODEV* Ch.1/16; *ADDSYS* driver chapter |
| `p_loadldd`/`p_loadpdd`/`p_devdel`/`p_devqu`/`p_devfnd`; `.LDD`/`.PDD`; device segments | *PLIB* Ch.9, Ch.7 |
| `TTY:`, `P_SRCHAR`, control lines | *IODEV* Ch.4 |
| `PAR:` | *IODEV* Ch.3 |
| `FRC:` | *IODEV* Ch.7 |
| `TIM:` | *PLIB* Time chapter (referenced from *IODEV* Ch.1) |
| `SND:` | *IODEV* Ch.5 |
| `MCR:` | *IODEV* Ch.12 |
| IR (`AIR:`, `IRP:`, IrMUX) | *IODEV* Ch.15–17 |
| External `BAR:` wand decoders | *IODEV* Ch.13 |
| Intelligent reader / `TTY:` escape commands | *IODEV* Ch.14 |
| Workabout MX integral laser (`WL2`/`Symbol2`) | **`SCANNER-API.md`** (not these manuals) |
