pxnode HTTP API

Every setting of a pxnode, and its live state, as JSON over HTTP. Read a section with GET, change it with PUT, trigger an action with POST.

Base URLhttp://<node-ip>/api/
FormatJSON, UTF-8
AuthenticationNone, or a password
CORSOpen to any origin

Connecting

Each node serves the API on TCP port 80 of its own IP address, shown on its screen. Talk to every node directly: start from any one of them, and GET /api/nodes lists the others.

Paths outside /api/ serve the node's own web page.

Authentication

A node is open by default: no password, nothing to send. If someone sets a password on it (PUT /api/password), every API request must then carry that password. The one exception is GET /api/node, which always answers.

1. Does this node need a password?

Read GET /api/node: "password": true means yes.

2. Send it in every request

Add this header, where the value is the password with a colon in front, base64-encoded:

Authorization: Basic base64(":" + password)

For the password stage left: ":stage left" encodes to OnN0YWdlIGxlZnQ=, so the header is:

Authorization: Basic OnN0YWdlIGxlZnQ=

This is standard HTTP Basic authentication with an empty user name, so most tools build the header for you:

# curl: an empty user name, then the password
curl -u ':stage left' http://10.2.1.99/api/name

// JavaScript
fetch("http://10.2.1.99/api/name", {headers: {Authorization: "Basic " + btoa(":stage left")}})

3. When it is refused

StatuserrorMeaning
401password-requiredthe node has a password and the request had none
401password-wrongthe password is wrong
429password-waittoo soon after a wrong password: the node did not check it. Wait and retry. The wait is 1 s after a wrong password, then doubles with each wrong one in a row, up to 16 s

A browser will not show its own login box for these: ask the user for the password in your interface.

The password keeps the settings safe from mistakes and curious visitors. It travels unencrypted, so it does not protect against someone listening to the network. Art-Net and DMX are never password protected.

Conventions

Each save takes a few milliseconds. About once every hundred saves the node first erases its settings sector, which freezes the outputs for 1 to 2 s. Avoid writing settings in a show loop: use the live routes for that.

Errors

Errors are JSON objects with an error code, often a message, and for invalid input the field at fault:

{"error":"invalid","field":"groups[0].pixels","message":"integer from 0 to 512"}
StatuserrorWhen
400jsonthe body is not a JSON object
400invalida field is out of range, of the wrong type, or breaks a rule
400requestmalformed request
400firmwarea firmware file refused (/api/firmware)
401password-requiredthe node has a password, the request none
401password-wrongwrong password
404not-foundunknown path
405methodmethod not allowed on this path
409conflictthe revision sent is not the current one
409unsupportedthe node cannot do it (recovery on an old bootloader)
413too-largerequest body too large
429password-waita password sent too soon after a wrong one, not checked
500flashthe settings could not be saved
503busythe node is busy: retry shortly
GET/api/node#

The node's identity. The one request that never needs the password.

FieldTypeDescription
uuidstringunique ID, 24 hex digits
namestringnode name
zonestringits zone (/api/network), "" for none
manufacturerstringKinora
modelstringpxnode 4 or px4i
firmwarestringfirmware version
apiintegerAPI version: 1. Changes only for an incompatible change
capabilitiesarrayoptional features: rgbw (RGBW outputs), fx (effects), sync (nodes kept in step)
macstringMAC address, aa:bb:cc:dd:ee:ff
slotstringfirmware slot running: A or B
bootloaderstringbootloader version; "" for one without HTTP recovery
httpintegerHTTP port, 80
passwordbooleanthe other requests need a password
ip, mode, revisionas in their sections

Older firmware may leave out zone, bootloader and password: read them as "", "" and false.

Response
{"uuid":"003000513431500d20343556","name":"Stage left","zone":"","manufacturer":"Kinora","model":"pxnode 4",
 "firmware":"0.1.8","api":1,"capabilities":["rgbw","fx","sync"],"mac":"00:80:e1:62:b2:52","slot":"B",
 "bootloader":"1.0.0","http":80,"password":false,"ip":"10.2.1.99","mode":"standalone","revision":479}
GET/api/stats#

Health and live counters: what the node is doing now, and what went wrong since it started. Nothing here is saved; everything restarts at boot.

Node

