Skip to content

Protocol

Version: 1.0Date: January 2026Status: Draft


  1. Introduction
  2. Protocol Architecture
  3. Discovery Mechanism
  4. Session Lifecycle
  5. Timecode Streaming
  6. Control Commands
  7. Sync Protocol
  8. Device Types
  9. Security
  10. Error Handling
  11. Appendix

The Helm Protocol (SLP) is a communication protocol designed for real-time synchronization in live production environments. It enables ultra-low latency timecode distribution, show data synchronization, and device management across the Helm product family.

GoalTargetRationale
Timecode Latency<1msRequired for frame-accurate sync
Reliability99.99% uptimeLive production critical
Scalability100+ devicesLarge production requirements
SecuritySession-based authPrevent unauthorized control
DiscoverabilityZero-configEase of setup

This specification covers:

  • Wire protocol format
  • Transport layer requirements
  • Session management
  • Device discovery
  • Timecode streaming
  • Show data synchronization

Out of scope:

  • VOIP/talkback audio (future extension)
  • Video streaming
  • Cloud connectivity

SLP uses QUIC (RFC 9000) as its transport protocol, providing:

  • Connection migration
  • Multiplexed streams
  • Built-in TLS 1.3
  • Unreliable datagrams (RFC 9221)

Implementation: quinn crate v0.11+

ChannelQUIC FeatureReliabilityUse Case
TimecodeDatagramUnreliableUltra-low latency TC
ControlUnidirectional StreamReliableCommands, config
SyncBidirectional StreamReliableShow data
HeartbeatDatagramUnreliableKeep-alive

All messages follow this binary format:

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic (0x534C) | Version | Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags | Payload Length | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
+ Payload (MessagePack) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Header Fields (7 bytes):

FieldOffsetSizeDescription
Magic020x534C (“SL”)
Version21Protocol version (currently 0x01)
Type31Message type enum
Flags41Message flags
Length52Payload length (big-endian)

Maximum payload size: 65,536 bytes

ValueTypeDirectionChannel
0x01TimecodeBothDatagram
0x02HeartbeatClient→MasterDatagram
0x03HeartbeatAckMaster→ClientDatagram
0x10JoinRequestClient→MasterStream
0x11JoinResponseMaster→ClientStream
0x12LeaveBothStream
0x20ControlMaster→ClientStream
0x21ControlResponseClient→MasterStream
0x30ShowSyncMaster→ClientStream
0x31TrackUpdateMaster→ClientStream
0x32CueUpdateMaster→ClientStream
0x33ActiveTrackChangeBothStream
0x34PlaybackStateChangeMaster→ClientStream
0x40PermissionUpdateMaster→ClientStream
0x50DiscoveryBroadcastAnyUDP
0x51DiscoveryResponseAnyUDP
0xFFErrorBothStream
BitFlagDescription
0COMPRESSEDPayload is zstd compressed
1PRIORITYHigh priority, skip queue
2REQUIRES_ACKSender expects acknowledgment
3IS_ACKThis is an acknowledgment
4-7ReservedMust be zero

Payloads are encoded using MessagePack for compact binary serialization. This provides:

  • Smaller size than JSON
  • Faster parsing
  • Schema flexibility
  • Cross-language support

SLP uses mDNS/DNS-SD for zero-configuration discovery.

Service Type: _showlink._udp.local.

TXT Records:

KeyValueDescription
device_idUUIDUnique device identifier
device_typeStringDevice type enum name
nameStringHuman-readable name
clientsIntegerConnected client count
versionStringSLP version

Example:

_showlink._udp.local. PTR cuemaster-abc123._showlink._udp.local.
cuemaster-abc123._showlink._udp.local. SRV 0 0 5353 cuemaster-abc123.local.
cuemaster-abc123._showlink._udp.local. TXT "device_id=abc123..." "device_type=CueMaster" "name=Main Console" "clients=5"

For networks where mDNS doesn’t work well:

Port: 5354

Broadcast Packet:

struct DiscoveryBroadcast {
device_id: UUID,
device_type: DeviceType,
device_name: String,
is_hosting: bool,
session_name: Option<String>,
client_count: Option<u32>,
}

