indi/
lib.rs

1//! # A general purpose library for interacting with the INDI protocol.
2//! The Instrument Neutral Distributed Interface (INDI for short) protocol is
3//! an XML-like communicatinos protocol used in the astronomical community
4//! to control and monitor astronomical equipment.  For more information on INDI see
5//! the project's website [here](https://indilib.org/).
6//!
7//! The purpose of this crate is to provide a convinent way to interact with devices
8//! using the INDI protocol.  Details on the protocol can be found [here](http://docs.indilib.org/protocol/INDI.pdf).
9//!
10//! ### Simple usage.
11//!
12//! The simpliest way to use this crate is to open a [TcpStream](std::net::TcpStream) and read/write INDI [commands](crate::serialization::Command).
13//! #### Example
14//! ```no_run
15//! use tokio::net::TcpStream;
16//! use tokio_stream::{Stream, StreamExt};
17//! use crate::indi::client::{AsyncClientConnection,AsyncReadConnection,AsyncWriteConnection};
18//! #[tokio::main]
19//! async fn main() {
20//!     // Connect to local INDI server.
21//!     let connection = TcpStream::connect("127.0.0.1:7624").await.expect("Connecting to INDI server");
22//!     let (mut writer, mut reader) = connection.to_indi();
23//!
24//!     // Write command to server instructing it to track all properties.
25//!     writer.write(indi::serialization::Command::GetProperties(indi::serialization::GetProperties {
26//!         version: indi::INDI_PROTOCOL_VERSION.to_string(),
27//!         device: None,
28//!         name: None,
29//!     }))
30//!     .await
31//!     .expect("Sending GetProperties command");
32//!
33//!     // Loop through commands recieved from the INDI server
34//!     loop {
35//!         let command = match reader.read().await {
36//!             Some(command) => command,
37//!             None => break,
38//!         }.unwrap();
39//!         println!("Received from server: {:?}", command);
40//!     }
41//! }
42//! ```
43//!
44//! ### Using the Client interface
45//! The simple usage above has its uses, but if you want to track and modify the state of devices at an INDI server it is recommended to use
46//! the [client interface](crate::client::Client).  The client allows you to get [devices](crate::client::active_device::ActiveDevice),
47//! be [notified](crate::client::notify) of changes to those devices, and request [changes](crate::client::active_device::ActiveDevice::change).
48//! #### Example
49//! ```no_run
50//! use std::time::Duration;
51//! use tokio::net::TcpStream;
52//! use twinkle_client::task::Task;
53//! use twinkle_client::task::Status;
54//! use std::ops::Deref;
55//! use indi::serialization::Sexagesimal;
56//! use tokio_stream::StreamExt;
57//!
58//! #[tokio::main]
59//! async fn main() {
60//!     // Create a client with a connection to localhost listening for all device properties.
61//!     let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
62//!     let client = indi::client::Client::new(Some(tx));
63//!     let connection = TcpStream::connect("127.0.0.1:7624").await.unwrap();
64//!     let _client_task: tokio::task::JoinHandle<()> = tokio::task::spawn(indi::client::start(client.get_devices().clone(), rx, connection));
65//!
66//!     // Get an specific camera device
67//!     let camera = client
68//!         .get_device("ZWO CCD ASI294MM Pro")
69//!         .await
70//!         .expect("Getting camera device");
71//!
72//!     // Setting the 'CONNECTION' parameter to `on` to ensure the indi device is connected.
73//!     let _ = camera
74//!         .change("CONNECTION", vec![("CONNECT", true)])
75//!         .await
76//!         .expect("Connecting to camera");
77//!
78//!     // Enabling blob transport for the camera.
79//!     camera
80//!         .enable_blob(Some("CCD1"), indi::BlobEnable::Also)
81//!         .await
82//!         .expect("Enabling image retrieval");
83//!
84//!     // Subscribing to changes to the CCD parameter so we can get the next Blob
85//!     let ccd = camera.get_parameter("CCD1").await.expect("Getting ccd parameter");
86//!     let mut ccd_sub = ccd.changes();
87//!
88//!     // Configuring a varienty of the camera's properties at the same time.
89//!     let _ = tokio::try_join!(
90//!         camera.change("CCD_CAPTURE_FORMAT", vec![("ASI_IMG_RAW16", true)]),
91//!         camera.change("CCD_TRANSFER_FORMAT", vec![("FORMAT_FITS", true)]),
92//!         camera.change("CCD_CONTROLS", vec![("Offset", Sexagesimal::from(10.0)), ("Gain", Sexagesimal::from(240.0))]),
93//!         camera.change("CCD_BINNING", vec![("HOR_BIN", Sexagesimal::from(2.0)), ("VER_BIN", Sexagesimal::from(2.0))]),
94//!         camera.change("CCD_FRAME_TYPE", vec![("FRAME_FLAT", true)]),
95//!         )
96//!         .expect("Configuring camera");
97//!
98//!     // Set exposure
99//!     let _ = camera.parameter("CCD_EXPOSURE").await.unwrap().change(vec![("CCD_EXPOSURE_VALUE", Sexagesimal::from(5.0))]).await.expect("Setting the exposure");
100//!
101//!     // Get the image
102//!     if let indi::Parameter::BlobVector(blob) =  ccd_sub.next().await.unwrap().unwrap().as_ref() {
103//!         let _image = blob.values.get("CCD1").unwrap();
104//!     }
105//! }
106//! ```
107//!
108//! ### Using the Telescope interface
109//! The client interface is fine if you want generic controll of indi devices, but isn't aware of things like "Camera"s, and only maintains one connection
110//! per client.  This can be a problem when streaming images, as no device updates can be received while an image is in transit.  To solve these problems
111//! you can use the [telescope] module.  It manages separate image and control connections and has type representing various device types.
112//! #### Example
113//! ```no_run
114//!use indi::telescope::Telescope;
115//!use indi::telescope::settings::Settings;
116//!use tokio::net::TcpStream;
117//!use std::time::Duration;
118//!
119//!#[tokio::main]
120//!async fn main() {
121//!    let settings = Settings::default();
122//!
123//!    // Create a Telescope connected to localhost.
124//!    let mut telescope = Telescope::new(settings.telescope_config.clone());
125//!    telescope.connect_from_settings::<TcpStream>(&settings).await;
126//!
127//!    // Get an specific camera device
128//!    let camera = telescope.get_primary_camera().await.expect("Getting camera");
129//!
130//!    // Make sure the camera is connected
131//!    let _ = camera.connect().await.expect("Connecting to camera");
132//!
133//!    let capture_format_param = camera.capture_format().await.expect("Getting capture format");
134//!    let transfer_format_param = camera.transfer_format().await.expect("Getting transfer format");
135//!    let gain_param = camera.gain().await.expect("getting gain");
136//!    let binning_param = camera.binning().await.expect("getting binning");
137//!    let image_type_param = camera.image_type().await.expect("Getting image type");
138//!
139//!    // Configuring a variety of the camera's properties at the same time.
140//!    tokio::try_join!(
141//!        capture_format_param.change(indi::telescope::camera::CaptureFormat::Raw16),
142//!        transfer_format_param.change(indi::telescope::camera::TransferFormat::Fits),
143//!        gain_param.change(120.0),
144//!        binning_param.change(indi::telescope::camera::Binning {hor: 2, ver: 2}),
145//!        image_type_param.change(indi::telescope::camera::ImageType::Light),
146//!    ).expect("Configuring the camera");
147//!
148//!    // Capture an immage
149//!    let _blob = camera.capture_image(Duration::from_secs(500)).await.expect("Capturing an image");
150//!}
151//!```
152
153pub use tokio;
154
155use quick_xml::events::attributes::AttrError;
156use serde::Deserialize;
157use serde::Serialize;
158
159use std::borrow::Cow;
160
161use std::num;
162
163use std::str;
164use std::sync::Arc;
165
166use chrono::format::ParseError;
167use chrono::prelude::*;
168use std::str::FromStr;
169
170use std::collections::HashMap;
171
172pub static INDI_PROTOCOL_VERSION: &str = "1.7";
173
174pub mod serialization;
175use serialization::*;
176
177pub mod client;
178pub mod telescope;
179
180#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
181pub enum PropertyState {
182    Idle,
183    Ok,
184    Busy,
185    Alert,
186}
187
188#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
189pub enum SwitchState {
190    On,
191    Off,
192}
193
194impl From<bool> for SwitchState {
195    fn from(value: bool) -> Self {
196        match value {
197            true => SwitchState::On,
198            false => SwitchState::Off,
199        }
200    }
201}
202#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
203pub enum SwitchRule {
204    OneOfMany,
205    AtMostOne,
206    AnyOfMany,
207}
208
209#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
210pub enum PropertyPerm {
211    #[serde(rename = "ro")]
212    RO,
213    #[serde(rename = "wo")]
214    WO,
215    #[serde(rename = "rw")]
216    RW,
217}
218
219#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
220pub enum BlobEnable {
221    Never,
222    Also,
223    Only,
224}
225
226pub trait FromParamValue {
227    fn values_from(w: &Parameter) -> Result<&Self, TypeError>
228    where
229        Self: Sized;
230}
231
232#[derive(Debug, PartialEq, Clone)]
233pub struct Switch {
234    pub label: Option<String>,
235    pub value: SwitchState,
236}
237
238impl Into<SwitchState> for Switch {
239    fn into(self) -> SwitchState {
240        self.value
241    }
242}
243
244#[derive(Debug, PartialEq, Clone)]
245pub struct SwitchVector {
246    pub name: String,
247    pub group: Option<String>,
248    pub label: Option<String>,
249    pub state: PropertyState,
250    pub perm: PropertyPerm,
251    pub rule: SwitchRule,
252    pub timeout: Option<u32>,
253    pub timestamp: Option<DateTime<Utc>>,
254
255    pub values: HashMap<String, Switch>,
256}
257
258impl FromParamValue for HashMap<String, Switch> {
259    fn values_from(p: &Parameter) -> Result<&Self, TypeError> {
260        match p {
261            Parameter::SwitchVector(p) => Ok(&p.values),
262            _ => Err(TypeError::TypeMismatch),
263        }
264    }
265}
266
267#[derive(Debug, PartialEq, Clone)]
268pub struct Number {
269    pub label: Option<String>,
270    pub format: String,
271    pub min: f64,
272    pub max: f64,
273    pub step: f64,
274    pub value: Sexagesimal,
275}
276
277impl Into<Sexagesimal> for Number {
278    fn into(self) -> Sexagesimal {
279        self.value
280    }
281}
282
283#[derive(Debug, PartialEq, Clone)]
284pub struct NumberVector {
285    pub name: String,
286    pub group: Option<String>,
287    pub label: Option<String>,
288    pub state: PropertyState,
289    pub perm: PropertyPerm,
290    pub timeout: Option<u32>,
291    pub timestamp: Option<DateTime<Utc>>,
292
293    pub values: HashMap<String, Number>,
294}
295
296impl FromParamValue for HashMap<String, Number> {
297    fn values_from(p: &Parameter) -> Result<&Self, TypeError> {
298        match p {
299            Parameter::NumberVector(p) => Ok(&p.values),
300            _ => Err(TypeError::TypeMismatch),
301        }
302    }
303}
304
305#[derive(Debug, PartialEq, Clone)]
306pub struct Light {
307    pub label: Option<String>,
308    pub value: PropertyState,
309}
310
311#[derive(Debug, PartialEq, Clone)]
312pub struct LightVector {
313    pub name: String,
314    pub label: Option<String>,
315    pub group: Option<String>,
316    pub state: PropertyState,
317    pub timestamp: Option<DateTime<Utc>>,
318
319    pub values: HashMap<String, Light>,
320}
321
322impl FromParamValue for HashMap<String, Light> {
323    fn values_from(p: &Parameter) -> Result<&Self, TypeError> {
324        match p {
325            Parameter::LightVector(p) => Ok(&p.values),
326            _ => Err(TypeError::TypeMismatch),
327        }
328    }
329}
330
331#[derive(Debug, PartialEq, Clone)]
332pub struct Text {
333    pub label: Option<String>,
334    pub value: String,
335}
336
337impl FromParamValue for HashMap<String, Text> {
338    fn values_from(p: &Parameter) -> Result<&Self, TypeError> {
339        match p {
340            Parameter::TextVector(p) => Ok(&p.values),
341            _ => Err(TypeError::TypeMismatch),
342        }
343    }
344}
345
346#[derive(Debug, PartialEq, Clone)]
347pub struct TextVector {
348    pub name: String,
349    pub group: Option<String>,
350    pub label: Option<String>,
351
352    pub state: PropertyState,
353    pub perm: PropertyPerm,
354    pub timeout: Option<u32>,
355    pub timestamp: Option<DateTime<Utc>>,
356
357    pub values: HashMap<String, Text>,
358}
359
360#[derive(Debug, PartialEq, Clone)]
361pub struct Blob {
362    pub label: Option<String>,
363    pub format: Option<String>,
364    pub value: Option<Arc<Vec<u8>>>,
365}
366
367#[derive(Debug, PartialEq, Clone)]
368pub struct BlobVector {
369    pub name: String,
370    pub label: Option<String>,
371    pub group: Option<String>,
372    pub state: PropertyState,
373    pub perm: PropertyPerm,
374    pub timeout: Option<u32>,
375    pub timestamp: Option<DateTime<Utc>>,
376
377    pub values: HashMap<String, Blob>,
378}
379
380impl FromParamValue for HashMap<String, Blob> {
381    fn values_from(p: &Parameter) -> Result<&Self, TypeError> {
382        match p {
383            Parameter::BlobVector(p) => Ok(&p.values),
384            _ => Err(TypeError::TypeMismatch),
385        }
386    }
387}
388
389#[derive(Debug, PartialEq, Clone)]
390pub enum Parameter {
391    TextVector(TextVector),
392    NumberVector(NumberVector),
393    SwitchVector(SwitchVector),
394    LightVector(LightVector),
395    BlobVector(BlobVector),
396}
397
398impl Parameter {
399    pub fn get_group(&self) -> &Option<String> {
400        match self {
401            Parameter::TextVector(p) => &p.group,
402            Parameter::NumberVector(p) => &p.group,
403            Parameter::SwitchVector(p) => &p.group,
404            Parameter::LightVector(p) => &p.group,
405            Parameter::BlobVector(p) => &p.group,
406        }
407    }
408
409    pub fn get_name(&self) -> &String {
410        match self {
411            Parameter::TextVector(p) => &p.name,
412            Parameter::NumberVector(p) => &p.name,
413            Parameter::SwitchVector(p) => &p.name,
414            Parameter::LightVector(p) => &p.name,
415            Parameter::BlobVector(p) => &p.name,
416        }
417    }
418    pub fn get_label(&self) -> &Option<String> {
419        match self {
420            Parameter::TextVector(p) => &p.label,
421            Parameter::NumberVector(p) => &p.label,
422            Parameter::SwitchVector(p) => &p.label,
423            Parameter::LightVector(p) => &p.label,
424            Parameter::BlobVector(p) => &p.label,
425        }
426    }
427    pub fn get_label_display(&self) -> &String {
428        match self.get_label() {
429            Some(label) => label,
430            None => self.get_name(),
431        }
432    }
433    pub fn get_state(&self) -> &PropertyState {
434        match self {
435            Parameter::TextVector(p) => &p.state,
436            Parameter::NumberVector(p) => &p.state,
437            Parameter::SwitchVector(p) => &p.state,
438            Parameter::LightVector(p) => &p.state,
439            Parameter::BlobVector(p) => &p.state,
440        }
441    }
442    pub fn get_timeout(&self) -> &Option<u32> {
443        match self {
444            Parameter::TextVector(p) => &p.timeout,
445            Parameter::NumberVector(p) => &p.timeout,
446            Parameter::SwitchVector(p) => &p.timeout,
447            Parameter::LightVector(_) => &None,
448            Parameter::BlobVector(p) => &p.timeout,
449        }
450    }
451
452    pub fn get_values<T: FromParamValue>(&self) -> Result<&T, TypeError> {
453        T::values_from(self)
454    }
455}
456
457#[derive(Debug)]
458pub enum TypeError {
459    TypeMismatch,
460}
461pub trait TryEq<T> {
462    fn try_eq(&self, other: &T) -> Result<bool, TypeError>;
463}
464
465impl TryEq<Parameter> for Vec<OneSwitch> {
466    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
467        let current_values = other.get_values::<HashMap<String, Switch>>()?;
468
469        Ok(self.iter().all(|other_value| {
470            Some(other_value.value) == current_values.get(&other_value.name).map(|x| x.value)
471        }))
472    }
473}
474
475impl<I: Into<SwitchState> + Copy> TryEq<Parameter> for Vec<(&str, I)> {
476    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
477        let current_values = other.get_values::<HashMap<String, Switch>>()?;
478
479        Ok(self.iter().all(|other_value| {
480            Some(other_value.1.into()) == current_values.get(other_value.0).map(|x| x.value)
481        }))
482    }
483}
484
485impl TryEq<Parameter> for Vec<(&str, f64)> {
486    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
487        let current_values = other.get_values::<HashMap<String, Number>>()?;
488
489        Ok(self.iter().all(|other_value| {
490            Some(other_value.1) == current_values.get(other_value.0).map(|x| x.value.into())
491        }))
492    }
493}
494
495impl TryEq<Parameter> for Vec<(&str, Sexagesimal)> {
496    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
497        let current_values = other.get_values::<HashMap<String, Number>>()?;
498
499        Ok(self.iter().all(|other_value| {
500            Some(other_value.1) == current_values.get(other_value.0).map(|x| x.value.into())
501        }))
502    }
503}
504
505impl TryEq<Parameter> for Vec<OneNumber> {
506    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
507        let current_values = other.get_values::<HashMap<String, Number>>()?;
508
509        Ok(self.iter().all(|other_value| {
510            Some(other_value.value) == current_values.get(&other_value.name).map(|x| x.value)
511        }))
512    }
513}
514
515impl TryEq<Parameter> for Vec<(&str, &str)> {
516    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
517        let current_values = other.get_values::<HashMap<String, Text>>()?;
518
519        Ok(self.iter().all(|other_value| {
520            Some(other_value.1) == current_values.get(other_value.0).map(|x| x.value.as_str())
521        }))
522    }
523}
524
525impl TryEq<Parameter> for Vec<OneText> {
526    fn try_eq(&self, other: &Parameter) -> Result<bool, TypeError> {
527        let current_values = other.get_values::<HashMap<String, Text>>()?;
528
529        Ok(self.iter().all(|other_value| {
530            Some(&other_value.value) == current_values.get(&other_value.name).map(|x| &x.value)
531        }))
532    }
533}