voltex hid controller

This commit is contained in:
4yn
2022-02-02 15:14:56 +08:00
parent 2b20902179
commit 93240ef4af
16 changed files with 566 additions and 123 deletions

View File

@@ -0,0 +1,11 @@
// extern crate slidershim;
use serialport::available_ports;
use std::io;
fn main() {
let res = available_ports();
println!("{:?}", res);
let mut input = String::new();
let string = io::stdin().read_line(&mut input).unwrap();
}

View File

@@ -9,6 +9,31 @@ fn main() {
.filter_level(log::LevelFilter::Debug)
.init();
// voltex?
let config = Config::from_str(
r#"{
"deviceMode": "yuancon",
"outputMode": "gamepad-voltex",
"keyboardSensitivity": 50,
"ledMode": "reactive-voltex",
"ledSensitivity": 50
}"#,
)
.unwrap();
// serial?
// let config = Config::from_str(
// r#"{
// "deviceMode": "yuancon",
// "outputMode": "kb-32-tasoller",
// "keyboardSensitivity": 50,
// "ledMode": "serial",
// "ledSerialPort": "COM5"
// }"#,
// )
// .unwrap();
// basic
// let config = Config::from_str(
// r#"{
// "deviceMode": "yuancon",
@@ -20,17 +45,7 @@ fn main() {
// )
// .unwrap();
let config = Config::from_str(
r#"{
"deviceMode": "yuancon",
"outputMode": "kb-voltex",
"keyboardSensitivity": 50,
"ledMode": "reactive-voltex",
"ledSensitivity": 50
}"#,
)
.unwrap();
// tasoller/
// let config = Config::from_str(
// r#"{
// "deviceMode": "tasoller-two",

View File

@@ -20,6 +20,7 @@ pub enum KeyboardLayout {
Yuancon,
Deemo,
Voltex,
GamepadVoltex,
}
#[derive(Debug, Clone)]
@@ -52,6 +53,9 @@ pub enum LedMode {
Websocket {
url: String,
},
Serial {
port: String,
},
}
#[derive(Debug, Clone)]
@@ -95,8 +99,12 @@ impl Config {
layout: KeyboardLayout::Voltex,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"gamepad-voltex" => OutputMode::Keyboard {
layout: KeyboardLayout::GamepadVoltex,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"websocket" => OutputMode::Websocket {
url: v["outputWebsocketUrl"].to_string(),
url: v["outputWebsocketUrl"].as_str()?.to_string(),
},
_ => panic!("Invalid output mode"),
},
@@ -121,7 +129,10 @@ impl Config {
"attract" => LedMode::Attract,
"test" => LedMode::Test,
"websocket" => LedMode::Websocket {
url: v["ledWebsocketUrl"].to_string(),
url: v["ledWebsocketUrl"].as_str()?.to_string(),
},
"serial" => LedMode::Serial {
port: v["ledSerialPort"].as_str()?.to_string(),
},
_ => panic!("Invalid led mode"),
},
@@ -137,7 +148,8 @@ impl Config {
"keyboardSensitivity": 20,
"outputWebsocketUrl": "localhost:3000",
"ledSensitivity": 20,
"ledWebsocketUrl": "localhost:3001"
"ledWebsocketUrl": "localhost:3001",
"ledSerialPort": "COM5"
}"#,
)
.unwrap()

View File

