Skip to content

Hardware integration

This guide explains how to integrate the Helm Protocol (SLP) into hardware devices such as TimeKeeper, CueKeeper, and CueMaster.

  1. Architecture Overview
  2. Hardware Requirements
  3. SDK Integration
  4. Timecode Implementation
  5. Display Integration
  6. Network Configuration
  7. Testing
  8. Troubleshooting

DeviceRolePrimary Function
TimeKeeperClientTimecode display/generation
CueKeeperClientCue list display
CueMasterMaster/ClientStandalone host or client
┌─────────────────┐
│ Session Master │
│ (Cues/CueMaster)│
└────────┬────────┘
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│TimeKeeper│ │CueKeeper │ │CueKeeper │
└──────────┘ └──────────┘ └──────────┘

ComponentRequirement
CPUARM Cortex-A53 or better
RAM512MB minimum, 1GB recommended
Storage4GB for OS and SDK
Network100Mbps Ethernet, WiFi optional
OSLinux (Raspberry Pi OS, Buildroot)

Raspberry Pi Compute Module 4 with:

  • Custom carrier board
  • Industrial-grade eMMC
  • Gigabit Ethernet
  • USB-C power

For TimeKeeper/CueMaster:

InterfaceChipPurpose
Audio ADCPCM1808LTC input
Audio DACPCM5102LTC output
MIDIUART + optoisolatorMTC I/O

Add to your Cargo.toml:

[dependencies]
slp-client = { path = "../helm-sdk/slp-client" }
slp-core = { path = "../helm-sdk/slp-core" }
tokio = { version = "1", features = ["full"] }
  1. Build the FFI library:

    Terminal window
    cd helm-sdk
    cargo build --release -p slp-ffi
  2. Link against libslp_ffi.so (or .a for static)

  3. Include the header:

    #include "helm-slp.h"
use slp_client::{SlpClient, ClientConfig};
use slp_core::{DeviceType, Role, TimecodePacket};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure device
let config = ClientConfig {
device_name: "Studio TimeKeeper".to_string(),
device_type: DeviceType::TimeKeeper,
requested_role: Role::Hardware,
version: env!("CARGO_PKG_VERSION").to_string(),
};
// Create client
let client = SlpClient::new(config)?;
// Start discovery
client.start_discovery().await?;
// Wait for sessions
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// List discovered sessions
for session in client.discovered_sessions().await {
println!("Found: {} at {}", session.name, session.address);
}
// Connect to first session
if let Some(session) = client.discovered_sessions().await.first() {
client.connect(&session.address).await?;
// Listen for timecode
let mut tc_rx = client.subscribe_timecode();
while let Ok(packet) = tc_rx.recv().await {
update_display(&packet);
}
}
Ok(())
}
fn update_display(packet: &TimecodePacket) {
println!("TC: {}", packet.to_timecode_string());
}
#include "helm-slp.h"
#include <stdio.h>
void on_timecode(const slp_timecode_packet_t* packet, void* userdata) {
char buffer[16];
slp_timecode_to_string(packet, buffer, sizeof(buffer));
printf("TC: %s\n", buffer);
}
int main() {
SlpClientHandle* client = NULL;
// Create client
slp_result_t result = slp_client_create(
"Studio TimeKeeper",
SLP_DEVICE_TYPE_TIME_KEEPER,
&client
);
if (result != SLP_RESULT_OK) {
fprintf(stderr, "Failed to create client\n");
return 1;
}
// Start discovery
slp_start_discovery(client);
// Connect (example address)
result = slp_connect(client, "192.168.1.100:5353");
if (result != SLP_RESULT_OK) {
fprintf(stderr, "Failed to connect\n");
slp_client_destroy(client);
return 1;
}
// Main loop
while (slp_is_connected(client)) {
// Process events...
sleep(1);
}
slp_client_destroy(client);
return 0;
}

Timecode packets arrive via QUIC datagrams at the frame rate (e.g., 30Hz for 30fps).

Key Fields:

  • position_frames: Absolute frame count from 00:00:00:00
  • frame_rate: Encoded frame rate (see spec)
  • state: Playback state (Stopped, Playing, Paused, etc.)

