Skip to main content

ados_sdk/client/
config.rs

1//! Config, process, node-info and mDNS calls.
2
3use super::*;
4
5impl PluginIpcClient {
6    /// Read a config key, returning the host's `value` (nil when absent).
7    /// Mirrors `config_get`.
8    pub async fn config_get(&self, key: &str, default: Value) -> Result<Value, ClientError> {
9        let args = Value::Map(vec![
10            (Value::from("key"), Value::from(key)),
11            (Value::from("default"), default),
12        ]);
13        let resp = self.send_request("config.get", "", args).await?;
14        Ok(map_get(&resp.args, "value").unwrap_or(Value::Nil))
15    }
16
17    /// Write a config key in the given scope (`drone` or `global`). Mirrors
18    /// `config_set`.
19    pub async fn config_set(
20        &self,
21        key: &str,
22        value: Value,
23        scope: &str,
24    ) -> Result<Value, ClientError> {
25        let args = Value::Map(vec![
26            (Value::from("key"), Value::from(key)),
27            (Value::from("value"), value),
28            (Value::from("scope"), Value::from(scope)),
29        ]);
30        Ok(self.send_request("config.set", "", args).await?.args)
31    }
32
33    /// Authorize a vendor-binary spawn through the supervisor. The host enforces
34    /// the manifest allowlist and audit-logs the attempt. Mirrors
35    /// `process_spawn`. The required cap is `process.spawn`.
36    pub async fn process_spawn(
37        &self,
38        basename: &str,
39        args: Vec<String>,
40        env: Vec<(String, String)>,
41    ) -> Result<Value, ClientError> {
42        let args_val = Value::Array(args.into_iter().map(Value::from).collect());
43        let env_val = Value::Map(
44            env.into_iter()
45                .map(|(k, v)| (Value::from(k), Value::from(v)))
46                .collect(),
47        );
48        let payload = Value::Map(vec![
49            (Value::from("basename"), Value::from(basename)),
50            (Value::from("args"), args_val),
51            (Value::from("env"), env_val),
52        ]);
53        Ok(self
54            .send_request("process.spawn", "process.spawn", payload)
55            .await?
56            .args)
57    }
58}
59
60impl PluginIpcClient {
61    /// Read this node's facts: profile, board (with NPU presence), the
62    /// ground-station role, camera readiness and the main stream's geometry.
63    /// Each absent source is `None`, never a guess. Gated on `node.info.read`.
64    ///
65    /// A host that predates the method answers `not_implemented`, surfaced as
66    /// [`ClientError::Rpc`]`("not_implemented: node.info")`.
67    pub async fn node_info(&self) -> Result<NodeInfo, ClientError> {
68        use ados_protocol::node_info::{CAPABILITY, METHOD};
69        let args = self
70            .send_request(METHOD, CAPABILITY, Value::Map(Vec::new()))
71            .await?
72            .args;
73        if let Some(error) = map_get_str(&args, "error") {
74            return Err(ClientError::Rpc(format!("{error}: {METHOD}")));
75        }
76        rmpv::ext::from_value(args)
77            .map_err(|e| ClientError::Rpc(format!("{METHOD} reply did not decode: {e}")))
78    }
79
80    /// Publish one of this plugin's listen ports on the local network over
81    /// mDNS, as `service_type` (`_<name>._tcp`) with `txt`.
82    ///
83    /// The host runs the responder: a sandboxed plugin cannot bind the
84    /// multicast port. `port` must be a listen port the plugin declares for a
85    /// service that runs on this node, and the agent's own service types
86    /// (`_ados._tcp`, `_ados-receiver._tcp`) are refused. The SRV target is the
87    /// system hostname. The record lives as long as this connection: the host
88    /// withdraws it when the connection ends, and calling again with the same
89    /// service type replaces it (a TXT update). Gated on `network.listen`.
90    pub async fn mdns_advertise(
91        &self,
92        service_type: &str,
93        port: u16,
94        txt: &std::collections::BTreeMap<String, String>,
95    ) -> Result<ados_protocol::plugin_mdns::Advertised, ClientError> {
96        use ados_protocol::plugin_mdns::{ADVERTISE_CAPABILITY, ADVERTISE_METHOD};
97        let txt = txt
98            .iter()
99            .map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
100            .collect();
101        let args = Value::Map(vec![
102            (Value::from("service_type"), Value::from(service_type)),
103            (Value::from("port"), Value::from(port)),
104            (Value::from("txt"), Value::Map(txt)),
105        ]);
106        let args = self
107            .send_request(ADVERTISE_METHOD, ADVERTISE_CAPABILITY, args)
108            .await?
109            .args;
110        decode_typed_reply(args, ADVERTISE_METHOD)
111    }
112
113    /// Every instance of `service_type` (`_<name>._tcp` or `_<name>._udp`)
114    /// that answers on the local network within `window` (clamped by the host
115    /// to 0.1-5 s). The call returns once the window has closed. Gated on
116    /// `network.outbound`.
117    pub async fn mdns_browse(
118        &self,
119        service_type: &str,
120        window: Duration,
121    ) -> Result<Vec<ados_protocol::plugin_mdns::DiscoveredService>, ClientError> {
122        use ados_protocol::plugin_mdns::{
123            BrowseReply, BROWSE_CAPABILITY, BROWSE_MAX_TIMEOUT_MS, BROWSE_METHOD,
124        };
125        let window_ms = u32::try_from(window.as_millis()).unwrap_or(u32::MAX);
126        let args = Value::Map(vec![
127            (Value::from("service_type"), Value::from(service_type)),
128            (Value::from("timeout_ms"), Value::from(window_ms)),
129        ]);
130        // The host answers after its window, so the wait must outlast the
131        // longest one it will run.
132        let timeout = self.request_timeout.max(
133            Duration::from_millis(u64::from(window_ms.min(BROWSE_MAX_TIMEOUT_MS)))
134                + MDNS_BROWSE_REPLY_SLACK,
135        );
136        let args = self
137            .send_request_within(BROWSE_METHOD, BROWSE_CAPABILITY, args, timeout)
138            .await?
139            .args;
140        decode_typed_reply::<BrowseReply>(args, BROWSE_METHOD).map(|r| r.services)
141    }
142
143    // Internals
144}