Initial prototype
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
use serde::Deserialize;
|
||||
use serde_dhall::StaticType;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Deserialize, StaticType, Debug)]
|
||||
pub enum Action {
|
||||
MQTTPub { topic: String, payload: String }, // Publish something (server, topic, payload)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, StaticType, Debug)]
|
||||
pub enum Mapping {
|
||||
NOP, // Do nothing.
|
||||
Passthrough, // Passthrough to the layer below
|
||||
ActivateLayer(String), // Activate a layer
|
||||
Print(String), // Print a string to console
|
||||
Trigger(Action), // Trigger an action
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Layer {
|
||||
pub default_mapping: Mapping, // What do we do if there's no mapping for a button?
|
||||
pub mappings: HashMap<String, Mapping>, // Button id -> mapping
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MQTTServer {
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub user: Option<String>,
|
||||
pub pass: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub topic_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub mqtt_servers: HashMap<String, MQTTServer>,
|
||||
pub layers: HashMap<String, Layer>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_file(p: &Path) -> Result<Self, Box<dyn Error>> {
|
||||
match serde_dhall::from_file(p)
|
||||
.with_builtin_type("Mapping".to_string(), Mapping::static_type())
|
||||
.with_builtin_type("Action".to_string(), Action::static_type())
|
||||
.parse::<Self>()
|
||||
{
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
Err(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use bitvec::prelude::*;
|
||||
use hidapi::{DeviceInfo, HidApi, HidDevice};
|
||||
use std::collections::VecDeque;
|
||||
use std::error::Error;
|
||||
|
||||
const HID_USAGE_PAGE: u16 = 0xFF;
|
||||
const HID_USAGE: u16 = 0x1;
|
||||
|
||||
pub fn get_devices<'a>(hid: &'a HidApi, print: bool) -> Vec<&'a DeviceInfo> {
|
||||
if print {
|
||||
println!("All HIDs attached:");
|
||||
println!("Vendor ID, Product ID, Manufacturer, Produce, Usage Page, Usage");
|
||||
}
|
||||
let mut devices = vec![];
|
||||
for device in hid.device_list() {
|
||||
if (device.usage_page(), device.usage()) == (HID_USAGE_PAGE, HID_USAGE) {
|
||||
devices.push(device);
|
||||
}
|
||||
if print {
|
||||
println!(
|
||||
"{}, {}, {}, {}, {}, {}",
|
||||
device.vendor_id(),
|
||||
device.product_id(),
|
||||
device.manufacturer_string().unwrap_or("Unknown"),
|
||||
device.product_string().unwrap_or("Unknown"),
|
||||
device.usage_page(),
|
||||
device.usage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
devices
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ButtonEvent {
|
||||
KeyDown(u8),
|
||||
KeyUp(u8),
|
||||
}
|
||||
|
||||
type State = u16;
|
||||
|
||||
pub struct ButtonPad<'a> {
|
||||
dev: &'a HidDevice,
|
||||
state: State,
|
||||
queued: VecDeque<ButtonEvent>,
|
||||
}
|
||||
|
||||
impl ButtonPad<'_> {
|
||||
pub fn new(dev: &HidDevice) -> ButtonPad {
|
||||
ButtonPad {
|
||||
dev,
|
||||
state: 0,
|
||||
queued: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ButtonPad<'_> {
|
||||
type Item = Result<ButtonEvent, Box<dyn Error>>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
// If we have queued events pop them first
|
||||
if let Some(ev) = self.queued.pop_front() {
|
||||
return Some(Ok(ev));
|
||||
}
|
||||
// Read from the device, blocking until the next report
|
||||
let mut buf: [u8; 2] = [0; 2];
|
||||
self.dev.read(&mut buf).expect("error reading");
|
||||
log::info!("{:?}", buf);
|
||||
|
||||
let new: u16 = buf[0] as u16 | (buf[1] as u16) << 8;
|
||||
|
||||
// Work out what, if anything, changed between the last report and this one, and
|
||||
// queue all the
|
||||
self.queued.extend(
|
||||
self.state
|
||||
.view_bits::<Lsb0>()
|
||||
.iter()
|
||||
.zip(new.view_bits::<Lsb0>().iter())
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(i, (state_last, state_new))| match (*state_last, *state_new) {
|
||||
(false, true) => Some(ButtonEvent::KeyDown(i.try_into().unwrap())),
|
||||
(true, false) => Some(ButtonEvent::KeyUp(i.try_into().unwrap())),
|
||||
_ => None,
|
||||
},
|
||||
),
|
||||
);
|
||||
self.state = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
use env_logger::Env;
|
||||
use hidapi::HidApi;
|
||||
use log::info;
|
||||
use rumqttc::{Client, MqttOptions, QoS};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
|
||||
mod config;
|
||||
mod device;
|
||||
|
||||
struct State<'a> {
|
||||
mqtt_servers: HashMap<String, Client>,
|
||||
conf: &'a config::Config,
|
||||
key_state: [bool; 16],
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
fn init(c: &'a config::Config) -> Self {
|
||||
let mut s = State {
|
||||
mqtt_servers: HashMap::new(),
|
||||
conf: &c,
|
||||
key_state: [false; 16],
|
||||
};
|
||||
|
||||
for (id, srv) in c.mqtt_servers.iter() {
|
||||
let mut opts = MqttOptions::new(
|
||||
srv.client_id
|
||||
.as_ref()
|
||||
.unwrap_or(&"samspad-client".to_owned()),
|
||||
&srv.host,
|
||||
srv.port.unwrap_or(1883),
|
||||
);
|
||||
// Only set credentials if we have both user and pass specified
|
||||
if let (Some(u), Some(p)) = (&srv.user, &srv.pass) {
|
||||
opts.set_credentials(u, p);
|
||||
}
|
||||
let (cli, mut conn) = Client::new(opts, 10);
|
||||
thread::spawn(move || {
|
||||
for notification in conn.iter() {
|
||||
match notification {
|
||||
Ok(n) => println!("Notification = {:?}", n),
|
||||
Err(e) => {
|
||||
println!("MQTT error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
s.mqtt_servers.insert(id.to_string(), cli);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn handle_button(&mut self, ev: &device::ButtonEvent) {
|
||||
let layer = &self.conf.layers["default"];
|
||||
match ev {
|
||||
device::ButtonEvent::KeyDown(key_id) => {
|
||||
self.key_state[*key_id as usize] = true;
|
||||
let mapping = if let Some(mapping) = layer.mappings.get(&format!("key_{}", key_id))
|
||||
{
|
||||
mapping
|
||||
} else {
|
||||
&layer.default_mapping
|
||||
};
|
||||
self.execute_mapping(mapping);
|
||||
}
|
||||
device::ButtonEvent::KeyUp(key_id) => {
|
||||
self.key_state[*key_id as usize] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_mapping(&mut self, m: &config::Mapping) {
|
||||
match m {
|
||||
config::Mapping::Print(s) => println!("{}", s),
|
||||
config::Mapping::Trigger(config::Action::MQTTPub { topic, payload }) => {
|
||||
let mut topic = topic.to_owned();
|
||||
let srv = &self.conf.mqtt_servers["default"];
|
||||
if let Some(prefix) = &srv.topic_prefix {
|
||||
topic.insert_str(0, &prefix);
|
||||
}
|
||||
let cli = &mut self.mqtt_servers.get_mut("default").unwrap();
|
||||
cli.publish(topic, QoS::AtLeastOnce, false, payload.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_led_data(&self) -> [u8; 48] {
|
||||
let data = self
|
||||
.key_state
|
||||
.iter()
|
||||
.flat_map(|s| if *s { [255, 255, 255] } else { [0, 0, 0] })
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
data[0..48].try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
let conf = config::Config::from_file(Path::new("./config.dhall"))?;
|
||||
|
||||
let mut state = State::init(&conf);
|
||||
|
||||
let hid = HidApi::new().unwrap();
|
||||
|
||||
// TODO: handle multiple devices
|
||||
let device = device::get_devices(&hid, true)
|
||||
.get(0)
|
||||
.expect("No device found")
|
||||
.open_device(&hid)
|
||||
.expect("Couldn't open device");
|
||||
|
||||
let pad = device::ButtonPad::new(&device);
|
||||
for ev in pad {
|
||||
if let Ok(ev) = ev {
|
||||
info!("{:?}", ev);
|
||||
state.handle_button(&ev);
|
||||
let mut data = state.get_led_data().to_vec();
|
||||
data.insert(0, 0x0);
|
||||
data.insert(0, 0x0);
|
||||
log::info!("{:?}", data);
|
||||
device.write(data.as_slice())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user