Converting to Display Format:

fn frames_to_timecode(frames: u64, fps: u8) -> String {
let fps = fps as u64;
let hours = frames / (fps * 3600);
let remaining = frames % (fps * 3600);
let minutes = remaining / (fps * 60);
let remaining = remaining % (fps * 60);
let seconds = remaining / fps;
let frame = remaining % fps;
format!("{:02}:{:02}:{:02}:{:02}", hours, minutes, seconds, frame)
}

For devices that can inject timecode:

// From LTC decoder
fn on_ltc_frame(hours: u8, mins: u8, secs: u8, frames: u8, fps: u8) {
let packet = TimecodePacket::from_timecode(hours, mins, secs, frames, fps);
// Modify source to indicate LTC input
let packet = TimecodePacket {
source: TimecodeSource::LtcInput,
..packet
};
client.send_timecode(packet).await?;
}

For reading LTC from audio input:

// Use a library like `ltc` crate
use ltc::LTCDecoder;
fn process_audio_buffer(buffer: &[i16], sample_rate: u32) {
let mut decoder = LTCDecoder::new(sample_rate);
for frame in decoder.decode(buffer) {
let tc = frame.timecode();
// Convert and send
}
}

The CueKeeper supports multiple display modes:

Shows upcoming and previous cues with current highlighted.

┌─────────────────────────────────┐
│ ← PREV: Light cue 5 │
├─────────────────────────────────┤
│ ▶ CURRENT: Light cue 6 │
│ 01:23:45:15 │
├─────────────────────────────────┤
│ → NEXT: Light cue 7 │
│ in 00:00:15:00 │
├─────────────────────────────────┤
│ Light cue 8 │
│ Light cue 9 │
└─────────────────────────────────┘

Large countdown to next cue.

┌─────────────────────────────────┐
│ │
│ ▶ Light Cue 6 │
│ │
│ 00:15:22 │
│ │
│ NEXT: Light Cue 7 │
│ │
└─────────────────────────────────┘

Shows current timecode in large format.

┌─────────────────────────────────┐
│ │
│ │
│ 01:23:45:15 │
│ │
│ ▶ PLAYING │
│ │
└─────────────────────────────────┘
ElementUpdate RateNotes
Timecode30HzMatch frame rate
Countdown10HzSmooth animation
Cue listOn changeEvent-driven
Status1HzBattery, network, etc.

PortProtocolPurpose
5353QUIC/UDPSLP connections
5354UDPDiscovery broadcast

For Linux/iptables:

Terminal window
# Allow SLP connections
iptables -A INPUT -p udp --dport 5353 -j ACCEPT
iptables -A INPUT -p udp --dport 5354 -j ACCEPT
# Allow mDNS
iptables -A INPUT -p udp --dport 5353 -d 224.0.0.251 -j ACCEPT

For production use, devices should use static IPs:

/etc/dhcpcd.conf:
interface eth0
static ip_address=192.168.1.100/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

Run SDK tests:

Terminal window
cd helm-sdk
cargo test
  1. Discovery Test
    • Start Helm Cues as master
    • Verify device discovers session
    • Verify mDNS and UDP broadcast both work
  2. Connection Test
    • Connect to session
    • Verify token received
    • Verify show sync received
  3. Timecode Test
    • Start playback on master
    • Measure latency on device
    • Verify <1ms end-to-end
  4. Stress Test
    • Connect 50+ devices
    • Run for 24 hours
    • Monitor memory, CPU, packet loss
Terminal window
# Monitor network traffic
tcpdump -i eth0 port 5353 or port 5354
# Test discovery
avahi-browse -a
# Measure latency
ping <master-ip>

  1. Check mDNS is working: avahi-browse -a
  2. Check UDP broadcast enabled
  3. Verify same subnet
  4. Check firewall rules
  1. Check network congestion
  2. Disable WiFi, use Ethernet
  3. Check CPU usage (should be <50%)
  4. Verify QoS not throttling UDP
  1. Check heartbeat interval (5 seconds)
  2. Verify network stability
  3. Check master logs for errors
  4. Verify session token valid

Enable verbose logging:

tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();

For hardware integration support: