diff options
Diffstat (limited to 'src/logitech.rs')
| -rw-r--r-- | src/logitech.rs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/src/logitech.rs b/src/logitech.rs new file mode 100644 index 0000000..28ff45c --- /dev/null +++ b/src/logitech.rs @@ -0,0 +1,182 @@ +// This file is part of elem <https://github.com/Fuwn/elem>. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::collections::HashMap; + +use serde_derive::{Deserialize, Serialize}; +use tungstenite::{client::IntoClientRequest, Message}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct DeviceInfo { + pub id: String, + #[serde(rename = "connectionType")] + connection_type: String, + #[serde(rename = "deviceType")] + device_type: String, + #[serde(rename = "displayName")] + pub display_name: String, +} + +impl DeviceInfo { + pub fn new( + id: &str, + connection_type: &str, + device_type: &str, + display_name: &str, + ) -> Self { + Self { + id: id.to_string(), + connection_type: connection_type.to_string(), + device_type: device_type.to_string(), + display_name: display_name.to_string(), + } + } + + pub fn from_device_info(device_info: &Self) -> Self { + Self { + id: device_info.id.clone(), + connection_type: device_info.connection_type.clone(), + device_type: device_info.device_type.clone(), + display_name: device_info.display_name.clone(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct DeviceListPayload { + #[serde(rename = "deviceInfos")] + device_infos: Vec<DeviceInfo>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct DeviceList { + payload: DeviceListPayload, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DevicePayload { + percentage: u64, +} + +impl DevicePayload { + pub const fn percentage(&self) -> u64 { self.percentage } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Device { + payload: DevicePayload, +} + +impl Device { + pub const fn payload(&self) -> &DevicePayload { &self.payload } +} + +/// Create a connection to the Logitech G HUB `WebSocket` (backtick-ed because +/// rustfmt is forcing me to) +fn connection() -> tungstenite::WebSocket< + tungstenite::stream::MaybeTlsStream<std::net::TcpStream>, +> { + let url = url::Url::parse("ws://localhost:9010").unwrap(); + + let (mut ws_stream, _) = tungstenite::connect({ + let mut request = url.into_client_request().unwrap(); + + // https://github.com/snapview/tungstenite-rs/issues/279 + // https://github.com/snapview/tungstenite-rs/issues/145#issuecomment-713581499 + request + .headers_mut() + .insert("Sec-WebSocket-Protocol", "json".parse().unwrap()); + + request + }) + .unwrap(); + + ws_stream.read_message().unwrap(); + + ws_stream +} + +/// Get a list of only wireless devices from the Logitech G HUB `WebSocket` +pub fn wireless_devices() -> HashMap<String, DeviceInfo> { + let mut stream = connection(); + + stream + .write_message(Message::binary( + serde_json::json!({ + "path": "/devices/list", + "verb": "GET" + }) + .to_string(), + )) + .unwrap(); + + let devices = serde_json::from_value::<DeviceList>( + serde_json::from_str(&stream.read_message().unwrap().into_text().unwrap()) + .unwrap(), + ) + .unwrap(); + let wireless = devices + .payload + .device_infos + .iter() + .filter(|device_info| device_info.connection_type == "WIRELESS") + .map(DeviceInfo::from_device_info) + .collect::<Vec<DeviceInfo>>(); + let mut mapped = HashMap::new(); + + for device in wireless { + mapped.insert(device.display_name.clone(), device); + } + + // Adding a dummy device to the device list for testing purposes. + // + // I'm also going to keep this in because it's a nice way for the user to make + // sure everything is working properly. + mapped.insert( + "Dummy (Debug)".to_string(), + DeviceInfo::new("dummy_debug", "WIRELESS", "MOUSE", "Dummy (Debug)"), + ); + + mapped +} + +/// Get the battery percentage of a specific wireless device +pub fn device(display_name: &str) -> Device { + if display_name == "Dummy (Debug)" { + return Device { + payload: DevicePayload { percentage: 100 }, + }; + } + + let mut stream = connection(); + + stream + .write_message(Message::binary( + serde_json::json!({ + "path": format!("/battery/{}/state", wireless_devices().get(display_name).unwrap().id), + "verb": "GET" + }) + .to_string(), + )) + .unwrap(); + + serde_json::from_value::<Device>( + serde_json::from_str(&stream.read_message().unwrap().into_text().unwrap()) + .unwrap(), + ) + .unwrap() +} |