kaleidoscope_focus/
lib.rs

1// kaleidoscope -- Talk with Kaleidoscope powered devices
2// Copyright (C) 2022  Keyboard.io, Inc.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16#![warn(missing_docs)]
17#![allow(rustdoc::broken_intra_doc_links)]
18
19//! **Talking to [`Kaleidoscope`] powered devices with Rust**
20//!
21//! This library is a very thin layer on top of `serialport`, implementing a
22//! handful of convenience functions to make it easy to communicate with devices
23//! speaking Kaleidoscope's [`Focus`] protocol.
24//!
25//! Start at [`struct.Focus`] to discover what the crate provides.
26//!
27//! [`struct.Focus`]: ./struct.Focus.html
28//! [`Kaleidoscope`]: https://github.com/keyboardio/Kaleidoscope
29//! [`Focus`]: https://kaleidoscope.readthedocs.io/en/latest/plugins/Kaleidoscope-FocusSerial.html
30
31use serialport::SerialPort;
32use std::io::{self, Write};
33use std::thread;
34use std::time::Duration;
35
36/// The representation of a connection to a keyboard, used for all communication.
37///
38/// Constructed using a builder pattern, using [`Focus::create`].
39pub struct Focus {
40    port: Box<dyn SerialPort>,
41    chunk_size: usize,
42    interval: u64,
43    progress_report: Box<dyn Fn(usize) + 'static>,
44}
45
46impl Focus {
47    /// Create a new connection using a Builder pattern.
48    ///
49    /// A `device` to open must be specified. What the `device` is, is platform
50    /// dependent, see [`serialport::new`] for more information.
51    ///
52    /// # Examples
53    ///
54    /// ```no_run
55    /// # use kaleidoscope_focus::Focus;
56    /// # fn main() -> Result<(), std::io::Error> {
57    /// let mut conn = Focus::create("/dev/ttyACM0")
58    ///     .chunk_size(32)
59    ///     .interval(50)
60    ///     .open()?;
61    /// #   Ok(())
62    /// # }
63    /// ```
64    pub fn create(device: &str) -> FocusBuilder {
65        FocusBuilder {
66            device,
67            chunk_size: 32,
68            interval: 50,
69        }
70    }
71
72    /// Send a request to the keyboard.
73    ///
74    /// Sends a `command` request to the keyboard, with optional `args`. Returns
75    /// the reply to the request.
76    ///
77    /// May return an empty string if the command is unknown, or if it does not
78    /// have any output.
79    ///
80    /// # Examples
81    ///
82    /// ```no_run
83    /// # use kaleidoscope_focus::Focus;
84    /// # fn main() -> Result<(), std::io::Error> {
85    /// let mut conn = Focus::create("/dev/ttyACM0").open()?;
86    /// let reply = conn.request("help", None);
87    /// assert!(reply.is_ok());
88    /// #   Ok(())
89    /// # }
90    /// ```
91    ///
92    /// ```no_run
93    /// # use kaleidoscope_focus::Focus;
94    /// # use indicatif::ProgressBar;
95    /// # fn main() -> Result<(), std::io::Error> {
96    /// let progress = ProgressBar::new(0);
97    /// let mut conn = Focus::create("/dev/ttyACM0").open()?;
98    /// conn.set_progress_report(move |delta| {
99    ///   progress.inc(delta.try_into().unwrap());
100    /// });
101    /// let reply = conn.request("settings.version", None)?;
102    /// assert_eq!(reply, "1 ");
103    /// #   Ok(())
104    /// # }
105    /// ```
106    pub fn request(
107        &mut self,
108        command: &str,
109        args: Option<&[String]>,
110    ) -> Result<String, std::io::Error> {
111        self.send(command, args)?.receive()
112    }
113
114    fn send(
115        &mut self,
116        command: &str,
117        args: Option<&[String]>,
118    ) -> Result<&mut Self, std::io::Error> {
119        let request = format!("{} {}\n", command, args.unwrap_or_default().join(" "));
120        self.port.write_data_terminal_ready(true)?;
121
122        if self.chunk_size > 0 {
123            for c in request.as_bytes().chunks(self.chunk_size) {
124                self.port.write_all(c)?;
125                thread::sleep(Duration::from_millis(self.interval));
126                (self.progress_report)(c.len());
127            }
128        } else {
129            self.port.write_all(request.as_bytes())?;
130            (self.progress_report)(request.len());
131        }
132
133        Ok(self)
134    }
135
136    fn receive(&mut self) -> Result<String, std::io::Error> {
137        let mut buffer = [0; 1024];
138        let mut reply = vec![];
139
140        self.port.read_data_set_ready()?;
141        self.wait_for_data()?;
142
143        loop {
144            match self.port.read(buffer.as_mut_slice()) {
145                // EOF
146                Ok(0) => break,
147                Ok(t) => {
148                    reply.extend(&buffer[..t]);
149                    (self.progress_report)(t);
150                }
151                Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
152                    break;
153                }
154                Err(e) => {
155                    return Err(e);
156                }
157            }
158
159            thread::sleep(Duration::from_millis(self.interval));
160        }
161
162        Ok(String::from_utf8_lossy(&reply)
163            .lines()
164            .filter(|l| !l.is_empty() && *l != ".")
165            .collect::<Vec<&str>>()
166            .join("\n"))
167    }
168
169    /// Send a command - a request without arguments - to the keyboard.
170    ///
171    /// See [`Focus::request`], this is the same, but without any arguments.
172    ///
173    /// ```no_run
174    /// # use kaleidoscope_focus::Focus;
175    /// # fn main() -> Result<(), std::io::Error> {
176    /// let mut conn = Focus::create("/dev/ttyACM0").open()?;
177    /// let reply = conn.command("settings.version")?;
178    /// assert_eq!(reply, "1 ");
179    /// #   Ok(())
180    /// # }
181    /// ```
182    pub fn command(&mut self, command: &str) -> Result<String, std::io::Error> {
183        self.request(command, None)
184    }
185
186    /// Set the progress reporter function for I/O operations.
187    ///
188    /// Whenever I/O happens, the progress reporter function is called. This can
189    /// be used to display progress bars and the like. The reporter function
190    /// takes a single `usize` argument, and returns nothing.
191    ///
192    /// ```no_run
193    /// # use kaleidoscope_focus::Focus;
194    /// # use indicatif::ProgressBar;
195    /// # fn main() -> Result<(), std::io::Error> {
196    /// let progress = ProgressBar::new(0);
197    /// let mut conn = Focus::create("/dev/ttyACM0").open()?;
198    /// conn.set_progress_report(move |delta| {
199    ///   progress.inc(delta.try_into().unwrap());
200    /// });
201    /// let reply = conn.command("version");
202    /// assert!(reply.is_ok());
203    /// #   Ok(())
204    /// # }
205    /// ```
206    pub fn set_progress_report(&mut self, progress_report: impl Fn(usize) + 'static) {
207        self.progress_report = Box::new(progress_report);
208    }
209
210    /// Flush any pending data.
211    ///
212    /// Sends an empty command, and then waits until the keyboard stops sending
213    /// data. The intended use is to clear any pending I/O operations in flight.
214    ///
215    /// ```no_run
216    /// # use kaleidoscope_focus::Focus;
217    /// # fn main() -> Result<(), std::io::Error> {
218    /// let mut conn = Focus::create("/dev/ttyACM0").open()?;
219    ///
220    /// /// Send a request whose output we're not interested in.
221    /// conn.command("help")?;
222    /// /// Flush it!
223    /// conn.flush()?;
224    ///
225    /// /// ...and then send the request we want the output of.
226    /// let reply = conn.command("settings.version")?;
227    /// assert_eq!(reply, "1 ");
228    /// #   Ok(())
229    /// # }
230    /// ```
231    pub fn flush(&mut self) -> Result<&mut Self, std::io::Error> {
232        self.command(" ")?;
233        Ok(self)
234    }
235
236    /// Find supported devices, and return the paths to their ports.
237    ///
238    /// Iterates over available USB serial ports, and keeps only those that belong
239    /// to a supported keyboard. The crate only recognises Keyboardio devices as
240    /// supported keyboards.
241    ///
242    /// ```no_run
243    /// # use kaleidoscope_focus::Focus;
244    /// let devices = Focus::find_devices().unwrap();
245    /// assert!(devices.len() > 0);
246    /// ```
247    pub fn find_devices() -> Option<Vec<String>> {
248        #[derive(PartialEq)]
249        struct DeviceDescriptor {
250            vid: u16,
251            pid: u16,
252        }
253        impl From<&serialport::UsbPortInfo> for DeviceDescriptor {
254            fn from(port: &serialport::UsbPortInfo) -> Self {
255                Self {
256                    vid: port.vid,
257                    pid: port.pid,
258                }
259            }
260        }
261
262        let supported_keyboards = [
263            // Keyboardio Model100
264            DeviceDescriptor {
265                vid: 0x3496,
266                pid: 0x0006,
267            },
268            // Keyboardio Atreus
269            DeviceDescriptor {
270                vid: 0x1209,
271                pid: 0x2303,
272            },
273            // Keyboardio Model01
274            DeviceDescriptor {
275                vid: 0x1209,
276                pid: 0x2301,
277            },
278        ];
279
280        let devices: Vec<String> = serialport::available_ports()
281            .ok()?
282            .iter()
283            .filter_map(|p| match &p.port_type {
284                serialport::SerialPortType::UsbPort(port_info) => supported_keyboards
285                    .contains(&port_info.into())
286                    .then(|| p.port_name.to_string()),
287                _ => None,
288            })
289            .collect();
290
291        if devices.is_empty() {
292            return None;
293        }
294
295        Some(devices)
296    }
297
298    fn wait_for_data(&mut self) -> Result<(), std::io::Error> {
299        while self.port.bytes_to_read()? == 0 {
300            thread::sleep(Duration::from_millis(self.interval));
301        }
302        Ok(())
303    }
304}
305
306/// Provides a builder pattern for [`Focus`].
307///
308/// Use [`Focus::create`] to start building.
309pub struct FocusBuilder<'a> {
310    device: &'a str,
311    chunk_size: usize,
312    interval: u64,
313}
314
315impl FocusBuilder<'_> {
316    /// Set the chunk size to use for writes.
317    ///
318    /// The library uses chunked writes by default, to work around old firmware
319    /// bugs, and operating system quirks at times. Use this method to set the
320    /// chunk size to your desired value.
321    ///
322    /// Setting the size to 0 disables chunking.
323    ///
324    /// See [`Focus::create`] for an example.
325    pub fn chunk_size(mut self, chunk_size: usize) -> Self {
326        self.chunk_size = chunk_size;
327        self
328    }
329
330    /// Set the interval between chunks.
331    ///
332    /// See [`Focus::create`] for an example.
333    pub fn interval(mut self, interval: u64) -> Self {
334        self.interval = interval;
335        self
336    }
337
338    /// Open a connection to the keyboard.
339    ///
340    /// Stops building the configuration for the [`Focus`] struct, and opens a
341    /// connection to the keyboard.
342    ///
343    /// See [`Focus::create`] for an example.
344    pub fn open(&self) -> Result<Focus, serialport::Error> {
345        let port = serialport::new(self.device, 115200)
346            .timeout(Duration::from_millis(self.interval))
347            .open()?;
348
349        Ok(Focus {
350            port,
351            chunk_size: self.chunk_size,
352            interval: self.interval,
353            progress_report: Box::new(|_| {}),
354        })
355    }
356}