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}