use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ados_protocol::frame::{decode_len, FrameError, HEADER_SIZE, PLUGIN_MAX_FRAME};
use ados_protocol::node_info::NodeInfo;
use ados_protocol::plugin::{
CapabilityToken, Envelope, PROTOCOL_VERSION, TELEMETRY_STATE_TOPIC, TOKEN_REFRESH_METHOD,
};
use ados_protocol::vision_rpc;
use rmpv::Value;
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::UnixStream;
use tokio::sync::{oneshot, watch, Mutex as AsyncMutex};
use tokio::task::JoinHandle;
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const RECORD_REPLY_SLACK: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OffloadAdvertisement {
pub paired: bool,
pub bearer_acceptable: bool,
pub target: Option<String>,
pub device_id: Option<String>,
pub model_id: Option<String>,
}
pub type EventCallback = Arc<dyn Fn(Value) + Send + Sync>;
#[derive(Debug, Error)]
pub enum ClientError {
#[error("ipc client not connected")]
NotConnected,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("framing error: {0}")]
Frame(#[from] FrameError),
#[error("envelope codec error: {0}")]
Envelope(#[from] ados_protocol::plugin::PluginError),
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("connection closed before response")]
ConnectionClosed,
#[error("capability denied: {0}")]
CapabilityDenied(String),
#[error("allowlist violation: {0}")]
AllowlistViolation(String),
#[error("rpc error: {0}")]
Rpc(String),
}
pub(crate) fn vision_args_error(e: vision_rpc::VisionArgsError) -> ClientError {
ClientError::Rpc(format!("vision request encode failed: {e}"))
}
type PendingMap = Arc<Mutex<HashMap<String, oneshot::Sender<Envelope>>>>;
pub type ToolHandler = Arc<
dyn Fn(
String,
Value,
)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, String>> + Send>>
+ Send
+ Sync,
>;
pub const TOOL_INVOKE_METHOD: &str = "tool.invoke";
type CallbackMap = Arc<Mutex<HashMap<String, Vec<EventCallback>>>>;
pub struct PluginIpcClient {
plugin_id: String,
token: Arc<Mutex<String>>,
socket_path: PathBuf,
writer: Arc<AsyncMutex<Option<OwnedWriteHalf>>>,
pending: PendingMap,
event_callbacks: CallbackMap,
mavlink_callbacks: CallbackMap,
vision_callbacks: CallbackMap,
detection_callbacks: CallbackMap,
button_callbacks: CallbackMap,
msp_callbacks: CallbackMap,
aux_callbacks: CallbackMap,
tool_handler: Arc<Mutex<Option<ToolHandler>>>,
reader_task: Mutex<Option<JoinHandle<()>>>,
session_lost: Arc<watch::Sender<bool>>,
next_id: AtomicU64,
request_timeout: Duration,
}
const VISION_ANY_CAMERA: &str = "*";
impl PluginIpcClient {
pub fn new(
plugin_id: impl Into<String>,
token: impl Into<String>,
socket_path: impl AsRef<Path>,
) -> Self {
Self {
plugin_id: plugin_id.into(),
token: Arc::new(Mutex::new(token.into())),
socket_path: socket_path.as_ref().to_path_buf(),
writer: Arc::new(AsyncMutex::new(None)),
pending: Arc::new(Mutex::new(HashMap::new())),
event_callbacks: Arc::new(Mutex::new(HashMap::new())),
mavlink_callbacks: Arc::new(Mutex::new(HashMap::new())),
vision_callbacks: Arc::new(Mutex::new(HashMap::new())),
detection_callbacks: Arc::new(Mutex::new(HashMap::new())),
button_callbacks: Arc::new(Mutex::new(HashMap::new())),
msp_callbacks: Arc::new(Mutex::new(HashMap::new())),
aux_callbacks: Arc::new(Mutex::new(HashMap::new())),
tool_handler: Arc::new(Mutex::new(None)),
reader_task: Mutex::new(None),
session_lost: Arc::new(watch::channel(false).0),
next_id: AtomicU64::new(0),
request_timeout: DEFAULT_REQUEST_TIMEOUT,
}
}
pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = timeout;
self
}
pub fn set_tool_handler(&self, handler: ToolHandler) {
*self.tool_handler.lock().expect("tool handler lock") = Some(handler);
}
pub fn plugin_id(&self) -> &str {
&self.plugin_id
}
pub async fn connect(&self) -> Result<(), ClientError> {
let stream = UnixStream::connect(&self.socket_path).await?;
let (read_half, write_half) = stream.into_split();
{
let mut w = self.writer.lock().await;
*w = Some(write_half);
}
self.session_lost.send_replace(false);
let task = self.spawn_reader_loop(read_half);
{
let mut slot = self.reader_task.lock().expect("reader_task lock");
*slot = Some(task);
}
self.send_request("hello", "", Value::Map(vec![])).await?;
tracing::info!(plugin_id = %self.plugin_id, "plugin ipc client connected");
Ok(())
}
pub async fn close(&self) {
if let Some(task) = self.reader_task.lock().expect("reader_task lock").take() {
task.abort();
}
let mut w = self.writer.lock().await;
*w = None;
}
pub async fn session_lost(&self) {
let mut rx = self.session_lost.subscribe();
let _ = rx.wait_for(|lost| *lost).await;
}
pub async fn ping(&self) -> Result<Value, ClientError> {
Ok(self
.send_request("ping", "", Value::Map(vec![]))
.await?
.args)
}
pub async fn event_publish(&self, topic: &str, payload: Value) -> Result<i64, ClientError> {
let args = Value::Map(vec![
(Value::from("topic"), Value::from(topic)),
(Value::from("payload"), payload),
]);
let resp = self
.send_request("event.publish", "event.publish", args)
.await?;
Ok(map_get_i64(&resp.args, "delivered").unwrap_or(0))
}
pub async fn event_subscribe(
&self,
topic_pattern: &str,
callback: EventCallback,
) -> Result<(), ClientError> {
register_callback(&self.event_callbacks, topic_pattern, callback);
let args = Value::Map(vec![(Value::from("topic"), Value::from(topic_pattern))]);
self.send_request("event.subscribe", "event.subscribe", args)
.await?;
Ok(())
}
}
mod cloud;
mod config;
mod dispatch;
mod mavlink;
mod media;
mod peripheral;
mod transport;
mod vision;
pub use dispatch::driver_kind_capability;
use dispatch::*;
pub use media::WhepAnswer;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_host;