use super::*;
pub(super) async fn read_envelope<R: AsyncReadExt + Unpin>(
reader: &mut R,
) -> Result<Option<Envelope>, ClientError> {
let mut header = [0u8; HEADER_SIZE];
match reader.read_exact(&mut header).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(ClientError::Io(e)),
}
let len = decode_len(header, PLUGIN_MAX_FRAME, true)?;
let mut body = vec![0u8; len];
reader.read_exact(&mut body).await?;
Ok(Some(Envelope::from_msgpack(&body)?))
}
pub(super) fn register_callback(map: &CallbackMap, pattern: &str, callback: EventCallback) {
map.lock()
.expect("callback lock")
.entry(pattern.to_string())
.or_default()
.push(callback);
}
pub(super) fn dispatch_event(map: &CallbackMap, env: &Envelope) {
let Some(topic) = map_get_str(&env.args, "topic") else {
return;
};
invoke_matching(map, topic, &env.args);
}
pub(super) fn adopt_refreshed_token(slot: &Arc<Mutex<String>>, plugin_id: &str, env: &Envelope) {
let Some(wire) = map_get_str(&env.args, "token").filter(|s| !s.is_empty()) else {
tracing::warn!(plugin_id = %plugin_id, "plugin_token_refresh_malformed");
return;
};
let parsed = match CapabilityToken::from_token_string(wire) {
Ok(t) => t,
Err(e) => {
tracing::warn!(
plugin_id = %plugin_id,
error = %e,
"plugin_token_refresh_unparseable"
);
return;
}
};
let mut held = slot.lock().expect("token lock");
let revoked: Vec<String> = CapabilityToken::from_token_string(held.as_str())
.map(|old| {
old.granted_caps
.difference(&parsed.granted_caps)
.cloned()
.collect()
})
.unwrap_or_default();
*held = wire.to_string();
drop(held);
tracing::info!(
plugin_id = %plugin_id,
expires_at = parsed.expires_at,
revoked = ?revoked,
"plugin_token_refreshed"
);
}
pub(super) fn dispatch_mavlink(map: &CallbackMap, env: &Envelope) {
let Some(msg_name) = map_get_str(&env.args, "msg_name") else {
return;
};
invoke_matching(map, msg_name, &env.args);
}
pub(super) fn dispatch_detection(map: &CallbackMap, env: &Envelope) {
let Some(Value::Binary(blob)) = map_get(&env.args, "batch") else {
return;
};
let Ok(batch) = ados_protocol::framebus::DetectionBatch::from_msgpack(&blob) else {
return;
};
let matched: Vec<EventCallback> = {
let guard = map.lock().expect("callback lock");
guard
.iter()
.filter(|(key, _)| {
key.as_str() == batch.camera_id.as_str() || key.as_str() == VISION_ANY_CAMERA
})
.flat_map(|(_, cbs)| cbs.iter().cloned())
.collect()
};
for cb in matched {
cb(env.args.clone());
}
}
pub(super) fn dispatch_button(map: &CallbackMap, env: &Envelope) {
let matched: Vec<EventCallback> = {
let guard = map.lock().expect("callback lock");
guard.values().flat_map(|cbs| cbs.iter().cloned()).collect()
};
for cb in matched {
cb(env.args.clone());
}
}
pub(super) fn dispatch_vision(map: &CallbackMap, env: &Envelope) {
let Some(Value::Binary(blob)) = map_get(&env.args, "descriptor") else {
return;
};
let Ok(descriptor) = ados_protocol::framebus::FrameDescriptor::from_msgpack(&blob) else {
return;
};
let matched: Vec<EventCallback> = {
let guard = map.lock().expect("callback lock");
let mut out = Vec::new();
if let Some(cbs) = guard.get(&descriptor.camera_id) {
out.extend(cbs.iter().cloned());
}
if descriptor.camera_id != VISION_ANY_CAMERA {
if let Some(cbs) = guard.get(VISION_ANY_CAMERA) {
out.extend(cbs.iter().cloned());
}
}
out
};
for cb in matched {
cb(env.args.clone());
}
}
pub(super) fn invoke_matching(map: &CallbackMap, subject: &str, args: &Value) {
let matched: Vec<EventCallback> = {
let guard = map.lock().expect("callback lock");
guard
.iter()
.filter(|(pattern, _)| pattern.as_str() == subject || fnmatch(pattern, subject))
.flat_map(|(_, cbs)| cbs.iter().cloned())
.collect()
};
for cb in matched {
cb(args.clone());
}
}
pub(super) fn map_error_string(err: &str) -> ClientError {
if let Some(rest) = err.strip_prefix("capability_denied:") {
ClientError::CapabilityDenied(rest.trim().to_string())
} else if let Some(rest) = err.strip_prefix("allowlist_violation:") {
ClientError::AllowlistViolation(rest.trim().to_string())
} else if err.contains("not permitted") {
ClientError::CapabilityDenied(err.to_string())
} else {
ClientError::Rpc(err.to_string())
}
}
pub(super) fn map_get(args: &Value, key: &str) -> Option<Value> {
match args {
Value::Map(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.map(|(_, v)| v.clone()),
_ => None,
}
}
pub(super) const MDNS_BROWSE_REPLY_SLACK: Duration = Duration::from_secs(2);
pub(super) fn decode_typed_reply<T: serde::de::DeserializeOwned>(
args: Value,
method: &str,
) -> Result<T, ClientError> {
if let Some(error) = map_get_str(&args, "error") {
return Err(ClientError::Rpc(format!("{error}: {method}")));
}
rmpv::ext::from_value(args)
.map_err(|e| ClientError::Rpc(format!("{method} reply did not decode: {e}")))
}
pub(super) fn map_get_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
match args {
Value::Map(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.and_then(|(_, v)| v.as_str()),
_ => None,
}
}
pub fn driver_kind_capability(kind: &str) -> Option<&'static str> {
match kind {
"camera" => Some("sensor.camera.register"),
"lidar" => Some("sensor.lidar.register"),
"gimbal" | "gps" | "esc" | "payload-actuator" => Some("sensor.payload.register"),
_ => None,
}
}
pub(super) fn map_get_i64(args: &Value, key: &str) -> Option<i64> {
match args {
Value::Map(entries) => entries
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.and_then(|(_, v)| v.as_i64()),
_ => None,
}
}
pub(super) fn fnmatch(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
let (mut pi, mut ti) = (0usize, 0usize);
let (mut star_p, mut star_t): (Option<usize>, usize) = (None, 0);
while ti < t.len() {
if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == '*' {
star_p = Some(pi);
star_t = ti;
pi += 1;
} else if let Some(sp) = star_p {
pi = sp + 1;
star_t += 1;
ti = star_t;
} else {
return false;
}
}
while pi < p.len() && p[pi] == '*' {
pi += 1;
}
pi == p.len()
}