inputflow_kmbox/
lib.rs

1//! KMBox plugin for inputflow.
2//! Controls user input over serial interface to KMBox device.
3
4use ::std::time::Duration;
5
6use dataview::PodMethods;
7use inputflow::prelude::*;
8use keycodes::KMBoxKeyboardKeyCode;
9use serialport::{SerialPort, SerialPortType, UsbPortInfo};
10use format_bytes::format_bytes;
11
12mod args;
13pub mod keycodes;
14
15struct KMBoxPluginRoot {
16    controller: InputFlowKMBox,
17}
18
19impl KMBoxPluginRoot {
20    pub fn new(args: args::Args) -> std::result::Result<Self, Box<dyn std::error::Error>> {
21        log::info!("Initializing KMBox plugin with config {}", ron::to_string(&args)?);
22
23        let mut port_path = args.com_port;
24
25        if args.auto_select {
26            let ports = serialport::available_ports()?;
27
28            for port in ports {
29                log::trace!("Found serial port {} : {:?}", port.port_name, port.port_type);
30
31                match port.port_type {
32                    SerialPortType::UsbPort(UsbPortInfo{product: Some(product_name),..}) => {
33                        if product_name.starts_with(&args.device_name) {
34                            log::info!("Automatically loaded port {} from device {}", port.port_name, product_name);
35                            port_path = port.port_name;
36                            break;
37                        }
38                    },
39                    _=> {}
40                }
41            }
42                
43        }
44
45        Ok(KMBoxPluginRoot {
46            controller: InputFlowKMBox {
47                port: serialport::new(port_path, args.baud_rate)
48                        .timeout(Duration::from_millis(args.timeout_ms))
49                        .open()?,
50            }
51        })
52    }
53}
54
55impl<'a> PluginInner<'a> for KMBoxPluginRoot {
56    type BorrowedType = Fwd<&'a mut InputFlowKMBox>;
57
58    type OwnedType = InputFlowKMBox;
59    type OwnedTypeMut = InputFlowKMBox;
60
61    fn borrow_features(&'a mut self) -> Self::BorrowedType {
62        self.controller.forward_mut()
63    }
64
65    fn into_features(self) -> Self::OwnedType {
66        self.controller
67    }
68
69    fn mut_features(&'a mut self) -> &'a mut Self::OwnedTypeMut {
70        &mut self.controller
71    }
72}
73
74#[derive(Debug)]
75pub struct InputFlowKMBox {
76    pub port: Box<dyn SerialPort>,
77}
78
79impl InputFlowKMBox {
80
81    /// calls km.left() with value to set current left click
82    /// 1 = set down
83    /// 0 = release
84    pub fn km_set_left(&mut self, is_down: i32) -> Result<()> {
85        let cmd = format_bytes!(b"km.left({})\r\n", is_down);
86
87        self.port.write(cmd.as_bytes()).map_err(|e| {
88            // log serial failure details if logging is enabled
89            log::warn!("command km.left({is_down}) \"{cmd:?}\" failed: {e:?}.");
90            // return error to result as InputFlowError type.
91            InputFlowError::SendError
92        })?;
93        Ok(())
94    }
95
96    pub fn km_set_key(&mut self, key: KeyboardKey, is_down: bool) -> Result<()> {
97        let km_key = KMBoxKeyboardKeyCode::try_from(key)?;
98
99        let cmd = if is_down {
100            format_bytes!(b"km.down({})\r\n", km_key)
101        } else {
102            format_bytes!(b"km.up({})\r\n", km_key)
103        };
104
105        self.port.write(cmd.as_bytes()).map_err(|e| {
106            // log serial failure details if logging is enabled
107            log::warn!("command km.down/km.up \"{cmd:?}\" failed: {e:?}.");
108            // return error to result as InputFlowError type.
109            InputFlowError::SendError
110        })?;
111        Ok(())
112    }
113
114    pub fn km_press_key(&mut self, key: KeyboardKey) -> Result<()> {
115        let km_key = KMBoxKeyboardKeyCode::try_from(key)?;
116        
117        // press key command with some timing variation
118        let cmd = format_bytes!(b"km.press({},15,50)\r\n", km_key);
119
120        self.port.write(cmd.as_bytes()).map_err(|e| {
121            // log serial failure details if logging is enabled
122            log::warn!("command km.press({km_key}) \"{cmd:?}\" failed: {e:?}.");
123            // return error to result as InputFlowError type.
124            InputFlowError::SendError
125        })?;
126        Ok(())
127    }
128}
129
130impl Loadable for InputFlowKMBox {
131    fn name(&self) -> abi_stable::std_types::RString {
132        "inputflow_kmbox".into()
133    }
134
135    fn capabilities(&self) -> u8 {
136        IF_PLUGIN_HEAD.features.bits()
137    }
138}
139
140impl KeyboardWriter for InputFlowKMBox {
141    #[doc = r"Sends keyboard press down event"]
142    fn send_key_down(&mut self, key: KeyboardKey) -> Result<()> {
143        self.km_set_key(key, true)
144    }
145
146    #[doc = r" Releases a key that was set to down previously"]
147    fn send_key_up(&mut self, key: KeyboardKey) -> Result<()> {
148        self.km_set_key(key, false)
149    }
150
151    #[doc = r" Presses a key and lets it go all in one for when users do not care about specific timings"]
152    fn press_key(&mut self, key: KeyboardKey) -> Result<()> {
153        self.km_press_key(key)
154    }
155
156    #[doc = r" clears all active pressed keys. Useful for cleaning up multiple keys presses in one go."]
157    #[doc = r" Ensures that keyboard writer is set back into a neutral state."]
158    fn clear_keys(&mut self) -> Result<()> {
159        // TODO: Add a currently pressed keys map and recursively set them unpressed
160        log::info!("kmbox clear_keys not implemented yet...");
161        Ok(())
162    }
163}
164
165/// Takes in an inputflow mouse button and tries to
166/// convert it to the equivilent kmbox button id
167fn mouse_button_to_km(button: MouseButton) -> Option<u32> {
168    Some(match button {
169        MouseButton::Left => 0,
170        MouseButton::Right => 1,
171        MouseButton::Middle => 3,
172        MouseButton::XButton1 => 4,
173        MouseButton::XButton2 => 5,
174        _ => {
175            return None;
176        }
177    })
178}
179
180impl MouseWriter for InputFlowKMBox {
181    #[doc = r" Sends mouse button press down event"]
182    fn send_button_down(&mut self, button: MouseButton) -> Result<()> {
183        match button {
184            MouseButton::Left => {
185                self.km_set_left(1)
186            },
187            _=> {Err(InputFlowError::Parameter)}
188        }
189    }
190
191    #[doc = r" Releases a mouse button that was set to down previously"]
192    fn send_button_up(&mut self, button: MouseButton) -> Result<()> {
193        match button {
194            MouseButton::Left => {
195                self.km_set_left(0)
196            },
197            _=> {Err(InputFlowError::Parameter)}
198        }
199    }
200
201    #[doc = r" Presses a  mouse button and lets it go all in one for when users do not care about specific timings"]
202    fn click_button(&mut self, button: MouseButton) -> Result<()> {
203
204        let Some(km_button) = mouse_button_to_km(button) else {
205            return Err(InputFlowError::InvalidKey);
206        };
207        
208        let cmd = match button {
209            MouseButton::Left => {
210                format_bytes!(b"km.click({})\r\n", km_button)
211            },
212            _=> {return Err(InputFlowError::Parameter);}
213        };
214
215        // TODO: find anything other than km.click so that it may have some human-like delay rather than instantanious clicks
216        self.port.write(cmd.as_bytes()).map_err(|e| {
217            // log serial failure details if logging is enabled
218            log::warn!("command km.click({button:?}) \"{cmd:?}\" failed: {e:?}.");
219            // return error to result as InputFlowError type.
220            InputFlowError::SendError
221        })?;
222         
223        Ok(())
224    }
225
226    #[doc = r" clears all active pressed  mouse buttons. Useful for cleaning up multiple mouse button presses in one go."]
227    #[doc = r" Ensures that mouse writer is set back into a neutral state."]
228    fn clear_buttons(&mut self) -> Result<()> {
229        Ok(())
230    }
231
232    #[doc = r" Sends a mouse move command to move it x dpi-pixels horizontally, and y vertically"]
233    fn mouse_move_relative(&mut self, x: i32, y: i32) -> Result<()> {
234        let cmd = format_bytes!(b"km.move({},{})\r\n", x,y);
235        self.port.write(cmd.as_bytes()).map_err(|e| {
236            // log serial failure details if logging is enabled
237            log::warn!("command km.move({x},{y}) \"{cmd:?}\" failed: {e:?}.");
238            // return error to result as InputFlowError type.
239            InputFlowError::SendError
240        })?;
241        Ok(())
242    }
243}
244
245// ================================================================================================================= 
246// =================================== CGlue Plugin init and Header definitions ====================================
247// ================================================================================================================= 
248
249cglue_impl_group!(InputFlowKMBox, ControllerFeatures,{KeyboardWriter, MouseWriter}, {KeyboardWriter, MouseWriter} );
250
251/// Exposed interface that is called by the user of the plugin to instantiate it
252#[allow(improper_ctypes_definitions)] // the linter is being stupid and not noticing the repr(u8)
253extern "C" fn create_plugin(lib: &CArc<cglue::trait_group::c_void>, args: *const std::ffi::c_char) -> Result<PluginInnerArcBox<'static>> {
254    env_logger::builder()
255    // .filter_level(log::LevelFilter::Info)
256    .init();
257    Ok(trait_obj!(
258        (
259            KMBoxPluginRoot::new(
260                args::parse_args(args).map_err(|e| {
261                    log::error!("Invalid parameters were passed to inputflow_kmbox: {e:?}.");
262                    InputFlowError::Parameter
263                })?
264            ).map_err(|e| {
265                log::error!("Failed to load KMBox device: {e:?}.");
266                InputFlowError::Loading
267            })?,
268            lib.clone()
269        ) as PluginInner
270    ))
271}
272
273/// Static plugin header values defining the plugin's capabilities
274#[no_mangle]
275pub static IF_PLUGIN_HEAD: PluginHeader = PluginHeader {
276    features: FeatureSupport::from_bits_retain(
277        FeatureSupport::WRITE_KEYBOARD.bits() | FeatureSupport::WRITE_MOUSE.bits(),
278    ),
279    layout: ROOT_LAYOUT,
280    create: create_plugin,
281};