Interval: Every 5 seconds while hosting

Devices may also connect via direct IP address input, bypassing discovery.


  • One device acts as Master (Helm Cues or CueMaster)
  • Other devices connect as Clients
  • Master controls session state and permissions
  • Master distributes show data and timecode
Client Master
| |
|------- JoinRequest --------------->|
| | (validate)
|<------ JoinResponse ---------------|
| |
|<------ ShowSync -------------------|
| |
|-------- Heartbeat ---------------->|
|<------- HeartbeatAck --------------|
| ... |
struct JoinRequest {
device_id: UUID,
device_type: DeviceType,
device_name: String,
version: String,
requested_role: Role,
}
struct JoinResponse {
accepted: bool,
session_token: Option<String>, // 32-char random string
assigned_role: Role,
session_id: UUID,
master_name: String,
rejection_reason: Option<String>,
}
  • 32 alphanumeric characters
  • Included in all subsequent requests
  • Validated by master on every message
  • Revoked on disconnect or kick

Interval: Every 5 seconds

Timeout: 15 seconds (3 missed heartbeats)

struct Heartbeat {
sequence: u32,
}
struct HeartbeatAck {
sequence: u32,
}
struct Leave {
reason: String,
}

RequirementTarget
End-to-end latency<1ms
Packet rateFrame rate (e.g., 30Hz)
Packet loss toleranceUp to 10%
Jitter tolerance±5ms

Optimized for minimal size (26 bytes):

struct TimecodePacket {
timestamp_us: u64, // Sender timestamp (µs)
position_frames: u64, // Absolute frame position
frame_rate: u8, // FrameRate enum
state: u8, // PlaybackState enum
source: u8, // TimecodeSource enum
reserved: u8, // Must be zero
track_id: u32, // Active track ID
sequence: u16, // Packet sequence number
}

Binary Layout:

Offset | Size | Field
-------|------|----------------
0 | 8 | timestamp_us (BE)
8 | 8 | position_frames (BE)
16 | 1 | frame_rate
17 | 1 | state
18 | 1 | source
19 | 1 | reserved
20 | 4 | track_id (BE)
24 | 2 | sequence (BE)
ValueFrame Rate
023.976 fps
124 fps
225 fps
329.97 drop-frame
429.97 non-drop
530 fps
650 fps
759.94 fps
860 fps
ValueState
0Stopped
1Playing
2Paused
3Seeking
4Cueing
ValueSource
0Internal (master generated)
1LTC Input
2MTC Input
3Network

Master → Clients: Broadcast current playback position

Client → Master: TimeKeeper/CueMaster can inject external TC:

  • LTC received from audio input
  • MTC received from MIDI input
  • Requires InjectTimecode permission

Clients calculate round-trip time using:

RTT = current_time_us - packet.timestamp_us

For accurate sync, clients should compensate for half RTT.


Control commands are reliable messages sent over QUIC streams. They require appropriate permissions.

Flash LED or show message on device screen.

Identify {
device_id: UUID,
message: Option<String>,
duration_ms: u32,
}

Configure device timecode source.

SetTimecodeSource {
device_id: UUID,
config: TimecodeSourceConfig,
}
struct TimecodeSourceConfig {
primary: TimecodeInputSource,
fallback: Option<TimecodeInputSource>,
passthrough_output: bool,
frame_rate: u8,
}

Configure display mode.

SetDisplayMode {
device_id: UUID,
mode: DisplayMode,
}
enum DisplayMode {
CueList { show_upcoming: u8, show_previous: u8 },
Countdown { show_cue_name: bool },
TimecodeOnly { show_track_name: bool },
Message { headline: String, body: Option<String> },
Split { position: SplitPosition, percent: u8 },
Off,
}

Request device status.

GetStatus { device_id: UUID }

Response:

Status {
device_id: UUID,
uptime_secs: u64,
cpu_percent: u8,
memory_percent: u8,
temperature_c: Option<f32>,
network_status: NetworkStatus,
timecode_source: TimecodeInputSource,
timecode_valid: bool,
firmware_version: String,
errors: Vec<String>,
}

Measure round-trip latency.