FieldDescription
linkEthernet link up
uptime_sseconds since start
reset_causewhy it last started: power-on, brown-out (supply dipped), watchdog, window-watchdog, low-power, software (a reboot or firmware update), reset-pin, unknown
temperature_cprocessor temperature in °C (not the air around the board); null during the first second
cpu_load% of the last second the processor was busy
stack_freebytes of stack never used since start

Input

FieldDescription
inputok: data the node uses arrived in the last 4 s; lost: none for 4 s; none: standalone mode, or nothing received since start
input_age_msms since that data last arrived, -1: never
artnet_sourceIP of the last Art-Net sender, "" if none. A value that keeps changing means two controllers are sending
artsyncboolean
blackoutthe outputs are dark or fading to it (/api/blackout)

Rates, over the last second

FieldDescription
artnet_fpsArtDmx packets received
artnet_universesuniverses of those packets
artnet_frame_ratethe controller's frame rate
artsync_fpspackets per second
pixel_fpspixel frames sent
dmx_in_fps, dmx_out_fpsDMX frames received and sent
network_fpsEthernet frames received, all traffic
artnet_ignored_fpsArt-Net packets dropped because sent to the broadcast address (ignore_broadcast, /api/mode)

Pixel output

FieldDescription
frame_usframe period in µs: 16666 at 60 frames/s
frame_rate_maxthe frame rate it gives (/api/timing)
render_usslowest effect frame of the last second, in µs. Must stay well under frame_us
pixel_overrunsframes started before the previous one was sent, since start

Network errors, since start

FieldDescription
net_rx_missedEthernet frames dropped under a traffic burst. Universes lost show here
net_rx_errorsframes received damaged (cable, connector, switch)
pbuf_exhausted, heap_exhaustedtimes the network stack ran out of memory

Totals and settings storage

FieldDescription
totalscounters since start: artnet_packets, dmx_in_frames, dmx_out_frames, pixel_frames, pixel_overruns. A short problem still shows after it is over
config.savessettings saves, ever (the revision)
config.erasessaves since start that erased the settings sector (1 to 2 s of frozen outputs each)
config.free_bytesroom before the next erase; a save takes about 1.2 KB
sync.events, sync.last_age_msinput events that restarted the effects to keep nodes in step, and ms since the last (-1: none)

Zone sync

The pxsync object: how the node keeps in step with the other nodes of its zone (pxsync in /api/timing).

