{
  "protocol": "ProXR",
  "family": "proxr",
  "revision": "public-release-2026-10-20",
  "note": "Protocol-as-data for ProXR / ProXR Lite relay controllers (NCD). Derived from controller firmware and reconciled with host software and NCD's published guides. ProXR family only: identify the device before sending (other NCD firmware families use some of these bytes differently).",
  "framing": {
    "api": {
      "recommended": true,
      "format": "170, LEN, <payload bytes>, CHECKSUM",
      "start_byte": 170,
      "len": "count of payload bytes (2..24), not counting checksum",
      "checksum": "(170 + LEN + sum(payload_bytes)) mod 256",
      "response": "170, OUTLEN, <bytes>, (170 + OUTLEN + sum(bytes)) mod 256"
    },
    "dropnet": {
      "format": "171, DEVICE_NUM, LEN, <payload>, CHECKSUM",
      "start_byte": 171,
      "device_num": "E3C device number (EEPROM addr 1); only the matching controller acts",
      "checksum_note": "checksum = (170 + LEN + sum(payload)) mod 256 \u2014 computed as if the header were 170; the DEVICE_NUM byte is NOT included"
    },
    "raw": {
      "recommended": false,
      "format": "<payload bytes>",
      "framing": "inter-byte gap > UARTTimeout ms (EEPROM 7)",
      "valid_first_bytes": [
        254,
        0,
        70,
        253,
        188,
        64,
        65
      ],
      "note": "timing-dependent; prefer API mode"
    }
  },
  "responses": {
    "85": "ACK / success / Run mode",
    "86": "Configuration mode",
    "87": "Lockdown (AVA; dormant)",
    "90": "Calibrator: timer start marker",
    "91": "Calibrator: refresh/finish marker",
    "status": "raw byte(s): relay bit 0/1, bank byte 0-255, or A/D value(s)",
    "spontaneous_event": "171, DEVICE_NUM, 1, MOMENTARY, TOGGLE, RBANK1 (AD8 event engine when EEPROM 122=1)"
  },
  "concepts": {
    "bank": "1 byte = 8 relays (bit0=relay1 .. bit7=relay8)",
    "current_bank": "selected via select_bank (254,49,bank); bank 0 in banked cmds = all banks",
    "group": "32-bank window; effective_bank = bank + group*32 (group 0..3)",
    "relay_by_number": "flat index N: bank = N div 8 + 1, bit = N mod 8 (+ group*32 banks). 0-based per firmware \u2014 see CONCERNS #1",
    "spread": "on current-bank on/off, applies action to next N relays",
    "reporting_mode": "if ON, 85 precedes accepted action commands",
    "auto_refresh": "if OFF, changes buffer in RBank[] until manual_refresh (254,37)"
  },
  "commands": [
    {
      "id": "relay_off_current",
      "summary": "Turn OFF relay 1-8 in current bank",
      "cmd": {
        "range": [
          0,
          7
        ],
        "formula": "cmd = relay-1"
      },
      "args": [
        {
          "name": "spread",
          "pos": 2,
          "optional": true,
          "desc": "also apply to next N relays"
        }
      ],
      "payload": "254, <0..7> [,spread]",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_on_current",
      "summary": "Turn ON relay 1-8 in current bank",
      "cmd": {
        "range": [
          8,
          15
        ],
        "formula": "cmd = 7+relay"
      },
      "args": [
        {
          "name": "spread",
          "pos": 2,
          "optional": true
        }
      ],
      "payload": "254, <8..15> [,spread]",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_status_current",
      "summary": "Read status of relay 1-8 in current bank",
      "cmd": {
        "range": [
          16,
          23
        ],
        "formula": "cmd = 15+relay"
      },
      "args": [],
      "payload": "254, <16..23>",
      "returns": "1 byte (0 or 1)"
    },
    {
      "id": "status_all_current",
      "summary": "Read all relays (current bank byte; bank 0 streams all banks)",
      "cmd": 24,
      "args": [
        {
          "name": "group",
          "pos": 2,
          "optional": true,
          "desc": "32-bank window when streaming"
        }
      ],
      "payload": "254, 24 [,group]",
      "returns": "1 bank byte, or stream of bank bytes if current bank=0"
    },
    {
      "id": "autorefresh_on",
      "summary": "Enable auto-refresh",
      "cmd": 25,
      "args": [],
      "payload": "254, 25",
      "returns": "85 if reporting on"
    },
    {
      "id": "autorefresh_off",
      "summary": "Disable auto-refresh (buffer until manual refresh)",
      "cmd": 26,
      "args": [],
      "payload": "254, 26",
      "returns": "85 if reporting on"
    },
    {
      "id": "reporting_on",
      "summary": "Reporting mode ON",
      "cmd": 27,
      "args": [],
      "payload": "254, 27",
      "returns": "85"
    },
    {
      "id": "reporting_off",
      "summary": "Reporting mode OFF",
      "cmd": 28,
      "args": [],
      "payload": "254, 28",
      "returns": "none"
    },
    {
      "id": "all_off_current",
      "summary": "All relays OFF (current bank)",
      "cmd": 29,
      "args": [],
      "payload": "254, 29",
      "returns": "85 if reporting on"
    },
    {
      "id": "all_on_current",
      "summary": "All relays ON (current bank)",
      "cmd": 30,
      "args": [],
      "payload": "254, 30",
      "returns": "85 if reporting on"
    },
    {
      "id": "invert_current",
      "summary": "Invert all relays (current bank)",
      "cmd": 31,
      "args": [],
      "payload": "254, 31",
      "returns": "85 if reporting on"
    },
    {
      "id": "reverse_current",
      "summary": "Reverse relay bit order (current bank)",
      "cmd": 32,
      "args": [],
      "payload": "254, 32",
      "returns": "85 if reporting on"
    },
    {
      "id": "report_bank",
      "summary": "Report current bank number",
      "cmd": 34,
      "args": [],
      "payload": "254, 34",
      "returns": "1 byte"
    },
    {
      "id": "report_refresh",
      "summary": "Report auto-refresh setting",
      "cmd": 36,
      "args": [],
      "payload": "254, 36",
      "returns": "1 byte"
    },
    {
      "id": "manual_refresh",
      "summary": "Push RBank[] to physical outputs now",
      "cmd": 37,
      "args": [],
      "payload": "254, 37",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_off_bank",
      "summary": "Turn OFF relay 1-8 in specified bank",
      "cmd": {
        "range": [
          100,
          107
        ],
        "formula": "cmd = 99+relay"
      },
      "args": [
        {
          "name": "bank",
          "pos": 2,
          "desc": "0=all banks"
        },
        {
          "name": "spread",
          "pos": 3,
          "optional": true
        }
      ],
      "payload": "254, <100..107>, bank [,spread]",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_on_bank",
      "summary": "Turn ON relay 1-8 in specified bank",
      "cmd": {
        "range": [
          108,
          115
        ],
        "formula": "cmd = 107+relay"
      },
      "args": [
        {
          "name": "bank",
          "pos": 2
        },
        {
          "name": "spread",
          "pos": 3,
          "optional": true
        }
      ],
      "payload": "254, <108..115>, bank [,spread]",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_status_bank",
      "summary": "Read status of relay 1-8 in specified bank",
      "cmd": {
        "range": [
          116,
          123
        ],
        "formula": "cmd = 115+relay"
      },
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, <116..123>, bank",
      "returns": "1 byte (0/1)"
    },
    {
      "id": "status_all_bank",
      "summary": "Read all relays in specified bank (bank 0 streams all)",
      "cmd": 124,
      "args": [
        {
          "name": "bank",
          "pos": 2
        },
        {
          "name": "group",
          "pos": 3,
          "optional": true
        }
      ],
      "payload": "254, 124, bank [,group]",
      "returns": "1 bank byte or stream"
    },
    {
      "id": "all_off_bank",
      "summary": "All relays OFF in specified bank",
      "cmd": 129,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 129, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "all_on_bank",
      "summary": "All relays ON in specified bank",
      "cmd": 130,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 130, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "invert_bank",
      "summary": "Invert specified bank",
      "cmd": 131,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 131, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "reverse_bank",
      "summary": "Reverse specified bank",
      "cmd": 132,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 132, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "set_all_current",
      "summary": "Set all relays in current bank to a bitmap",
      "cmd": 40,
      "args": [
        {
          "name": "value",
          "pos": 2,
          "desc": "0-255 bitmap"
        }
      ],
      "payload": "254, 40, value",
      "returns": "85 if reporting on"
    },
    {
      "id": "store_default_current",
      "summary": "Store current bank state as power-up default (config/write-protect gated)",
      "cmd": 42,
      "args": [],
      "payload": "254, 42",
      "returns": "85 if reporting on"
    },
    {
      "id": "recall_default_current",
      "summary": "Recall current bank power-up default from EEPROM",
      "cmd": 43,
      "args": [],
      "payload": "254, 43",
      "returns": "1 byte (or stream if bank 0)"
    },
    {
      "id": "read_relay_by_number",
      "summary": "Read status of relay by number N (0-based: relay 1 of bank 1 is 0)",
      "cmd": 44,
      "args": [
        {
          "name": "N",
          "pos": 2,
          "desc": "relay number (see relay_by_number)"
        },
        {
          "name": "group",
          "pos": 3,
          "optional": true
        }
      ],
      "payload": "254, 44, N [,group]",
      "returns": "1 byte (0/1)"
    },
    {
      "id": "flash",
      "summary": "Flash control",
      "cmd": 45,
      "args": [
        {
          "name": "sel",
          "pos": 2,
          "desc": "0=set rate; 1-16=relay channel"
        },
        {
          "name": "opt",
          "pos": 3,
          "desc": "rate (if sel=0) or on/off"
        }
      ],
      "payload": "254, 45, sel, opt",
      "returns": "85 if reporting on"
    },
    {
      "id": "safe_make_before_break",
      "summary": "All relays OFF, then relay N ON",
      "cmd": 46,
      "args": [
        {
          "name": "N",
          "pos": 2
        }
      ],
      "payload": "254, 46, N",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_off_by_number",
      "summary": "Turn OFF relay by number (5th byte>0 => toggle) (N is 0-based)",
      "cmd": 47,
      "args": [
        {
          "name": "N",
          "pos": 2
        },
        {
          "name": "group",
          "pos": 3,
          "optional": true
        },
        {
          "name": "momentary_toggle",
          "pos": 4,
          "optional": true,
          "desc": ">0 toggles instead"
        }
      ],
      "payload": "254, 47, N [,group] [,mom]",
      "returns": "85 if reporting on"
    },
    {
      "id": "relay_on_by_number",
      "summary": "Turn ON relay by number (N is 0-based)",
      "cmd": 48,
      "args": [
        {
          "name": "N",
          "pos": 2
        },
        {
          "name": "group",
          "pos": 3,
          "optional": true
        }
      ],
      "payload": "254, 48, N [,group]",
      "returns": "85 if reporting on"
    },
    {
      "id": "select_bank",
      "summary": "Select active bank (clamped to ATBanks)",
      "cmd": 49,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 49, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "set_all_bank",
      "summary": "Set all relays in specified bank to bitmap",
      "cmd": 140,
      "args": [
        {
          "name": "value",
          "pos": 2
        },
        {
          "name": "bank",
          "pos": 3
        }
      ],
      "payload": "254, 140, value, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "store_default_bank",
      "summary": "Store specified bank as power-up default",
      "cmd": 142,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 142, bank",
      "returns": "85 if reporting on"
    },
    {
      "id": "recall_default_bank",
      "summary": "Recall specified bank power-up default",
      "cmd": 143,
      "args": [
        {
          "name": "bank",
          "pos": 2
        }
      ],
      "payload": "254, 143, bank",
      "returns": "1 byte"
    },
    {
      "id": "timer_set_activate",
      "summary": "Set & activate latching timer t (0-15)",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "50+t"
        },
        {
          "name": "hour",
          "pos": 3
        },
        {
          "name": "min",
          "pos": 4
        },
        {
          "name": "sec",
          "pos": 5
        },
        {
          "name": "relayN",
          "pos": 6
        }
      ],
      "payload": "254, 50, 50+t, H, M, S, relayN",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_set_activate_pulse",
      "summary": "Set & activate pulse timer t (0-15)",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "70+t"
        },
        {
          "name": "hour",
          "pos": 3
        },
        {
          "name": "min",
          "pos": 4
        },
        {
          "name": "sec",
          "pos": 5
        },
        {
          "name": "relayN",
          "pos": 6
        }
      ],
      "payload": "254, 50, 70+t, H, M, S, relayN",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_set",
      "summary": "Set latching timer t without activating",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "90+t"
        },
        {
          "name": "hour",
          "pos": 3
        },
        {
          "name": "min",
          "pos": 4
        },
        {
          "name": "sec",
          "pos": 5
        },
        {
          "name": "relayN",
          "pos": 6
        }
      ],
      "payload": "254, 50, 90+t, H, M, S, relayN",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_set_pulse",
      "summary": "Set pulse timer t without activating",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "110+t"
        },
        {
          "name": "hour",
          "pos": 3
        },
        {
          "name": "min",
          "pos": 4
        },
        {
          "name": "sec",
          "pos": 5
        },
        {
          "name": "relayN",
          "pos": 6
        }
      ],
      "payload": "254, 50, 110+t, H, M, S, relayN",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_query",
      "summary": "Query timer t -> H,M,S,relay",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "130"
        },
        {
          "name": "t",
          "pos": 3
        }
      ],
      "payload": "254, 50, 130, t",
      "returns": "4 bytes: H,M,S,relay"
    },
    {
      "id": "timer_register",
      "summary": "Activate a 16-bit bitmask of timers at once",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "131"
        },
        {
          "name": "maskLo",
          "pos": 3
        },
        {
          "name": "maskHi",
          "pos": 4
        }
      ],
      "payload": "254, 50, 131, maskLo, maskHi",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_store_cal",
      "summary": "Store second-length calibration (SECS)",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "132"
        },
        {
          "name": "lsb",
          "pos": 3
        },
        {
          "name": "msb",
          "pos": 4
        }
      ],
      "payload": "254, 50, 132, lsb, msb",
      "returns": "85 if reporting on"
    },
    {
      "id": "timer_get_cal",
      "summary": "Get SECS calibration",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "133"
        }
      ],
      "payload": "254, 50, 133",
      "returns": "2 bytes: SECS lo, hi"
    },
    {
      "id": "calibrator_on",
      "summary": "Calibrator ON (emits 90/91 markers)",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "134"
        }
      ],
      "payload": "254, 50, 134",
      "returns": "85 if reporting on"
    },
    {
      "id": "calibrator_off",
      "summary": "Calibrator OFF",
      "cmd": 50,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "135"
        }
      ],
      "payload": "254, 50, 135",
      "returns": "85 if reporting on"
    },
    {
      "id": "ad8_read_8bit",
      "summary": "Read A/D channel 0-7, 8-bit",
      "cmd": {
        "range": [
          150,
          157
        ],
        "formula": "cmd = 150+channel"
      },
      "args": [],
      "payload": "254, <150..157>",
      "returns": "1 byte (0-255)"
    },
    {
      "id": "ad8_read_10bit",
      "summary": "Read A/D channel 0-7, 10-bit",
      "cmd": {
        "range": [
          158,
          165
        ],
        "formula": "cmd = 158+channel"
      },
      "args": [],
      "payload": "254, <158..165>",
      "returns": "2 bytes MSB, LSB \u2014 value = MSB*256 + LSB (0..1023)"
    },
    {
      "id": "ad8_read_all_8bit",
      "summary": "Read all 8 A/D channels, 8-bit",
      "cmd": 166,
      "args": [],
      "payload": "254, 166",
      "returns": "8 bytes"
    },
    {
      "id": "ad8_read_all_10bit",
      "summary": "Read all 8 A/D channels, 10-bit",
      "cmd": 167,
      "args": [],
      "payload": "254, 167",
      "returns": "16 bytes"
    },
    {
      "id": "scratchpad_read",
      "summary": "Read scratchpad byte (loc 1-8)",
      "cmd": 51,
      "args": [
        {
          "name": "loc",
          "pos": 2,
          "desc": "1-8"
        }
      ],
      "payload": "254, 51, loc",
      "returns": "1 byte"
    },
    {
      "id": "scratchpad_write",
      "summary": "Write scratchpad byte (loc 1-8)",
      "cmd": 52,
      "args": [
        {
          "name": "loc",
          "pos": 2
        },
        {
          "name": "value",
          "pos": 3
        }
      ],
      "payload": "254, 52, loc, value",
      "returns": "85"
    },
    {
      "id": "mode_query",
      "summary": "Query mode (85=Run, 86=Config)",
      "cmd": 33,
      "args": [],
      "payload": "254, 33",
      "returns": "85 or 86"
    },
    {
      "id": "reboot",
      "summary": "Reboot controller",
      "cmd": 33,
      "args": [
        {
          "name": "a",
          "pos": 2,
          "desc": "140"
        },
        {
          "name": "b",
          "pos": 3,
          "desc": "99"
        }
      ],
      "payload": "254, 33, 140, 99",
      "returns": "none (restarts)"
    },
    {
      "id": "config_timer",
      "summary": "Start config-mode timer (needs remote-config allowed, EEPROM 12 bit0)",
      "cmd": 33,
      "args": [
        {
          "name": "a",
          "pos": 2,
          "desc": "140"
        },
        {
          "name": "b",
          "pos": 3,
          "desc": "86"
        },
        {
          "name": "seconds",
          "pos": 4
        },
        {
          "name": "stall",
          "pos": 5,
          "optional": true,
          "desc": "1=pause countdown"
        }
      ],
      "payload": "254, 33, 140, 86, seconds [,stall]",
      "returns": "85 if reporting on"
    },
    {
      "id": "eeprom_read",
      "summary": "Read EEPROM byte(s). API: count reads count+1 bytes. 192-220 protected (skipped).",
      "cmd": 53,
      "args": [
        {
          "name": "addr",
          "pos": 2,
          "desc": "0-255"
        },
        {
          "name": "count",
          "pos": 3,
          "optional": true,
          "desc": "API block read: extra bytes"
        }
      ],
      "payload": "254, 53, addr [,count]",
      "returns": "1..N bytes"
    },
    {
      "id": "eeprom_write",
      "summary": "Write EEPROM byte(s). CONFIG MODE ONLY. 192-220 blocked; 224-247 need NCD access code appended.",
      "cmd": 54,
      "args": [
        {
          "name": "addr",
          "pos": 2
        },
        {
          "name": "data",
          "pos": 3,
          "desc": "one or more bytes"
        }
      ],
      "payload": "254, 54, addr, data [,data...]  (+ access code for 224-247)",
      "returns": "85",
      "ncd_access_code": [
        140,
        85,
        33,
        102,
        39,
        12,
        24,
        19,
        71,
        240,
        185,
        203
      ]
    },
    {
      "id": "device_id",
      "summary": "Device identity packet",
      "cmd": 246,
      "args": [],
      "payload": "254, 246",
      "returns": "5 bytes: typeHi,typeLo,year,fwVer,E3Cnum (e.g. 6,0,212,2,dev#)"
    },
    {
      "id": "e3c_report_number",
      "summary": "Report this device's E3C number",
      "cmd": 247,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "0"
        }
      ],
      "payload": "254, 247, 0",
      "returns": "1 byte (dev#) if enabled"
    },
    {
      "id": "e3c_discover",
      "summary": "Discover device numbers (staggered response)",
      "cmd": 247,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "1"
        }
      ],
      "payload": "254, 247, 1",
      "returns": "85, dev#, checksum (after (255-dev#)*1s)"
    },
    {
      "id": "e3c_discover_names",
      "summary": "Discover device numbers and names",
      "cmd": 247,
      "args": [
        {
          "name": "sub",
          "pos": 2,
          "desc": "2"
        }
      ],
      "payload": "254, 247, 2",
      "returns": "86, dev#, [16-byte name], checksum"
    },
    {
      "id": "e3c_enable_all",
      "summary": "Enable all devices",
      "cmd": 248,
      "args": [],
      "payload": "254, 248",
      "returns": "none"
    },
    {
      "id": "e3c_disable_all",
      "summary": "Disable all devices",
      "cmd": 249,
      "args": [],
      "payload": "254, 249",
      "returns": "none"
    },
    {
      "id": "e3c_enable_only",
      "summary": "Enable only matching device, disable rest",
      "cmd": 252,
      "args": [
        {
          "name": "dev",
          "pos": 2
        }
      ],
      "payload": "254, 252, dev#",
      "returns": "dev# if matched"
    }
  ]
}
