Skip to main content

seyd_core/
control.rs

1// Copyright 2026 Anton Gravestam
2// SPDX-License-Identifier: Apache-2.0
3//! Control-stream vocabulary (docs/protocol/control-stream.md). NDJSON.
4
5use crate::channels::ChannelSpec;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize)]
9#[serde(tag = "type", rename_all = "kebab-case")]
10pub enum FromPilot {
11    Hello {
12        #[serde(default)]
13        proto: u8,
14        #[serde(default)]
15        session_id: String,
16        #[serde(default)]
17        client: serde_json::Value,
18        #[serde(default)]
19        token: Option<String>,
20    },
21    Ping {
22        t1: u64,
23    },
24    Loss {
25        ch: u8,
26        frame_id: u16,
27        #[serde(default)]
28        key: bool,
29    },
30    RequestKeyframe {
31        ch: u8,
32    },
33    SetQos {
34        profile: String,
35    },
36    PilotStats(serde_json::Value),
37    Bye,
38    #[serde(other)]
39    Unknown,
40}
41
42#[derive(Debug, Clone, Serialize)]
43#[serde(tag = "type", rename_all = "kebab-case")]
44pub enum ToPilot<'a> {
45    Welcome {
46        session_id: &'a str,
47        role: &'a str,
48        channels: &'a [ChannelSpec],
49        qos: serde_json::Value,
50        t_agent_us: u64,
51    },
52    Denied {
53        reason: &'a str,
54    },
55    Pong {
56        t1: u64,
57        t2: u64,
58    },
59    QosAck {
60        profile: &'a str,
61        qos: serde_json::Value,
62        publisher: &'a str,
63    },
64    AgentStats(serde_json::Value),
65}
66
67pub fn encode_line<T: Serialize>(msg: &T) -> String {
68    let mut s = serde_json::to_string(msg).expect("control messages are serialisable");
69    s.push('\n');
70    s
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn parses_hello_and_unknown() {
79        let m: FromPilot = serde_json::from_str(
80            r#"{"type":"hello","proto":2,"session_id":"abc","client":{"kind":"browser"}}"#,
81        )
82        .unwrap();
83        assert!(matches!(m, FromPilot::Hello { proto: 2, .. }));
84        let m: FromPilot = serde_json::from_str(r#"{"type":"whatever"}"#).unwrap();
85        assert!(matches!(m, FromPilot::Unknown));
86        let m: FromPilot =
87            serde_json::from_str(r#"{"type":"loss","ch":1,"frame_id":9,"key":true}"#).unwrap();
88        assert!(matches!(
89            m,
90            FromPilot::Loss {
91                ch: 1,
92                frame_id: 9,
93                key: true
94            }
95        ));
96    }
97
98    #[test]
99    fn welcome_line() {
100        let line = encode_line(&ToPilot::Welcome {
101            session_id: "s",
102            role: "driver",
103            channels: &[],
104            qos: seyd_qos::BALANCED.pilot_config(),
105            t_agent_us: 1,
106        });
107        assert!(line.starts_with(r#"{"type":"welcome""#) && line.ends_with('\n'));
108    }
109}