Ping { device_id: UUID, timestamp_us: u64 }
Pong {
device_id: UUID,
original_timestamp_us: u64,
response_timestamp_us: u64,
}

Restart device.

Reboot { device_id: UUID, delay_ms: u32 }

On join, master sends complete show state:

struct ShowSync {
show_id: UUID,
show_name: String,
version: u64,
frame_rate: String,
tracks: Vec<SyncTrack>,
active_track_id: Option<UUID>,
metadata: ShowMetadata,
}
struct SyncTrack {
id: UUID,
name: String,
order: u32,
duration_seconds: f64,
offset_seconds: f64,
cues: Vec<SyncCue>,
cue_stacks: Vec<SyncCueStack>,
enabled: bool,
}
struct SyncCue {
id: UUID,
number: String,
name: String,
time_seconds: f64,
cue_type: String,
sector: String,
notes: Option<String>,
outputs: Vec<SyncCueOutput>,
}

After initial sync, only changes are sent:

struct TrackUpdate {
track_id: UUID,
operation: UpdateOperation,
track: Option<SyncTrack>,
version: u64,
}
struct CueUpdate {
track_id: UUID,
cue_id: UUID,
operation: UpdateOperation,
cue: Option<SyncCue>,
version: u64,
}
enum UpdateOperation {
Add,
Modify,
Delete,
Reorder,
}

Each update increments the show version. Clients track version to detect missed updates. If version gap detected, client requests full resync.

Master wins. Client changes are proposals that master may accept or reject.


Desktop application acting as session master.

Capabilities:

  • Host sessions
  • Full show editing
  • Timecode generation (LTC/MTC)
  • All protocol outputs

Standalone hardware device that can host sessions.

Capabilities:

  • Host sessions without PC
  • Timecode I/O
  • Audio playback
  • Display for cue lists

MIF4-style timecode device.

Capabilities:

  • Timecode display
  • LTC/MTC input/output
  • Audio monitoring
  • Jam sync

Cue display device.

Capabilities:

  • High-resolution display
  • Countdown timers
  • Cue list display
  • Configurable modes

All QUIC connections use TLS 1.3 with:

  • Self-signed certificates (auto-generated)
  • ALPN: slp/1

Token-based authentication:

  • 32-character random token issued on join
  • Included in message headers
  • Validated by master
PermissionObserverOperatorEditorAdminHardwareMaster
ReadShow
EditShow
ControlPlayback
ChangeTrack
ViewDevices
ConfigureDevices
ManageMembers
InjectTimecode
Admin

struct SlpError {
code: u16,
message: String,
related_sequence: Option<u32>,
}
CodeNameDescription
1INVALID_MESSAGEMalformed message
2PERMISSION_DENIEDInsufficient permissions
3SESSION_FULLMaximum clients reached
4SESSION_NOT_FOUNDNo active session
5DEVICE_NOT_FOUNDUnknown device ID
6INVALID_TOKENSession token invalid
7VERSION_MISMATCHProtocol version incompatible
100INTERNAL_ERRORServer-side error

PortProtocolPurpose
5353QUIC/UDPSLP connections
5354UDPDiscovery broadcast

Input:

timestamp_us: 1704067200000000 (2024-01-01 00:00:00 UTC)
position_frames: 108000 (1 hour at 30fps)
frame_rate: 5 (30fps)
state: 1 (Playing)
source: 0 (Internal)
track_id: 42
sequence: 100

Expected bytes (hex):

00 06 0F 4B F4 D9 80 00 <- timestamp_us
00 00 00 00 00 01 A5 E0 <- position_frames
05 01 00 00 <- frame_rate, state, source, reserved
00 00 00 2A <- track_id
00 64 <- sequence

Input:

magic: 0x534C
version: 1
type: Timecode (0x01)
flags: PRIORITY (0x02)
length: 26

Expected bytes (hex):

53 4C 01 01 02 00 1A
  • RFC 9000: QUIC Transport Protocol
  • RFC 9221: QUIC Unreliable Datagrams
  • MessagePack Specification: https://msgpack.org/
  • SMPTE ST 12-1:2014 (Timecode)