NCDProXR Programming Guide· Markdown · commands JSON · all files · ← boards

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:

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


17. Minimal working code

Python (pyserial)

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)

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 <ip>: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.