@@ -1,5 +1,5 @@
use std::{
error,
error::Error,
ops::{Deref, DerefMut},
thread,
time::Duration,
@@ -12,27 +12,10 @@ use rusb::{self, DeviceHandle, GlobalContext};
use crate::slider_io::{
config::DeviceMode,
controller_state::{ControllerState, FullState, LedState},
utils::{Buffer, ShimError},
worker::Job,
};
pub struct Buffer {
pub data: [u8; 256],
pub len: usize,
}
impl Buffer {
pub fn new() -> Self {
Buffer {
data: [0; 256],
len: 0,
}
}
fn slice(&self) -> &[u8] {
&self.data[0..self.len]
}
}
type HidReadCallback = fn(&Buffer, &mut ControllerState) -> ();
type HidLedCallback = fn(&mut Buffer, &LedState) -> ();
@@ -204,11 +187,12 @@ impl HidDeviceJob {
}
}
fn setup_impl(&mut self) -> Result<(), Box<dyn error::Error>> {
fn setup_impl(&mut self) -> Result<(), Box<dyn Error>> {
info!("Device finding vid {} pid {}", self.vid, self.pid);
let handle = rusb::open_device_with_vid_pid(self.vid, self.pid);
if handle.is_none() {
error!("Could not find device");
error!("Device not found");
return Err(Box::new(ShimError));
}
let mut handle = handle.unwrap();
info!("Device found {:?}", handle);
@@ -229,8 +213,17 @@ impl HidDeviceJob {
const TIMEOUT: Duration = Duration::from_millis(20);
impl Job for HidDeviceJob {
fn setup(&mut self) {
self.setup_impl().unwrap();
fn setup(&mut self) -> bool {
match self.setup_impl() {
Ok(r) => {
info!("Device OK");
true
}
Err(e) => {
error!("Device setup failed, exiting thread early");
false
}
}
}
fn tick(&mut self) {
@@ -276,7 +269,9 @@ impl Job for HidDeviceJob {
}
fn teardown(&mut self) {
let handle = self.handle.as_mut().unwrap();
handle.release_interface(0).ok();
if self.handle.is_some() {
let handle = self.handle.as_mut().unwrap();
handle.release_interface(0).ok();
}
}
}

View File

@@ -0,0 +1,101 @@
use std::sync::{Arc, Mutex};
use vigem_client::{Client, TargetId, XButtons, XGamepad, Xbox360Wired};
use crate::slider_io::{output::OutputHandler, voltex::VoltexState};
pub struct GamepadOutput {
target: Xbox360Wired<Client>,
gamepad: XGamepad,
}
impl GamepadOutput {
pub fn new() -> Self {
let client = Client::connect().unwrap();
let mut target = Xbox360Wired::new(client, TargetId::XBOX360_WIRED);
target.plugin().unwrap();
target.wait_ready().unwrap();
Self {
target,
gamepad: XGamepad::default(),
}
}
}
impl OutputHandler for GamepadOutput {
fn tick(&mut self, flat_controller_state: &Vec<bool>) {
let voltex_state = VoltexState::from_flat(flat_controller_state);
let buttons = voltex_state
.bt
.iter()
.chain(voltex_state.fx.iter())
.chain(voltex_state.extra.iter())
.zip([
XButtons::A,
XButtons::B,
XButtons::X,
XButtons::Y,
XButtons::LB,
XButtons::RB,
XButtons::START,
XButtons::BACK,
XButtons::GUIDE,
])
.fold(0, |buttons, (state, code)| {
buttons
| match state {
true => code,
false => 0,
}
});
let lx = (match voltex_state.laser[0] {
true => -30000,
false => 0,
} + match voltex_state.laser[1] {
true => 30000,
false => 0,
});
let rx = (match voltex_state.laser[2] {
true => -30000,
false => 0,
} + match voltex_state.laser[3] {
true => 30000,
false => 0,
});
let mut dirty = false;
if self.gamepad.buttons.raw != buttons {
self.gamepad.buttons.raw = buttons;
dirty = true;
}
if self.gamepad.thumb_lx != lx {
self.gamepad.thumb_lx = lx;
dirty = true;
}
if self.gamepad.thumb_rx != rx {
self.gamepad.thumb_rx = rx;
dirty = true;
}
if dirty {
self.target.update(&self.gamepad).unwrap();
}
}
fn reset(&mut self) {
self.gamepad = XGamepad::default();
self.target.update(&self.gamepad).unwrap();
}
}
impl Drop for GamepadOutput {
fn drop(&mut self) {
self.target.unplug().unwrap();
}
}
// dammit vigem_client::Event
unsafe impl Send for GamepadOutput {}

View File

@@ -5,7 +5,7 @@ use winapi::{
um::winuser::{SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP},
};
use crate::slider_io::config::KeyboardLayout;
use crate::slider_io::{config::KeyboardLayout, output::OutputHandler};
#[rustfmt::skip]
const TASOLLER_KB_MAP: [usize; 41] = [
@@ -80,6 +80,7 @@ impl KeyboardOutput {
KeyboardLayout::Yuancon => &YUANCON_KB_MAP,
KeyboardLayout::Deemo => &DEEMO_KB_MAP,
KeyboardLayout::Voltex => &VOLTEX_KB_MAP,
_ => panic!("Not implemented"),
};
let mut ground_to_idx = [0 as usize; 41];
@@ -121,21 +122,6 @@ impl KeyboardOutput {
}
}
pub fn tick(&mut self, flat_controller_state: &Vec<bool>) {
self.next_keys.fill(false);
for (idx, x) in flat_controller_state.iter().enumerate() {
if *x {
self.next_keys[self.ground_to_idx[idx]] = true;
}
}
self.send();
}
pub fn reset(&mut self) {
self.next_keys.fill(false);
self.send();
}
fn send(&mut self) {
self.n_kb_buf = 0;
@@ -178,3 +164,26 @@ impl KeyboardOutput {
}
}
}
impl OutputHandler for KeyboardOutput {
fn tick(&mut self, flat_controller_state: &Vec<bool>) {
self.next_keys.fill(false);
for (idx, x) in flat_controller_state.iter().enumerate() {
if *x {
self.next_keys[self.ground_to_idx[idx]] = true;
}
}
self.send();
}
fn reset(&mut self) {
self.next_keys.fill(false);
self.send();
}
}
impl Drop for KeyboardOutput {
fn drop(&mut self) {
self.reset();
}
}

View File

@@ -4,17 +4,23 @@ use std::{
time::{Duration, Instant},
};
use log::{error, info};
use palette::{FromColor, Hsv, Srgb};
use serialport::{ClearBuffer, SerialPort, StopBits};
use crate::slider_io::{
config::{LedMode, ReactiveLayout},
controller_state::{FullState, LedState},
utils::Buffer,
voltex::VoltexState,
worker::Job,
};
pub struct LedJob {
state: FullState,
mode: LedMode,
serial_port: Option<Box<dyn SerialPort>>,
}
impl LedJob {
@@ -22,10 +28,16 @@ impl LedJob {
Self {
state: state.clone(),
mode: mode.clone(),
serial_port: None,
}
}
fn calc_lights(&self, flat_controller_state: Option<&Vec<bool>>, led_state: &mut LedState) {
fn calc_lights(
&self,
flat_controller_state: Option<&Vec<bool>>,
serial_buffer: Option<&Buffer>,
led_state: &mut LedState,
) {
match self.mode {
LedMode::Reactive { layout, .. } => {
let flat_controller_state = flat_controller_state.unwrap();
@@ -63,47 +75,40 @@ impl LedJob {
}
led_state.paint(27, &[64, 0, 0]);
// Left laser left
if flat_controller_state[0..4].contains(&true) {
for idx in 0..3 {
led_state.paint(idx, &[0, 0, 255]);
}
};
let voltex_state = VoltexState::from_flat(flat_controller_state);
// Left laser right
if flat_controller_state[4..8].contains(&true) {
for idx in 4..7 {
led_state.paint(idx, &[0, 0, 255]);
// Left laser
for (idx, state) in voltex_state.laser[0..2].iter().enumerate() {
if *state {
led_state.paint(0 + idx * 4, &[0, 0, 255]);
led_state.paint(1 + idx * 4, &[0, 0, 255]);
led_state.paint(2 + idx * 4, &[0, 0, 255]);
}
};
}
// Right laser left
if flat_controller_state[24..28].contains(&true) {
for idx in 24..27 {
led_state.paint(idx, &[255, 0, 0]);
// Right laser
for (idx, state) in voltex_state.laser[2..4].iter().enumerate() {
if *state {
led_state.paint(24 + idx * 4, &[255, 0, 0]);
led_state.paint(25 + idx * 4, &[255, 0, 0]);
led_state.paint(26 + idx * 4, &[255, 0, 0]);
}
};
// Right laser right
if flat_controller_state[28..32].contains(&true) {
for idx in 28..31 {
led_state.paint(idx, &[255, 0, 0]);
}
};
}
// Buttons
for (btn_idx, btn_banks) in flat_controller_state[8..24].chunks(4).enumerate() {
if btn_banks.iter().skip(1).step_by(2).any(|x| *x) {
led_state.paint(8 + btn_idx * 4, &[255, 255, 255]);
led_state.paint(10 + btn_idx * 4, &[255, 255, 255]);
for (idx, state) in voltex_state.bt.iter().enumerate() {
if *state {
led_state.paint(8 + idx * 4, &[255, 255, 255]);
led_state.paint(10 + idx * 4, &[255, 255, 255]);
}
}
// Fx
for (fx_idx, fx_banks) in flat_controller_state[8..24].chunks(8).enumerate() {
if fx_banks.iter().step_by(2).any(|x| *x) {
led_state.paint(9 + fx_idx * 8, &[255, 0, 0]);
led_state.paint(11 + fx_idx * 8, &[255, 0, 0]);
led_state.paint(13 + fx_idx * 8, &[255, 0, 0]);
for (idx, state) in voltex_state.fx.iter().enumerate() {
if *state {
led_state.paint(9 + idx * 8, &[255, 0, 0]);
led_state.paint(11 + idx * 8, &[255, 0, 0]);
led_state.paint(13 + idx * 8, &[255, 0, 0]);
}
}
}
@@ -118,6 +123,26 @@ impl LedJob {
led_state.paint(idx, &[color.red, color.green, color.blue]);
}
}
LedMode::Serial { .. } => {
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialProcessor.h
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialProcessor.cpp
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialLeds.h
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialLeds.cpp
if let Some(serial_buffer) = serial_buffer {
// println!("buffer {:?}", serial_buffer.data);
if serial_buffer.data[0] == 0xaa && serial_buffer.data[1] == 0xaa {
for (idx, buf_chunk) in serial_buffer.data[2..95]
.chunks(3)
.take(31)
.rev()
.enumerate()
{
led_state.paint(idx, &[(*buf_chunk)[1], (*buf_chunk)[2], (*buf_chunk)[0]]);
}
println!("leds {:?}", led_state.led_state);
}
}
}
_ => panic!("Not implemented"),
}
@@ -126,22 +151,77 @@ impl LedJob {
}
impl Job for LedJob {
fn setup(&mut self) {}
fn setup(&mut self) -> bool {
match &self.mode {
LedMode::Serial { port } => {
info!(
"Serial port for led opening at {} {:?}",
port.as_str(),
115200
);
self.serial_port = match serialport::new(port, 115200).open() {
Ok(s) => {
info!("Serial port opened");
Some(s)
}
Err(e) => {
error!("Serial port could not open, exiting thread early");
None
}
};
self.serial_port.is_some()
}
_ => true,
}
}
fn tick(&mut self) {
let flat_controller_state: Option<Vec<bool>> = match self.mode {
let mut flat_controller_state: Option<Vec<bool>> = None;
let mut serial_buffer: Option<Buffer> = None;
// Do the IO here
match self.mode {
LedMode::Reactive { sensitivity, .. } => {
let controller_state_handle = self.state.controller_state.lock().unwrap();
Some(controller_state_handle.flat(&sensitivity))
flat_controller_state = Some(controller_state_handle.flat(&sensitivity));
}
_ => None,
};
LedMode::Serial { .. } => {
if let Some(serial_port) = self.serial_port.as_mut() {
let mut serial_data_avail = serial_port.bytes_to_read().unwrap_or(0);
if serial_data_avail < 100 {
return;
}
if serial_data_avail % 100 == 0 {
let mut serial_buffer_working = Buffer::new();
serial_port
.as_mut()
.read_exact(&mut serial_buffer_working.data[..100])
.ok()
.unwrap();
serial_data_avail -= 100;
serial_buffer = Some(serial_buffer_working);
}
if serial_data_avail > 0 {
serial_port.clear(ClearBuffer::All);
}
}
}
_ => {}
}
// Then calculate and transfer
{
let mut led_state_handle = self.state.led_state.lock().unwrap();
self.calc_lights(flat_controller_state.as_ref(), led_state_handle.deref_mut());
self.calc_lights(
flat_controller_state.as_ref(),
serial_buffer.as_ref(),
led_state_handle.deref_mut(),
);
}
thread::sleep(Duration::from_millis(33));
thread::sleep(Duration::from_millis(30));
}
fn teardown(&mut self) {}

View File

@@ -2,7 +2,7 @@ use log::info;
use crate::slider_io::{
config::Config, controller_state::FullState, device::HidDeviceJob, led::LedJob,
output::KeyboardOutputJob, worker::Worker,
output::OutputJob, worker::Worker,
};
pub struct Manager {
@@ -15,13 +15,16 @@ pub struct Manager {
impl Manager {
pub fn new(config: Config) -> Self {
info!("Starting manager");
info!("Device config {:?}", config.device_mode);
info!("Output config {:?}", config.output_mode);
info!("LED config {:?}", config.led_mode);
let state = FullState::new();
let device_worker = Worker::new(HidDeviceJob::from_config(&state, &config.device_mode));
let output_worker = Worker::new(KeyboardOutputJob::new(&state, &config.output_mode));
let output_worker = Worker::new(OutputJob::new(&state, &config.output_mode));
let led_worker = Worker::new(LedJob::new(&state, &config.led_mode));
info!("Starting manager with config: {:?}", config);
Self {
state,
config,

View File

@@ -1,8 +1,11 @@
mod config;
mod controller_state;
mod utils;
mod worker;
mod controller_state;
mod voltex;
mod gamepad;
mod keyboard;
mod device;

View File

@@ -1,16 +1,25 @@
use std::{thread, time::Duration};
use crate::slider_io::{
config::OutputMode, controller_state::FullState, keyboard::KeyboardOutput, worker::Job,
config::{KeyboardLayout, OutputMode},
controller_state::FullState,
gamepad::GamepadOutput,
keyboard::KeyboardOutput,
worker::Job,
};
pub struct KeyboardOutputJob {
state: FullState,
sensitivity: u8,
keyboard_output: KeyboardOutput,
pub trait OutputHandler: Send + Drop {
fn tick(&mut self, flat_controller_state: &Vec<bool>);
fn reset(&mut self);
}
impl KeyboardOutputJob {
pub struct OutputJob {
state: FullState,
sensitivity: u8,
handler: Box<dyn OutputHandler>,
}
impl OutputJob {
pub fn new(state: &FullState, mode: &OutputMode) -> Self {
match mode {
OutputMode::Keyboard {
@@ -19,15 +28,20 @@ impl KeyboardOutputJob {
} => Self {
state: state.clone(),
sensitivity: *sensitivity,
keyboard_output: KeyboardOutput::new(layout.clone()),
handler: match layout {
KeyboardLayout::GamepadVoltex => Box::new(GamepadOutput::new()),
layout => Box::new(KeyboardOutput::new(layout.clone())),
},
},
_ => panic!("Not implemented"),
}
}
}
impl Job for KeyboardOutputJob {
fn setup(&mut self) {}
impl Job for OutputJob {
fn setup(&mut self) -> bool {
true
}
fn tick(&mut self) {
let flat_controller_state: Vec<bool>;
@@ -36,11 +50,11 @@ impl Job for KeyboardOutputJob {
flat_controller_state = controller_state_handle.flat(&self.sensitivity);
}
self.keyboard_output.tick(&flat_controller_state);
self.handler.tick(&flat_controller_state);
thread::sleep(Duration::from_millis(10));
}
fn teardown(&mut self) {
self.keyboard_output.reset();
self.handler.reset();
}
}

View File

@@ -0,0 +1,34 @@
use std::{error::Error, fmt};
pub struct Buffer {
pub data: [u8; 256],
pub len: usize,
}
impl Buffer {
pub fn new() -> Self {
Buffer {
data: [0; 256],
len: 0,
}
}
fn slice(&self) -> &[u8] {
&self.data[0..self.len]
}
}
#[derive(Debug)]
pub struct ShimError;
impl<'a> fmt::Display for ShimError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ShimError")
}
}
impl Error for ShimError {
fn description(&self) -> &str {
"shimError"
}
}

View File

@@ -0,0 +1,39 @@
pub struct VoltexState {
pub laser: [bool; 4],
pub bt: [bool; 4],
pub fx: [bool; 2],
pub extra: [bool; 3],
}
impl VoltexState {
pub fn from_flat(flat_controller_state: &Vec<bool>) -> Self {
let mut voltex_state = Self {
laser: [false; 4],
bt: [false; 4],
fx: [false; 2],
extra: [false; 3],
};
voltex_state.laser[0] = flat_controller_state[0..4].contains(&true);
voltex_state.laser[1] = flat_controller_state[4..8].contains(&true);
voltex_state.laser[2] = flat_controller_state[24..28].contains(&true);
voltex_state.laser[3] = flat_controller_state[28..32].contains(&true);
for i in 0..4 {
voltex_state.bt[i] = flat_controller_state[9 + i * 4] || flat_controller_state[11 + i * 4];
}
for i in 0..2 {
voltex_state.fx[i] = flat_controller_state[8 + i * 8]
|| flat_controller_state[10 + i * 8]
|| flat_controller_state[12 + i * 8]
|| flat_controller_state[14 + i * 8];
}
for i in 0..3 {
voltex_state.extra[i] = flat_controller_state[38 + i];
}
voltex_state
}
}

View File

@@ -7,7 +7,7 @@ use std::{
};
pub trait Job: Send {
fn setup(&mut self);
fn setup(&mut self) -> bool;
fn tick(&mut self);
fn teardown(&mut self);
}
@@ -24,12 +24,14 @@ impl Worker {
let stop_signal_clone = Arc::clone(&stop_signal);
Self {
thread: Some(thread::spawn(move || {
job.setup();
let setup_res = job.setup();
stop_signal_clone.store(!setup_res, Ordering::SeqCst);
loop {
job.tick();
if stop_signal_clone.load(Ordering::SeqCst) {
break;
}
job.tick();
}
job.teardown();
})),
@@ -40,7 +42,7 @@ impl Worker {
impl Drop for Worker {
fn drop(&mut self) {
self.stop_signal.swap(true, Ordering::SeqCst);
self.stop_signal.store(true, Ordering::SeqCst);
if self.thread.is_some() {
self.thread.take().unwrap().join().ok();
}