ProXR Programming Guide· Markdown · commands JSON · all files · ← boardsSelf-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.
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)
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.
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).254,246 (§4).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).
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])
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).
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.
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.
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).
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.
| 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.
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 |
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.
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.)
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,….)
| 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 |
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.
| 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) |
| 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).
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.
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.
| 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) |
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.
| 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).
| 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 |
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.254,33).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.254,50,MSB,LSB,N, 254,52,…) to a ProXR board: 254,52,L,V
is the scratchpad write.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
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]
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.
254,124,0 is "read all 32 banks" (32 bytes), not bank 0.255,255 if in doubt.254,42/142) is allowed in run mode unless EEPROM 0 bit 0.FF FF; the timer-query rows should have length byte 04; the scratchpad-write checksums were computed
with value 0.254,50..53 block forms) before you know it is not a
ProXR board.87 lockdown reply is terminal until power-cycle; 84 means not activated on some old firmware.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.