|=====================================| | ArtiPulsarUDP Protocol Version 0.02 | | Written by: Michel van Osenbruggen | | CopyRight 2026 ArtiLED B.V. | |=====================================| Latest change : 05-10-2026 Status : Review draft Device : ArtiLED Pulsar (PLSR-001) Purpose : Bluetooth + 433 MHz RF transmitter for AV equipment. The Pulsar is deliberately "dumb": Control holds all codes and logic and sends raw Bluetooth HID reports and raw RF pulse trains; the Pulsar only transmits them, captures raw RF for learning, and reports state. Controlled by : Control only (the Hub goes through Control) |=====================| | Protocol Definition | |=====================| Protocol : UDP unicast Command port : TBD Control event port : TBD Encoding : UTF-8 JSON; one object per datagram Protocol marker : ArtiPulsarUDP Wire version : 1 Maximum datagram : 1200 bytes, including envelope Oversize code : HTTP; no fragmentation Authentication : token in requests Response/event auth : Session MAC; encoding/key derivation TBD Firewall : firewall_udp=1 -> requests only from controller Use : Fast transmit commands and events. Pairing, host management, learning and captures use ArtiPulsarAPI |================| | Request Fields | |================| protocol : Protocol marker version : 1 kind : request ident : Target device ident boot_id : Current device boot ID session_id : Current control session ID token : Device token domain : bt | rf data : {command,request_id,sequence,...} |=============| | Error Codes | |=============| 1 : Login failed 2 : Invalid token 3 : Incomplete data (no token, no data, no field) 4 : Invalid data (any invalid or unknown field; whole request rejected) 6 : Invalid command (unknown domain or command) 7 : Unauthorized IP (firewall) 10 : Busy; nothing sent 11 : Stale or wrong ident, boot_id or session_id 12 : Not admitted (sequence / request_id); nothing sent 13 : Host not connected; nothing sent Additional codes : TBD |=================| | Response Fields | |=================| kind : response Identity fields : protocol, version, ident, boot_id, session_id Correlation : request_id Result fields : success, error, error_text, execution_time, data Destination : Request source IP/port |====================| | Response Structure | |====================| success : 1 = success, 0 = error error : Numeric error code; 0 = none error_text : Error description execution_time : Request processing time, milliseconds data : Command-specific response fields Response : {"success":1,"error":0,"error_text":"Success", "execution_time":5,"data":{}} |===========| | Admission | |===========| Namespace : Per session; HTTP and UDP share it New request : sequence > highest admitted sequence, request_id not retained -> admitted once Retry, retained : sequence <= highest; same request_id + payload as the retained result -> that result; nothing sent again Retry, evicted : sequence <= highest; no retained result -> error 12, state unknown; nothing sent Other : Any other request -> error 12; nothing sent hold_renew retry : That result; the lease is not extended again Old boot_id/session_id : Error 11 Session close/expiry : Session ends for good; its holds end; its results are dropped unknown : Outcome not known; never a reason to resend |====================| | Bluetooth Commands | |====================| send : request_id, sequence, host_id, report (press, then neutral report after Tap release) hold_start : request_id, sequence, hold_id, host_id, report hold_renew : request_id, sequence, hold_id hold_stop : request_id, sequence, hold_id Hold binding : session_id + host_id + link_id Hold end : hold_stop, expiry, session close/expiry, connect, disconnect, remove, pair start, link loss Release at hold end : Neutral report on the hold's own link only; never on another link; never replayed on reconnect Ended hold : Final; hold_renew/hold_stop -> its final state, nothing sent Overlap : send/hold_start while a send or hold is active -> error 10 Host change mid-tap : connect/disconnect/remove/pair start run after the tap's neutral report Not connected : Report to a host that is not connected -> error 13 |=============| | RF Commands | |=============| send : request_id, sequence, frequency_hz, modulation, format, code, repeats, gap_us RF radio : One operation at a time: send or learn Busy : rf send or learn start while sending or learning -> error 10; no queue Operation end : sent | failed | learned | expired | stopped -> radio idle before the next operation Bluetooth : Independent of RF Capture complete : >= 2 pulses, then silence >= capture_gap_us Capture overflow : More than max_pulses -> failed, reason overflow Incomplete capture : Failed; no capture_id Scope : Fixed-code 433 MHz AV equipment (screens, masking, lifts). Rolling-code devices are not supported. Somfy is not supported (use a Somfy gateway). |==============| | Event Fields | |==============| kind : event Identity fields : protocol, version, ident, boot_id, session_id event_id : Increasing integer per boot event : Event name uptime : Device uptime data : Event-specific fields auth : Session MAC; encoding TBD Destination : Registered Control event listener ACK : event_id; exact envelope TBD bt.paired : pair_id, host_id, profile bt.pair_timeout : pair_id bt.connected : host_id, link_id, rssi bt.disconnected : host_id, link_id, reason bt.signal_poor : host_id, rssi bt.signal_ok : host_id, rssi bt.sent : request_id bt.stopped : request_id, hold_id, reason, released rf.sent : request_id rf.learned : learn_id, capture_id rf.learn_failed : learn_id, reason rf.learn_timeout : learn_id error : request_id, pair_id or learn_id, error details |===========| | Sequences | |===========| Host switch during hold: hold_start (host 1) -> connect host 2 -> neutral report to host 1 -> bt.stopped {"reason":"switched","released":1} -> host 1 disconnected -> host 2 connected -> hold_renew/hold_stop of that hold: state stopped, nothing sent Learn versus send : learn start -> rf send: error 10 -> learn cancel -> rf send -> rf.sent Retry after eviction : rf send (sequence 7) -> sent, reply lost -> result evicted -> same request: error 12, state unknown, nothing sent |===========| | Variables | |===========| name : 1-25 characters; no "|", no control characters controller : IPv4 address; 0.0.0.0 = none boot_id : 8 lowercase hex characters, random per boot uptime : Seconds since boot request_id : 1-32 characters [A-Za-z0-9_-]; unique per session sequence : Integer 1-2147483647, increasing per session ident : Target device ident session_id : Current control session ID host_id : Paired Bluetooth device, 1-based; 0 = none link_id : Bluetooth connection, +1 per connect (per boot) profile : Bluetooth identity the Pulsar presents when pairing: keyboard = HID keyboard + consumer (PS5, Steam) switch_pro = reserved (Nintendo Switch Pro Controller) A profile defines ONLY the identity/descriptor, never key meaning report : One raw HID input report, from Control: {"type":"keyboard","modifiers":m,"keys":[u,...]} m = 0-255 modifier bitmask u = 4-231 (0x04-0xE7, page 0x07), 0-6 distinct {"type":"consumer","usage":u} u = 1-65535 (page 0x0C) {"type":"gamepad",...} = reserved Report per profile : keyboard -> keyboard, consumer; other -> error 4 Neutral report : keyboard {"modifiers":0,"keys":[]}; consumer usage 0 hold_id : Unique held-report ID pair_id : Unique pairing ID frequency_hz : RF carrier, 387000000-464000000 (e.g. 433920000, 433420000); within capabilities modulation : ook; 2fsk reserved format : raw code : JSON array of pulse durations, microseconds, integers, alternating on/off, starting with on, even count, e.g. [350,1050,1050,350] Pulse: min_pulse_us-max_pulse_us (protocol 1-65535) Count: 2-max_pulses (protocol 2-1024) repeats : Additional repeats of code, 0-max_repeats (protocol 0-50) gap_us : Silence between repeats, 0-max_gap_us (protocol 0-1000000) Total TX : total_us = sum(code) x (repeats + 1) + gap_us x repeats total_us <= max_tx_ms x 1000, 64-bit, after the field range checks; max_tx_ms <= 10000 Over -> error 4, nothing sent learn_id : Unique RF learning ID capture_id : Retained RF capture ID capture_gap_us : 10000 timeout_ms : Pairing/learning timeout, milliseconds rssi : Bluetooth signal strength of the active connection, dBm; null = not measurable power_source : poe | adapter | unknown reason : stop | expired | session | switched | disconnected | removed | pairing | link_lost | cancelled | timeout | overflow | error released : 1 = neutral report sent; 0 = link gone, release not confirmed state : accepted | transmitting | sent | stopped | expired | failed | pairing | paired | connecting | connected | disconnected | learning | learned | unknown sent = local transmission completed (tap incl. neutral report), not target action |===================| | Timing and Limits | |===================| Session expiry : 60 s; renew every 20 s Tap release : Neutral report 50 ms after the press Hold expiry : 750 ms; renew every 250 ms; maximum hold 30 s Pairing timeout : Default 60000 ms; maximum 120000 ms Learning timeout : Default 15000 ms; maximum 60000 ms Host switch : connect to another host_id takes seconds; reports to a host that is not connected fail (no hidden queue) Result retention : 128 operations / 60 s; active operations retained Capture retention : 8 captures / 60 s Active transmissions : 1 Bluetooth + 1 RF; no hidden queue UDP initial reply wait : 250 ms; then HTTP result query, no automatic resend Event retransmission : 100 ms and 300 ms after first notification Paired hosts : Maximum TBD; advertised by /api/info RSSI reporting : bt.signal_poor below the "poor" threshold (TBD); bt.signal_ok back above it; once per crossing Code/body limits : Advertised by /api/info |===========| | Discovery | |===========| Protocol : UDP broadcast Port : 50087 Magic : 2k5jIulJ Interval : 30 seconds Data : 2k5jIulJ|ident|type|ip|name