FieldDescription
enabledas pxsync in /api/timing
roleleader, follower, alone (no other node of the zone heard) or off
nodes, peersthe other nodes of the zone heard in the last second, and their name, ip, leader; more_peers: how many more than listed
leader, leader_name, leader_ip, leader_frame_rate the node followed
frames, frame_error_usframes started with the leader's, and the last phase difference measured
frames_reasonwhy frames are not followed: measuring, artsync, leader-artsync, frame-rate (another frame rate than the leader's)
effects, effects_reasoneffects in phase with the leader's; why not: measuring, not-standalone, leader-not-standalone, other-preset
Response
{"link":true,"uptime_s":3612,"reset_cause":"power-on","temperature_c":46.4,"cpu_load":36,"stack_free":17192,
 "input":"ok","input_age_ms":24,"artnet_source":"10.2.1.215","artsync":false,"blackout":false,
 "artnet_fps":468,"artnet_universes":12,"artnet_frame_rate":40,"artsync_fps":0,"pixel_fps":60,"dmx_in_fps":0,
 "dmx_out_fps":43,"network_fps":470,"artnet_ignored_fps":0,"frame_us":16666,"frame_rate_max":60,"render_us":702,
 "pixel_overruns":1,"net_rx_missed":0,"net_rx_errors":0,"pbuf_exhausted":0,"heap_exhausted":0,
 "totals":{"artnet_packets":1690245,"dmx_in_frames":0,"dmx_out_frames":155316,"pixel_frames":216720,"pixel_overruns":1},
 "config":{"saves":76,"erases":0,"free_bytes":43520},"sync":{"events":0,"last_age_ms":-1},
 "pxsync":{"enabled":true,"role":"alone","nodes":0,"peers":[],"more_peers":0}}
GET/api/artnet#

The Art-Net controllers sending to the node, up to 10, most recent first. When several send the same universe, the node takes the last packet received.

FieldDescription
ipthe sender's address
activea packet in the last 4 s
last_seen_msms since its last packet
packets_per_sits ArtDmx packets over the last second
dropped_per_sof them, dropped by ignore_broadcast
broadcastits last packet went to the broadcast address
artsyncboolean
universesthe universes it sent in the last 4 s, up to 16: universe, packets_per_s, used (the node uses its data), shared (another active sender sends it too)
more_universesmore universes than listed
Response
{"senders":[{"ip":"10.2.1.215","active":true,"last_seen_ms":12,"packets_per_s":480,"dropped_per_s":0,
  "broadcast":false,"artsync":true,"universes":[{"universe":0,"packets_per_s":40,"used":true,"shared":false}],
  "more_universes":false}]}
GET/api/nodes#

The nodes of the network this node has found, itself first, up to 32. Only nodes of its own zone are listed. Each entry has the fields of GET /api/node, plus mask, online (heard in the last 32 s) and last_seen_ms. A node that went silent stays listed, offline, until this node restarts. The list refreshes every 10 s.

Response
{"nodes":[{"uuid":"003000513431500d20343556","name":"Stage left","zone":"","manufacturer":"Kinora",
  "model":"pxnode 4","firmware":"0.1.8","api":1,"capabilities":["rgbw","fx","sync"],"mac":"00:80:e1:62:b2:52",
  "slot":"B","bootloader":"1.0.0","http":80,"password":false,"ip":"10.2.1.99","mask":"255.255.255.0",
  "mode":"standalone","revision":479,"online":true,"last_seen_ms":0}]}
POST/api/nodes/refresh#

Searches the network now instead of waiting for the next refresh. Replies arrive within milliseconds: read /api/nodes a few hundred ms later. Answers {"ok":true}.

GETPUT/api/name#
FieldTypeDescriptionDefault
namestring1 to 32 characters, Latin-1, no control characterspxnode 4
Request
curl -X PUT -d '{"name":"Stage left"}' http://10.2.1.99/api/name
Response
{"name":"Stage left","revision":480}
GETPUT/api/network#
FieldTypeDescriptionDefault
ipIPv4not 0.0.0.010.0.0.99
maskIPv4contiguous, not 0.0.0.0 or 255.255.255.255255.255.255.0
gatewayIPv410.0.0.1
zonestring0 to 16 printable ASCII characters. Nodes of a zone are listed together and keep their effects and frames in step. It separates rigs sharing a network (two stages); it is not a security boundary""
A new address is saved at once but applied after the response is sent, about 200 ms later: the response comes from the old address, the next request goes to the new one. No restart needed.
Response
{"ip":"10.2.1.99","mask":"255.255.255.0","gateway":"10.2.1.1","zone":"","revision":479}
GETPUT/api/mode#

What drives the pixels.

FieldTypeDescriptionDefault
modestringstandalone (the node's own presets), dmx-shape, artnet-standard, artnet-advanced, artnet-shapedmx-shape
input_address1-512first channel of the input profile (shape modes)1
input_profilestringrgb (dimmer, R, G, B, and W on RGBW outputs: 4 or 5 channels) or fx (effects: 12 or 14 channels)fx
input_universe0-32767universe read in Art-Net shape mode. Same setting as output_universe of /api/dmx: writing one changes the other0
control_universe0-32767universe of the control channels (Art-Net advanced mode)28
control_address1-512first control channel: global dimmer, shutter, then one dimmer per group1
ignore_broadcastbooleanonly Art-Net data sent to the node's own address drives it; data sent to a broadcast address is droppedfalse
Request
curl -X PUT -d '{"mode":"artnet-shape","input_profile":"fx","input_address":101}' http://10.2.1.99/api/mode
Response
{"mode":"artnet-shape","input_address":101,"input_profile":"fx","input_universe":0,"control_universe":28,
 "control_address":1,"ignore_broadcast":false,"revision":480}
GETPUT/api/dmx#

The DMX output, fed from an Art-Net universe.

FieldTypeDescriptionDefault
output_enabledbooleanthe DMX output sends (Art-Net modes)true
output_universe0-32767the Art-Net universe sent on the DMX output0
Response
{"output_enabled":true,"output_universe":0,"revision":479}
GETPUT/api/display#

The node's screen and keys.

FieldTypeDescriptionDefault
brightness0-100backlight %. Rounded to the nearest of 5 levels (20 to 100) and read back as that level100
screensaver_s0-3600the screen turns off after that many seconds without a key press; 0: never10
rotation0 or 180pxnode 4 mounted upside down: the screen and the keys turn0
lockbooleanthe keys are locked. Holding back and enter for 2 s unlocks them for a minute; false unlocks for good. It protects the screen, not the APIfalse
Response
{"brightness":100,"screensaver_s":10,"rotation":0,"lock":false,"revision":479}
GETPUT/api/timing#
FieldTypeDescriptionDefault
frame_rate_max20-120pixel frames per second. Above 60 each output sends fewer pixels (max_pixels, /api/outputs/{X})60
artsyncbooleanArt-Net standard and advanced modestrue
effect_syncbooleaneffects restart together on every node on an input event (effect or shutter change, dimmer leaving 0)true
pxsyncbooleankeep in step with the other nodes of the zone: frames started together, effects in phase, zone-wide preset changes receivedtrue
Response
{"frame_rate_max":60,"artsync":true,"effect_sync":true,"pxsync":true,"revision":479}
PUT/api/password#

Sets, changes or removes the node's password (see Authentication). Changing it needs the current one. It protects the settings, not the show: Art-Net, DMX and the presets keep working.

FieldTypeDescription
passwordstringrequired: 1 to 64 printable ASCII characters, or "" to remove it

The password is never read back, and GET /api/config leaves it out. Forgotten: on the node's screen, Menu, Factory reset, Network only (the address returns to 10.0.0.99).

Request
curl -X PUT -d '{"password":"stage left"}' http://10.2.1.99/api/password
Response
{"password":true,"revision":481}
GET/api/outputs#

The four pixel outputs at a glance.

Response
{"outputs":[{"output":"A","color_mode":"rgb","pixels":340,"groups":2,"max_pixels":512},
  {"output":"B","color_mode":"rgb","pixels":340,"groups":2,"max_pixels":512},
  {"output":"C","color_mode":"rgb","pixels":340,"groups":2,"max_pixels":512},
  {"output":"D","color_mode":"rgb","pixels":340,"groups":2,"max_pixels":512}],"revision":479}
GETPUT/api/outputs/{A..D}#

One output: its pixel type and its patch, up to 16 groups of pixels, each on an Art-Net universe and address.

FieldTypeDescriptionDefault
outputstringread only: A to D
color_modestringrgb (WS2812, 3 channels a pixel) or rgbw (SK6812, 4 channels)rgb
max_pixelsintegerread only: pixels the output can send, 512 RGB or 384 RGBW up to 60 frames/s, fewer above
groupsarray16 groups, below6 groups of 64 pixels, two per universe (A on universes 0-2, B 3-5, C 6-8, D 9-11)

Group

FieldTypeDescriptionDefault
pixels0-512pixels in the group, 0: unused64 (groups 0-5), 0
universe0-32767Art-Net universe0
address1-512first channel in the universe1
orderstringcolour order: rgb, bgr, gbr, grb, rbg. W always comes lastgrb
flipbooleanpixels in reverse orderfalse
brightness0-255cap on the group's levels255
highlightbooleanthe group lights red to find it on the rig. Preview only, never savedfalse

Writing a patch

  • Element i of groups updates group i; its missing fields, and the groups past the end of the array, are unchanged. Send all 16 groups to set a whole patch.
  • A group does not continue on the next universe: pixels past channel 512 stay black (170 RGB or 128 RGBW pixels fit from address 1).
  • More pixels than max_pixels are accepted; the extra ones are not sent.
  • The other outputs are not changed.
Request
curl -X PUT -d '{"color_mode":"rgbw","groups":[{"pixels":128,"universe":4},{"pixels":128,"universe":5},{"pixels":0}]}' \
     http://10.2.1.99/api/outputs/B
Response (shortened)
{"output":"A","color_mode":"rgb","max_pixels":512,"groups":[
  {"pixels":170,"universe":0,"address":1,"order":"grb","flip":false,"brightness":255,"highlight":false},
  {"pixels":170,"universe":1,"address":1,"order":"grb","flip":false,"brightness":255,"highlight":false},
  {"pixels":0,"universe":0,"address":1,"order":"grb","flip":false,"brightness":255,"highlight":false}],
 "revision":479}
POST/api/outputs/{A..D}/preview#

Same body and rules as the PUT, plus highlight, applied to the output at once and not saved: for live patching in an editor, each change sent as it is made. Answers with the output as previewed. A restart, or a PUT of the output, returns to the saved patch.

Request
curl -X POST -d '{"groups":[{},{"highlight":true}]}' http://10.2.1.99/api/outputs/A/preview
GETPUT/api/presets#

In standalone mode the pixels show one of the node's presets: a colour, or an effect between two colours. Up to 12 presets; the first is always there and always a single colour.

FieldTypeDescriptionDefault
active0-11the default preset, shown at start: an index of presets (also /api/presets/default)0
presetsarray1 to 12 presets, below. A PUT with it replaces the list; element i starts from preset i (a new one from the first), and its missing fields keep their value6 factory presets

Preset

FieldTypeDescription
namestringup to 15 characters
profilestringrgb (one colour) or shape (an effect between two colours). The first preset is always rgb
dimmer0-255overall level
colorhexrgb: "rrggbbww" (or "rrggbb"); W drives the white LED of RGBW outputs
bg, fghexshape: background and effect colours, as color
effectstringshape: sine, gauss, triangle, tan, square, random, twinkle, fire, rainbow, noise, bounce, lightning, drops, segment-flash, glitter, ocean, rain, plasma, none
speed, scale, fx_dimmer0-255shape: effect speed, size and level

Factory presets: Solid colour (white), Fire, Ocean, Rainbow, Starry night, Plasma.

Response (shortened)
{"active":0,"presets":[{"name":"Solid colour","profile":"rgb","dimmer":255,"color":"ffffff00"},
  {"name":"Fire","profile":"shape","dimmer":255,"bg":"46000000","fg":"ff820000","effect":"fire","speed":150,
   "scale":60,"fx_dimmer":255}],"revision":479}
GETPUT/api/presets/active#

The preset shown now, for show control: changed with a crossfade, not saved. After a restart the node shows its default. In a mode other than standalone, the choice is kept for when the node returns to it.

FieldTypeDescription
presetintegerindex in /api/presets. Required in a PUT
fade_ms0-600000crossfade from what the outputs show now; 0 or absent: at once
groupbooleanPUT: show it on every node of the zone, this one included, each with its own preset at that index. Default false
savebooleanPUT: also make it the default (a flash write). Default false. Prefer /api/presets/default
name, fading, saved, previewingread onlyits name, a crossfade running, it is the default, a preview is shown instead
Request
curl -X PUT -d '{"preset":2,"fade_ms":4000}' http://10.2.1.99/api/presets/active
Response
{"preset":2,"name":"Ocean","fading":true,"saved":false,"previewing":false}
GETPUT/api/presets/default#

The default preset, shown when the node starts. Setting it saves it (one flash write, none if it already is the default) and does not change the preset shown now.

FieldTypeDescription
presetintegerindex in /api/presets. Required in a PUT
nameread onlyits name
Request
curl -X PUT -d '{"preset":3}' http://10.2.1.99/api/presets/default
Response
{"preset":3,"name":"Rainbow"}
POST/api/presets/preview#

Shows a preset being edited instead of the preset shown, without saving it. {"preset": {...}} takes the fields of a preset; the ones left out come from the previous preview, or from the preset shown. The preview ends with {"stop": true}, when a preset is shown, or 10 s after the last preview: an editor resends it every few seconds while it stays open. Answers like GET /api/presets/active.

Request
curl -X POST -d '{"preset":{"effect":"plasma","speed":200}}' http://10.2.1.99/api/presets/preview
GETPUT/api/blackout#

Every output dark, pixels and DMX, whatever the input. Not saved: a restart releases it. Inputs are still read and effects keep running, so a release shows the outputs as they are now.

FieldTypeDescription
onbooleandark. Required in a PUT
fade_ms0-600000fade to black (or back) over that time; 0 or absent: at once. A new PUT during a fade starts from the level reached
levelread onlyoutput level now, 255 (as the input) to 0 (black)
Request
curl -X PUT -d '{"on":true,"fade_ms":3000}' http://10.2.1.99/api/blackout
Response
{"on":true,"level":255,"fade_ms":3000}
GETPUT/api/config#

The whole configuration in one document, to back up a node or copy it onto another. Its sections are those of their own routes: name, network, mode, dmx, display, timing, presets, and outputs (A to D, as /api/outputs/{X}). About 7 KB.

  • GET: one revision at the top; read-only and preview fields are left out.
  • PUT: up to 32 KB. Each section is checked like its own PUT, then everything is saved once and applied: all or nothing. A section left out keeps its settings, so a document with only mode and outputs changes only those. Members other than the sections are ignored.
  • With revision, the import is refused (409) if the node was changed since. Leave it out to restore a backup.
  • Answers {"ok":true,"revision":152}. An error names its section: "field":"outputs[1].groups[0].pixels". A new network address applies once the answer is sent.
Response (shortened)
{"api":1,"model":"pxnode 4","firmware":"0.1.8","revision":151,
 "name":{"name":"Stage left"},
 "network":{"ip":"10.2.1.99","mask":"255.255.255.0","gateway":"10.2.1.1","zone":""},
 "mode":{...},"dmx":{...},"display":{...},"timing":{...},"presets":{"active":0,"presets":[...]},
 "outputs":[{"output":"A","color_mode":"rgb","groups":[{"pixels":170,"universe":0,"address":1,...}]}]}
Back up a node, copy it onto another
curl http://10.2.1.99/api/config > stage-left.json
curl -X PUT --data-binary @stage-left.json http://10.2.1.100/api/config
The file carries the network section: the node at 10.2.1.100 moves to the address in it. Remove network (and name) from the file to copy everything else.
POST/api/actions/identify#

Shows an identify screen on the node and lights its identify LED, to find it on the rig. Answers {"ok":true}.

POST/api/actions/reboot#

Restarts the node about 200 ms after answering {"ok":true}.

POST/api/actions/recovery#

Restarts into recovery: the node stays at the same address and serves a page to install a firmware. The outputs stay dark until it restarts. Needs bootloader 1.0 or later (bootloader in /api/node); 409 unsupported otherwise.

POST/api/actions/factory-reset#

Restores factory settings, then restarts. Without a body: everything, the name included (address 10.0.0.99/24, no zone, no password, DMX shape mode, default patch and presets). With {"sections": [...]}, only those parts:

SectionResets
networkIP, mask, gateway, zone and password
modemode, input, control channels, DMX output
patchgroups and colour modes of the outputs
advanced/api/timing, the display and its lock, the broadcast filter
presetsthe factory presets
Request
curl -X POST -d '{"sections":["patch","presets"]}' http://10.2.1.99/api/actions/factory-reset
POST/api/firmware#

Installs a firmware. The body is the .pxfw file of a release, sent as is with its Content-Length. The node writes it next to the firmware it runs, checks it, answers {"ok":true,"slot":"A"} and restarts on it. During the write the outputs freeze for a few seconds.

If the new firmware fails to start three times, the node goes back to the previous one by itself. A file that is not a valid firmware for the node is refused with 400 firmware and a message, and nothing changes.

Request
curl --data-binary @pxnode-2.0.0.pxfw -H 'Content-Type: application/octet-stream' http://10.2.1.99/api/firmware
Response
{"ok":true,"slot":"A"}

Recipes

# who is this node
curl http://10.2.1.99/api/node

# rename it: only this field changes
curl -X PUT -d '{"name":"Stage left"}' http://10.2.1.99/api/name

# show preset 2 with a 2 s crossfade on every node of the zone
curl -X PUT -d '{"preset":2,"fade_ms":2000,"group":true}' http://10.2.1.99/api/presets/active

# fade everything to black over 3 s, then back
curl -X PUT -d '{"on":true,"fade_ms":3000}' http://10.2.1.99/api/blackout
curl -X PUT -d '{"on":false,"fade_ms":3000}' http://10.2.1.99/api/blackout

# a write based on revision 10: refused if someone saved since
curl -X PUT -d '{"name":"x","revision":10}' http://10.2.1.99/api/name
{"error":"conflict","message":"the configuration changed since this revision"}

# an invalid value: nothing is changed
curl -X PUT -d '{"groups":[{"pixels":600}]}' http://10.2.1.99/api/outputs/A
{"error":"invalid","field":"groups[0].pixels","message":"integer from 0 to 512"}

# a node with a password
curl -u ':stage left' http://10.2.1.99/api/stats