# Programming NCD ProXR and ProXR Lite relay controllers — complete guide for an AI or developer *Self-contained. Everything here was verified against NCD's shipped Base Station source (VB.NET, 2012–2019), the readable `NCDEnterprise` C# library, NCD's published API Codec / ProXR Quick Start guides, **and the controller firmware itself** (PicBasic source of the ProXR Lite build — the device side of the wire), cross-checked byte for byte (National Control Devices, ncd.io). Public release, 2026-10-20. Decimal unless prefixed `0x`. "Bank" = a group of 8 relays; relays and banks are numbered from 1 in prose and in most commands, but see §6 and §9 for the 0-based exceptions.* --- ## 0. The thirty-second version ``` Transport : serial 115200 8N1 (USB virtual COM, RS-232, RS-485 multi-drop, Bluetooth SPP, wireless modem) or TCP to the controller's IP, port 2101 (Ethernet / Wi-Fi modules; found by UDP broadcast on 13000 / 55555). §2.1 Protocol : "speak when spoken to" — send a command, read the reply, never stream unsolicited data. Framing : wrap every command in the API frame 0xAA, LEN, payload…, CKSUM with CKSUM = (0xAA + LEN + Σpayload) AND 255. Replies come back wrapped the same way. Unwrap, verify checksum, use the payload. Alive? : payload 254,33 → 85 (run mode) / 86 (configuration mode) / 87 (security lockdown). Relay on : payload 254,108+R,B (R = relay 0..7 within the bank, B = bank 1..32, B = 0 means every bank) → 85 Relay off : payload 254,100+R,B → 85 Read bank : payload 254,124,B → one byte, bit0 = relay 1 … bit7 = relay 8 Identify : payload 254,246 (RAW, unframed) → 3 bytes; then EEPROM 243..247 for the feature bits (§4). ``` Example, turn on relay 1 of bank 1 and read the bank back: ``` TX AA 03 FE 6C 01 18 (170,3,254,108,1,24) RX AA 01 55 00 (ack 85) TX AA 03 FE 7C 01 28 (170,3,254,124,1,40) RX AA 01 01 AC (bank 1 = 0b00000001) ``` --- ## 1. What a ProXR controller is ProXR is NCD's relay-control **command set** and firmware family, shipped since ~2005 on PIC microcontrollers (PIC16F690 / PIC16F648A on older "Standard" boards, PIC18F14K22 on the 2012+ "Enhanced" boards, PIC18F46K22 on Fusion-class boards). The board you talk to is always a serial device; the interface module plugged into it (USB, RS-232, RS-485, Ethernet, Wi-Fi, Bluetooth, ZigBee/802.15.4/900 MHz) only changes how the serial bytes get to you. | Variant | What it means for you | |---|---| | **ProXR Standard** (pre-2012) | Same relay commands, but **no API framing** (raw bytes only), a fixed command menu, and a few extra setup commands (§13). Identify by the ID triple (§4). | | **ProXR Enhanced** (2012+, part numbers with `ZUXP`, `ZADR`, `ZSCAN`, …) | API framing supported (firmware ≥ 3.2), EEPROM-described capabilities, timers/flashers/grouping, up to 32 banks (256 relays) over the XR expansion port, optional UXP expansion port (contact closure, 12-bit ADC, potentiometers). | | **ProXR Lite** (part numbers `R1x…R8x` with `ProXR Lite` firmware, 1–8 relays) | The Enhanced command set on a small board: **one bank**, **8 built-in 8/10-bit analog inputs (AD8)** with the Relay Activator feature (§15.1), **no XR/UXP expansion ports**. Everything in this guide applies; bank is always 1 (or 0 = all). | | **ProXR Advanced / Fusion** | A Fusion-class CPU running ProXR plus Reactor/Taralist logic. Same relay commands, but **bank-command acks are 2 bytes** instead of 1, and a few opcodes are shifted (§14). Identify by EEPROM 244 bit 7 / 245 bit 7. | All variants: default **115200 baud, 8 data bits, no parity, 1 stop bit**. Ethernet modules listen on **TCP 2101** and announce themselves by UDP broadcast on **port 13000** (CSV with IP, MAC, port, description, firmware); Roving Networks Wi-Fi modules broadcast a 120-byte packet on **UDP 55555**. --- ## 2. Transport rules 1. **One command in flight at a time.** Send, wait for the reply (or a timeout), then send the next. The controller does not queue. 2. **Raw framing (no API) relies on a timeout**: the firmware waits until bytes stop arriving, then parses. So in raw mode you must send the whole command in one write and not dribble bytes; and you cannot distinguish "no reply" from "slow reply" except by waiting. Legacy software used ~1 ms pacing between bytes on TCP in raw mode. 3. **API framing removes the timeout**: the length byte tells the firmware when the command is complete and the checksum rejects corrupted frames. Use it whenever EEPROM 246 bit 7 says it is supported (every board with ProXR firmware ≥ 3.2). API frames are written unpaced. 4. **Replies**: with API framing you receive `0xAA, LEN, data…, CKSUM`; collect until you have LEN+3 bytes, verify the checksum, and discard anything that does not start with `0xAA`. In raw mode you receive the bare data bytes and must know how many to expect from the command table (§5–§12). 5. **Timeouts**: 1 s is generous for any single command on serial; EEPROM reads sometimes need a retry (legacy software retried 4× with 100 ms gaps). After a reboot command wait 7 s. After switching baud wait 100 ms. 6. **Purge** the receive buffer before each command so a stale reply cannot be mistaken for the new one. 7. **Mixing is allowed**: an API-capable board answers raw commands raw and API commands API, so you may mix per command. Identification deliberately uses raw for `254,246` (§4). ### 2.1 Interface modules — what changes per transport The ProXR CPU always speaks 8N1 serial at its EEPROM-8 baud (default 115200). The plug-in interface module decides how those bytes reach you. Nothing in the command set changes; only the connection step and a few timing habits do. | Module / part suffix | How you connect | Specifics | |---|---|---| | **USB** (`ZUSB`, FTDI FT232) | Virtual COM port: Windows `COMn`, Linux `/dev/ttyUSB*` (stable name under `/dev/serial/by-id/`), macOS `/dev/cu.usbserial-*` | FTDI VCP driver; the "USB" LED on the module lights when enumerated. Open at 115200 8N1, no flow control. Opening the port may pulse DTR; harmless. The USB interface is the only path that can change "vital" settings on some newer boards | | **RS-232** (`ZRS`) | Any 9-pin serial port or USB-to-RS-232 adapter, straight cable | 115200 8N1 default. Pre-2012 Standard boards store the baud with `254,168/169` (§13); Enhanced boards use EEPROM 8. No handshaking lines are used | | **RS-485** (`ZRS485`) | 2-wire half-duplex bus, one master, many controllers | Give each controller a distinct E3C device number (EEPROM 1), enable one with `254,252,N` or use the 171 unified frame (§3.2). The adapter must turn its driver around fast; API framing is strongly recommended so you never depend on silence to delimit commands. `254,248` enables all devices, `254,249` disables all | | **Ethernet** (`ZETH` Lantronix XPort, Digi Connect ME, "Web-i" with a web page, BeagleBone-based) | TCP client to `ip:2101` (the module's channel 1 is bridged to the CPU serial port at 115200) | Discovery: the module (or NCD's firmware on it) broadcasts a CSV line on **UDP 13000** — 5 or 6 comma-separated fields: `ip, mac, port, description, firmware` (BeagleBone adds one); otherwise `ip[:port], mac, description, firmware, "---"`. "WiNet"-type entries are web pages, not TCP endpoints. Only one TCP connection at a time; close it when done or the next client is refused. Older NCD software paced raw bytes 1 ms apart over TCP ("high speed profile" turns that off); API frames never need pacing. Digi Connect ME modules also answer the Digi ADDP multicast discovery and offer RealPort | | **Wi-Fi** (Roving Networks WiFly `ZWiFi`, later Digi Wi-Fi) | TCP to `ip:2101` after the module joins your network (set up via the module's own ad-hoc/web/AT interface) | WiFly discovery: exactly **120-byte UDP broadcast on port 55555**: bytes 8–9 = TCP port (big-endian), 32–57 = version string, 60–91 = device ID string, 110–115 = MAC. First-power-up boards may return an all-zero ID triple to `254,246`; send it again | | **Bluetooth** (`ZBT`, SPP) | Pair, then open the resulting serial port (`/dev/rfcomm0`, `COMn`) | Behaves like a slow serial link: keep API framing on and allow longer timeouts (2–3 s). Pairing PIN is on the module label | | **ZigBee / 802.15.4 / 900 MHz wireless** (`ZZB`, `Z802`, XSC long range) | A matching **modem** (USB/RS-232/Ethernet) on the host side; the pair is transparent serial | Point-to-point modems are pre-paired by NCD; the host talks to the modem's serial port exactly as if wired, at the modem's baud (115200 for XBee S3B profiles, 57600 for XSC long range — EEPROM 8 index 3). Expect tens of ms extra latency and occasional retries; API framing's checksum is what makes this reliable. Many boards flag "API on port 2" in EEPROM 131 (=15) when the radio sits on the second UART | | **Fiber optic** | Serial over fiber transceivers | Plain serial; nothing special | **Dual-port boards** (EEPROM 246 bit 6): the second UART ("port 2") carries the same command set; Base Station shows a Port 1 / Port 2 choice for AD8/SCAN/POT panels when capability byte C marks them on port 2. **Push data exceptions** to "speak when spoken to": the AD8 Relay Activator's "Send Event on Port 1" option and the UXP/AD8 "SCAN data stream" (capability B bit 2) emit unsolicited packets when inputs change. If you enable either, your receive path must tolerate frames arriving between your commands (in API mode they still arrive as `0xAA, len, …, cksum`, so the resync logic in §17 handles them). --- ## 3. Framing in detail ### 3.1 API frame (the one you should use) ``` TX: 0xAA LEN P0 P1 … P(LEN-1) CKSUM RX: 0xAA LEN D0 D1 … D(LEN-1) CKSUM CKSUM = (0xAA + LEN + P0 + P1 + … ) AND 0xFF (sum of every byte before the checksum, low 8 bits) ``` `LEN` must be **2..24** (the firmware rejects longer payloads). A frame with a bad checksum is dropped silently — no reply at all — so a timeout after an API command usually means a checksum slip, not a dead board. Worked examples (all verified): | Meaning | Payload (dec) | Frame (hex) | Reply (hex) | |---|---|---|---| | Test 2-way | 254,33 | `AA 02 FE 21 CB` | `AA 01 55 00` (run) or `AA 01 56 01` (config) | | Relay 1 bank 1 on | 254,108,1 | `AA 03 FE 6C 01 18` | `AA 01 55 00` | | Relay 1 all banks off | 254,100,0 | `AA 03 FE 64 00 0F` | `AA 01 55 00` | | Read bank 1 | 254,124,1 | `AA 03 FE 7C 01 28` | `AA 01 xx CK` | | Read all 32 banks | 254,124,0 | `AA 03 FE 7C 00 27` | `AA 20 …32 bytes… CK` | | Set bank 1 = 0x55 | 254,140,85,1 | `AA 04 FE 8C 55 01 8E` | `AA 01 55 00` | | Read EEPROM 246 | 254,53,246 | `AA 03 FE 35 F6 D6` | `AA 01 xx CK` | | Read EEPROM 240..247 | 254,53,240,7 | `AA 04 FE 35 F0 07 D8` | `AA 08 …8 bytes… CK` | | Timer 0: relay 0 on 5 s | 254,50,50,0,0,5,0 | `AA 07 FE 32 32 00 00 05 00 18` | `AA 01 55 00` | | Config mode 60 s | 254,33,140,86,60 | `AA 05 FE 21 8C 56 3C EC` | `AA 01 56 01` | Checksum pseudo-code: ```python def api_frame(payload): # payload: list of ints 0..255 frame = [0xAA, len(payload), *payload] return bytes(frame + [sum(frame) & 0xFF]) def api_unwrap(buf): # returns payload or raises if len(buf) < 3 or buf[0] != 0xAA or len(buf) != buf[1] + 3: raise ValueError("incomplete") if sum(buf[:-1]) & 0xFF != buf[-1]: raise ValueError("bad checksum") return list(buf[2:-1]) ``` ### 3.2 E3C DropNet unified frame (optional, multi-drop RS-485 networks, firmware from Sept 2015) ``` TX: 171 DEVICE LEN payload… CKSUM CKSUM computed exactly as if the header were 170 and WITHOUT the DEVICE byte ``` Example from NCD: `171 0 3 254 131 0 46` — checksum 46 = (170+3+254+131+0) AND 255. It targets E3C device number DEVICE (0..255) and executes the command in one packet; otherwise use `254,252,N` first (§12). ### 3.3 Raw framing (Standard boards, or when you must) Send the payload bytes bare. Expect bare reply bytes; you must know the reply length. The firmware only accepts a raw packet whose first byte is one of `254, 0, 70, 253, 188, 64, 65`; everything else is discarded without reply. Only `254,246` (device ID) is deliberately sent raw by NCD's own software because every generation answers it. --- ## 4. Identifying the controller (do this before anything family-specific) Some opcodes mean different things on other NCD firmware families (Enterprise boards use `254,50..53` for 16-bit block memory access; on ProXR `254,52` is a **scratchpad write**). Always identify first, in this order: ``` P0 254,33 framed (API); if no reply, raw; if still nothing send raw 254,248 (E3C "enable all"), wait 100 ms, retry. 85 → run mode 86 → configuration mode 87 → security lockdown (power-cycle required; stop) 84 → not activated (old docs) P1 254,53,244 framed → if bit 7 set → Fusion-class. (Fusion-class boards may be the I/O-sensor variant; identify it by its EEPROM capability bytes) P2 254,246 RAW → R1, R2, R3 [, FW, E3C] — firmware sends 5 bytes (type hi, type lo, year, firmware version, E3C device number); NCD's host software only used the first three. Read up to 5 with a short timeout and accept 3. If all zero, send again — Wi-Fi boards do this on first power-up R1 = 6 and R2 ∈ {0,1} and R3 ≥ 212 → ProXR Enhanced (R3 = 200 + two-digit year, inferred) 0 ≤ R1 < 12 or R1 = 32 → ProXR Standard (R1: 1 ProXR, 2 +AD8, 3 +POT, 4 +AD1216, 5 +UPCS, 6 +UXP, 7 +PWM12, 8 +FPWM, 9/253 +Motion, 10/252 +Keypad, 11 +LCD, 32 DIM6x) R1 ∈ {64,67} → Reactor R1 = 65 → Taralist R1 = 128 → ZR1x/ZR2x (not ProXR) R2 = interface: 0 RS-232, 1 USB, 2 ZigBee, 3 Ethernet, 4 Wi-Fi, 5 Bluetooth, 6 fiber P3 Enhanced/Fusion: read EEPROM 240..247 (254,53,240,7 framed → 8 bytes, or eight single reads 254,53,ADDR): 240.241 = firmware major.minor 242 = year (2-digit) 243..247 = capability bytes A..E (§4.1) Also useful: EEPROM 10 = number of attached relay banks (1..32), 235 = max banks the firmware supports, 144..159 = 16-char device name, 224..233 = activation record (Σ > 0 ⇒ activated). ``` If none of P0–P2 answers at 115200, scan bauds in this order (NCD's own order): `57600, 115200, 230400, 2000000, 9600, 38400, 19200, 250000, 266660, 285710, 307700, 333330, 363620, 400000, 444440, 500000, 571410, 666660, 800000, 1000000, 1333300`. ### 4.1 Capability bytes (EEPROM 243–247; also readable as `254,53,243,4` → 5 bytes) | Byte | bit0 | bit1 | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 | |---|---|---|---|---|---|---|---|---| | **243 A** | ProXR relay command set | AD8 (8 analog inputs, built-in) | UXP SCAN contact closure | UXP potentiometer | UXP AD1216 | Scratchpad memory | AVA security | Current monitoring / I2C mirror | | **244 B** | E3C networking | Pulsar PWM / light dimmer | SCAN data stream (Base) / reserved (public doc) | AD8 relay activator / event generator | Digital I/O port 1 | Digital I/O port 2 | Lifetime counter (Reactor) | Fusion decision-maker logic (Reactor II); **the Fusion marker** | | **245 C** | Taralist | AD8 on port 2 | SCAN on port 2 | POT on port 2 | AD1216 on port 2 | Ultrasonic/sonar port 1 | Ultrasonic/sonar port 2 | Fusion-class controller | | **246 D** | AC (else DC) | 50 Hz (else 60 Hz) | voltage bits 2–3: 0=12 V, 8=24 V, 4=120 V, 12=240 V | ↑ | internal-bus I2C | I2C ports / external-bus I2C | dual communication ports | **API framing supported** | | **247 E** | — | linear actuator w/ speed | — | — | KFX key fob | MirW | — | Fusion base CPU only | A **ProXR Lite** typically reports A = 0b00100011 (ProXR + AD8 + scratchpad), B bit 3 (activator) and bit 0 (E3C), D bit 7 (API). --- ## 5. Relay control — bank-addressed (use these) R = relay 0..7 within the bank (relay 1 is R=0). B = bank 1..32; **B = 0 applies to every bank**. Reply is one byte 85 on Standard/Enhanced/Lite, **two bytes** on Fusion. | Payload | Meaning | |---|---| | `254,100+R,B` | Turn relay R+1 **off** in bank B | | `254,108+R,B` | Turn relay R+1 **on** in bank B | | `254,100+R,B,N` / `254,108+R,B,N` | Same, plus the next N relays ("neighbours", N = 0..7, cannot cross the bank boundary: relay 1 may take up to 7, relay 8 none). Simulates DPDT/3PDT/… with SPDT relays | | `254,116+R,B` | Read relay R+1 in bank B → 0 / 1 | | `254,124,B` (B = 1..32) | Read bank B → bitmask (bit0 = relay 1 … bit7 = relay 8) | | `254,124,0` | Read banks 1–32 → 32 bytes | | `254,124,0,1` / `,2` / `,3` | Banks 33–64 / 65–96 / 97–128 → 32 bytes each (not on Fusion: see §14) | | `254,129,B` | All relays **off** in bank B | | `254,130,B` | All relays **on** in bank B | | `254,131,B` | Invert bank B | | `254,132,B` | Reverse (mirror bit order) bank B | | `254,140,P,B` | Set bank B to bitmask P (`0,0` everything off; `255,0` everything on; `85,0` odd relays on) | | `254,142,B` | Store the current relay states of bank B as the **power-up default** (B = 0 all banks). Blocked in run mode if EEPROM 0 bit 0 is set | | `254,143,B` | Read power-up default of bank B; `254,143,0` → 32 bytes banks 1–32; `,0,1` `,0,2` `,0,3` higher pages | **Status is the controller's memory of what it told the relays**, not a measured contact state. Damaged relays or an unpowered expansion board can disagree with it. ### 5.1 Bank-less legacy form (acts on a "selected bank") | Payload | Meaning | |---|---| | `254,49,B` | Select bank B (0 = all) for the bank-less commands below | | `254,34` | Read selected bank | | `254,R` (0..7) / `254,8+R` | Relay R+1 off / on in the selected bank; `254,R,N` adds N neighbours | | `254,16+R` | Read relay R+1 → 0/1 | | `254,24` | Read all 8 → bitmask | | `254,29` / `254,30` / `254,31` / `254,32` | All off / all on / invert / reverse | | `254,40,P` | Set from bitmask | | `254,42` / `254,43` | Store / read power-up default of the selected bank | Prefer §5; the bank-less form exists for very old software. --- ## 6. Individual relay addressing (0-based index across all banks) N = relay index **0..255** (relay 1 of bank 1 is 0; relay 1 of bank 2 is 8). Two bytes follow the opcode: `LSB = N AND 255`, `MSB = (N AND 65280) / 255` — NCD's published formula; with ≤ 256 relays MSB is simply 0, and MSB = 1 addresses relays 256–511. | Payload | Reply | Meaning | |---|---|---| | `254,48,LSB,MSB` | 85 | Relay N on | | `254,47,LSB,MSB` | 85 | Relay N off | | `254,47,LSB,MSB,1` | 85 | **Toggle** relay N (firmware ≥ ProXR 3.9) | | `254,46,LSB,MSB` | 85 | All relays off, then relay N on (break-before-make selector) | | `254,44,LSB,MSB` | 0/1 | Read relay N | --- ## 7. Flashers (16) Flashers blink a relay in the background. One speed for all flashers. | Payload | Meaning | |---|---| | `254,45,0,S` | Set flash speed S (0 fastest … 255 slowest) | | `254,45,F,1` / `254,45,F,0` | Flasher F (1..16) on / off. Flasher F drives relay index F−1 | Flasher F is coupled to **timer F**: start flasher 1, then activate timer 1 with a duration, and the flashing stops when the timer expires. --- ## 8. Relay refreshing By default relays follow the memory immediately. Turn automatic refreshing off to stage many changes and apply them at once. | Payload | Reply | Meaning | |---|---|---| | `254,25` | 85 | Automatic refreshing on | | `254,26` | 85 | Automatic refreshing off | | `254,37` | 85 | Refresh all relays now | | `254,36` | 0/1 | Read refreshing setting | | `254,35` | 85 | Store refreshing setting as power-up default (not present in the ProXR Lite firmware; ignored there) | | `254,27` / `254,28` | 85 | Reporting mode on / off — whether action commands answer with `85`. Forced on in Enhanced/Lite firmware; status reads always answer | (Fusion uses `254,125 / 126 / 137 / 136` for the same functions.) --- ## 9. Timers (16; failsafe control) Timers count down in seconds on a shared 1 s tick (≈3 % accuracy, ±1 s start jitter). T = timer 0..15. H, M, S = hours, minutes, seconds. R = **relay index 0..255** (see §6). Two kinds: **duration** (relay on now, off when time expires — the failsafe pattern: keep re-sending before it expires) and **pulse** (relay pulses when time expires — gate/garage pattern). | Payload | Reply | Meaning | |---|---|---| | `254,50,50+T,H,M,S,R` | 85 | Duration timer T, start now | | `254,50,70+T,H,M,S,R` | 85 | Pulse timer T, start now | | `254,50,90+T,H,M,S,R` | 85 | Set duration timer T, do not start | | `254,50,110+T,H,M,S,R` | 85 | Set pulse timer T, do not start | | `254,50,131,LSB,MSB` | 85 | Activate/resume timers by 16-bit mask (bit0 = timer 0 … ). **Any timer whose bit is 0 is paused**; `0,0` pauses all; `255,255` resumes all | | `254,50,130,T` | 4 bytes H,M,S,R | Query timer T | | `254,50,132,LSB,MSB` | 85 | Timer calibration: counts per "second" (default ≈ 0x7A00-ish; experiment with the Timer Calibration panel) | | `254,50,133` | 2 bytes LSB,MSB | Read calibration | | `254,50,134` / `254,50,135` | 85 | Enable / disable the calibration marker: the controller emits **90** when a timer starts and **91** when it stops | | `254,50,144` | 85 | Restore timer/setup defaults (same bytes the Standard-era "restore factory settings" used; configuration mode) | Semantics above (duration = on now, off at expiry; pulse = pulse at expiry) are what NCD's host software and public guide implement. A firmware-derived summary describes 50–65 as "latch on expiry" and 70–85 as "energise for the duration"; treat that as unverified until checked on a board. (Fusion uses `254,150,…` instead of `254,50,…`.) --- ## 10. Scratchpad memory (8 user bytes; capability A bit 5) | Payload | Reply | Meaning | |---|---|---| | `254,51,L` | 1 byte | Read location L (1..8) | | `254,52,L,V` | 85 | Write V to location L — **configuration mode required** | --- ## 11. Modes, EEPROM, configuration ### 11.1 Run vs configuration mode Configuration mode unlocks EEPROM writes. Enter it with the PGM/RUN jumper, or in software for D seconds: | Payload | Reply | Meaning | |---|---|---| | `254,33,140,86,D` | 86 | Configuration mode for D seconds (D = 1..255; legacy software uses 120) | | `254,33,140,86,0` | 85 | Cancel, back to run mode | | `254,33,140,86,D,1` | ack | Enter and **stall** the countdown (MirW/KFX boards) | | `254,33,140,99` | none | Soft reboot (wait 7 s) | Recovery: power off, move the jumper to Program, power on → the controller boots with safe settings so you can talk to it and fix the configuration. ### 11.2 EEPROM access | Payload | Reply | Meaning | |---|---|---| | `254,53,ADDR` | 1 byte | Read EEPROM byte | | `254,53,ADDR,N` | N+1 bytes (API) | Read block ADDR..ADDR+N | | `254,54,ADDR,V` | ack | Write EEPROM byte (configuration mode) | ### 11.3 Configuration block (0..15) and name | Addr | Meaning | |---|---| | 0 | Write protection in run mode: bit0 = relay power-up storage protected, bit1 = scratchpad protected | | 1 | E3C device number | | 2 | Auto refresh on power-up (1) | | 3, 4 | Timer tick calibration MSB, LSB | | 5 | Induction repetitions (how many times relay data is clocked out) | | 6 | UART delay between TX bytes | | 7 | UART RX timeout (ms) — the raw-mode "end of command" timeout | | 8 | Baud index: 0 9600, 1 19200, 2 38400, 3 57600, 4 **115200**, 5 230400, 6 250000, 7 266660, 8 285710, 9 307700, 10 333330, 11 363620, 12 400000, 13 444440, 14 500000, 15 571410, 16 666660, 17 800000, 18 1000000, 19 1333300, 20 2000000 | | 9 | Interface index: 0 RS-232, 1 RS-485, 2 USB, 3 ZigBee, 4 802.15.4, 5 Bluetooth, 6 fiber, 7 Connect ME slow, 8 Ethernet NET, 9 Web-i, 10 Wi-Fi (Roving), 11 XSC long range, 12 DigiMesh, 13 Connect ME RealPort, 14 Wi-Fi (Digi), 15 XPort, 16 E3C DropNet | | 10 | **Number of attached relay banks** (1..32) — size your UI from this | | 11 | Serial clock delay | | 12 | bit0 = remote configuration (software mode change) allowed | | 13 | API timeout (seconds) | | 16..79 | Power-up relay defaults, banks 1..64 (what `254,42/142` store) | | 114..121 | AD8 input → relay function map, one byte per input (codes in §15.1) | | 122 | AD8 API event mode: 1 = emit an event packet on input change (§15.1) | | 125 | AD8 key-fob input inversion | | 144..159 | Device name, 16 ASCII chars, space padded | | 160..191 | Potentiometer defaults (UXP POT boards) | | 192..220, 222..223 | AVA security keys and counters — **never readable or writable** through `254,53/54` (reads skip them) | | 224..247 | Factory identification (§11.4) — writable only with an NCD-internal factory procedure, not with plain `254,54` | | 248..255 | Storage behind the scratchpad commands (§10) | | 131 | 15 = API on port 2 (ZigBee) | Saving a configuration: write the bytes you change, then wait ~7 s before re-reading (the firmware commits). ### 11.4 Identification / activation block (224..247) 224..225 invoice (×256), 226..231 activation date and time (yy, mo, dd, hh, mm, ss), 232..233 reference code, 234 security status, 235 max banks supported, 236 max current sensors, 237 PWM outputs, 238 current-sensor model, 239 I2C ports, 240..242 firmware major/minor/year, 243..247 capability bytes (§4.1). Activated ⇔ Σ(224..233) > 0. --- ## 12. E3C device networking (multi-drop) Several controllers can share one RS-485/RS-232 line; each has a device number (EEPROM 1). | Payload | Meaning | |---|---| | `254,247` / `254,247,0` | Read this controller's device number | | `254,247,1` | Discover: every device answers after a delay of (255 − its number) seconds with `85, number, checksum` so replies do not collide | | `254,247,2` | Discover with names: `86, number, 16-byte name, checksum`, same staggering | | `254,248` | Enable all devices (also used as a wake-up during baud scanning) | | `254,249` | Disable all devices | | `254,252,N` | Enable only device N (then send normal commands) | | `254,255,N` | Store device number N (configuration mode) | Or use the 171-framed unified packet (§3.2) to target and command in one shot. --- ## 13. ProXR Standard-only setup commands (pre-2012 boards) | Payload | Meaning | |---|---| | `254,50,136` / `254,50,137,V` | Read / set repetitions (1..255) | | `254,50,138` / `254,50,139,V` | Read / set character delay (3..255, default 35) | | `254,50,140` / `254,50,141,V` | Read / set attached banks (Enhanced boards use EEPROM 10 instead) | | `254,50,145` / `254,50,146,V` | Read / set test-cycle banks at power-up | | `254,50,144` | Restore factory settings (configuration mode) | | `254,168` / `254,169,C` | Read / set stored baud code: 1 2400, 2 4800, 3 9600, 4 19200, 5 38400, 6 57600, 7 115200 (set needs configuration mode) | --- ## 14. Fusion-class differences (ProXR Advanced) Bank commands return **2 bytes**. Timers are `254,150,…`, flashers `254,145,…`, refreshing `254,125/126/136/137`. `254,124,0,1` sent API-framed returns a 32-byte **relay usage** record instead of banks 33–64. Extra: memory cells `254,71/72/73,CELL` (view/recall/store a 32-byte relay pattern; needs TLEE), and `64,100+R,B` / `64,108+R,B[,1|2|3]` to hand a relay to Taralist / ProXR / Reactor / both. --- ## 15. Inputs on ProXR Lite and ZAD boards (AD8) and UXP expansions ### 15.1 AD8 (built-in 8 analog inputs, 0–5 V; capability A bit 1) | Payload | Reply | Meaning | |---|---|---| | `254,150+n` (n 0..7) | 1 byte | Channel n, 8-bit | | `254,158+n` | 2 bytes MSB,LSB | Channel n, 10-bit (value = MSB×256+LSB, 0..1023) | | `254,166` | 8 bytes | All channels, 8-bit | | `254,167` | 16 bytes | All channels, 10-bit | A "Pull Up/Down" jumper pulls the inputs to 5 V (default) or ground through 10 k, so a switch to ground reads near 0. The **Relay Activator / Event Generator** (capability B bit 3) lets each input drive a relay without a host: per-input function codes 0 none; 1–8 momentary relay 1–8; 9–16 toggle; 17–24 on-only; 25–32 off-only; 33–40 momentary flash; 41–48 toggle flash; 49 banks 1–4 off; 50 banks 1–4 on; 51 banks follow inputs. "Send event on port 1" (EEPROM 122 = 1) makes the board push an input-change packet — the one place ProXR is not speak-when-spoken-to. Its format is `171, deviceNumber, 1, MOMENTARY, TOGGLE, BANK1` (source 1 = AD8, then the momentary-input bitmask, the toggle-state bitmask and the resulting relay bank 1). ### 15.2 UXP expansion port (Enhanced boards with UXP; one expansion type per port) | Payload | Reply | Meaning | |---|---|---| | `254,175,B` | 1 byte | Contact-closure bank B (0-based), bit0 = input 1 | | `254,175,S,C` | C+1 bytes | Banks S..S+C | | `254,195,I` / `254,203,I` / `254,208,I` | 1 byte | 8-bit ADC, device 0/1/2, channel I (0..15) | | `254,192` / `193` / `194` | 16 bytes | All 16 channels 8-bit, device 0/1/2 | | `254,199,I` / `254,207,I` / `254,209,I` | 2 bytes LSB,MSB | 12-bit ADC single channel | | `254,196` / `197` / `198` | 32 bytes | All 16 channels 12-bit (LSB,MSB each) | | `254,170,C,V` | 85 | Potentiometer channel C wiper = V | | `254,171,V` | 85 | All potentiometers = V | --- ## 16. Reply conventions and error handling - `85` = accepted (run mode); `86` = accepted and the controller is in configuration mode; `87` = security lockdown (stop, power-cycle); `84` = not activated (old firmware docs); timeout = no controller / wrong baud / wrong framing / wrong E3C device enabled. - Bitmasks are LSB-first: bit0 = relay 1. - In API mode a reply whose length is not what you expected means the command was parsed differently than you intended (wrong family or wrong firmware) — treat as an error, do not guess. - Writes that require configuration mode return 85 but do nothing in run mode; check the mode first (`254,33`). - Short packets are ignored outright: the firmware wants ≥ 2 bytes for commands 0–39, ≥ 3 for 40–49 and 100–139, ≥ 4 for 140. Always send the full documented length. - The `85` acknowledgement is sent **before** the relay outputs are refreshed. At serial speeds the refresh completes within milliseconds; on long XR expansion chains allow one refresh cycle before trusting an immediate status read. - Do not send Enterprise-family block commands (`254,50,MSB,LSB,N`, `254,52,…`) to a ProXR board: `254,52,L,V` is the scratchpad write. --- ## 17. Minimal working code ### Python (pyserial) ```python import serial, time class ProXR: def __init__(self, port, baud=115200, timeout=1.0): self.s = serial.Serial(port, baud, timeout=timeout) # 8N1 defaults time.sleep(0.1) @staticmethod def frame(payload): f = [0xAA, len(payload), *payload] return bytes(f + [sum(f) & 0xFF]) def cmd(self, payload, expect=None): self.s.reset_input_buffer() self.s.write(self.frame(payload)) head = self.s.read(2) if len(head) < 2 or head[0] != 0xAA: raise TimeoutError("no API reply") n = head[1] rest = self.s.read(n + 1) buf = head + rest if len(buf) != n + 3 or sum(buf[:-1]) & 0xFF != buf[-1]: raise IOError("bad frame") data = list(buf[2:-1]) if expect is not None and len(data) != expect: raise IOError(f"expected {expect} bytes, got {data}") return data def raw(self, payload, expect): self.s.reset_input_buffer(); self.s.write(bytes(payload)) return list(self.s.read(expect)) # --- high level def mode(self): return self.cmd([254, 33], 1)[0] # 85 run / 86 config / 87 lockdown def identify(self): return self.raw([254, 246], 3) # R1, R2, R3 def caps(self): return self.cmd([254, 53, 240, 7], 8) # fw major, minor, year, A..E def banks(self): return self.cmd([254, 53, 10], 1)[0] def relay_on(self, bank, r): return self.cmd([254, 108 + r, bank], 1) # r = 0..7 def relay_off(self, bank, r):return self.cmd([254, 100 + r, bank], 1) def read_bank(self, bank): return self.cmd([254, 124, bank], 1)[0] def set_bank(self, bank, m): return self.cmd([254, 140, m & 255, bank], 1) def all_banks(self): return self.cmd([254, 124, 0], 32) def timer_on(self, t, h, m, s, relay_index): return self.cmd([254, 50, 50 + t, h, m, s, relay_index], 1) def toggle(self, n): return self.cmd([254, 47, n & 255, (n & 65280) // 255, 1], 1) p = ProXR("/dev/ttyUSB0") assert p.mode() in (85, 86) r1, r2, r3 = p.identify() fw = p.caps(); print("firmware %d.%d (20%02d) caps" % (fw[0], fw[1], fw[2]), fw[3:]) p.relay_on(1, 0); print(bin(p.read_bank(1))) # relay 1 of bank 1 on → 0b1 ``` ### Node.js (serialport) ```js import { SerialPort } from "serialport" const frame = (p) => { const f = [0xaa, p.length, ...p]; return Buffer.from([...f, f.reduce((a, b) => a + b) & 0xff]) } const port = new SerialPort({ path: "/dev/ttyUSB0", baudRate: 115200 }) function cmd(payload, timeoutMs = 1000) { return new Promise((resolve, reject) => { let buf = Buffer.alloc(0) const onData = (d) => { buf = Buffer.concat([buf, d]) while (buf.length && buf[0] !== 0xaa) buf = buf.subarray(1) // resync if (buf.length >= 2 && buf.length >= buf[1] + 3) { const f = buf.subarray(0, buf[1] + 3) port.off("data", onData); clearTimeout(t) if ((f.subarray(0, -1).reduce((a, b) => a + b) & 0xff) !== f[f.length - 1]) return reject(new Error("bad checksum")) resolve([...f.subarray(2, -1)]) } } const t = setTimeout(() => { port.off("data", onData); reject(new Error("timeout")) }, timeoutMs) port.flush(() => { port.on("data", onData); port.write(frame(payload)) }) }) } console.log(await cmd([254, 33])) // [85] await cmd([254, 108, 1]) // relay 1 bank 1 on console.log(await cmd([254, 124, 1])) // [1] ``` ### TCP instead of serial Open a TCP socket to `:2101` and use exactly the same bytes. With raw framing some Ethernet modules want ~1 ms between bytes; with API framing write the whole frame at once. --- ## 18. Pitfalls checklist 1. **Relay numbering**: bank commands use R = 0..7 inside the bank (relay 1 → 0); individual-relay and timer commands use a 0-based index across all banks. Status bitmasks are bit0 = relay 1. 2. **Bank 0 = all banks** for on/off/set commands, but `254,124,0` is "read all 32 banks" (32 bytes), not bank 0. 3. **Neighbours cannot cross a bank**: relay 8 has no neighbours. 4. **Timer activation mask pauses every timer whose bit is 0**. Resume with `255,255` if in doubt. 5. **Configuration mode** is required for EEPROM writes, scratchpad writes and factory restore; run mode silently ignores them with a cheerful 85. Power-up storage (`254,42/142`) is allowed in run mode unless EEPROM 0 bit 0. 6. **Expansion boards must power up together with the main board**; never hot-plug the XR/UXP bus. 7. **Raw mode is timeout-based**: send a command in one write; never interleave bytes from two commands. 8. **Fusion acks are 2 bytes**; Enhanced/Lite/Standard acks are 1 byte. Size your reads from the identification. 9. **NCD's public ProXR guide has three table errata** (recompute, do not copy): "Activate all timers" should be mask `FF FF`; the timer-query rows should have length byte `04`; the scratchpad-write checksums were computed with value 0. 10. **Do not probe with Enterprise/USB-to-I2C opcodes** (`254,50..53` block forms) before you know it is not a ProXR board. 11. **Discovery**: Ethernet boards broadcast on UDP 13000; connect to TCP 2101. One TCP client at a time. 12. The `87` lockdown reply is terminal until power-cycle; `84` means not activated on some old firmware. --- ## 19. Glossary **API codec** — the `0xAA,len,payload,checksum` wrapper. **Bank** — 8 relays. **E3C** — multi-drop device networking by device number. **UXP** — universal expansion port for inputs/ADC/pots. **XR port** — relay expansion bus (up to 32 banks). **AD8** — the 8 built-in analog inputs on Lite/ZAD boards. **Flasher** — background relay blinker (16). **Timer** — countdown that controls one relay (16). **Scratchpad** — 8 user bytes in EEPROM. **Run / configuration mode** — EEPROM locked / unlocked. **Lockdown (87)** — security state requiring power-cycle. *Provenance: NCD controller firmware source (ProXR / ProXR Lite builds), NCD Base Station host software source, and NCD's published API Codec Quick Start Guide, API Calculator and ProXR Quick Start Guide (text copies in `reference/`). Where the three disagreed the differences are called out in the text.*