├── crates ├── dualshock-sys │ ├── src │ │ ├── output.rs │ │ ├── lib.rs │ │ └── input.rs │ └── Cargo.toml ├── joycon-sys │ ├── src │ │ ├── input │ │ │ ├── mod.rs │ │ │ ├── values.rs │ │ │ └── report.rs │ │ ├── output │ │ │ ├── mod.rs │ │ │ ├── rumble.rs │ │ │ └── report.rs │ │ ├── mcu │ │ │ ├── ir.rs │ │ │ └── ir_register.rs │ │ ├── light.rs │ │ ├── accessory.rs │ │ ├── imu.rs │ │ ├── lib.rs │ │ └── common.rs │ └── Cargo.toml ├── hid-gamepad-types │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── hid-gamepad │ ├── src │ │ ├── error.rs │ │ └── lib.rs │ └── Cargo.toml ├── hid-gamepad-sys │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── dualshock │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── lib.rs └── joycon │ ├── Cargo.toml │ └── src │ ├── calibration.rs │ ├── image.rs │ ├── lib.rs │ └── imu_handler.rs ├── .gitignore ├── wireshark-joycon ├── src │ ├── wrapper.h │ └── lib.rs ├── Cargo.toml └── build.rs ├── joy-infrared ├── src │ ├── render │ │ ├── shaders │ │ │ ├── uniform.glsl │ │ │ ├── 2d.vert │ │ │ ├── 2d.frag │ │ │ ├── 3d.vert │ │ │ ├── 3d.frag │ │ │ └── compute.comp │ │ ├── buffer.rs │ │ ├── d2.rs │ │ ├── uniforms.rs │ │ ├── camera.rs │ │ ├── d3.rs │ │ ├── texture.rs │ │ └── ir_compute.rs │ ├── mouse.rs │ └── main.rs └── Cargo.toml ├── joy-music ├── Cargo.toml └── src │ └── main.rs ├── Cargo.toml ├── trace ├── README.md ├── exit-ringfit └── reopen-ringfit ├── joytk ├── Cargo.toml └── src │ ├── opts.rs │ ├── camera.rs │ ├── relay.rs │ └── interface.rs ├── README.md ├── LICENSE ├── .github └── workflows │ ├── rustsec.yml │ └── rust.yml └── .vscode ├── tasks.json └── launch.json /crates/dualshock-sys/src/output.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | flamegraph.svg 4 | perf.data* 5 | -------------------------------------------------------------------------------- /wireshark-joycon/src/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include -------------------------------------------------------------------------------- /crates/joycon-sys/src/input/mod.rs: -------------------------------------------------------------------------------- 1 | mod values; 2 | mod report; 3 | 4 | pub use values::*; 5 | pub use report::*; 6 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/output/mod.rs: -------------------------------------------------------------------------------- 1 | mod report; 2 | mod rumble; 3 | 4 | pub use report::*; 5 | pub use rumble::*; 6 | -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/uniform.glsl: -------------------------------------------------------------------------------- 1 | layout(std140) 2 | uniform Uniforms { 3 | mat4 ir_proj; 4 | mat4 ir_rotation; 5 | mat4 view_proj; 6 | mat4 normal_transform; 7 | } u; -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/2d.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec2 in_position; 4 | layout(location = 1) in vec2 in_uv; 5 | 6 | layout(location = 0) out VertexData { 7 | vec2 uv; 8 | } o; 9 | 10 | void main() { 11 | gl_Position = vec4(in_position, 0., 1.); 12 | o.uv = in_uv; 13 | } 14 | -------------------------------------------------------------------------------- /crates/hid-gamepad-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-gamepad-types" 3 | license = "MIT" 4 | version = "0.1.0" 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | enum-map = "2.7" 11 | cgmath = { version = "0.18", default-features = false } 12 | -------------------------------------------------------------------------------- /joy-music/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "joy-music" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Mikaël Fourrier "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | anyhow = "1.0" 12 | joycon = { path = "../crates/joycon" } 13 | -------------------------------------------------------------------------------- /crates/hid-gamepad/src/error.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use hidapi::HidError; 3 | use thiserror::Error; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | #[derive(Error, Debug)] 8 | pub enum GamepadError { 9 | #[error("communication error")] 10 | Hidapi(#[from] HidError), 11 | #[error("other error")] 12 | Anyhow(#[from] Error), 13 | } 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/dualshock-sys", 5 | "crates/dualshock", 6 | "crates/hid-gamepad", 7 | "crates/hid-gamepad-sys", 8 | "crates/hid-gamepad-types", 9 | "crates/joycon-sys", 10 | "crates/joycon", 11 | # removed for huge number of deps 12 | #"joy-infrared", 13 | "joy-music", 14 | "joytk", 15 | ] 16 | -------------------------------------------------------------------------------- /crates/hid-gamepad-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use hid_gamepad_types::*; 2 | 3 | pub trait GamepadDriver { 4 | fn init( 5 | &self, 6 | api: &hidapi::HidApi, 7 | device_info: &hidapi::DeviceInfo, 8 | ) -> anyhow::Result>>; 9 | } 10 | 11 | pub trait GamepadDevice { 12 | fn recv(&mut self) -> anyhow::Result; 13 | fn as_any(&mut self) -> &mut dyn std::any::Any; 14 | } 15 | -------------------------------------------------------------------------------- /wireshark-joycon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wireshark-joycon" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | joycon-sys = { path = "../joycon-sys" } 15 | 16 | [build-dependencies] 17 | bindgen = "0.57" 18 | -------------------------------------------------------------------------------- /crates/hid-gamepad-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-gamepad-sys" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | 9 | [dependencies] 10 | enum-map = "2.7" 11 | hidapi = { version = "1.2", default-features = false, features = ["linux-static-hidraw"] } 12 | cgmath = { version = "0.18", default-features = false } 13 | anyhow = "1.0" 14 | hid-gamepad-types = { path = "../hid-gamepad-types/" } 15 | -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/2d.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in VertexData { 4 | vec2 uv; 5 | } i; 6 | 7 | layout(location = 0) out vec4 out_color; 8 | 9 | layout(set = 0, binding = 0) uniform texture2D ir_pixels; 10 | layout(set = 0, binding = 1) uniform sampler ir_sampler; 11 | 12 | void main() { 13 | float intensity = texture(sampler2D(ir_pixels, ir_sampler), i.uv).r; 14 | out_color = vec4(intensity, intensity, intensity, 1.); 15 | } 16 | -------------------------------------------------------------------------------- /crates/dualshock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dualshock" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | anyhow = "1.0" 12 | cgmath = "0.18" 13 | dualshock-sys = { path = "../dualshock-sys/" } 14 | hidapi = { version = "1.2", default-features = false, features = ["linux-static-hidraw"] } 15 | hid-gamepad-sys = { path = "../hid-gamepad-sys/" } 16 | enum-map = "2.7" 17 | -------------------------------------------------------------------------------- /crates/dualshock-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dualshock-sys" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | num = { version = "0.4", optional = false, default-features = false } 12 | num-traits = { version = "0.2", optional = false, default-features = false } 13 | num-derive = { version = "0.3", optional = false, default-features = false } 14 | cgmath = "0.18" 15 | bitfield = "0.13" 16 | -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/3d.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout(location = 0) in vec3 in_position; 4 | layout(location = 1) in vec2 in_uv; 5 | layout(location = 2) in float in_depth; 6 | 7 | layout(location = 0) out VertexData { 8 | vec3 position; 9 | vec2 uv; 10 | float depth; 11 | } o; 12 | 13 | layout(set = 0, binding = 0) 14 | #include "uniform.glsl" 15 | 16 | void main() { 17 | vec4 moved = u.ir_rotation * vec4(in_position, 1.0); 18 | gl_Position = u.view_proj * moved; 19 | o.position = moved.xyz / moved.w; 20 | o.uv = in_uv; 21 | o.depth = in_depth; 22 | } 23 | -------------------------------------------------------------------------------- /crates/hid-gamepad/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-gamepad" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | anyhow = "1.0" 12 | cgmath = "0.18" 13 | dualshock = { path = "../dualshock/" } 14 | enum-map = "0.6" 15 | hid-gamepad-sys = { path = "../hid-gamepad-sys/" } 16 | hidapi = { version = "1.2", default-features = false, features = ["linux-static-hidraw"] } 17 | joycon = { path = "../joycon/" } 18 | thiserror = "1.0" 19 | -------------------------------------------------------------------------------- /crates/joycon-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "joycon-sys" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | bitfield = { version = "0.13", optional = false, default-features = false } 12 | num = { version = "0.4", optional = false, default-features = false } 13 | num-traits = { version = "0.2", optional = false, default-features = false } 14 | num-derive = { version = "0.3", optional = false, default-features = false } 15 | cgmath = { version = "0.18", optional = false, default-features = false } -------------------------------------------------------------------------------- /crates/joycon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "joycon" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Yamakaky "] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [features] 11 | ir = ["image"] 12 | 13 | [dependencies] 14 | anyhow = "1.0" 15 | cgmath = { version = "0.18", optional = false, default-features = false } 16 | hidapi = { version = "1.2", default-features = false, features = ["linux-static-hidraw"] } 17 | image = { version = "0.24", features = ["png"], optional = true, default-features = false } 18 | joycon-sys = { path = "../joycon-sys" } 19 | hid-gamepad-sys = { path = "../hid-gamepad-sys/" } 20 | enum-map = "2.7" 21 | tracing = "0.1" 22 | hex = "0.4" 23 | -------------------------------------------------------------------------------- /joy-infrared/src/mouse.rs: -------------------------------------------------------------------------------- 1 | use enigo::*; 2 | 3 | #[derive(Debug)] 4 | pub struct Mouse { 5 | enigo: Enigo, 6 | diff_x: f32, 7 | diff_y: f32, 8 | } 9 | 10 | impl Mouse { 11 | pub fn new() -> Self { 12 | Self { 13 | enigo: Enigo::new(), 14 | diff_x: 0., 15 | diff_y: 0., 16 | } 17 | } 18 | 19 | #[allow(dead_code)] 20 | pub fn move_relative(&mut self, mut x: f32, mut y: f32) { 21 | // enigo works with pixels, so we keep the remainder to not smooth the small movements. 22 | x += self.diff_x; 23 | y += self.diff_y; 24 | let round_x = x.round(); 25 | let round_y = y.round(); 26 | self.diff_x = x - round_x; 27 | self.diff_y = y - round_y; 28 | 29 | self.enigo 30 | .mouse_move_relative(round_x as i32, round_y as i32); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /trace/README.md: -------------------------------------------------------------------------------- 1 | ## How to capture 2 | 3 | Use this script https://github.com/Yamakaky/joycontrol/blob/capture-text-file/scripts/relay_joycon.py. 4 | `python -m venv` is useful to not have to install everything on `/usr`. 5 | 6 | Example: 7 | 8 | `sudo python scripts/relay_joycon.py -l /tmp/bt.log` 9 | 10 | Then use this command for next runs: 11 | 12 | `sudo python scripts/relay_joycon.py -r -l /tmp/bt.log` 13 | 14 | It can take some time and doesn't always work so try restarting it a few times. 15 | 16 | ## How to parse 17 | 18 | Send a capture file to stdin of `joytk decode`. For example to filter out the 19 | standard input reports and show less output for subcommand requests and 20 | replies: 21 | 22 | ```bash 23 | cat trace/homescreen.log \ 24 | | cargo run --bin joytk decode \ 25 | | grep -vw '(StandardFull|RumbleOnly)' \ 26 | | sed -re 's/.*(subcommand_reply|subcmd): //' 27 | ``` 28 | -------------------------------------------------------------------------------- /joytk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "joytk" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Mikaël Fourrier "] 6 | edition = "2018" 7 | 8 | [features] 9 | interface = ["tui", "crossterm"] 10 | 11 | [dependencies] 12 | anyhow = "1.0.53" 13 | cgmath = "0.18.0" 14 | clap = { version = "3.1.0", features = ["derive"] } 15 | colored = "2.0.0" 16 | hex = "0.4.3" 17 | image = "0.24.0" 18 | joycon = { path = "../crates/joycon", features = ["ir"] } 19 | tracing = "0.1.31" 20 | tracing-subscriber = { version = "0.3.8", features = ["env-filter"] } 21 | crossterm = { version = "0.23.0", optional = true } 22 | tui = { version = "0.17.0", optional = true, default-features = false, features = ["crossterm"] } 23 | pixels = "0.9.0" 24 | winit = "0.26.1" 25 | winit_input_helper = "0.11.0" 26 | 27 | [target.'cfg(target_os = "linux")'.dependencies] 28 | libc = "0.2.118" 29 | socket2 = "0.4.4" 30 | bluetooth-sys = "0.1.0" 31 | -------------------------------------------------------------------------------- /joy-infrared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "joy-infrared" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["yamakaky"] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | anyhow = "1.0" 10 | enigo = { version = "0.0.14", optional = false, default-features = false } 11 | env_logger = "0.8" 12 | image = { version = "0.23", optional = false, default-features = false } 13 | iced_core = { version = "0.3", optional = false, default-features = false } 14 | iced_wgpu = { version = "0.3", optional = false, default-features = false } 15 | iced_winit = { version = "0.2", optional = false, default-features = false } 16 | vk-shader-macros = { version = "0.2", optional = false, default-features = false } 17 | cgmath = { version = "0.18", optional = false, default-features = false } 18 | bytemuck = { version = "1.4", optional = false, default-features = false } 19 | joycon = { path = "../joycon" } 20 | smol = { version = "1.2", optional = false, default-features = false } -------------------------------------------------------------------------------- /crates/hid-gamepad/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | 3 | use dualshock::DS4Driver; 4 | use hid_gamepad_sys::{GamepadDevice, GamepadDriver}; 5 | use hidapi::{DeviceInfo, HidApi}; 6 | 7 | pub use hid_gamepad_sys as sys; 8 | 9 | pub use error::*; 10 | use joycon::{JoyCon, JoyconDriver}; 11 | 12 | pub fn open_gamepad( 13 | api: &HidApi, 14 | device_info: &DeviceInfo, 15 | ) -> Result>> { 16 | let mut drivers: Vec> = 17 | vec![Box::new(DS4Driver), Box::new(JoyconDriver)]; 18 | for driver in drivers.drain(..) { 19 | if let Some(device) = driver.init(api, device_info)? { 20 | return Ok(Some(device)); 21 | } 22 | } 23 | Ok(None) 24 | } 25 | 26 | pub fn pote() { 27 | let api = HidApi::new().unwrap(); 28 | let device_info = api.device_list().next().unwrap(); 29 | let mut x = open_gamepad(&api, device_info).unwrap().unwrap(); 30 | x.as_any().downcast_ref::>(); 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joy 2 | 3 | Suite of tools and libraries for interactions with Nintendo Switch and DualShock 4 controllers. 4 | 5 | ## External dependencies 6 | 7 | On Linux, you'll need `libusb`, `libbluetooth` and `libudev`. On Ubuntu, you can install these by running: 8 | 9 | ```sh 10 | sudo apt-get install libusb-1.0-0-dev libbluetooth-dev libudev-dev 11 | ``` 12 | 13 | ## Tools 14 | 15 | The tools can be run with `cargo run --bin `. 16 | 17 | - `joytk`: main front-facing tool. 18 | - `joy-infrared`: visualize the images captured by the infrared camera of the Joycon(R) as a realtime 3D view. 19 | 20 | ## Libraries 21 | 22 | - [`joycon-sys`](https://yamakaky.github.io/joy/joycon_sys): decoding and encoding HID reports. Doesn't include any I/O. 23 | - [`joycon`](https://yamakaky.github.io/joy/joycon): implements I/O and communication protocols on top of `joycon-sys`. 24 | - [`dualshock`](https://yamakaky.github.io/joy/dualshock): decoding HID reports from the DS4 controller. 25 | - [`hid-gamepad`](https://yamakaky.github.io/joy/hid_gamepad): abstraction above `dualshock` and `joycon`. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yamakaky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/joycon/src/calibration.rs: -------------------------------------------------------------------------------- 1 | use cgmath::*; 2 | use joycon_sys::imu::IMU_SAMPLES_PER_SECOND; 3 | use std::collections::VecDeque; 4 | 5 | type Entry = Vector3; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Calibration { 9 | history: VecDeque, 10 | capacity: usize, 11 | } 12 | 13 | impl Calibration { 14 | pub fn with_capacity(capacity: usize) -> Calibration { 15 | Calibration { 16 | history: VecDeque::with_capacity(capacity), 17 | capacity, 18 | } 19 | } 20 | 21 | pub fn push(&mut self, entry: Entry) { 22 | if self.history.len() == self.capacity { 23 | self.history.pop_back(); 24 | } 25 | self.history.push_front(entry); 26 | } 27 | 28 | pub fn reset(&mut self) { 29 | self.history.clear(); 30 | } 31 | 32 | pub fn get_average(&mut self) -> Entry { 33 | let zero = Vector3::new(0., 0., 0.); 34 | let len = self.history.len() as f64; 35 | if len == 0. { 36 | return zero; 37 | } 38 | self.history 39 | .iter() 40 | .cloned() 41 | .fold(zero, |acc, val| acc + val) 42 | / len 43 | } 44 | } 45 | 46 | impl Default for Calibration { 47 | fn default() -> Self { 48 | Calibration::with_capacity(3 * IMU_SAMPLES_PER_SECOND as usize) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /wireshark-joycon/build.rs: -------------------------------------------------------------------------------- 1 | extern crate bindgen; 2 | 3 | use std::env; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | // Tell cargo to tell rustc to link the system bzip2 8 | // shared library. 9 | 10 | // Tell cargo to invalidate the built crate whenever the wrapper changes 11 | println!("cargo:rerun-if-changed=src/wrapper.h"); 12 | 13 | // The bindgen::Builder is the main entry point 14 | // to bindgen, and lets you build up options for 15 | // the resulting bindings. 16 | let bindings = bindgen::Builder::default() 17 | // The input header we would like to generate 18 | // bindings for. 19 | .header("src/wrapper.h") 20 | // Tell cargo to invalidate the built crate whenever any of the 21 | // included header files changed. 22 | .parse_callbacks(Box::new(bindgen::CargoCallbacks)) 23 | .clang_args(&[ 24 | "-I/usr/include/wireshark", 25 | "-I/usr/include/glib-2.0", 26 | "-I/usr/lib/glib-2.0/include", 27 | "-D", 28 | "HAVE_PLUGINS=1", 29 | ]) 30 | // Finish the builder and generate the bindings. 31 | .generate() 32 | // Unwrap the Result and panic on failure. 33 | .expect("Unable to generate bindings"); 34 | 35 | // Write the bindings to the $OUT_DIR/bindings.rs file. 36 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 37 | bindings 38 | .write_to_file(out_path.join("bindings.rs")) 39 | .expect("Couldn't write bindings!"); 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/rustsec.yml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 actions-rs team and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | # This workflow checks for relevant RustSec advisories and automatically opens 24 | # issues for them. See here for details: 25 | # 26 | name: Security audit 27 | on: 28 | schedule: 29 | - cron: '0 0 * * *' 30 | jobs: 31 | audit: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v1 35 | - uses: actions-rs/audit-check@v1 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "cargo", 6 | "command": "test", 7 | "args": [ 8 | "--workspace" 9 | ], 10 | "problemMatcher": [ 11 | "$rustc" 12 | ], 13 | "group": { 14 | "kind": "test", 15 | "isDefault": true 16 | }, 17 | "label": "rust: cargo test" 18 | }, 19 | { 20 | "type": "cargo", 21 | "command": "run", 22 | "problemMatcher": [ 23 | "$rustc" 24 | ], 25 | "options": { 26 | "env": { 27 | "RUST_BACKTRACE": "1", 28 | } 29 | }, 30 | "label": "rust: cargo run" 31 | }, 32 | { 33 | "type": "cargo", 34 | "command": "doc", 35 | "args": [ 36 | "--open", 37 | "--workspace", 38 | " --no-deps" 39 | ], 40 | "problemMatcher": [ 41 | "$rustc" 42 | ], 43 | "group": "build", 44 | "label": "rust: cargo doc --open --no-deps" 45 | }, 46 | { 47 | "type": "cargo", 48 | "command": "check", 49 | "args": [ 50 | "--workspace" 51 | ], 52 | "problemMatcher": [ 53 | "$rustc" 54 | ], 55 | "group": "test", 56 | "label": "rust: cargo check" 57 | }, 58 | { 59 | "type": "cargo", 60 | "command": "clippy", 61 | "args": [ 62 | "--workspace" 63 | ], 64 | "problemMatcher": [ 65 | "$rustc" 66 | ], 67 | "group": "test", 68 | "label": "rust: cargo clippy" 69 | }, 70 | { 71 | "type": "cargo", 72 | "command": "build", 73 | "args": [ 74 | "--workspace" 75 | ], 76 | "problemMatcher": [ 77 | "$rustc" 78 | ], 79 | "group": { 80 | "kind": "build", 81 | "isDefault": true 82 | }, 83 | "label": "rust: cargo build" 84 | }, 85 | { 86 | "type": "cargo", 87 | "command": "build", 88 | "args": [ 89 | "--workspace", 90 | "--release" 91 | ], 92 | "problemMatcher": [ 93 | "$rustc" 94 | ], 95 | "group": "build", 96 | "label": "rust: cargo build --release" 97 | } 98 | ] 99 | } -------------------------------------------------------------------------------- /trace/exit-ringfit: -------------------------------------------------------------------------------- 1 | 0:05:44.915731 OutputReport { id: RumbleAndSubcmd, counter: 15, subcmd: SubcommandRequest { subcommand: Unknown0x5b, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 2 | 0:05:44.933635 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: Unknown0x5b, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 3 | 0:05:44.966857 OutputReport { id: RumbleAndSubcmd, counter: 0, subcmd: SubcommandRequest { set_imu_mode: _Unknown0x02 } } 4 | 0:05:44.995158 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: SetIMUMode } } 5 | 0:05:45.040751 OutputReport { id: RumbleAndSubcmd, counter: 2, subcmd: SubcommandRequest { subcommand: Unknown0x5c, raw: [0, 0, 150, 227, 28, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 51, 195, 115, 49, 211, 87, 0, 0, 48, 38, 179, 227, 28, 0, 0, 0, 0, 242, 5, 42, 1, 0] } } 6 | 0:05:45.083662 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: Unknown0x5c, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 7 | 0:05:45.100626 OutputReport { id: RumbleAndSubcmd, counter: 3, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(88), set_ir_mode: MCUIRModeData { ir_mode: IRSensorReset, no_of_frags: 0, mcu_fw_version: (0x0, 0x0) } } } } 8 | 0:05:45.128938 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(32), mcu_report: MCUReport { unknown_id: None } } } 9 | 0:05:45.165642 OutputReport { id: RumbleAndSubcmd, counter: 4, subcmd: SubcommandRequest { mcu_state: Suspend } } 10 | 0:05:45.203716 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, mcu_report: MCUReport { type: Some(Empty) } } } 11 | -------------------------------------------------------------------------------- /crates/dualshock/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use cgmath::{Deg, Euler, One, Quaternion}; 4 | use dualshock_sys::{ 5 | input::InputReport, ConnectionType, DS4_REPORT_DT, HID_PRODUCT_ID_NEW, HID_PRODUCT_ID_OLD, 6 | HID_VENDOR_ID, 7 | }; 8 | 9 | fn main() -> anyhow::Result<()> { 10 | let hidapi = hidapi::HidApi::new()?; 11 | let device_info = hidapi 12 | .device_list() 13 | .filter(|d| { 14 | d.vendor_id() == HID_VENDOR_ID 15 | && [HID_PRODUCT_ID_OLD, HID_PRODUCT_ID_NEW].contains(&d.product_id()) 16 | }) 17 | .next() 18 | .unwrap(); 19 | let device = device_info.open_device(&hidapi)?; 20 | 21 | let mut report = InputReport::new(); 22 | let buffer = report.as_bytes_mut(); 23 | let nb_read = device.read(buffer)?; 24 | let conn_type = InputReport::conn_type(nb_read); 25 | 26 | let mut now = Instant::now(); 27 | let mut orientation = Quaternion::one(); 28 | loop { 29 | let mut report = InputReport::new(); 30 | let buffer = report.as_bytes_mut(); 31 | let _nb_read = device.read(buffer)?; 32 | let gyro_speed = match conn_type { 33 | ConnectionType::Bluetooth => { 34 | let report = report.bt_full().unwrap(); 35 | report.full.gyro.normalize() 36 | } 37 | ConnectionType::USB => { 38 | let report = report.usb_full().unwrap(); 39 | report.full.gyro.normalize() 40 | } 41 | }; 42 | 43 | let delta = Euler::new( 44 | Deg(gyro_speed.x * DS4_REPORT_DT), 45 | Deg(gyro_speed.y * DS4_REPORT_DT), 46 | Deg(gyro_speed.z * DS4_REPORT_DT), 47 | ); 48 | 49 | orientation = orientation * Quaternion::from(delta); 50 | if now.elapsed() > Duration::from_millis(500) { 51 | let rot = Euler::from(orientation); 52 | dbg!(Deg::from(rot.x)); 53 | now = Instant::now(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/3d.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | struct Light { 4 | vec4 position; 5 | 6 | vec3 ambient; 7 | vec3 diffuse; 8 | vec3 specular; 9 | 10 | float constant; 11 | float linear; 12 | float quadratic; 13 | }; 14 | 15 | layout(location = 0) in VertexData { 16 | vec3 position; 17 | vec2 uv; 18 | float depth; 19 | } i; 20 | 21 | layout(location = 0) out vec4 out_color; 22 | layout(location = 1) out float out_depth; 23 | 24 | layout(set = 0, binding = 0) 25 | #include "uniform.glsl" 26 | 27 | layout(set = 1, binding = 0) uniform texture2D normals; 28 | layout(set = 1, binding = 1) uniform sampler normals_sampler; 29 | 30 | layout(set = 2, binding = 0, std140) uniform Lights { 31 | uint count; 32 | Light item[10]; 33 | } lights; 34 | 35 | void main() { 36 | vec3 normal_sample = texture(sampler2D(normals, normals_sampler), i.uv).xyz; 37 | vec3 normal = normalize(mat3(u.normal_transform) * normal_sample); 38 | 39 | vec3 lighting = vec3(0.); 40 | for (int idx = 0; idx < lights.count; idx++) { 41 | Light light = lights.item[idx]; 42 | 43 | vec3 light_dir; 44 | float attenuation; 45 | if (light.position.w == 0.) { 46 | // Directional light 47 | light_dir = normalize(light.position.xyz); 48 | attenuation = 0.3; 49 | } else { 50 | // Point light 51 | vec3 light_vec = light.position.xyz / light.position.w - i.position; 52 | light_dir = normalize(light_vec); 53 | float distance = length(light_vec); 54 | attenuation = 1. / ( 55 | 1. + light.linear * distance + light.quadratic * (distance * distance) 56 | ); 57 | } 58 | 59 | float diffuse_strength = max(dot(normal, light_dir), 0.); 60 | vec3 diffuse_color = light.diffuse * diffuse_strength; 61 | 62 | lighting += (light.ambient + diffuse_color) * attenuation; 63 | } 64 | out_color = vec4(1.) * vec4(lighting, 1.); 65 | out_depth = i.depth; 66 | } 67 | -------------------------------------------------------------------------------- /wireshark-joycon/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | non_upper_case_globals, 3 | non_camel_case_types, 4 | non_snake_case, 5 | improper_ctypes 6 | )] 7 | 8 | use std::{ 9 | convert::TryInto, 10 | ffi::{c_void, CString}, 11 | os::raw::{c_int, c_uint}, 12 | }; 13 | 14 | #[no_mangle] 15 | pub static plugin_version: &[u8; 6] = b"0.0.0\0"; 16 | #[no_mangle] 17 | pub static plugin_want_major: c_uint = WIRESHARK_VERSION_MAJOR; 18 | #[no_mangle] 19 | pub static plugin_want_minor: c_uint = WIRESHARK_VERSION_MINOR; 20 | 21 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 22 | 23 | static mut proto: c_int = -1; 24 | 25 | unsafe extern "C" fn dissect_foo( 26 | tvb: *mut tvbuff, 27 | packet_info: *mut _packet_info, 28 | _proto_tree: *mut _proto_node, 29 | _data: *mut c_void, 30 | ) -> i32 { 31 | dbg!("prout"); 32 | let name = CString::new("FOO").unwrap(); 33 | col_set_str( 34 | (*packet_info).cinfo, 35 | COL_PROTOCOL.try_into().unwrap(), 36 | name.as_ptr(), 37 | ); 38 | col_clear((*packet_info).cinfo, COL_INFO.try_into().unwrap()); 39 | tvb_captured_length(tvb).try_into().unwrap() 40 | } 41 | 42 | unsafe extern "C" fn proto_register_foo() { 43 | dbg!("pat"); 44 | let name = CString::new("FOO Protocol").unwrap(); 45 | let short_name = CString::new("FOO").unwrap(); 46 | let filter_name = CString::new("foo").unwrap(); 47 | proto = proto_register_protocol(name.as_ptr(), short_name.as_ptr(), filter_name.as_ptr()); 48 | } 49 | 50 | unsafe extern "C" fn proto_reg_handoff_foo() { 51 | dbg!("fdsfds"); 52 | let handle = create_dissector_handle(Some(dissect_foo), proto); 53 | let psm = CString::new("btl2cap.psm").unwrap(); 54 | dissector_add_uint(psm.as_ptr(), 17, handle); 55 | dissector_add_uint(psm.as_ptr(), 19, handle); 56 | } 57 | 58 | #[no_mangle] 59 | pub unsafe extern "C" fn plugin_register() { 60 | let plugin_foo = proto_plugin { 61 | register_protoinfo: Some(proto_register_foo), 62 | register_handoff: Some(proto_reg_handoff_foo), 63 | }; 64 | proto_register_plugin(&plugin_foo); 65 | dbg!("sfds"); 66 | } 67 | -------------------------------------------------------------------------------- /joy-infrared/src/render/shaders/compute.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | struct Vertex { 4 | vec3 position; 5 | vec2 uv; 6 | float depth; 7 | }; 8 | 9 | layout(std430, set = 0, binding = 0) buffer VertOut { 10 | Vertex[] vertices; 11 | }; 12 | layout(std430, set = 0, binding = 1) buffer IndexOut { 13 | uint[] indices; 14 | }; 15 | 16 | layout(set = 1, binding = 0) uniform texture2D ir_pixels; 17 | layout(set = 1, binding = 1) uniform sampler ir_sampler; 18 | 19 | layout(set = 2, binding = 0, RGBA32F) uniform restrict writeonly image2D normal_texture; 20 | 21 | layout(set = 3, binding = 0) 22 | #include "uniform.glsl" 23 | 24 | #define ir sampler2D(ir_pixels, ir_sampler) 25 | #define tex_size textureSize(ir, 0) 26 | 27 | float depth(int x, int y) { 28 | return 1. - texelFetch(ir, ivec2(x, tex_size.y - 1 - y), 0).r; 29 | } 30 | 31 | vec3 pos(int x, int y) { 32 | vec4 pos = vec4( 33 | 1. - 2. * x / (tex_size.x - 1), 34 | 2. * y / (tex_size.y - 1) - 1., 35 | depth(x, y), 36 | 1.); 37 | vec4 proj = u.ir_proj * pos; 38 | return vec3(proj.xy, -proj.z) / proj.w; 39 | } 40 | 41 | vec3 norm(vec3 z, vec3 a, vec3 b) { 42 | return normalize(cross(a - z, b - z)); 43 | } 44 | 45 | void main() { 46 | int x = int(gl_GlobalInvocationID.x); 47 | int y = int(gl_GlobalInvocationID.y); 48 | vec3 z = pos(x , y ); 49 | vec3 a = pos(x , y - 1); 50 | vec3 b = pos(x + 1, y - 1); 51 | vec3 c = pos(x + 1, y ); 52 | vec3 d = pos(x , y + 1); 53 | vec3 e = pos(x - 1, y + 1); 54 | vec3 f = pos(x - 1, y ); 55 | 56 | vec3 sum_norm = norm(z, a, b) + norm(z, b, c) + norm(z, c, d) + norm(z, d, e) + norm(z, e, f) + norm(z, f, a); 57 | 58 | uint id = y * tex_size.x + x; 59 | vec3 normal = normalize(sum_norm); 60 | vec2 uv = vec2(float(x) / (tex_size.x - 1), float(y) / (tex_size.y - 1)); 61 | vertices[id] = Vertex(z, uv, depth(x, y)); 62 | imageStore(normal_texture, ivec2(x, y), vec4(normal, 1.0)); 63 | 64 | if (x < tex_size.x - 1 && y < tex_size.y - 1) { 65 | // since we skip the last row and column, we remove uy to not leave a gap. 66 | uint base_id = (id - y) * 6; 67 | indices[base_id ] = id; 68 | indices[base_id + 1] = id + 1; 69 | indices[base_id + 2] = id + tex_size.x; 70 | indices[base_id + 3] = id + tex_size.x; 71 | indices[base_id + 4] = id + 1; 72 | indices[base_id + 5] = id + tex_size.x + 1; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /crates/dualshock-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate num_derive; 3 | 4 | use std::{fmt, marker::PhantomData}; 5 | 6 | use num::{FromPrimitive, ToPrimitive}; 7 | 8 | pub mod input; 9 | pub mod output; 10 | 11 | pub const HID_VENDOR_ID: u16 = 0x54c; 12 | pub const HID_PRODUCT_ID_NEW: u16 = 0x9cc; 13 | pub const HID_PRODUCT_ID_OLD: u16 = 0x5c4; 14 | 15 | pub const DS4_REPORT_RATE: u32 = 250; 16 | pub const DS4_REPORT_DT: f64 = 1. / DS4_REPORT_RATE as f64; 17 | 18 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 19 | pub enum ConnectionType { 20 | Bluetooth, 21 | USB, 22 | } 23 | 24 | #[repr(transparent)] 25 | #[derive(Copy, Clone)] 26 | pub struct RawId(u8, PhantomData); 27 | 28 | impl RawId { 29 | pub fn try_into(self) -> Option { 30 | Id::from_u8(self.0) 31 | } 32 | } 33 | 34 | impl From for RawId { 35 | fn from(id: Id) -> Self { 36 | RawId(id.to_u8().expect("always one byte"), PhantomData) 37 | } 38 | } 39 | 40 | impl fmt::Debug for RawId { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | if let Some(id) = self.try_into() { 43 | write!(f, "{:?}", id) 44 | } else { 45 | f.debug_tuple("RawId") 46 | .field(&format!("0x{:x}", self.0)) 47 | .finish() 48 | } 49 | } 50 | } 51 | 52 | impl fmt::Display for RawId { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | if let Some(id) = self.try_into() { 55 | write!(f, "{}", id) 56 | } else { 57 | f.debug_tuple("RawId") 58 | .field(&format!("0x{:x}", self.0)) 59 | .finish() 60 | } 61 | } 62 | } 63 | 64 | impl PartialEq for RawId { 65 | fn eq(&self, other: &Id) -> bool { 66 | self.try_into().map(|x| x == *other).unwrap_or(false) 67 | } 68 | } 69 | 70 | #[derive(Copy, Clone, Default, Eq, PartialEq)] 71 | pub struct I16LE(pub [u8; 2]); 72 | 73 | impl From for I16LE { 74 | fn from(u: i16) -> I16LE { 75 | I16LE(u.to_le_bytes()) 76 | } 77 | } 78 | 79 | impl From for i16 { 80 | fn from(u: I16LE) -> i16 { 81 | i16::from_le_bytes(u.0) 82 | } 83 | } 84 | 85 | impl fmt::Debug for I16LE { 86 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 87 | i16::from(*self).fmt(f) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /joy-music/src/main.rs: -------------------------------------------------------------------------------- 1 | use joycon::{ 2 | hidapi::{self, HidApi}, 3 | joycon_sys::{ 4 | input::BatteryLevel, 5 | light::{self, PlayerLight}, 6 | output::RumbleData, 7 | output::RumbleSide, 8 | NINTENDO_VENDOR_ID, 9 | }, 10 | JoyCon, 11 | }; 12 | 13 | fn main() -> anyhow::Result<()> { 14 | let mut api = HidApi::new()?; 15 | loop { 16 | api.refresh_devices()?; 17 | if let Some(device_info) = api 18 | .device_list() 19 | .find(|x| x.vendor_id() == NINTENDO_VENDOR_ID) 20 | { 21 | let device = device_info.open_device(&api)?; 22 | match hid_main(device, device_info) { 23 | Ok(()) => std::thread::sleep(std::time::Duration::from_secs(2)), 24 | Err(e) => println!("Joycon error: {}", e), 25 | } 26 | } else { 27 | std::thread::sleep(std::time::Duration::from_secs(1)); 28 | } 29 | } 30 | } 31 | 32 | fn hid_main(device: hidapi::HidDevice, device_info: &hidapi::DeviceInfo) -> anyhow::Result<()> { 33 | let mut device = JoyCon::new(device, device_info.clone())?; 34 | println!("new dev: {:?}", device.get_dev_info()?); 35 | 36 | dbg!(device.set_home_light(light::HomeLight::new( 37 | 0x8, 38 | 0x2, 39 | 0x0, 40 | &[(0xf, 0xf, 0), (0x2, 0xf, 0)], 41 | ))?); 42 | 43 | let battery_level = device.tick()?.info.battery_level(); 44 | 45 | device.set_player_light(light::PlayerLights::new( 46 | (battery_level >= BatteryLevel::Full).into(), 47 | (battery_level >= BatteryLevel::Medium).into(), 48 | (battery_level >= BatteryLevel::Low).into(), 49 | if battery_level >= BatteryLevel::Low { 50 | PlayerLight::On 51 | } else { 52 | PlayerLight::Blinking 53 | }, 54 | ))?; 55 | 56 | println!("Running..."); 57 | let mut freq = 261.63; 58 | let step = [2, 2, 1, 2, 2, 2, 1]; 59 | let mut i = 0; 60 | while freq < 1050. { 61 | dbg!(freq); 62 | 63 | let rumble = RumbleSide::from_freq(freq, 0.4, 400., 0.); 64 | device.set_rumble(RumbleData { 65 | left: rumble, 66 | right: rumble, 67 | })?; 68 | 69 | freq *= 1.0594630943f32.powi(step[i]); 70 | i = (i + 1) % step.len(); 71 | 72 | std::thread::sleep(std::time::Duration::from_millis(500)); 73 | } 74 | 75 | dbg!(device.set_home_light(light::HomeLight::new(0x8, 0x4, 0x0, &[]))?); 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build, test and doc 2 | 3 | on: 4 | # At the moment, this doesn’t work due to a bug with GitHub: 5 | # 6 | # Once that bug is fixed, this version should be used. 7 | #push: &push_settings 8 | #paths: 9 | #- '**/Cargo.lock' 10 | #- '**/Cargo.toml' 11 | #- '**.rs' 12 | #- '.github/workflows/rust.yml' 13 | #pull_request: *push_settings 14 | # In the meantime, we can just repeat ourselves. 15 | push: 16 | paths: 17 | - '**/Cargo.lock' 18 | - '**/Cargo.toml' 19 | - '**.rs' 20 | - '.github/workflows/rust.yml' 21 | pull_request: 22 | paths: 23 | - '**/Cargo.lock' 24 | - '**/Cargo.toml' 25 | - '**.rs' 26 | - '.github/workflows/rust.yml' 27 | 28 | jobs: 29 | build: 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | platform: [linux, windows, macos] 34 | include: 35 | - platform: linux 36 | os: ubuntu-latest 37 | - platform: windows 38 | os: windows-latest 39 | - platform: macos 40 | os: macos-latest 41 | runs-on: ${{ matrix.os }} 42 | 43 | steps: 44 | - uses: actions/checkout@v2 45 | - uses: actions-rs/toolchain@v1 46 | with: 47 | toolchain: stable 48 | - uses: actions/cache@v2 49 | id: cache-cargo 50 | with: 51 | path: | 52 | ~/.cargo/bin/ 53 | ~/.cargo/registry/index/ 54 | ~/.cargo/registry/cache/ 55 | ~/.cargo/git/db/ 56 | target 57 | key: ${{ runner.os }}-cargo-v1-${{ hashFiles('**/Cargo.lock') }} 58 | 59 | - name: ⚙️ Install OS dependencies 60 | if: matrix.platform == 'linux' 61 | run: | 62 | sudo apt-get update 63 | sudo apt-get install libusb-1.0-0-dev libbluetooth-dev libudev-dev 64 | 65 | - name: ⚙️ Run tests 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: test 69 | args: --verbose --release --workspace 70 | 71 | - name: 🔨 Build Documentation 72 | uses: actions-rs/cargo@v1 73 | if: matrix.platform == 'linux' 74 | with: 75 | command: doc 76 | args: --workspace --no-deps 77 | 78 | - name: ☁️ Deploy Documentation 79 | uses: peaceiris/actions-gh-pages@v3 80 | if: matrix.platform == 'linux' && github.event_name == 'push' && github.ref == 'refs/heads/master' 81 | with: 82 | github_token: ${{ secrets.GITHUB_TOKEN }} 83 | publish_dir: ./target/doc 84 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/output/rumble.rs: -------------------------------------------------------------------------------- 1 | #[repr(packed)] 2 | #[derive(Copy, Clone, Debug, Default)] 3 | pub struct RumbleData { 4 | pub left: RumbleSide, 5 | pub right: RumbleSide, 6 | } 7 | 8 | #[repr(packed)] 9 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 10 | #[allow(non_snake_case)] 11 | pub struct RumbleSide { 12 | hb_freq_msB: u8, 13 | hb_freq_lsb_amp_high: u8, 14 | lb_freq_amp_low_msb: u8, 15 | amp_low_lsB: u8, 16 | } 17 | 18 | impl RumbleSide { 19 | pub fn from_freq( 20 | mut hi_freq: f32, 21 | mut hi_amp: f32, 22 | mut low_freq: f32, 23 | mut low_amp: f32, 24 | ) -> RumbleSide { 25 | hi_freq = hi_freq.max(82.).min(1253.); 26 | low_freq = low_freq.max(41.).min(626.); 27 | low_amp = low_amp.max(0.).min(1.); 28 | hi_amp = hi_amp.max(0.).min(1.); 29 | 30 | let hi_freq_hex = (Self::encode_freq(hi_freq) - 0x60) * 4; 31 | let low_freq_hex = (Self::encode_freq(low_freq) - 0x40) as u8; 32 | let hi_amp_hex = ((100. * hi_amp) as u8) << 1; 33 | let low_amp_hex = ((228. - 128.) * low_amp) as u8 + 0x80; 34 | RumbleSide::from_encoded( 35 | [hi_freq_hex as u8, (hi_freq_hex >> 8) as u8], 36 | hi_amp_hex, 37 | low_freq_hex, 38 | [(low_amp_hex & 1) << 7, low_amp_hex >> 1], 39 | ) 40 | } 41 | 42 | fn encode_freq(f: f32) -> u16 { 43 | ((f / 10.).log2() * 32.).round() as u16 44 | } 45 | 46 | fn from_encoded( 47 | high_freq: [u8; 2], 48 | high_amp: u8, 49 | low_freq: u8, 50 | low_amp: [u8; 2], 51 | ) -> RumbleSide { 52 | assert_eq!(high_freq[0] & 0b11, 0); 53 | assert_eq!(high_freq[1] & 0xfe, 0); 54 | assert_eq!(high_amp & 1, 0); 55 | assert!(high_amp <= 0xc8); 56 | assert_eq!(low_freq & 0x80, 0); 57 | assert_eq!(low_amp[0] & 0x7f, 0); 58 | assert!(0x40 <= low_amp[1] && low_amp[1] <= 0x72); 59 | RumbleSide { 60 | hb_freq_msB: high_freq[0], 61 | hb_freq_lsb_amp_high: high_freq[1] | high_amp, 62 | lb_freq_amp_low_msb: low_freq | low_amp[0], 63 | amp_low_lsB: low_amp[1], 64 | } 65 | } 66 | } 67 | 68 | impl Default for RumbleSide { 69 | fn default() -> Self { 70 | RumbleSide::from_freq(320., 0., 160., 0.) 71 | } 72 | } 73 | 74 | #[test] 75 | fn encode_rumble() { 76 | let rumble = RumbleSide::from_freq(320., 0., 160., 0.); 77 | assert_eq!( 78 | rumble, 79 | RumbleSide { 80 | hb_freq_msB: 0x00, 81 | hb_freq_lsb_amp_high: 0x01, 82 | lb_freq_amp_low_msb: 0x40, 83 | amp_low_lsB: 0x40, 84 | } 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /joy-infrared/src/render/buffer.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::Pod; 2 | use iced_wgpu::wgpu; 3 | use std::{ 4 | any::type_name, 5 | ops::{Deref, DerefMut}, 6 | }; 7 | use wgpu::util::DeviceExt; 8 | 9 | pub trait Staged { 10 | fn update(&self, queue: &mut wgpu::Queue, data: &[A]); 11 | } 12 | 13 | impl Staged for wgpu::Buffer { 14 | fn update(&self, queue: &mut wgpu::Queue, data: &[A]) { 15 | queue.write_buffer(self, 0, bytemuck::cast_slice(data)); 16 | } 17 | } 18 | 19 | pub struct BoundBuffer { 20 | inner: T, 21 | buffer: wgpu::Buffer, 22 | bind_group: wgpu::BindGroup, 23 | bind_group_layout: wgpu::BindGroupLayout, 24 | dirty: bool, 25 | } 26 | 27 | impl BoundBuffer { 28 | pub fn new( 29 | device: &wgpu::Device, 30 | usage: wgpu::BufferUsage, 31 | visibility: wgpu::ShaderStage, 32 | ) -> Self { 33 | let inner = T::default(); 34 | let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 35 | usage: usage | wgpu::BufferUsage::COPY_DST, 36 | contents: bytemuck::bytes_of(&inner), 37 | label: Some("2D Vertex Buffer"), 38 | }); 39 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 40 | entries: &[wgpu::BindGroupLayoutEntry { 41 | binding: 0, 42 | visibility, 43 | ty: wgpu::BindingType::UniformBuffer { 44 | dynamic: false, 45 | min_binding_size: None, 46 | }, 47 | count: None, 48 | }], 49 | label: Some(&format!("{} bind group layout", type_name::())), 50 | }); 51 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 52 | layout: &bind_group_layout, 53 | entries: &[wgpu::BindGroupEntry { 54 | binding: 0, 55 | resource: wgpu::BindingResource::Buffer(buffer.slice(..)), 56 | }], 57 | label: Some(&format!("{} bind group", type_name::())), 58 | }); 59 | Self { 60 | inner, 61 | buffer, 62 | bind_group, 63 | bind_group_layout, 64 | dirty: true, 65 | } 66 | } 67 | 68 | pub fn bind_group(&self) -> &wgpu::BindGroup { 69 | &self.bind_group 70 | } 71 | 72 | pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout { 73 | &self.bind_group_layout 74 | } 75 | 76 | pub fn upload(&mut self, queue: &mut wgpu::Queue) { 77 | if self.dirty { 78 | self.buffer.update(queue, &[self.inner]); 79 | self.dirty = false; 80 | } 81 | } 82 | } 83 | 84 | impl Deref for BoundBuffer { 85 | type Target = T; 86 | 87 | fn deref(&self) -> &Self::Target { 88 | &self.inner 89 | } 90 | } 91 | 92 | impl DerefMut for BoundBuffer { 93 | fn deref_mut(&mut self) -> &mut Self::Target { 94 | self.dirty = true; 95 | &mut self.inner 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/dualshock/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use dualshock_sys::{ 3 | input::InputReport, ConnectionType, DS4_REPORT_RATE, HID_PRODUCT_ID_NEW, HID_PRODUCT_ID_OLD, 4 | HID_VENDOR_ID, 5 | }; 6 | use hid_gamepad_sys::{GamepadDevice, GamepadDriver, JoyKey, KeyStatus, Motion, Report}; 7 | use hidapi::{HidApi, HidDevice}; 8 | 9 | pub struct DS4Driver; 10 | 11 | pub struct DS4 { 12 | device: HidDevice, 13 | } 14 | 15 | impl GamepadDriver for DS4Driver { 16 | fn init( 17 | &self, 18 | api: &HidApi, 19 | device_info: &hidapi::DeviceInfo, 20 | ) -> Result>> { 21 | if device_info.vendor_id() == HID_VENDOR_ID 22 | && [HID_PRODUCT_ID_OLD, HID_PRODUCT_ID_NEW].contains(&device_info.product_id()) 23 | { 24 | Ok(Some(Box::new(DS4 { 25 | device: device_info.open_device(api)?, 26 | }))) 27 | } else { 28 | Ok(None) 29 | } 30 | } 31 | } 32 | 33 | impl GamepadDevice for DS4 { 34 | fn recv(&mut self) -> Result { 35 | let mut report = InputReport::new(); 36 | let buffer = report.as_bytes_mut(); 37 | let nb_read = self.device.read(buffer)?; 38 | let full = match InputReport::conn_type(nb_read) { 39 | ConnectionType::Bluetooth => &report.bt_full().unwrap().full, 40 | ConnectionType::USB => &report.usb_full().unwrap().full, 41 | }; 42 | let b = &full.base.buttons; 43 | let rot = full.gyro.normalize(); 44 | Ok(Report { 45 | left_joystick: full.base.left_stick.normalize(), 46 | right_joystick: full.base.right_stick.normalize(), 47 | motion: vec![Motion { 48 | acceleration: full.accel.normalize().into(), 49 | rotation_speed: rot.into(), 50 | }], 51 | keys: enum_map::enum_map! { 52 | JoyKey::Up => b.dpad().up().into(), 53 | JoyKey::Down => b.dpad().down().into(), 54 | JoyKey::Left => b.dpad().left().into(), 55 | JoyKey::Right=> b.dpad().right().into(), 56 | JoyKey::N => b.triangle().into(), 57 | JoyKey::S => b.cross().into(), 58 | JoyKey::E => b.circle().into(), 59 | JoyKey::W => b.square().into(), 60 | JoyKey::L=> b.l1().into(), 61 | JoyKey::R=> b.r1().into(), 62 | JoyKey::ZL => b.l2().into(), 63 | JoyKey::ZR => b.r2().into(), 64 | JoyKey::SL => KeyStatus::Released, 65 | JoyKey::SR => KeyStatus::Released, 66 | JoyKey::L3 => b.l3().into(), 67 | JoyKey::R3 => b.r3().into(), 68 | JoyKey::Minus => b.tpad().into(), 69 | JoyKey::Plus => b.options().into(), 70 | JoyKey::Capture => b.share().into(), 71 | JoyKey::Home => b.ps().into(), 72 | }, 73 | frequency: DS4_REPORT_RATE, 74 | }) 75 | } 76 | 77 | fn as_any(&mut self) -> &mut dyn std::any::Any { 78 | self 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/joycon/src/image.rs: -------------------------------------------------------------------------------- 1 | use joycon_sys::mcu::{ir::Resolution, *}; 2 | use joycon_sys::*; 3 | 4 | pub struct Image { 5 | buffer: Box<[[u8; 300]; 0x100]>, 6 | resolution: ir::Resolution, 7 | prev_fragment_id: u8, 8 | changing_resolution: bool, 9 | pub last_image: Option, 10 | } 11 | 12 | impl Image { 13 | pub fn new() -> Image { 14 | Image { 15 | buffer: Box::new([[0; 300]; 0x100]), 16 | resolution: Resolution::default(), 17 | prev_fragment_id: 0, 18 | changing_resolution: false, 19 | last_image: None, 20 | } 21 | } 22 | 23 | pub fn change_resolution(&mut self, resolution: ir::Resolution) { 24 | self.resolution = resolution; 25 | self.changing_resolution = true; 26 | } 27 | 28 | pub fn handle(&mut self, report: &MCUReport) -> [Option; 2] { 29 | // TODO: handle lossed packets 30 | if let Some(packet) = report.ir_data() { 31 | if self.changing_resolution { 32 | if packet.frag_number != 0 { 33 | return [Some(OutputReport::ir_ack(packet.frag_number)), None]; 34 | } 35 | self.changing_resolution = false; 36 | } 37 | 38 | self.buffer[packet.frag_number as usize] = packet.img_fragment; 39 | let resend = if packet.frag_number > 0 40 | && self.prev_fragment_id > 0 41 | && packet.frag_number - 1 > self.prev_fragment_id 42 | { 43 | println!("requesting again packet {}", packet.frag_number - 1); 44 | Some(OutputReport::ir_resend(packet.frag_number - 1)) 45 | } else { 46 | None 47 | }; 48 | let (width, height) = self.resolution.size(); 49 | let mut buffer = Vec::with_capacity((width * height) as usize); 50 | for fragment in self 51 | .buffer 52 | .iter() 53 | .take(self.resolution.max_fragment_id() as usize + 1) 54 | { 55 | buffer.extend(fragment.iter()); 56 | } 57 | self.last_image = Some(image::imageops::rotate90( 58 | &image::GrayImage::from_raw(width, height, buffer).unwrap(), 59 | )); 60 | //println!("got packet {}", packet.frag_number); 61 | if packet.frag_number == self.resolution.max_fragment_id() { 62 | self.prev_fragment_id = 0; 63 | } else { 64 | self.prev_fragment_id = packet.frag_number; 65 | } 66 | [Some(OutputReport::ir_ack(packet.frag_number)), resend] 67 | } else if report.id() == MCUReportId::Empty { 68 | [ 69 | Some(OutputReport::ir_resend(self.prev_fragment_id + 1)), 70 | None, 71 | ] 72 | } else if report.id() == MCUReportId::EmptyAwaitingCmd { 73 | [Some(OutputReport::ir_ack(self.prev_fragment_id)), None] 74 | } else { 75 | [None, None] 76 | } 77 | } 78 | } 79 | 80 | impl Default for Image { 81 | fn default() -> Self { 82 | Self::new() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/hid-gamepad-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Mul, time::Duration}; 2 | 3 | use cgmath::{vec3, Deg, Euler, Vector2, Vector3}; 4 | use enum_map::{Enum, EnumMap}; 5 | 6 | #[derive(Enum, Debug, Copy, Clone, Eq, PartialEq, Hash)] 7 | pub enum JoyKey { 8 | Up, 9 | Down, 10 | Left, 11 | Right, 12 | N, 13 | S, 14 | E, 15 | W, 16 | L, 17 | R, 18 | ZL, 19 | ZR, 20 | SL, 21 | SR, 22 | L3, 23 | R3, 24 | Minus, 25 | Plus, 26 | Capture, 27 | Home, 28 | } 29 | 30 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 31 | pub enum KeyStatus { 32 | Pressed, 33 | Released, 34 | } 35 | 36 | impl Default for KeyStatus { 37 | fn default() -> Self { 38 | KeyStatus::Released 39 | } 40 | } 41 | 42 | impl From for KeyStatus { 43 | fn from(b: bool) -> Self { 44 | if b { 45 | KeyStatus::Pressed 46 | } else { 47 | KeyStatus::Released 48 | } 49 | } 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct Report { 54 | pub keys: EnumMap, 55 | pub left_joystick: Vector2, 56 | pub right_joystick: Vector2, 57 | pub motion: Vec, 58 | pub frequency: u32, 59 | } 60 | 61 | #[derive(Debug, Clone, Copy)] 62 | pub struct Motion { 63 | pub rotation_speed: RotationSpeed, 64 | pub acceleration: Acceleration, 65 | } 66 | 67 | /// Uses the SDL convention. 68 | /// 69 | /// Units are deg/s 70 | #[derive(Debug, Clone, Copy)] 71 | pub struct RotationSpeed { 72 | /// -x ... +x is left ... right 73 | pub x: f64, 74 | /// -y ... +y is down ... up 75 | pub y: f64, 76 | /// -z ... +z is forward ... backward 77 | pub z: f64, 78 | } 79 | 80 | impl RotationSpeed { 81 | pub fn as_vec(self) -> Vector3 { 82 | vec3(self.x, self.y, self.z) 83 | } 84 | } 85 | 86 | impl From> for RotationSpeed { 87 | fn from(raw: Vector3) -> Self { 88 | Self { 89 | x: raw.x, 90 | y: raw.y, 91 | z: raw.z, 92 | } 93 | } 94 | } 95 | 96 | impl Mul for RotationSpeed { 97 | type Output = Euler>; 98 | 99 | fn mul(self, dt: Duration) -> Self::Output { 100 | Euler::new( 101 | Deg(self.x * dt.as_secs_f64()), 102 | Deg(self.y * dt.as_secs_f64()), 103 | Deg(self.z * dt.as_secs_f64()), 104 | ) 105 | } 106 | } 107 | 108 | /// Uses the SDL convention. 109 | /// 110 | /// Units are in g 111 | #[derive(Debug, Clone, Copy)] 112 | pub struct Acceleration { 113 | /// -x ... +x is left ... right 114 | pub x: f64, 115 | /// -y ... +y is down ... up 116 | pub y: f64, 117 | /// -z ... +z is forward ... backward 118 | pub z: f64, 119 | } 120 | 121 | impl Acceleration { 122 | pub fn as_vec(self) -> Vector3 { 123 | vec3(self.x, self.y, self.z) 124 | } 125 | } 126 | 127 | impl From> for Acceleration { 128 | fn from(raw: Vector3) -> Self { 129 | Self { 130 | x: raw.x, 131 | y: raw.y, 132 | z: raw.z, 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/joycon/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod calibration; 2 | mod hid; 3 | #[cfg(feature = "ir")] 4 | mod image; 5 | mod imu_handler; 6 | 7 | #[cfg(feature = "ir")] 8 | pub use crate::image::*; 9 | use anyhow::Result; 10 | pub use calibration::*; 11 | use cgmath::vec3; 12 | pub use hid::*; 13 | use hid_gamepad_sys::{GamepadDevice, GamepadDriver, JoyKey, Motion}; 14 | use hidapi::HidApi; 15 | pub use imu_handler::IMU; 16 | pub use joycon_sys; 17 | 18 | pub use hidapi; 19 | use joycon_sys::{imu::IMU_SAMPLES_PER_SECOND, NINTENDO_VENDOR_ID}; 20 | 21 | pub struct JoyconDriver; 22 | 23 | impl GamepadDriver for JoyconDriver { 24 | fn init( 25 | &self, 26 | api: &HidApi, 27 | device_info: &hidapi::DeviceInfo, 28 | ) -> Result>> { 29 | if device_info.vendor_id() == NINTENDO_VENDOR_ID { 30 | let mut joycon = JoyCon::new(device_info.open_device(api)?, device_info.clone())?; 31 | joycon.enable_imu()?; 32 | joycon.load_calibration()?; 33 | Ok(Some(Box::new(joycon))) 34 | } else { 35 | Ok(None) 36 | } 37 | } 38 | } 39 | 40 | impl GamepadDevice for JoyCon { 41 | fn recv(&mut self) -> Result { 42 | Ok(self.tick()?.into()) 43 | } 44 | 45 | fn as_any(&mut self) -> &mut dyn std::any::Any { 46 | self 47 | } 48 | } 49 | 50 | impl From for hid_gamepad_sys::Report { 51 | fn from(report: Report) -> Self { 52 | let b = &report.buttons; 53 | Self { 54 | left_joystick: report.left_stick, 55 | right_joystick: report.right_stick, 56 | motion: report 57 | .imu 58 | .unwrap() 59 | .iter() 60 | .map(|x| Motion { 61 | acceleration: vec3(-x.accel.y, x.accel.z, x.accel.x).into(), 62 | rotation_speed: vec3(x.gyro.y, -x.gyro.z, -x.gyro.x).into(), 63 | }) 64 | .collect(), 65 | keys: enum_map::enum_map! { 66 | JoyKey::Up => b.left.up().into(), 67 | JoyKey::Down => b.left.down().into(), 68 | JoyKey::Left => b.left.left().into(), 69 | JoyKey::Right=> b.left.right().into(), 70 | JoyKey::N => b.right.x().into(), 71 | JoyKey::S => b.right.b().into(), 72 | JoyKey::E => b.right.a().into(), 73 | JoyKey::W => b.right.y().into(), 74 | JoyKey::L=> b.left.l().into(), 75 | JoyKey::R=> b.right.r().into(), 76 | JoyKey::ZL => b.left.zl().into(), 77 | JoyKey::ZR => b.right.zr().into(), 78 | JoyKey::SL => (b.left.sl() | b.right.sl()).into(), 79 | JoyKey::SR => (b.left.sr() | b.right.sr()).into(), 80 | JoyKey::L3 => b.middle.lstick().into(), 81 | JoyKey::R3 => b.middle.rstick().into(), 82 | JoyKey::Minus => b.middle.minus().into(), 83 | JoyKey::Plus => b.middle.plus().into(), 84 | JoyKey::Capture => b.middle.capture().into(), 85 | JoyKey::Home => b.middle.home().into(), 86 | }, 87 | frequency: IMU_SAMPLES_PER_SECOND, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /joy-infrared/src/render/d2.rs: -------------------------------------------------------------------------------- 1 | use cgmath::vec2; 2 | use iced_wgpu::wgpu; 3 | use wgpu::util::DeviceExt; 4 | 5 | #[repr(C)] 6 | #[derive(Copy, Clone)] 7 | struct Vertex2D { 8 | position: cgmath::Vector2, 9 | uv: cgmath::Vector2, 10 | } 11 | unsafe impl bytemuck::Pod for Vertex2D {} 12 | unsafe impl bytemuck::Zeroable for Vertex2D {} 13 | 14 | pub struct D2 { 15 | index_buffer: wgpu::Buffer, 16 | vertex_buffer: wgpu::Buffer, 17 | pipeline: wgpu::RenderPipeline, 18 | } 19 | 20 | impl D2 { 21 | pub fn new( 22 | device: &wgpu::Device, 23 | texture_binding_layout: &wgpu::BindGroupLayout, 24 | sample_count: u32, 25 | ) -> Self { 26 | let vertices = &[ 27 | Vertex2D { 28 | position: vec2(0.5, -1.), 29 | uv: vec2(0., 1.), 30 | }, 31 | Vertex2D { 32 | position: vec2(1., -1.), 33 | uv: vec2(1., 1.), 34 | }, 35 | Vertex2D { 36 | position: vec2(0.5, 0.), 37 | uv: vec2(0., 0.), 38 | }, 39 | Vertex2D { 40 | position: vec2(1., 0.), 41 | uv: vec2(1., 0.), 42 | }, 43 | ]; 44 | let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 45 | usage: wgpu::BufferUsage::VERTEX, 46 | contents: bytemuck::cast_slice(vertices), 47 | label: Some("2D Vertex Buffer"), 48 | }); 49 | let indices: &[u32] = &[0, 1, 2, 2, 1, 3]; 50 | let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 51 | usage: wgpu::BufferUsage::INDEX, 52 | contents: bytemuck::cast_slice(indices), 53 | label: Some("2D Index Buffer"), 54 | }); 55 | 56 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 57 | bind_group_layouts: &[texture_binding_layout], 58 | push_constant_ranges: &[], 59 | label: Some("2D Pipeline Layout"), 60 | }); 61 | 62 | let pipeline = super::create_render_pipeline( 63 | &device, 64 | &pipeline_layout, 65 | &[wgpu::TextureFormat::Bgra8UnormSrgb], 66 | None, 67 | &[wgpu::VertexBufferDescriptor { 68 | stride: std::mem::size_of::() as wgpu::BufferAddress, 69 | step_mode: wgpu::InputStepMode::Vertex, 70 | attributes: &wgpu::vertex_attr_array![0 => Float2, 1 => Float2], 71 | }], 72 | vk_shader_macros::include_glsl!("src/render/shaders/2d.vert"), 73 | vk_shader_macros::include_glsl!("src/render/shaders/2d.frag"), 74 | sample_count, 75 | ); 76 | D2 { 77 | vertex_buffer, 78 | index_buffer, 79 | pipeline, 80 | } 81 | } 82 | 83 | pub fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>, texture: &'a wgpu::BindGroup) { 84 | pass.set_pipeline(&self.pipeline); 85 | pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); 86 | pass.set_index_buffer(self.index_buffer.slice(..)); 87 | pass.set_bind_group(0, texture, &[]); 88 | pass.draw_indexed(0..6, 0, 0..1); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /joy-infrared/src/render/uniforms.rs: -------------------------------------------------------------------------------- 1 | use crate::render::camera::Camera; 2 | use bytemuck::{Pod, Zeroable}; 3 | use cgmath::{prelude::*, vec3, vec4, Deg, Matrix4, Quaternion, Vector3, Vector4}; 4 | 5 | #[repr(C)] 6 | #[derive(Debug, Copy, Clone)] 7 | pub struct Uniforms { 8 | ir_proj: Matrix4, 9 | ir_rotation: Matrix4, 10 | view_proj: Matrix4, 11 | normal_transform: Matrix4, 12 | } 13 | 14 | unsafe impl Zeroable for Uniforms {} 15 | unsafe impl Pod for Uniforms {} 16 | 17 | impl Uniforms { 18 | pub fn new() -> Uniforms { 19 | let ir_proj = cgmath::perspective(Deg(110.), 3. / 4., 0.1, 1.) 20 | .invert() 21 | .unwrap(); 22 | Uniforms { 23 | ir_proj, 24 | ir_rotation: Matrix4::identity(), 25 | view_proj: Matrix4::identity(), 26 | normal_transform: Matrix4::identity(), 27 | } 28 | } 29 | 30 | pub fn update_view_proj(&mut self, camera: &Camera) { 31 | self.view_proj = camera.build_view_projection_matrix(); 32 | } 33 | 34 | pub fn set_ir_rotation(&mut self, rotation: Quaternion) { 35 | self.ir_rotation = Matrix4::from(rotation).cast().unwrap(); 36 | self.normal_transform = self.ir_rotation.invert().unwrap().transpose(); 37 | } 38 | } 39 | 40 | impl Default for Uniforms { 41 | fn default() -> Self { 42 | Self::new() 43 | } 44 | } 45 | 46 | #[repr(C, align(16))] 47 | #[derive(Debug, Copy, Clone)] 48 | pub struct Lights { 49 | count: u32, 50 | _pad: [u32; 3], 51 | lights: [Light; Self::MAX as usize], 52 | } 53 | unsafe impl Zeroable for Lights {} 54 | unsafe impl Pod for Lights {} 55 | 56 | impl Lights { 57 | const MAX: u32 = 10; 58 | 59 | pub fn lights() -> Self { 60 | Self::zeroed() 61 | .push(Light { 62 | position: vec4(0., 0., 0., 1.0), 63 | ambient: vec3(0., 1., 0.) * 0.05, 64 | diffuse: vec3(0., 1., 0.) * 0.8, 65 | specular: vec3(0., 1., 0.), 66 | constant: 1.0, 67 | linear: 0.7, 68 | quadratic: 1.8, 69 | ..Light::default() 70 | }) 71 | .push(Light { 72 | position: vec4(0.2, 1., -0.2, 0.0), 73 | ambient: vec3(0.05, 0.05, 0.05), 74 | diffuse: vec3(0.4, 0.4, 0.4), 75 | specular: vec3(0.5, 0.5, 0.5), 76 | constant: 1.0, 77 | linear: 0.7, 78 | quadratic: 1.8, 79 | ..Light::default() 80 | }) 81 | } 82 | 83 | fn push(mut self, light: Light) -> Self { 84 | assert_ne!(Self::MAX, self.count); 85 | self.lights[self.count as usize] = light; 86 | self.count += 1; 87 | self 88 | } 89 | } 90 | 91 | impl Default for Lights { 92 | fn default() -> Self { 93 | Self::zeroed() 94 | } 95 | } 96 | 97 | #[repr(C, align(16))] 98 | #[derive(Debug, Copy, Clone)] 99 | pub struct Light { 100 | position: Vector4, 101 | ambient: Vector3, 102 | _pad1: u32, 103 | diffuse: Vector3, 104 | _pad2: u32, 105 | specular: Vector3, 106 | constant: f32, 107 | linear: f32, 108 | quadratic: f32, 109 | } 110 | unsafe impl Zeroable for Light {} 111 | unsafe impl Pod for Light {} 112 | 113 | impl Default for Light { 114 | fn default() -> Self { 115 | Self::zeroed() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/joycon/src/imu_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::calibration::Calibration; 2 | use cgmath::*; 3 | use input::WhichController; 4 | use joycon_sys::*; 5 | 6 | /// Acceleration and gyroscope data for the controller. 7 | /// 8 | /// 9 | #[derive(Debug, Copy, Clone)] 10 | pub struct IMU { 11 | /// Current rotation speed. 12 | /// 13 | /// Yaw, pitch, roll in this order. Unit in degree per second (dps). 14 | pub gyro: Vector3, 15 | /// Current acceleration. 16 | pub accel: Vector3, 17 | } 18 | 19 | impl IMU { 20 | pub const SAMPLE_DURATION: f64 = imu::IMU_SAMPLE_DURATION; 21 | pub const SAMPLE_PER_SECOND: u32 = imu::IMU_SAMPLES_PER_SECOND; 22 | } 23 | 24 | pub struct Handler { 25 | device_type: WhichController, 26 | calib_gyro: Calibration, 27 | gyro_sens: imu::GyroSens, 28 | accel_sens: imu::AccSens, 29 | factory_calibration: spi::SensorCalibration, 30 | user_calibration: spi::UserSensorCalibration, 31 | calib_nb: u32, 32 | } 33 | 34 | impl Handler { 35 | pub fn new( 36 | device_type: WhichController, 37 | gyro_sens: imu::GyroSens, 38 | accel_sens: imu::AccSens, 39 | ) -> Self { 40 | Handler { 41 | device_type, 42 | calib_gyro: Calibration::with_capacity(200), 43 | gyro_sens, 44 | accel_sens, 45 | factory_calibration: spi::SensorCalibration::default(), 46 | user_calibration: spi::UserSensorCalibration::default(), 47 | calib_nb: 0, 48 | } 49 | } 50 | 51 | pub fn set_factory(&mut self, calib: spi::SensorCalibration) { 52 | self.factory_calibration = calib; 53 | } 54 | 55 | pub fn set_user(&mut self, calib: spi::UserSensorCalibration) { 56 | self.user_calibration = calib; 57 | } 58 | 59 | fn acc_calib(&self) -> Vector3 { 60 | self.user_calibration 61 | .acc_offset() 62 | .unwrap_or_else(|| self.factory_calibration.acc_offset()) 63 | } 64 | 65 | fn gyro_calib(&self) -> Vector3 { 66 | self.user_calibration 67 | .gyro_offset() 68 | .unwrap_or_else(|| self.factory_calibration.gyro_offset()) 69 | } 70 | 71 | pub fn handle_frames(&mut self, frames: &[imu::Frame]) -> [IMU; 3] { 72 | let gyro_offset = self.gyro_calib(); 73 | let acc_offset = self.acc_calib(); 74 | let mut out = [IMU { 75 | gyro: Vector3::zero(), 76 | accel: Vector3::zero(), 77 | }; 3]; 78 | for (frame, out) in frames.iter().rev().zip(out.iter_mut()) { 79 | let raw_rotation = frame.rotation_dps(gyro_offset, self.gyro_sens); 80 | let raw_acc = frame.accel_g(acc_offset, self.accel_sens); 81 | if self.calib_nb > 0 { 82 | self.calib_gyro.push(raw_rotation); 83 | self.calib_nb -= 1; 84 | } 85 | *out = IMU { 86 | gyro: raw_rotation - self.calib_gyro.get_average(), 87 | accel: raw_acc, 88 | }; 89 | // The devices don't have the same axis. 90 | match self.device_type { 91 | WhichController::LeftJoyCon | WhichController::ProController => { 92 | out.gyro.y = -out.gyro.y; 93 | out.gyro.z = -out.gyro.z; 94 | out.accel.x = -out.accel.x; 95 | } 96 | WhichController::RightJoyCon => { 97 | out.accel = -out.accel; 98 | } 99 | } 100 | } 101 | out 102 | } 103 | 104 | pub fn reset_calibration(&mut self) { 105 | self.calib_gyro.reset(); 106 | self.calib_nb = 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /joytk/src/opts.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | /// Access every feature of the Nintendo Switch controllers 6 | /// 7 | /// Env variables: 8 | /// 9 | /// - `RUST_LOG=`: 10 | /// 11 | /// - `trace`: log every bluetooth packet 12 | /// 13 | /// - `debug`: only log important packets 14 | /// 15 | /// - `LOG_PRETTY=1`: use a more verbose logging format 16 | /// 17 | /// - `LOG_TIMING=1`: show timings 18 | #[derive(Parser)] 19 | pub struct Opts { 20 | #[clap(subcommand)] 21 | pub subcmd: SubCommand, 22 | /// Wait for a controller to connect 23 | #[clap(short, long)] 24 | pub wait: bool, 25 | } 26 | 27 | #[derive(Parser)] 28 | pub enum SubCommand { 29 | /// Calibrate the controller 30 | /// 31 | /// The calibration will be stored on the controller and used by the Switch. 32 | Calibrate(Calibrate), 33 | /// Print settings from the controller 34 | Get, 35 | /// Configure settings of the controller 36 | Set(Set), 37 | /// Show live inputs from the controller 38 | Monitor, 39 | PulseRate, 40 | #[cfg(feature = "interface")] 41 | Tui, 42 | /// Dump the memory of the controller to a binary file 43 | Dump, 44 | /// Restore the memory of the controller from a dump file 45 | Restore, 46 | /// Decode raw reports exchanged between the controller and the Switch 47 | /// 48 | /// See the `relay` subcommand to record new traces. 49 | /// 50 | /// See the `trace/` folder for recorded dumps, and 51 | /// [relay_joycon.py](https://github.com/Yamakaky/joycontrol/blob/capture-text-file/scripts/relay_joycon.py) 52 | /// for capturing new dumps. 53 | Decode, 54 | /// Relay the bluetooth trafic between a controller and the Switch 55 | /// 56 | /// Important commands are decoded and shown, and a full log can be recorded. 57 | /// See the `decode` subcommand to decode logs. 58 | Relay(Relay), 59 | /// Ringcon-specific actions 60 | Ringcon(Ringcon), 61 | Camera, 62 | } 63 | 64 | #[derive(Parser)] 65 | pub struct Calibrate { 66 | #[clap(subcommand)] 67 | pub subcmd: CalibrateE, 68 | } 69 | 70 | #[derive(Parser)] 71 | pub enum CalibrateE { 72 | /// Calibrate the sticks 73 | Sticks, 74 | /// Calibrate the gyroscope 75 | Gyroscope, 76 | /// Reset gyroscope and sticks calibration to factory values 77 | Reset, 78 | } 79 | 80 | #[derive(Parser)] 81 | pub struct Set { 82 | #[clap(subcommand)] 83 | pub subcmd: SetE, 84 | } 85 | 86 | #[derive(Parser)] 87 | pub enum SetE { 88 | /// Change the color of the controller 89 | /// 90 | /// This is used by the switch for the controller icons. Every color is in `RRGGBB` format. 91 | Color(SetColor), 92 | } 93 | 94 | #[derive(Parser)] 95 | pub struct SetColor { 96 | /// Color of the body of the controller 97 | pub body: String, 98 | /// Color of the buttons, sticks and triggers 99 | pub buttons: String, 100 | /// Color of the left grip (Pro Controller only) 101 | pub left_grip: Option, 102 | /// Color of the right grip (Pro Controller only) 103 | pub right_grip: Option, 104 | } 105 | 106 | #[derive(Parser)] 107 | pub struct Ringcon { 108 | #[clap(subcommand)] 109 | pub subcmd: RingconE, 110 | } 111 | 112 | #[derive(Parser)] 113 | pub enum RingconE { 114 | /// Get the number of flex stored in the ringcon 115 | StoredFlex, 116 | /// Show the flex value in realtime 117 | Monitor, 118 | /// Random experiments 119 | Exp, 120 | } 121 | 122 | #[derive(Parser)] 123 | pub struct Relay { 124 | /// Bluetooth MAC address of the Switch 125 | #[clap(short, long, validator(is_mac))] 126 | pub address: String, 127 | /// Location of the log to write 128 | #[clap(short, long)] 129 | pub output: Option, 130 | /// Decode important HID reports and print them to stdout 131 | #[clap(short, long)] 132 | pub verbose: bool, 133 | } 134 | 135 | fn is_mac(input: &str) -> Result<(), String> { 136 | let mut i = 0; 137 | for x in input.split(":").map(|x| u8::from_str_radix(x, 16)) { 138 | match x { 139 | Ok(_) => i += 1, 140 | Err(e) => return Err(format!("MAC parsing error: {}", e)), 141 | } 142 | } 143 | if i == 6 { 144 | Ok(()) 145 | } else { 146 | Err("invalid MAC address".to_string()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/mcu/ir.rs: -------------------------------------------------------------------------------- 1 | use crate::mcu::*; 2 | pub use ir_register::*; 3 | 4 | #[repr(u8)] 5 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 6 | pub enum IRRequestId { 7 | GetSensorData = 0, 8 | GetState = 2, 9 | ReadRegister = 3, 10 | } 11 | 12 | raw_enum! { 13 | #[id: IRRequestId] 14 | #[union: IRRequestUnion] 15 | #[struct: IRRequest] 16 | pub enum IRRequestEnum { 17 | get_sensor_data get_sensor_data_mut: GetSensorData = IRAckRequestPacket, 18 | get_state get_state_mut: GetState = (), 19 | read_register read_register_mut: ReadRegister = IRReadRegisters 20 | } 21 | } 22 | 23 | impl From for IRRequest { 24 | fn from(ack_request_packet: IRAckRequestPacket) -> Self { 25 | IRRequest { 26 | id: IRRequestId::GetSensorData.into(), 27 | u: IRRequestUnion { 28 | get_sensor_data: ack_request_packet, 29 | }, 30 | } 31 | } 32 | } 33 | 34 | impl From for IRRequest { 35 | fn from(read_registers: IRReadRegisters) -> Self { 36 | IRRequest { 37 | id: IRRequestId::ReadRegister.into(), 38 | u: IRRequestUnion { 39 | read_register: read_registers, 40 | }, 41 | } 42 | } 43 | } 44 | 45 | #[repr(packed)] 46 | #[derive(Copy, Clone, Debug)] 47 | pub struct IRAckRequestPacket { 48 | pub packet_missing: RawId, 49 | pub missed_packet_id: u8, 50 | pub ack_packet_id: u8, 51 | } 52 | 53 | #[repr(packed)] 54 | #[derive(Copy, Clone, Debug)] 55 | pub struct IRReadRegisters { 56 | pub unknown_0x01: u8, 57 | pub page: u8, 58 | pub offset: u8, 59 | pub nb_registers: u8, 60 | } 61 | 62 | #[repr(u8)] 63 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 64 | pub enum MCUIRMode { 65 | IRSensorReset = 0, 66 | IRSensorSleep = 1, 67 | WaitingForConfigurationMaybe = 2, 68 | Moment = 3, 69 | /// Wii-style pointing 70 | Dpd = 4, 71 | Unknown5 = 5, 72 | Clustering = 6, 73 | ImageTransfer = 7, 74 | HandAnalysisSilhouette = 8, 75 | HandAnalysisImage = 9, 76 | HandAnalysisSilhouetteImage = 10, 77 | Unknown11 = 11, 78 | /// Used in ringfit for pulse rate detection 79 | PulseRate = 0xd, 80 | } 81 | 82 | #[repr(packed)] 83 | #[derive(Debug, Copy, Clone)] 84 | pub struct MCUIRModeData { 85 | pub ir_mode: RawId, 86 | /// Set number of packets to output per buffer 87 | pub no_of_frags: u8, 88 | /// Get it from MCUStatus 89 | pub mcu_fw_version: (U16LE, U16LE), 90 | } 91 | 92 | #[repr(packed)] 93 | #[derive(Copy, Clone, Debug)] 94 | pub struct IRStatus { 95 | _unknown_0x00: u8, 96 | pub ir_mode: RawId, 97 | pub required_fw_major_version: U16LE, 98 | pub required_fw_minor_version: U16LE, 99 | } 100 | 101 | // TODO: better debug 102 | #[repr(packed)] 103 | #[derive(Copy, Clone, Debug)] 104 | pub struct IRRegistersSlice { 105 | _unknown_0x00: u8, 106 | pub page: u8, 107 | pub offset: u8, 108 | pub nb_registers: u8, 109 | pub values: [u8; 0x7f], 110 | } 111 | 112 | #[repr(packed)] 113 | #[derive(Copy, Clone)] 114 | pub struct IRData { 115 | _unknown: [u8; 2], 116 | pub frag_number: u8, 117 | pub average_intensity: u8, 118 | // Only when EXFilter enabled 119 | _unknown3: u8, 120 | pub white_pixel_count: U16LE, 121 | pub ambient_noise_count: U16LE, 122 | pub img_fragment: [u8; 300], 123 | } 124 | 125 | impl fmt::Debug for IRData { 126 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 127 | f.debug_struct("IRData") 128 | .field("frag_number", &self.frag_number) 129 | .field("average_intensity", &self.average_intensity) 130 | .field("white_pixel_count", &self.white_pixel_count) 131 | .field("ambient_noise_count", &self.ambient_noise_count) 132 | .finish() 133 | } 134 | } 135 | 136 | #[repr(packed)] 137 | #[derive(Copy, Clone, Debug)] 138 | pub struct MCURegisters { 139 | pub len: u8, 140 | pub regs: [ir_register::Register; 9], 141 | } 142 | 143 | #[repr(packed)] 144 | #[derive(Copy, Clone)] 145 | pub struct MCUSetReg { 146 | pub cmd_id: MCUCommandId, 147 | pub subcmd_id: MCUSubCommandId, 148 | pub mode: RawId, 149 | } 150 | 151 | #[cfg(test)] 152 | #[test] 153 | fn check_output_layout() { 154 | unsafe { 155 | let report = crate::output::OutputReport::new(); 156 | let cmd = report.as_mcu_request(); 157 | assert_eq!(10, offset_of(&report, cmd)); 158 | assert_eq!(11, offset_of(&report, &cmd.u.get_ir_data)); 159 | assert_eq!( 160 | 15, 161 | offset_of(&report, &cmd.u.get_ir_data.u.read_register.nb_registers) 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/light.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[repr(packed)] 4 | #[derive(Copy, Clone, Debug)] 5 | pub struct PlayerLights(u8); 6 | 7 | impl PlayerLights { 8 | #[allow(clippy::identity_op)] 9 | pub fn new(p0: PlayerLight, p1: PlayerLight, p2: PlayerLight, p3: PlayerLight) -> PlayerLights { 10 | use PlayerLight::*; 11 | PlayerLights( 12 | ((p0 == On) as u8) << 0 13 | | ((p1 == On) as u8) << 1 14 | | ((p2 == On) as u8) << 2 15 | | ((p3 == On) as u8) << 3 16 | | ((p0 == Blinking) as u8) << 4 17 | | ((p1 == Blinking) as u8) << 5 18 | | ((p2 == Blinking) as u8) << 6 19 | | ((p3 == Blinking) as u8) << 7, 20 | ) 21 | } 22 | } 23 | 24 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 25 | pub enum PlayerLight { 26 | Off, 27 | Blinking, 28 | On, 29 | } 30 | 31 | impl From for PlayerLight { 32 | fn from(b: bool) -> Self { 33 | if b { 34 | PlayerLight::On 35 | } else { 36 | PlayerLight::Off 37 | } 38 | } 39 | } 40 | 41 | #[repr(packed)] 42 | #[derive(Copy, Clone)] 43 | pub struct HomeLight { 44 | s1: Settings1, 45 | s2: Settings2, 46 | cycles: [HomeLightCycle; 8], 47 | } 48 | 49 | impl HomeLight { 50 | pub fn new( 51 | mini_cycle_duration: u8, 52 | led_start_intensity: u8, 53 | nb_full_cycles: u8, 54 | led_cycles: &[(u8, u8, u8)], 55 | ) -> HomeLight { 56 | assert!(led_cycles.len() <= 15); 57 | assert!(mini_cycle_duration <= 0xf); 58 | assert!(led_start_intensity <= 0xf); 59 | assert!(nb_full_cycles <= 0xf); 60 | 61 | let mut s1 = Settings1(0); 62 | s1.set_nb_mini_cycles(led_cycles.len() as u8); 63 | s1.set_mini_cycle_duration(mini_cycle_duration); 64 | 65 | let mut s2 = Settings2(0); 66 | s2.set_led_start_intensity(led_start_intensity); 67 | s2.set_nb_full_cycles(nb_full_cycles); 68 | 69 | let mut cycles = [HomeLightCycle::default(); 8]; 70 | let mut it = led_cycles.iter(); 71 | let mut i = 0; 72 | while let (Some(c1), c2) = (it.next(), it.next()) { 73 | let entry = &mut cycles[i]; 74 | assert!(c1.0 <= 0xf); 75 | assert!(c1.1 <= 0xf); 76 | assert!(c1.2 <= 0xf); 77 | 78 | entry.intensity.set_first(c1.0); 79 | entry.first_duration.set_fading_transition(c1.1); 80 | entry.first_duration.set_led_duration(c1.2); 81 | if let Some(c2) = c2 { 82 | assert!(c2.0 <= 0xf); 83 | assert!(c2.1 <= 0xf); 84 | assert!(c2.2 <= 0xf); 85 | entry.intensity.set_second(c2.0); 86 | entry.second_duration.set_fading_transition(c2.1); 87 | entry.second_duration.set_led_duration(c2.2); 88 | } 89 | i += 1; 90 | } 91 | 92 | HomeLight { s1, s2, cycles } 93 | } 94 | 95 | fn cycles(&self) -> &[HomeLightCycle] { 96 | let nb = self.s1.nb_mini_cycles() as usize; 97 | &self.cycles[..(nb + 1) / 2] 98 | } 99 | } 100 | 101 | impl fmt::Debug for HomeLight { 102 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 103 | f.debug_struct("HomeLight") 104 | .field("mini_cycle_duration", &self.s1.mini_cycle_duration()) 105 | .field("start_intensity", &self.s2.led_start_intensity()) 106 | .field("nb_full_cycles", &self.s2.nb_full_cycles()) 107 | .field("cycles", &self.cycles()) 108 | .finish() 109 | } 110 | } 111 | 112 | bitfield::bitfield! { 113 | #[derive(Copy, Clone)] 114 | struct Settings1(u8); 115 | impl Debug; 116 | nb_mini_cycles, set_nb_mini_cycles: 7, 4; 117 | mini_cycle_duration, set_mini_cycle_duration: 3, 0; 118 | } 119 | 120 | bitfield::bitfield! { 121 | #[derive(Copy, Clone)] 122 | struct Settings2(u8); 123 | impl Debug; 124 | led_start_intensity, set_led_start_intensity: 7, 4; 125 | nb_full_cycles, set_nb_full_cycles: 3, 0; 126 | } 127 | 128 | #[repr(packed)] 129 | #[derive(Copy, Clone, Debug, Default)] 130 | struct HomeLightCycle { 131 | intensity: Intensity, 132 | first_duration: Durations, 133 | second_duration: Durations, 134 | } 135 | 136 | bitfield::bitfield! { 137 | #[derive(Copy, Clone, Default)] 138 | struct Intensity(u8); 139 | impl Debug; 140 | first, set_first: 7, 4; 141 | second, set_second: 3, 0; 142 | } 143 | 144 | bitfield::bitfield! { 145 | #[derive(Copy, Clone, Default)] 146 | struct Durations(u8); 147 | impl Debug; 148 | fading_transition, set_fading_transition: 7, 4; 149 | led_duration, set_led_duration: 3, 0; 150 | } 151 | 152 | #[cfg(test)] 153 | #[test] 154 | fn check_layout() { 155 | assert_eq!(26, std::mem::size_of::()); 156 | } 157 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/accessory.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{RawId, U16LE}; 4 | 5 | // subcommand id 0x58 6 | // 7 | // Maybe arg 2 is a device selector? Check with pokeball plus 8 | // 9 | // arg [4,0,0,2], ret [0,8,0,0,0,0,0,44] 10 | // arg [4,4,5,2], ret [0,8,0,0,0,0,200] 11 | // arg [4,4,50,2], ret [0,8,0,0,0,0,5,0,0,14] 12 | // arg [4,4,10,2], ret [0,20,0,0,0,0,244,22,0,0,230,5,0,0,243,11,0,0,234,12, 0, 0] 13 | // get ringcon calibration: arg [4,4,26,2] 14 | // ret [0,20,0,0,0,0] + [135, 8, 28, 0, 48, 247, 243, 0, 44, 12, 224] 15 | // write ringcon calibration: arg [20,4,26,1,16] + [135, 8, 28, 0, 48, 247, 243, 0, 44, 12, 224] 16 | // ret [0, 4] 17 | // get number steps offline ringcon: arg [4,4,49,2], ret [0,8,0,0,0,0,nb_steps, 0,0, 127|143] 18 | // reset number steps offline ringcon: arg [8,4,49,1,4], ret [0,4] 19 | 20 | #[repr(u8)] 21 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 22 | pub enum AccessoryCommandId { 23 | Get = 4, 24 | Reset = 8, 25 | Write = 20, 26 | } 27 | 28 | #[repr(u8)] 29 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 30 | pub enum AccessoryType { 31 | Ringcon = 4, 32 | } 33 | 34 | #[repr(u8)] 35 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 36 | pub enum RingconItemId { 37 | Calibration = 26, 38 | OfflineSteps = 49, 39 | } 40 | 41 | #[repr(packed)] 42 | #[derive(Copy, Clone, Debug)] 43 | #[allow(dead_code)] 44 | pub struct AccessoryCommand { 45 | id: RawId, 46 | ty: RawId, 47 | item: RawId, 48 | maybe_includes_arg: u8, 49 | maybe_arg_size: u8, 50 | raw: [u8; 18], 51 | } 52 | impl AccessoryCommand { 53 | pub fn get_offline_steps() -> Self { 54 | AccessoryCommand { 55 | id: AccessoryCommandId::Get.into(), 56 | ty: AccessoryType::Ringcon.into(), 57 | item: RingconItemId::OfflineSteps.into(), 58 | maybe_includes_arg: 2, 59 | maybe_arg_size: 0, 60 | raw: [0; 18], 61 | } 62 | } 63 | 64 | // Known CRC values: 65 | // 0 0 66 | // 1 127 67 | // 2 254 68 | // 3 129 69 | // 4 113 70 | // 5 14 71 | // 0x64 74 72 | // 0xf0 173 73 | // 0x100 200 74 | // 0x101 183 75 | // 0x103 73 76 | // 0x104 185 77 | // 0x1f4 20 78 | pub fn write_offline_steps(steps: u16, sum: u8) -> Self { 79 | let steps = steps.to_le_bytes(); 80 | AccessoryCommand { 81 | id: AccessoryCommandId::Reset.into(), 82 | ty: AccessoryType::Ringcon.into(), 83 | item: RingconItemId::OfflineSteps.into(), 84 | maybe_includes_arg: 1, 85 | maybe_arg_size: 4, 86 | raw: [ 87 | steps[0], steps[1], 0, sum, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88 | ], 89 | } 90 | } 91 | } 92 | 93 | #[repr(packed)] 94 | #[derive(Copy, Clone)] 95 | pub struct AccessoryResponse { 96 | //254: nothing connected 97 | error: u8, 98 | len: u8, 99 | unknown_0x00: [u8; 4], 100 | u: AccessoryResponseUnion, 101 | } 102 | 103 | impl AccessoryResponse { 104 | fn check_error(&self) -> Result<(), Error> { 105 | match self.error { 106 | 0 => Ok(()), 107 | 254 => Err(Error::NoAccessoryConnected), 108 | e => Err(Error::Other(e)), 109 | } 110 | } 111 | 112 | pub fn offline_steps(&self) -> Result { 113 | self.check_error()?; 114 | Ok(unsafe { self.u.offline_steps }) 115 | } 116 | } 117 | 118 | impl fmt::Debug for AccessoryResponse { 119 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 120 | f.debug_struct("AccessoryResponse") 121 | .field("maybe_error", &self.error) 122 | .field("always_0x00", &self.unknown_0x00) 123 | .field("data", unsafe { &&self.u.raw[..self.len as usize] }) 124 | .finish() 125 | } 126 | } 127 | 128 | #[derive(Copy, Clone)] 129 | union AccessoryResponseUnion { 130 | offline_steps: OfflineSteps, 131 | raw: [u8; 20], 132 | } 133 | 134 | #[repr(packed)] 135 | #[derive(Copy, Clone, Debug)] 136 | #[allow(dead_code)] 137 | pub struct OfflineSteps { 138 | pub steps: U16LE, 139 | unknown0x00: u8, 140 | maybe_crc: u8, 141 | } 142 | 143 | #[derive(Debug, Clone, Copy)] 144 | pub enum Error { 145 | NoAccessoryConnected, 146 | Other(u8), 147 | } 148 | 149 | impl std::error::Error for Error {} 150 | 151 | impl fmt::Display for Error { 152 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 153 | match *self { 154 | Error::NoAccessoryConnected => f.write_str("no accessory connected"), 155 | Error::Other(e) => f.write_fmt(format_args!("unknown accessory error: {}", e)), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/imu.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use cgmath::{Array, ElementWise, Vector3}; 3 | use std::fmt; 4 | 5 | pub const IMU_SAMPLE_DURATION: f64 = 0.005; 6 | pub const IMU_SAMPLES_PER_SECOND: u32 = 200; 7 | 8 | #[repr(u8)] 9 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 10 | pub enum IMUMode { 11 | Disabled = 0, 12 | GyroAccel = 1, 13 | _Unknown0x02 = 2, 14 | MaybeRingcon = 3, 15 | } 16 | 17 | #[repr(packed)] 18 | #[derive(Copy, Clone)] 19 | pub struct Frame { 20 | raw_accel: [I16LE; 3], 21 | raw_gyro: [I16LE; 3], 22 | } 23 | 24 | impl Frame { 25 | pub fn raw_ringcon(&self) -> u16 { 26 | let raw_self = unsafe { 27 | std::slice::from_raw_parts(self as *const _ as *const u8, std::mem::size_of_val(self)) 28 | }; 29 | u16::from_le_bytes([raw_self[2], raw_self[3]]) 30 | } 31 | pub fn raw_accel(&self) -> Vector3 { 32 | vector_from_raw(self.raw_accel) 33 | } 34 | 35 | pub fn raw_gyro(&self) -> Vector3 { 36 | vector_from_raw(self.raw_gyro) 37 | } 38 | 39 | /// Calculation from 40 | pub fn accel_g(&self, offset: Vector3, _sens: AccSens) -> Vector3 { 41 | // TODO: handle sens 42 | (self.raw_accel() * 4.).div_element_wise(Vector3::from_value(16383.) - offset) 43 | } 44 | 45 | /// The rotation described in this frame. 46 | /// 47 | pub fn rotation_dps(&self, offset: Vector3, sens: GyroSens) -> Vector3 { 48 | (self.raw_gyro() - offset) * sens.factor() 49 | } 50 | } 51 | 52 | impl fmt::Debug for Frame { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | f.debug_struct("imu::Frame") 55 | .field("accel", &self.raw_accel()) 56 | .field("gyro", &self.raw_gyro()) 57 | .finish() 58 | } 59 | } 60 | 61 | #[repr(packed)] 62 | #[derive(Copy, Clone, Default, Debug)] 63 | pub struct Sensitivity { 64 | pub gyro_sens: RawId, 65 | pub acc_sens: RawId, 66 | pub gyro_perf_rate: RawId, 67 | pub acc_anti_aliasing: RawId, 68 | } 69 | 70 | /// Sensitivity range of the gyroscope. 71 | /// 72 | /// If using DPS2000 for example, the gyroscope can measure values of 73 | /// up to +-2000 degree per second for a total range of 4000 DPS over 74 | /// the 16 bit raw value. 75 | #[repr(u8)] 76 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive)] 77 | pub enum GyroSens { 78 | DPS250 = 0, 79 | DPS500 = 1, 80 | DPS1000 = 2, 81 | DPS2000 = 3, 82 | } 83 | 84 | impl GyroSens { 85 | pub fn range_dps(self) -> u16 { 86 | match self { 87 | GyroSens::DPS250 => 500, 88 | GyroSens::DPS500 => 1000, 89 | GyroSens::DPS1000 => 2000, 90 | GyroSens::DPS2000 => 4000, 91 | } 92 | } 93 | 94 | /// factor from raw unit to dps 95 | pub fn factor(self) -> f64 { 96 | self.range_dps() as f64 * 1.147 / u16::MAX as f64 97 | } 98 | } 99 | 100 | impl Default for GyroSens { 101 | fn default() -> Self { 102 | GyroSens::DPS2000 103 | } 104 | } 105 | 106 | /// Sensitivity range of the accelerometer. 107 | /// 108 | /// If using G4 for example, the accelerometer can measure values of 109 | /// up to +-4G for a total range of 8G over the 16 bit raw value. 110 | #[repr(u8)] 111 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive)] 112 | pub enum AccSens { 113 | G8 = 0, 114 | G4 = 1, 115 | G2 = 2, 116 | G16 = 3, 117 | } 118 | 119 | impl AccSens { 120 | pub fn range_g(self) -> u16 { 121 | match self { 122 | AccSens::G8 => 16, 123 | AccSens::G4 => 8, 124 | AccSens::G2 => 4, 125 | AccSens::G16 => 32, 126 | } 127 | } 128 | } 129 | 130 | impl Default for AccSens { 131 | fn default() -> Self { 132 | AccSens::G8 133 | } 134 | } 135 | 136 | #[repr(u8)] 137 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive)] 138 | pub enum GyroPerfRate { 139 | Hz833 = 0, 140 | Hz208 = 1, 141 | } 142 | 143 | impl Default for GyroPerfRate { 144 | fn default() -> Self { 145 | GyroPerfRate::Hz208 146 | } 147 | } 148 | 149 | /// Anti-aliasing setting of the accelerometer. 150 | /// 151 | /// Accelerations frequencies above the value are ignored using a low-pass filter. 152 | /// 153 | /// See . 154 | #[repr(u8)] 155 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive)] 156 | pub enum AccAntiAliasing { 157 | Hz200 = 0, 158 | Hz100 = 1, 159 | } 160 | 161 | impl Default for AccAntiAliasing { 162 | fn default() -> Self { 163 | AccAntiAliasing::Hz100 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'joycon-sys'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=joycon-sys" 17 | ], 18 | "filter": { 19 | "name": "joycon-sys", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}" 25 | }, 26 | { 27 | "type": "lldb", 28 | "request": "launch", 29 | "name": "Debug unit tests in library 'joycon'", 30 | "cargo": { 31 | "args": [ 32 | "test", 33 | "--no-run", 34 | "--lib", 35 | "--package=joycon" 36 | ], 37 | "filter": { 38 | "name": "joycon", 39 | "kind": "lib" 40 | } 41 | }, 42 | "args": [], 43 | "cwd": "${workspaceFolder}" 44 | }, 45 | { 46 | "type": "lldb", 47 | "request": "launch", 48 | "name": "Debug executable 'joy-infrared'", 49 | "cargo": { 50 | "args": [ 51 | "build", 52 | "--bin=joy-infrared", 53 | "--package=joy-infrared" 54 | ], 55 | "filter": { 56 | "name": "joy-infrared", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | }, 63 | { 64 | "type": "lldb", 65 | "request": "launch", 66 | "name": "Debug unit tests in executable 'joy-infrared'", 67 | "cargo": { 68 | "args": [ 69 | "test", 70 | "--no-run", 71 | "--bin=joy-infrared", 72 | "--package=joy-infrared" 73 | ], 74 | "filter": { 75 | "name": "joy-infrared", 76 | "kind": "bin" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | }, 82 | { 83 | "type": "lldb", 84 | "request": "launch", 85 | "name": "Debug executable 'joy-music'", 86 | "cargo": { 87 | "args": [ 88 | "build", 89 | "--bin=joy-music", 90 | "--package=joy-music" 91 | ], 92 | "filter": { 93 | "name": "joy-music", 94 | "kind": "bin" 95 | } 96 | }, 97 | "args": [], 98 | "cwd": "${workspaceFolder}" 99 | }, 100 | { 101 | "type": "lldb", 102 | "request": "launch", 103 | "name": "Debug unit tests in executable 'joy-music'", 104 | "cargo": { 105 | "args": [ 106 | "test", 107 | "--no-run", 108 | "--bin=joy-music", 109 | "--package=joy-music" 110 | ], 111 | "filter": { 112 | "name": "joy-music", 113 | "kind": "bin" 114 | } 115 | }, 116 | "args": [], 117 | "cwd": "${workspaceFolder}" 118 | }, 119 | { 120 | "type": "lldb", 121 | "request": "launch", 122 | "name": "Debug executable 'gyromouse'", 123 | "cargo": { 124 | "args": [ 125 | "build", 126 | "--bin=gyromouse", 127 | "--package=gyromouse" 128 | ], 129 | "filter": { 130 | "name": "gyromouse", 131 | "kind": "bin" 132 | } 133 | }, 134 | "args": [], 135 | "cwd": "${workspaceFolder}" 136 | }, 137 | { 138 | "type": "lldb", 139 | "request": "launch", 140 | "name": "Debug unit tests in executable 'gyromouse'", 141 | "cargo": { 142 | "args": [ 143 | "test", 144 | "--no-run", 145 | "--bin=gyromouse", 146 | "--package=gyromouse" 147 | ], 148 | "filter": { 149 | "name": "gyromouse", 150 | "kind": "bin" 151 | } 152 | }, 153 | "args": [], 154 | "cwd": "${workspaceFolder}" 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /crates/joycon-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Helper crate for interacting with a JoyCon and Switch Pro Controller via HID. 2 | //! 3 | //! The main structs are [InputReport](input/struct.InputReport.html) and 4 | //! [OutputReport](output/struct.OutputReport.html). 5 | 6 | #[macro_use] 7 | extern crate num_derive; 8 | 9 | pub mod accessory; 10 | pub mod common; 11 | pub mod imu; 12 | pub mod input; 13 | pub mod light; 14 | pub mod mcu; 15 | pub mod output; 16 | pub mod spi; 17 | 18 | pub use common::*; 19 | pub use input::InputReport; 20 | pub use output::OutputReport; 21 | 22 | #[macro_export] 23 | macro_rules! raw_enum { 24 | ( 25 | $(#[pre_id $preid:ident $preidmut:ident: $preidty:ty])? 26 | #[id: $tyid:ident] 27 | $(#[post_id $postid:ident $postidmut:ident: $postidty:ty])? 28 | #[union: $union:ident] 29 | #[struct: $struct:ident] 30 | $(#[raw $rawty:ty])? 31 | $(#[field $field:ident $fieldmut:ident: $fieldty:ty])* 32 | pub enum $name:ident { 33 | $($varname:ident $varnamemut:ident: $id:ident = $var:ty),+ 34 | } 35 | ) => { 36 | #[repr(packed)] 37 | #[derive(Copy, Clone)] 38 | pub struct $struct { 39 | $($preid: $preidty,)? 40 | id: RawId<$tyid>, 41 | $($postid: $postidty,)? 42 | u: $union, 43 | } 44 | #[repr(packed)] 45 | #[derive(Copy, Clone)] 46 | union $union { 47 | $($varname: $var,)* 48 | $(raw: $rawty,)? 49 | $($field: $fieldty,)* 50 | } 51 | #[derive(Copy, Clone, Debug)] 52 | pub enum $name { 53 | $($id($var)),* 54 | } 55 | 56 | impl ::std::convert::TryFrom<$struct> for $name { 57 | type Error = $struct; 58 | fn try_from(x: $struct) -> Result { 59 | match x.id.try_into() { 60 | $(Some($tyid::$id) => Ok(Self::$id(unsafe {x.u.$varname}))),*, 61 | None => Err(x), 62 | } 63 | } 64 | } 65 | 66 | impl ::std::convert::From<$name> for $struct { 67 | fn from(x: $name) -> Self { 68 | let (id, u) = match x { 69 | $($name::$id(data) => ( 70 | $tyid::$id.into(), 71 | $union { $varname: data } 72 | )),*, 73 | }; 74 | $struct { 75 | $($preid: ::std::default::Default::default(),)? 76 | id, 77 | $($postid: ::std::default::Default::default(),)? 78 | u, 79 | } 80 | } 81 | } 82 | 83 | impl $struct { 84 | pub fn new() -> Self { 85 | unsafe { ::std::mem::zeroed() } 86 | } 87 | 88 | pub fn id(&self) -> RawId<$tyid> { 89 | self.id 90 | } 91 | 92 | $( 93 | pub fn $varname(&self) -> Option<&$var> { 94 | if self.id == $tyid::$id { 95 | Some(unsafe { &self.u.$varname }) 96 | } else { 97 | None 98 | } 99 | } 100 | 101 | pub fn $varnamemut(&mut self) -> Option<&mut $var> { 102 | if self.id == $tyid::$id { 103 | Some(unsafe { &mut self.u.$varname }) 104 | } else { 105 | None 106 | } 107 | } 108 | )* 109 | $( 110 | pub fn $preid(&self) -> &$preidty { 111 | &self.$preid 112 | } 113 | 114 | pub fn $preidmut(&mut self) -> &mut $preidty { 115 | &mut self.$preid 116 | } 117 | )? 118 | $( 119 | pub fn $postid(&self) -> &$postidty { 120 | &self.$postid 121 | } 122 | 123 | pub fn $postidmut(&mut self) -> &mut $postidty { 124 | &mut self.$postid 125 | } 126 | )? 127 | $( 128 | pub fn $field(&self) -> &$fieldty { 129 | unsafe { &self.u.$field} 130 | } 131 | 132 | pub fn $fieldmut(&mut self) -> &mut $fieldty { 133 | unsafe { &mut self.u.$field} 134 | } 135 | )* 136 | } 137 | 138 | impl ::std::fmt::Debug for $struct { 139 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> std::fmt::Result { 140 | let mut out = f.debug_struct(stringify!($struct)); 141 | match self.id.try_into() { 142 | $(Some($tyid::$id) => { 143 | out.field(::std::stringify!($varname), unsafe { &self.u.$varname }); 144 | }),* 145 | None => { 146 | out.field("id", &self.id); 147 | $(out.field("raw", unsafe { &self.u.raw as &$rawty });)? 148 | } 149 | }; 150 | out.finish() 151 | } 152 | } 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /joytk/src/camera.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use image::GrayImage; 3 | use joycon::{ 4 | joycon_sys::mcu::ir::{MCUIRMode, Resolution}, 5 | JoyCon, 6 | }; 7 | use pixels::{Pixels, SurfaceTexture}; 8 | use winit::{ 9 | dpi::{LogicalPosition, LogicalSize, PhysicalSize}, 10 | event::{Event, VirtualKeyCode}, 11 | event_loop::{ControlFlow, EventLoop}, 12 | }; 13 | use winit_input_helper::WinitInputHelper; 14 | 15 | #[derive(Debug)] 16 | enum Cmd { 17 | Image(GrayImage), 18 | Stop, 19 | } 20 | 21 | pub fn run(mut joycon: JoyCon) -> Result<()> { 22 | let res = Resolution::R160x120; 23 | joycon.enable_ir(res)?; 24 | joycon.set_ir_image_mode(MCUIRMode::HandAnalysisSilhouetteImage, 255)?; 25 | 26 | let event_loop = EventLoop::with_user_event(); 27 | 28 | std::thread::spawn({ 29 | let proxy = event_loop.create_proxy(); 30 | move || -> Result<()> { 31 | loop { 32 | let report = joycon.tick()?; 33 | if let Some(img) = report.image { 34 | proxy.send_event(Cmd::Image(img))?; 35 | } 36 | } 37 | } 38 | }); 39 | 40 | let mut input = WinitInputHelper::new(); 41 | let (window, p_width, p_height, mut _hidpi_factor) = 42 | create_window("Joycon camera", &event_loop); 43 | 44 | let surface_texture = SurfaceTexture::new(p_width, p_height, &window); 45 | 46 | let mut image = None; 47 | 48 | let (p_width, p_height) = (300, 400); 49 | let mut pixels = Pixels::new(p_width, p_height, surface_texture)?; 50 | event_loop.run(move |event, _, control_flow| { 51 | if input.update(&event) { 52 | // Close events 53 | if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { 54 | *control_flow = ControlFlow::Exit; 55 | return; 56 | } 57 | // Adjust high DPI factor 58 | if let Some(factor) = input.scale_factor_changed() { 59 | _hidpi_factor = factor; 60 | } 61 | // Resize the window 62 | if let Some(size) = input.window_resized() { 63 | pixels.resize_surface(size.width, size.height); 64 | } 65 | } else if let Event::UserEvent(cmd) = event { 66 | match cmd { 67 | Cmd::Image(img) => { 68 | image = Some(img); 69 | window.request_redraw(); 70 | } 71 | Cmd::Stop => { 72 | *control_flow = ControlFlow::Exit; 73 | return; 74 | } 75 | } 76 | } else if let Event::RedrawRequested(_) = event { 77 | let frame = pixels.get_frame(); 78 | frame.fill(0); 79 | 80 | if let Some(ref img) = image { 81 | for (x, y, pixel) in img.enumerate_pixels() { 82 | let offset = ((x + y * p_width) * 4) as usize; 83 | let color = &mut frame[offset..offset + 4]; 84 | color[0] = 0; 85 | color[1] = 0; 86 | color[2] = pixel.0[0]; 87 | color[3] = 255; 88 | } 89 | } 90 | 91 | if pixels 92 | .render() 93 | .map_err(|e| eprintln!("pixels.render() failed: {}", e)) 94 | .is_err() 95 | { 96 | *control_flow = ControlFlow::Exit; 97 | return; 98 | } 99 | } 100 | }); 101 | } 102 | 103 | const SCREEN_WIDTH: u32 = 600; 104 | const SCREEN_HEIGHT: u32 = 600; 105 | 106 | fn create_window( 107 | title: &str, 108 | event_loop: &EventLoop, 109 | ) -> (winit::window::Window, u32, u32, f64) { 110 | // Create a hidden window so we can estimate a good default window size 111 | let window = winit::window::WindowBuilder::new() 112 | .with_visible(false) 113 | .with_title(title) 114 | .build(&event_loop) 115 | .unwrap(); 116 | let hidpi_factor = window.scale_factor(); 117 | 118 | // Get dimensions 119 | let width = SCREEN_WIDTH as f64; 120 | let height = SCREEN_HEIGHT as f64; 121 | let (monitor_width, monitor_height) = { 122 | if let Some(monitor) = window.current_monitor() { 123 | let size = monitor.size().to_logical(hidpi_factor); 124 | (size.width, size.height) 125 | } else { 126 | (width, height) 127 | } 128 | }; 129 | let scale = (monitor_height / height * 2.0 / 3.0).round().max(1.0); 130 | 131 | // Resize, center, and display the window 132 | let min_size: winit::dpi::LogicalSize = 133 | PhysicalSize::new(width, height).to_logical(hidpi_factor); 134 | let default_size = LogicalSize::new(width * scale, height * scale); 135 | let center = LogicalPosition::new( 136 | (monitor_width - width * scale) / 2.0, 137 | (monitor_height - height * scale) / 2.0, 138 | ); 139 | window.set_inner_size(default_size); 140 | window.set_min_inner_size(Some(min_size)); 141 | window.set_outer_position(center); 142 | window.set_visible(true); 143 | 144 | let size = default_size.to_physical::(hidpi_factor); 145 | 146 | ( 147 | window, 148 | size.width.round() as u32, 149 | size.height.round() as u32, 150 | hidpi_factor, 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /joy-infrared/src/render/camera.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{prelude::*, Deg, Euler, Matrix4, Quaternion, Vector3}; 2 | use iced_wgpu::wgpu; 3 | 4 | use iced_winit::winit::{ 5 | self, 6 | event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent}, 7 | }; 8 | 9 | pub struct Camera { 10 | eye: Vector3, 11 | pitch: Deg, 12 | yaw: Deg, 13 | aspect: f32, 14 | fovy: f32, 15 | znear: f32, 16 | zfar: f32, 17 | 18 | controller: CameraController, 19 | } 20 | 21 | impl Camera { 22 | pub fn new(sc_desc: &wgpu::SwapChainDescriptor) -> Camera { 23 | let mut camera = Camera { 24 | eye: (0., 0., 0.).into(), 25 | pitch: Deg(0.), 26 | yaw: Deg(180.), 27 | aspect: sc_desc.width as f32 / sc_desc.height as f32, 28 | fovy: 45.0, 29 | znear: 0.1, 30 | zfar: 10.0, 31 | controller: CameraController::default(), 32 | }; 33 | camera.update_aspect(sc_desc.width, sc_desc.height); 34 | camera 35 | } 36 | 37 | pub fn update_aspect(&mut self, width: u32, height: u32) { 38 | self.aspect = width as f32 / height as f32 39 | } 40 | 41 | fn eye_rot(&self) -> Quaternion { 42 | Quaternion::from(Euler::new(self.pitch, self.yaw, Deg(0.))) 43 | } 44 | 45 | pub fn build_view_projection_matrix(&self) -> Matrix4 { 46 | let proj = cgmath::perspective(cgmath::Deg(self.fovy), self.aspect, self.znear, self.zfar); 47 | let view = Matrix4::from(self.eye_rot()) * Matrix4::from_translation(self.eye); 48 | OPENGL_TO_WGPU_MATRIX * proj * view 49 | } 50 | 51 | pub fn input(&mut self, event: &winit::event::WindowEvent) -> bool { 52 | self.controller.input(event) 53 | } 54 | 55 | pub fn mouse_move(&mut self, delta: (f64, f64)) { 56 | self.controller.mouse_move(delta); 57 | } 58 | 59 | pub fn update(&mut self, dt: std::time::Duration) { 60 | let sens_dpu = 1.; 61 | self.pitch = Deg({ 62 | let pitch = self.pitch.0 + self.controller.mouse_delta.1 as f32 * sens_dpu; 63 | if pitch > 90. { 64 | 90. 65 | } else if pitch < -90. { 66 | -90. 67 | } else { 68 | pitch 69 | } 70 | }); 71 | self.yaw += Deg(self.controller.mouse_delta.0 as f32 * sens_dpu); 72 | self.yaw = self.yaw.normalize(); 73 | self.controller.mouse_delta = (0., 0.); 74 | 75 | let c = &self.controller; 76 | let mut sum = Vector3::zero(); 77 | if c.right { 78 | sum -= Vector3::unit_x(); 79 | } 80 | if c.left { 81 | sum += Vector3::unit_x(); 82 | } 83 | if c.up { 84 | sum += Vector3::unit_y(); 85 | } 86 | if c.down { 87 | sum -= Vector3::unit_y(); 88 | } 89 | if c.forward { 90 | sum += Vector3::unit_z(); 91 | } 92 | if c.backward { 93 | sum -= Vector3::unit_z(); 94 | } 95 | if sum != Vector3::zero() { 96 | let speed_ups = 2.; 97 | self.eye += self.eye_rot().invert().rotate_vector(sum.normalize()) 98 | * speed_ups 99 | * dt.as_millis() as f32 100 | / 1000.; 101 | } 102 | } 103 | } 104 | 105 | #[derive(Default)] 106 | struct CameraController { 107 | mouse_delta: (f64, f64), 108 | up: bool, 109 | down: bool, 110 | right: bool, 111 | left: bool, 112 | backward: bool, 113 | forward: bool, 114 | } 115 | 116 | impl CameraController { 117 | pub fn mouse_move(&mut self, delta: (f64, f64)) { 118 | self.mouse_delta = delta; 119 | } 120 | 121 | pub fn input(&mut self, event: &WindowEvent) -> bool { 122 | match event { 123 | WindowEvent::KeyboardInput { 124 | input: 125 | KeyboardInput { 126 | state, 127 | virtual_keycode: Some(keycode), 128 | .. 129 | }, 130 | .. 131 | } => { 132 | let pressed = *state == ElementState::Pressed; 133 | match keycode { 134 | VirtualKeyCode::Z => { 135 | self.forward = pressed; 136 | true 137 | } 138 | VirtualKeyCode::Q => { 139 | self.left = pressed; 140 | true 141 | } 142 | VirtualKeyCode::S => { 143 | self.backward = pressed; 144 | true 145 | } 146 | VirtualKeyCode::D => { 147 | self.right = pressed; 148 | true 149 | } 150 | VirtualKeyCode::A => { 151 | self.up = pressed; 152 | true 153 | } 154 | VirtualKeyCode::E => { 155 | self.down = pressed; 156 | true 157 | } 158 | _ => false, 159 | } 160 | } 161 | _ => false, 162 | } 163 | } 164 | } 165 | 166 | // from https://sotrh.github.io/learn-wgpu/beginner/tutorial6-uniforms/#a-perspective-camera 167 | #[rustfmt::skip] 168 | pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( 169 | 1.0, 0.0, 0.0, 0.0, 170 | 0.0, 1.0, 0.0, 0.0, 171 | 0.0, 0.0, 0.5, 0.0, 172 | 0.0, 0.0, 0.5, 1.0, 173 | ); 174 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/input/values.rs: -------------------------------------------------------------------------------- 1 | use num::FromPrimitive; 2 | use std::fmt; 3 | 4 | bitfield::bitfield! { 5 | #[repr(transparent)] 6 | #[derive(Copy, Clone)] 7 | pub struct DeviceStatus(u8); 8 | impl Debug; 9 | 10 | pub connected, _: 0; 11 | pub u8, into DeviceType, device_type, _: 2, 1; 12 | pub charging, _: 4; 13 | pub u8, into BatteryLevel, battery_level, _: 7, 5; 14 | } 15 | 16 | #[derive(Debug, Copy, Clone, FromPrimitive)] 17 | pub enum DeviceType { 18 | ProController = 0, 19 | // Used when the ringcon is plugged, maybe also for the pokeball? 20 | MaybeAccessory = 1, 21 | // Used in one InputReport when the ringcon is plugged, then switch to value 1. 22 | MaybeInitializingAccessory = 2, 23 | Joycon = 3, 24 | } 25 | 26 | impl From for DeviceType { 27 | fn from(v: u8) -> Self { 28 | match DeviceType::from_u8(v) { 29 | Some(t) => t, 30 | None => panic!("unknown device type 0x{:x}", v), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug, Copy, Clone, FromPrimitive, Eq, PartialEq, Ord, PartialOrd)] 36 | pub enum BatteryLevel { 37 | Empty = 0, 38 | Critical = 1, 39 | Low = 2, 40 | Medium = 3, 41 | Full = 4, 42 | } 43 | 44 | impl From for BatteryLevel { 45 | fn from(v: u8) -> Self { 46 | BatteryLevel::from_u8(v).expect("unexpected battery level") 47 | } 48 | } 49 | 50 | #[repr(packed)] 51 | #[derive(Copy, Clone, Default)] 52 | pub struct ButtonsStatus { 53 | pub right: RightButtons, 54 | pub middle: MiddleButtons, 55 | pub left: LeftButtons, 56 | } 57 | 58 | impl fmt::Debug for ButtonsStatus { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | f.debug_tuple("ButtonsStatus") 61 | .field(&format_args!("{}", self)) 62 | .finish() 63 | } 64 | } 65 | 66 | impl fmt::Display for ButtonsStatus { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | if self.right.a() { 69 | write!(f, " A")?; 70 | } 71 | if self.right.b() { 72 | write!(f, " B")?; 73 | } 74 | if self.right.x() { 75 | write!(f, " X")?; 76 | } 77 | if self.right.y() { 78 | write!(f, " Y")?; 79 | } 80 | if self.left.up() { 81 | write!(f, " UP")?; 82 | } 83 | if self.left.down() { 84 | write!(f, " DOWN")?; 85 | } 86 | if self.left.left() { 87 | write!(f, " LEFT")?; 88 | } 89 | if self.left.right() { 90 | write!(f, " RIGHT")?; 91 | } 92 | if self.left.l() { 93 | write!(f, " L")?; 94 | } 95 | if self.left.zl() { 96 | write!(f, " ZL")?; 97 | } 98 | if self.right.r() { 99 | write!(f, " R")?; 100 | } 101 | if self.right.zr() { 102 | write!(f, " ZR")?; 103 | } 104 | if self.left.sl() || self.right.sl() { 105 | write!(f, " SR")?; 106 | } 107 | if self.left.sr() || self.right.sr() { 108 | write!(f, " SR")?; 109 | } 110 | if self.middle.lstick() { 111 | write!(f, " L3")?; 112 | } 113 | if self.middle.rstick() { 114 | write!(f, " R3")?; 115 | } 116 | if self.middle.minus() { 117 | write!(f, " -")?; 118 | } 119 | if self.middle.plus() { 120 | write!(f, " +")?; 121 | } 122 | if self.middle.capture() { 123 | write!(f, " CAPTURE")?; 124 | } 125 | if self.middle.home() { 126 | write!(f, " HOME")?; 127 | } 128 | Ok(()) 129 | } 130 | } 131 | 132 | bitfield::bitfield! { 133 | #[repr(transparent)] 134 | #[derive(Copy, Clone, Default)] 135 | pub struct RightButtons(u8); 136 | impl Debug; 137 | pub y, _: 0; 138 | pub x, _: 1; 139 | pub b, _: 2; 140 | pub a, _: 3; 141 | pub sr, _: 4; 142 | pub sl, _: 5; 143 | pub r, _: 6; 144 | pub zr, _: 7; 145 | } 146 | bitfield::bitfield! { 147 | #[repr(transparent)] 148 | #[derive(Copy, Clone, Default)] 149 | pub struct MiddleButtons(u8); 150 | impl Debug; 151 | pub minus, _: 0; 152 | pub plus, _: 1; 153 | pub rstick, _: 2; 154 | pub lstick, _: 3; 155 | pub home, _: 4; 156 | pub capture, _: 5; 157 | pub _unused, _: 6; 158 | pub charging_grip, _: 7; 159 | } 160 | 161 | bitfield::bitfield! { 162 | #[repr(transparent)] 163 | #[derive(Copy, Clone, Default)] 164 | pub struct LeftButtons(u8); 165 | impl Debug; 166 | pub down, _: 0; 167 | pub up, _: 1; 168 | pub right, _: 2; 169 | pub left, _: 3; 170 | pub sr, _: 4; 171 | pub sl, _: 5; 172 | pub l, _: 6; 173 | pub zl, _: 7; 174 | } 175 | 176 | pub enum Button { 177 | N, 178 | S, 179 | E, 180 | W, 181 | L, 182 | R, 183 | ZL, 184 | ZR, 185 | L3, 186 | R3, 187 | UP, 188 | DOWN, 189 | LEFT, 190 | RIGHT, 191 | } 192 | 193 | #[repr(packed)] 194 | #[derive(Copy, Clone)] 195 | pub struct Stick { 196 | data: [u8; 3], 197 | } 198 | 199 | impl Stick { 200 | pub fn x(self) -> u16 { 201 | u16::from(self.data[0]) | u16::from(self.data[1] & 0xf) << 8 202 | } 203 | 204 | pub fn y(self) -> u16 { 205 | u16::from(self.data[1]) >> 4 | u16::from(self.data[2]) << 4 206 | } 207 | } 208 | 209 | impl fmt::Debug for Stick { 210 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 211 | f.debug_tuple("Stick") 212 | .field(&self.x()) 213 | .field(&self.y()) 214 | .finish() 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /joy-infrared/src/main.rs: -------------------------------------------------------------------------------- 1 | use hidapi::HidApi; 2 | use cgmath::prelude::One; 3 | use iced_winit::winit::{ 4 | self, 5 | event_loop::{EventLoop, EventLoopProxy}, 6 | }; 7 | use joycon::{ 8 | joycon_sys::{ 9 | input::BatteryLevel, 10 | light::{self, PlayerLight}, 11 | mcu::ir::*, 12 | NINTENDO_VENDOR_ID, 13 | }, 14 | *, 15 | }; 16 | use render::*; 17 | use std::f32::consts::PI; 18 | use std::sync::mpsc; 19 | 20 | mod mouse; 21 | mod render; 22 | 23 | fn main() { 24 | env_logger::init(); 25 | std::panic::set_hook(Box::new(|x| { 26 | println!("{}", x); 27 | std::thread::sleep(std::time::Duration::from_secs(5)); 28 | })); 29 | 30 | let event_loop = EventLoop::with_user_event(); 31 | let window = winit::window::WindowBuilder::new() 32 | .with_maximized(false) 33 | .with_title("Joy") 34 | .build(&event_loop) 35 | .unwrap(); 36 | let proxy = event_loop.create_proxy(); 37 | let (thread_contact, recv) = mpsc::channel(); 38 | let thread_handle = std::thread::spawn(|| { 39 | if let Err(e) = real_main(proxy, recv) { 40 | eprintln!("{:?}", e); 41 | } 42 | }); 43 | 44 | smol::block_on(render::run( 45 | event_loop, 46 | window, 47 | thread_contact, 48 | thread_handle, 49 | )); 50 | } 51 | 52 | fn real_main( 53 | proxy: EventLoopProxy, 54 | recv: mpsc::Receiver, 55 | ) -> anyhow::Result<()> { 56 | let mut image = ::image::GrayImage::new(240, 320); 57 | for (x, y, pixel) in image.enumerate_pixels_mut() { 58 | *pixel = [ 59 | (((x as f32 / 240. * PI).sin() * (y as f32 / 320. * PI).sin()).powf(10.) * 255.) as u8, 60 | ] 61 | .into(); 62 | } 63 | assert!(proxy 64 | .send_event(UserEvent::IRImage(image, cgmath::Quaternion::one())) 65 | .is_ok()); 66 | let mut api = HidApi::new()?; 67 | loop { 68 | api.refresh_devices()?; 69 | if let Some(device_info) = api 70 | .device_list() 71 | .find(|x| x.vendor_id() == NINTENDO_VENDOR_ID) 72 | { 73 | let device = device_info.open_device(&api)?; 74 | match hid_main(device, device_info, proxy.clone(), &recv) { 75 | Ok(true) => {} 76 | Ok(false) => return Ok(()), 77 | Err(e) => println!("Joycon error: {}", e), 78 | } 79 | } else { 80 | std::thread::sleep(std::time::Duration::from_secs(1)); 81 | } 82 | } 83 | } 84 | 85 | fn hid_main( 86 | device: hidapi::HidDevice, 87 | device_info: &hidapi::DeviceInfo, 88 | proxy: EventLoopProxy, 89 | recv: &mpsc::Receiver, 90 | ) -> anyhow::Result { 91 | let mut _mouse = mouse::Mouse::new(); 92 | 93 | let mut device = JoyCon::new(device, device_info.clone())?; 94 | println!("new dev: {:?}", device.get_dev_info()?); 95 | 96 | println!("Calibrating..."); 97 | device.enable_imu()?; 98 | device.load_calibration()?; 99 | 100 | if device.supports_ir() { 101 | device.enable_ir(Resolution::R160x120)?; 102 | } 103 | dbg!(device.set_home_light(light::HomeLight::new( 104 | 0x8, 105 | 0x2, 106 | 0x0, 107 | &[(0xf, 0xf, 0), (0x2, 0xf, 0)], 108 | ))?); 109 | 110 | let last_position = cgmath::Quaternion::one(); 111 | let battery_level = device.tick()?.info.battery_level(); 112 | 113 | device.set_player_light(light::PlayerLights::new( 114 | (battery_level >= BatteryLevel::Full).into(), 115 | (battery_level >= BatteryLevel::Medium).into(), 116 | (battery_level >= BatteryLevel::Low).into(), 117 | if battery_level >= BatteryLevel::Low { 118 | PlayerLight::On 119 | } else { 120 | PlayerLight::Blinking 121 | }, 122 | ))?; 123 | 124 | println!("Running..."); 125 | 'main_loop: loop { 126 | let report = device.tick()?; 127 | 128 | if let Some(image) = report.image { 129 | if proxy 130 | .send_event(UserEvent::IRImage(image, last_position)) 131 | .is_err() 132 | { 133 | dbg!("shutdown "); 134 | break 'main_loop; 135 | } 136 | // TODO: update last_position 137 | } 138 | 139 | 'recv_loop: loop { 140 | match recv.try_recv() { 141 | Ok(JoyconCmd::Stop) | Err(mpsc::TryRecvError::Disconnected) => { 142 | eprintln!("shutting down thread"); 143 | break 'main_loop; 144 | } 145 | Ok(JoyconCmd::SetResolution(resolution)) => { 146 | dbg!(device.change_ir_resolution(resolution)?); 147 | } 148 | Ok(JoyconCmd::SetRegister(register)) => { 149 | assert!(!register.same_address(Register::resolution(Resolution::R320x240))); 150 | dbg!(device.set_ir_registers(&[register, Register::finish()])?); 151 | } 152 | Ok(JoyconCmd::SetRegisters([r1, r2])) => { 153 | assert!(!r1.same_address(Register::resolution(Resolution::R320x240))); 154 | assert!(!r2.same_address(Register::resolution(Resolution::R320x240))); 155 | dbg!(device.set_ir_registers(&[r1, r2, Register::finish()])?); 156 | } 157 | Err(mpsc::TryRecvError::Empty) => break 'recv_loop, 158 | } 159 | } 160 | } 161 | 162 | dbg!(device.disable_mcu()?); 163 | 164 | dbg!(device.set_home_light(light::HomeLight::new(0x8, 0x4, 0x0, &[]))?); 165 | 166 | Ok(false) 167 | } 168 | -------------------------------------------------------------------------------- /joytk/src/relay.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use bluetooth_sys::*; 3 | use joycon::{ 4 | hidapi::HidDevice, 5 | joycon_sys::{ 6 | output::SubcommandRequestEnum, InputReport, InputReportId::StandardFull, OutputReport, 7 | }, 8 | }; 9 | use socket2::{SockAddr, Socket}; 10 | use std::{ 11 | convert::TryInto, 12 | ffi::CString, 13 | fs::OpenOptions, 14 | intrinsics::transmute, 15 | io::Write, 16 | mem::{size_of_val, zeroed, MaybeUninit}, 17 | thread::sleep, 18 | time::{Duration, Instant}, 19 | }; 20 | 21 | use crate::opts::Relay; 22 | 23 | pub fn relay(device: HidDevice, opts: &Relay) -> anyhow::Result<()> { 24 | let mut output = opts 25 | .output 26 | .as_ref() 27 | .map(|path| { 28 | OpenOptions::new() 29 | .write(true) 30 | .create(true) 31 | .truncate(true) 32 | .open(path) 33 | .context("opening the log file") 34 | }) 35 | .transpose()?; 36 | let (mut _client_ctl, mut client_itr) = connect_switch(&opts.address)?; 37 | 38 | // Force input reports to be generated so that we don't have to manually click on a button. 39 | device.write( 40 | OutputReport::from(SubcommandRequestEnum::SetInputReportMode( 41 | StandardFull.into(), 42 | )) 43 | .as_bytes(), 44 | )?; 45 | 46 | let start = Instant::now(); 47 | loop { 48 | { 49 | let mut buf = [0; 500]; 50 | buf[0] = 0xa1; 51 | let len = device 52 | .read_timeout(&mut buf[1..], 0) 53 | .context("joycon recv")?; 54 | if len > 0 { 55 | let mut report = InputReport::new(); 56 | let raw_report = report.as_bytes_mut(); 57 | raw_report.copy_from_slice(&buf[1..raw_report.len() + 1]); 58 | 59 | let elapsed = start.elapsed().as_secs_f64(); 60 | 61 | if let Some(subcmd) = report.subcmd_reply() { 62 | println!("{:0>9.4} {:?}", elapsed, subcmd); 63 | } else if let Some(mcu) = report.mcu_report() { 64 | println!("{:0>9.4} {:?}", elapsed, mcu); 65 | } 66 | 67 | if let Some(ref mut out) = output { 68 | writeln!(out, "> {:0>9.4} {}", elapsed, hex::encode(&buf[1..len + 1]))?; 69 | } 70 | 71 | if let Err(e) = client_itr.send(&buf[..len + 1]) { 72 | if e.raw_os_error() == Some(107) { 73 | eprintln!("Reconnecting the switch"); 74 | let x = connect_switch(&opts.address)?; 75 | _client_ctl = x.0; 76 | client_itr = x.1; 77 | 78 | // Force input reports to be generated so that we don't have to manually click on a button. 79 | device.write( 80 | OutputReport::from(SubcommandRequestEnum::SetInputReportMode( 81 | StandardFull.into(), 82 | )) 83 | .as_bytes(), 84 | )?; 85 | } 86 | } 87 | } 88 | } 89 | { 90 | let mut buf = [MaybeUninit::uninit(); 500]; 91 | if let Ok(len) = client_itr.recv(&mut buf).context("switch recv") { 92 | if len > 0 { 93 | let buf: [u8; 500] = unsafe { transmute(buf) }; 94 | let mut report = OutputReport::new(); 95 | let raw_report = report.as_bytes_mut(); 96 | raw_report.copy_from_slice(&buf[1..raw_report.len() + 1]); 97 | 98 | let elapsed = start.elapsed().as_secs_f64(); 99 | 100 | if let Some(subcmd) = report.rumble_subcmd() { 101 | println!("{:0>9.4} {:?}", elapsed, subcmd); 102 | } else if let Some(mcu) = report.request_mcu_data() { 103 | println!("{:0>9.4} {:?}", elapsed, mcu); 104 | } 105 | 106 | if let Some(ref mut out) = output { 107 | writeln!(out, "< {:0>9.4} {}", elapsed, hex::encode(&buf[1..len + 1]))?; 108 | } 109 | 110 | device.write(&buf[1..len]).context("joycon send")?; 111 | } 112 | } 113 | } 114 | sleep(Duration::from_millis(1)) 115 | } 116 | } 117 | 118 | fn connect_switch(address: &str) -> anyhow::Result<(Socket, Socket)> { 119 | let client_ctl = Socket::new( 120 | (AF_BLUETOOTH as i32).into(), 121 | (__socket_type_SOCK_SEQPACKET as i32).into(), 122 | Some((BTPROTO_L2CAP as i32).into()), 123 | )?; 124 | let client_itr = Socket::new( 125 | (AF_BLUETOOTH as i32).into(), 126 | (__socket_type_SOCK_SEQPACKET as i32).into(), 127 | Some((BTPROTO_L2CAP as i32).into()), 128 | )?; 129 | 130 | unsafe { 131 | let ctl_addr = create_sockaddr(address, 17)?; 132 | client_ctl 133 | .connect(&ctl_addr) 134 | .context("error connecting psm 17")?; 135 | client_ctl 136 | .set_nonblocking(true) 137 | .context("non blocking error")?; 138 | 139 | let itr_addr = create_sockaddr(address, 19)?; 140 | client_itr 141 | .connect(&itr_addr) 142 | .context("error connecting psm 17")?; 143 | client_itr 144 | .set_nonblocking(true) 145 | .context("non blocking error")?; 146 | } 147 | 148 | Ok((client_ctl, client_itr)) 149 | } 150 | 151 | unsafe fn create_sockaddr(address: &str, psm: u16) -> Result { 152 | Ok(SockAddr::init(|storage, len| { 153 | let storage = &mut *storage.cast::(); 154 | *len = size_of_val(storage) as u32; 155 | *storage = sockaddr_l2 { 156 | l2_family: AF_BLUETOOTH.try_into().unwrap(), 157 | // todo: watch out endian 158 | l2_psm: psm.to_le(), 159 | ..zeroed() 160 | }; 161 | let sa = CString::new(address)?; 162 | str2ba(sa.as_ptr(), &mut storage.l2_bdaddr); 163 | Ok(()) 164 | })? 165 | .1) 166 | } 167 | -------------------------------------------------------------------------------- /joytk/src/interface.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::{ 3 | event::{self, Event as CEvent, KeyCode}, 4 | terminal::{disable_raw_mode, enable_raw_mode}, 5 | }; 6 | use joycon::hidapi::{HidApi, DeviceInfo, HidDevice}; 7 | use joycon::joycon_sys::{HID_IDS, NINTENDO_VENDOR_ID}; 8 | use std::{ 9 | collections::{HashMap, HashSet}, 10 | mem::swap, 11 | sync::mpsc::Sender, 12 | thread::sleep, 13 | time::{Duration, Instant}, 14 | }; 15 | use tui::{ 16 | backend::CrosstermBackend, 17 | layout::{Constraint, Direction, Layout}, 18 | style::{Color, Style}, 19 | text::{Span, Spans}, 20 | widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Table, Tabs, Widget}, 21 | Terminal, 22 | }; 23 | 24 | enum Event { 25 | CEvent(CEvent), 26 | NewDevice(String, HidDevice), 27 | DisconnectedDevice(String), 28 | Tick, 29 | } 30 | 31 | pub fn run() -> Result<()> { 32 | enable_raw_mode()?; 33 | 34 | let (tx, rx) = std::sync::mpsc::channel(); 35 | start_input_loop(tx.clone()); 36 | start_hidapi_loop(tx)?; 37 | 38 | let mut devices = HashMap::::new(); 39 | let mut good = true; 40 | let mut device_selected = 0; 41 | 42 | let mut menu_selected = ListState::default(); 43 | menu_selected.select(Some(0)); 44 | 45 | let stdout = std::io::stdout(); 46 | let backend = CrosstermBackend::new(stdout); 47 | let mut terminal = Terminal::new(backend)?; 48 | terminal.clear()?; 49 | 50 | 'main: loop { 51 | terminal.draw(|frame| { 52 | let size = frame.size(); 53 | let chunks = Layout::default() 54 | .direction(Direction::Vertical) 55 | .constraints([Constraint::Length(3), Constraint::Min(2)]) 56 | .split(size); 57 | 58 | let mut macs = devices.keys().collect::>(); 59 | macs.sort(); 60 | let device_selector = Tabs::new( 61 | macs.into_iter() 62 | .map(|mac| Spans::from(mac.clone())) 63 | .collect(), 64 | ) 65 | .select(device_selected) 66 | .block(Block::default().title("Devices").borders(Borders::ALL)) 67 | .style(Style::default().fg(Color::White)) 68 | .highlight_style(Style::default().fg(Color::Yellow)) 69 | .divider(Span::raw("|")); 70 | 71 | frame.render_widget(device_selector, chunks[0]); 72 | 73 | let display_chunks = Layout::default() 74 | .direction(Direction::Horizontal) 75 | .constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref()) 76 | .split(chunks[1]); 77 | let (menu, display) = render_device(&menu_selected); 78 | frame.render_stateful_widget(menu, display_chunks[0], &mut menu_selected); 79 | frame.render_widget(menu, display_chunks[1]); 80 | })?; 81 | 82 | match rx.recv()? { 83 | Event::CEvent(CEvent::Key(k)) => match k.code { 84 | KeyCode::Char('q') => { 85 | disable_raw_mode()?; 86 | terminal.clear()?; 87 | terminal.show_cursor()?; 88 | break 'main; 89 | } 90 | KeyCode::Tab => { 91 | device_selected = (device_selected + 1).min(devices.len() - 1); 92 | } 93 | KeyCode::BackTab => { 94 | device_selected = device_selected.saturating_sub(1); 95 | } 96 | _ => {} 97 | }, 98 | Event::CEvent(CEvent::Mouse(_)) => {} 99 | Event::CEvent(CEvent::Resize(_, _)) => {} 100 | Event::NewDevice(serial, device) => { 101 | devices.insert(serial, device); 102 | } 103 | Event::DisconnectedDevice(serial) => { 104 | devices.remove(&serial); 105 | } 106 | Event::Tick => { 107 | good = false; 108 | if let Some(dev) = devices.values().next() { 109 | good = dev.read_timeout(&mut [0; 500], 1).is_ok(); 110 | } 111 | } 112 | }; 113 | } 114 | Ok(()) 115 | } 116 | 117 | fn render_device(menu_state: &ListState) -> (List, Table) { 118 | let menu = List::new() 119 | Table::new(vec![ListItem::new(content)]); 120 | (menu, values) 121 | } 122 | 123 | fn start_input_loop(tx: Sender) { 124 | let tick_rate = Duration::from_millis(200); 125 | std::thread::spawn(move || { 126 | let mut last_tick = Instant::now(); 127 | loop { 128 | let timeout = tick_rate 129 | .checked_sub(last_tick.elapsed()) 130 | .unwrap_or_else(|| Duration::from_secs(0)); 131 | 132 | if event::poll(timeout).expect("poll works") { 133 | let event = event::read().expect("can read events"); 134 | tx.send(Event::CEvent(event)).expect("can send events"); 135 | } 136 | 137 | if last_tick.elapsed() >= tick_rate { 138 | if let Ok(_) = tx.send(Event::Tick) { 139 | last_tick = Instant::now(); 140 | } 141 | } 142 | } 143 | }); 144 | } 145 | 146 | fn start_hidapi_loop(tx: Sender) -> Result<()> { 147 | let mut api = HidApi::new()?; 148 | std::thread::spawn(move || { 149 | let mut known = HashSet::new(); 150 | let mut new_known = HashSet::new(); 151 | loop { 152 | api.refresh_devices().unwrap(); 153 | let devices: HashMap = api 154 | .device_list() 155 | .filter(|x| { 156 | x.vendor_id() == NINTENDO_VENDOR_ID && HID_IDS.contains(&x.product_id()) 157 | }) 158 | .map(|i| (i.serial_number().unwrap().to_string(), i)) 159 | .collect(); 160 | for (serial, info) in devices { 161 | if !known.contains(&serial) { 162 | match info.open_device(&api) { 163 | Ok(device) => tx.send(Event::NewDevice(serial.clone(), device)).unwrap(), 164 | Err(e) => eprintln!("hidapi error: {:?}", e), 165 | } 166 | } 167 | new_known.insert(serial); 168 | } 169 | for serial in known.difference(&new_known) { 170 | tx.send(Event::DisconnectedDevice(serial.clone())).unwrap(); 171 | } 172 | swap(&mut known, &mut new_known); 173 | new_known.clear(); 174 | sleep(Duration::from_millis(200)); 175 | } 176 | }); 177 | Ok(()) 178 | } 179 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/common.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Vector3; 2 | use num::{FromPrimitive, ToPrimitive}; 3 | use std::{any::type_name, fmt, marker::PhantomData}; 4 | 5 | pub const NINTENDO_VENDOR_ID: u16 = 1406; 6 | 7 | pub const JOYCON_L_BT: u16 = 0x2006; 8 | pub const JOYCON_R_BT: u16 = 0x2007; 9 | pub const PRO_CONTROLLER: u16 = 0x2009; 10 | pub const JOYCON_CHARGING_GRIP: u16 = 0x200e; 11 | 12 | pub const HID_IDS: &[u16] = &[ 13 | JOYCON_L_BT, 14 | JOYCON_R_BT, 15 | PRO_CONTROLLER, 16 | JOYCON_CHARGING_GRIP, 17 | ]; 18 | 19 | #[repr(u8)] 20 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 21 | pub enum InputReportId { 22 | Normal = 0x3F, 23 | StandardAndSubcmd = 0x21, 24 | MCUFwUpdate = 0x23, 25 | StandardFull = 0x30, 26 | StandardFullMCU = 0x31, 27 | // 0x32 not used 28 | // 0x33 not used 29 | } 30 | 31 | // All unused values are a Nop 32 | #[repr(u8)] 33 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] 34 | pub enum SubcommandId { 35 | GetOnlyControllerState = 0x00, 36 | BluetoothManualPairing = 0x01, 37 | RequestDeviceInfo = 0x02, 38 | SetInputReportMode = 0x03, 39 | GetTriggerButtonsElapsedTime = 0x04, 40 | SetShipmentMode = 0x08, 41 | SPIRead = 0x10, 42 | SPIWrite = 0x11, 43 | SetMCUConf = 0x21, 44 | SetMCUState = 0x22, 45 | SetUnknownData = 0x24, 46 | SetPlayerLights = 0x30, 47 | SetHomeLight = 0x38, 48 | SetIMUMode = 0x40, 49 | SetIMUSens = 0x41, 50 | EnableVibration = 0x48, 51 | 52 | // arg [4,0,0,2], ret [0,8,0,0,0,0,0,44] 53 | // arg [4,4,5,2], ret [0,8,0,0,0,0,200] 54 | // arg [4,4,50,2], ret [0,8,0,0,0,0,5,0,0,14] 55 | // arg [4,4,10,2], ret [0,20,0,0,0,0,244,22,0,0,230,5,0,0,243,11,0,0,234,12, 0, 0] 56 | // get ringcon calibration: arg [4,4,26,2] 57 | // ret [0,20,0,0,0,0] + [135, 8, 28, 0, 48, 247, 243, 0, 44, 12, 224] 58 | // write ringcon calibration: arg [20,4,26,1,16] + [135, 8, 28, 0, 48, 247, 243, 0, 44, 12, 224] 59 | // ret [0, 4] 60 | // get number steps offline ringcon: arg [4,4,49,2], ret [0,8,0,0,0,0,nb_steps, 0,0, 127|143] 61 | // reset number steps offline ringcon: arg [8,4,49,1,4], ret [0,4] 62 | // Possibly accessory interaction like ringcon 63 | MaybeAccessory = 0x58, 64 | // Always [] arg, [0, 32] return 65 | Unknown0x59 = 0x59, 66 | // Always [4, 1, 1, 2] arg, [] return 67 | Unknown0x5a = 0x5a, 68 | // Always [] arg, [] return 69 | Unknown0x5b = 0x5b, 70 | // Always variable arg, [] return 71 | Unknown0x5c = 0x5c, 72 | } 73 | 74 | #[derive(Copy, Clone, Default, Eq, PartialEq)] 75 | pub struct U16LE([u8; 2]); 76 | 77 | impl From for U16LE { 78 | fn from(u: u16) -> Self { 79 | U16LE(u.to_le_bytes()) 80 | } 81 | } 82 | 83 | impl From for u16 { 84 | fn from(u: U16LE) -> u16 { 85 | u16::from_le_bytes(u.0) 86 | } 87 | } 88 | 89 | impl fmt::Debug for U16LE { 90 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 91 | f.write_fmt(format_args!("0x{:x}", u16::from(*self))) 92 | } 93 | } 94 | 95 | impl fmt::Display for U16LE { 96 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 97 | u16::from(*self).fmt(f) 98 | } 99 | } 100 | 101 | #[derive(Copy, Clone, Default, Eq, PartialEq)] 102 | pub struct I16LE(pub [u8; 2]); 103 | 104 | impl From for I16LE { 105 | fn from(u: i16) -> I16LE { 106 | I16LE(u.to_le_bytes()) 107 | } 108 | } 109 | 110 | impl From for i16 { 111 | fn from(u: I16LE) -> i16 { 112 | i16::from_le_bytes(u.0) 113 | } 114 | } 115 | 116 | impl fmt::Debug for I16LE { 117 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 118 | f.write_fmt(format_args!("0x{:x}", i16::from(*self))) 119 | } 120 | } 121 | 122 | #[derive(Copy, Clone, Default, Eq, PartialEq)] 123 | pub struct U32LE([u8; 4]); 124 | 125 | impl From for U32LE { 126 | fn from(u: u32) -> Self { 127 | U32LE(u.to_le_bytes()) 128 | } 129 | } 130 | 131 | impl From for u32 { 132 | fn from(u: U32LE) -> u32 { 133 | u32::from_le_bytes(u.0) 134 | } 135 | } 136 | 137 | impl fmt::Debug for U32LE { 138 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 139 | f.write_fmt(format_args!("0x{:x}", u32::from(*self))) 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | pub(crate) fn offset_of(a: &A, b: &B) -> usize { 145 | b as *const _ as usize - a as *const _ as usize 146 | } 147 | 148 | pub fn vector_from_raw(raw: [I16LE; 3]) -> Vector3 { 149 | Vector3::new( 150 | i16::from(raw[0]) as f64, 151 | i16::from(raw[1]) as f64, 152 | i16::from(raw[2]) as f64, 153 | ) 154 | } 155 | 156 | pub fn raw_from_vector(v: Vector3) -> [I16LE; 3] { 157 | [ 158 | (v.x as i16).into(), 159 | (v.y as i16).into(), 160 | (v.z as i16).into(), 161 | ] 162 | } 163 | 164 | #[repr(transparent)] 165 | #[derive(Copy, Clone, Default, PartialEq, Eq)] 166 | pub struct RawId(u8, PhantomData); 167 | 168 | impl RawId { 169 | pub fn new(id: u8) -> Self { 170 | RawId(id, PhantomData) 171 | } 172 | } 173 | 174 | impl RawId { 175 | pub fn try_into(self) -> Option { 176 | Id::from_u8(self.0) 177 | } 178 | } 179 | 180 | impl From for RawId { 181 | fn from(id: Id) -> Self { 182 | RawId(id.to_u8().expect("always one byte"), PhantomData) 183 | } 184 | } 185 | 186 | impl fmt::Debug for RawId { 187 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 188 | if let Some(id) = self.try_into() { 189 | write!(f, "{:?}", id) 190 | } else { 191 | f.debug_tuple(&format!("RawId<{}>", type_name::())) 192 | .field(&format!("0x{:x}", self.0)) 193 | .finish() 194 | } 195 | } 196 | } 197 | 198 | impl fmt::Display for RawId { 199 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 200 | if let Some(id) = self.try_into() { 201 | write!(f, "{}", id) 202 | } else { 203 | f.debug_tuple("RawId") 204 | .field(&format!("0x{:x}", self.0)) 205 | .finish() 206 | } 207 | } 208 | } 209 | 210 | impl PartialEq for RawId { 211 | fn eq(&self, other: &Id) -> bool { 212 | self.try_into().map(|x| x == *other).unwrap_or(false) 213 | } 214 | } 215 | 216 | #[derive(Debug, Clone, Copy, FromPrimitive, ToPrimitive)] 217 | pub enum Bool { 218 | False = 0, 219 | True = 1, 220 | } 221 | 222 | impl From for Bool { 223 | fn from(b: bool) -> Self { 224 | match b { 225 | false => Bool::False, 226 | true => Bool::True, 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /joy-infrared/src/render/d3.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | buffer::BoundBuffer, 3 | ir_compute::IRCompute, 4 | texture::Texture, 5 | uniforms::{Lights, Uniforms}, 6 | }; 7 | use iced_wgpu::wgpu; 8 | 9 | pub struct D3 { 10 | pipeline: wgpu::RenderPipeline, 11 | depth_texture: Texture, 12 | normal_texture_binding: wgpu::BindGroup, 13 | normal_texture_binding_layout: wgpu::BindGroupLayout, 14 | } 15 | 16 | impl D3 { 17 | pub fn new( 18 | device: &wgpu::Device, 19 | uniforms: &BoundBuffer, 20 | lights: &BoundBuffer, 21 | compute: &IRCompute, 22 | sc_desc: &wgpu::SwapChainDescriptor, 23 | sample_count: u32, 24 | ) -> Self { 25 | let depth_texture = 26 | Texture::create_depth_texture(&device, &sc_desc, sample_count, Some("Depth texture")); 27 | 28 | let normal_texture_binding_layout = 29 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 30 | entries: &[ 31 | wgpu::BindGroupLayoutEntry { 32 | binding: 0, 33 | visibility: wgpu::ShaderStage::FRAGMENT, 34 | ty: wgpu::BindingType::SampledTexture { 35 | dimension: wgpu::TextureViewDimension::D2, 36 | component_type: wgpu::TextureComponentType::Float, 37 | multisampled: false, 38 | }, 39 | count: None, 40 | }, 41 | wgpu::BindGroupLayoutEntry { 42 | binding: 1, 43 | visibility: wgpu::ShaderStage::FRAGMENT, 44 | ty: wgpu::BindingType::Sampler { comparison: false }, 45 | count: None, 46 | }, 47 | ], 48 | label: Some("Normal texture 3D pipeline"), 49 | }); 50 | let normal_texture_binding = device.create_bind_group(&wgpu::BindGroupDescriptor { 51 | layout: &normal_texture_binding_layout, 52 | entries: &[ 53 | wgpu::BindGroupEntry { 54 | binding: 0, 55 | resource: wgpu::BindingResource::TextureView(&compute.normal_texture.view), 56 | }, 57 | wgpu::BindGroupEntry { 58 | binding: 1, 59 | resource: wgpu::BindingResource::Sampler(&compute.normal_texture.sampler), 60 | }, 61 | ], 62 | label: Some("Normal texture sampled"), 63 | }); 64 | 65 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 66 | bind_group_layouts: &[ 67 | uniforms.bind_group_layout(), 68 | &normal_texture_binding_layout, 69 | lights.bind_group_layout(), 70 | ], 71 | push_constant_ranges: &[], 72 | label: Some("3D Pipeline Layout"), 73 | }); 74 | 75 | let pipeline = super::create_render_pipeline( 76 | &device, 77 | &pipeline_layout, 78 | &[ 79 | wgpu::TextureFormat::Bgra8UnormSrgb, 80 | wgpu::TextureFormat::R8Unorm, 81 | ], 82 | Some(Texture::DEPTH_FORMAT), 83 | &[wgpu::VertexBufferDescriptor { 84 | stride: 16 * 2, 85 | step_mode: wgpu::InputStepMode::Vertex, 86 | attributes: &[ 87 | wgpu::VertexAttributeDescriptor { 88 | offset: 0, 89 | format: wgpu::VertexFormat::Float3, 90 | shader_location: 0, 91 | }, 92 | wgpu::VertexAttributeDescriptor { 93 | offset: 16, 94 | format: wgpu::VertexFormat::Float2, 95 | shader_location: 1, 96 | }, 97 | wgpu::VertexAttributeDescriptor { 98 | offset: 16 + 8, 99 | format: wgpu::VertexFormat::Float, 100 | shader_location: 2, 101 | }, 102 | ], 103 | }], 104 | vk_shader_macros::include_glsl!("src/render/shaders/3d.vert"), 105 | vk_shader_macros::include_glsl!("src/render/shaders/3d.frag"), 106 | sample_count, 107 | ); 108 | 109 | D3 { 110 | pipeline, 111 | depth_texture, 112 | normal_texture_binding, 113 | normal_texture_binding_layout, 114 | } 115 | } 116 | 117 | pub fn depth_stencil_attachement(&self) -> wgpu::RenderPassDepthStencilAttachmentDescriptor { 118 | wgpu::RenderPassDepthStencilAttachmentDescriptor { 119 | attachment: &self.depth_texture.view, 120 | depth_ops: Some(wgpu::Operations { 121 | load: wgpu::LoadOp::Clear(1.0), 122 | store: true, 123 | }), 124 | stencil_ops: Some(wgpu::Operations { 125 | load: wgpu::LoadOp::Clear(0), 126 | store: true, 127 | }), 128 | } 129 | } 130 | 131 | pub fn resize( 132 | &mut self, 133 | device: &wgpu::Device, 134 | sc_desc: &wgpu::SwapChainDescriptor, 135 | sample_count: u32, 136 | ) { 137 | self.depth_texture = 138 | Texture::create_depth_texture(device, sc_desc, sample_count, Some("Depth texture")); 139 | } 140 | 141 | pub fn update_bindgroup(&mut self, device: &wgpu::Device, compute: &IRCompute) { 142 | self.normal_texture_binding = device.create_bind_group(&wgpu::BindGroupDescriptor { 143 | layout: &self.normal_texture_binding_layout, 144 | entries: &[ 145 | wgpu::BindGroupEntry { 146 | binding: 0, 147 | resource: wgpu::BindingResource::TextureView(&compute.normal_texture.view), 148 | }, 149 | wgpu::BindGroupEntry { 150 | binding: 1, 151 | resource: wgpu::BindingResource::Sampler(&compute.normal_texture.sampler), 152 | }, 153 | ], 154 | label: Some("Normal texture sampled"), 155 | }); 156 | } 157 | 158 | pub fn render<'a>( 159 | &'a self, 160 | pass: &mut wgpu::RenderPass<'a>, 161 | compute: &'a IRCompute, 162 | uniforms: &'a BoundBuffer, 163 | lights: &'a BoundBuffer, 164 | ) { 165 | pass.set_pipeline(&self.pipeline); 166 | pass.set_vertex_buffer(0, compute.vertices().slice(..)); 167 | pass.set_index_buffer(compute.indices().slice(..)); 168 | pass.set_bind_group(0, &uniforms.bind_group(), &[]); 169 | pass.set_bind_group(1, &self.normal_texture_binding, &[]); 170 | pass.set_bind_group(2, &lights.bind_group(), &[]); 171 | pass.draw_indexed(0..compute.indices_count(), 0, 0..1); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /joy-infrared/src/render/texture.rs: -------------------------------------------------------------------------------- 1 | use iced_wgpu::wgpu; 2 | 3 | pub struct Texture { 4 | pub texture: wgpu::Texture, 5 | pub view: wgpu::TextureView, 6 | pub sampler: wgpu::Sampler, 7 | pub size: wgpu::Extent3d, 8 | } 9 | 10 | impl Texture { 11 | pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; 12 | 13 | pub fn create_normal_texture(device: &wgpu::Device, size: (u32, u32)) -> Self { 14 | let size = wgpu::Extent3d { 15 | width: size.0, 16 | height: size.1, 17 | depth: 1, 18 | }; 19 | let texture = device.create_texture(&wgpu::TextureDescriptor { 20 | label: Some("Normal Texture"), 21 | size, 22 | mip_level_count: 1, 23 | sample_count: 1, 24 | dimension: wgpu::TextureDimension::D2, 25 | format: wgpu::TextureFormat::Rgba32Float, 26 | usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::SAMPLED, 27 | }); 28 | let view = texture.create_view(&wgpu::TextureViewDescriptor { 29 | label: Some("Normal Texture View"), 30 | dimension: Some(wgpu::TextureViewDimension::D2), 31 | format: Some(wgpu::TextureFormat::Rgba32Float), 32 | aspect: wgpu::TextureAspect::All, 33 | base_mip_level: 0, 34 | level_count: None, 35 | base_array_layer: 0, 36 | array_layer_count: None, 37 | }); 38 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 39 | address_mode_u: wgpu::AddressMode::ClampToEdge, 40 | address_mode_v: wgpu::AddressMode::ClampToEdge, 41 | address_mode_w: wgpu::AddressMode::ClampToEdge, 42 | mag_filter: wgpu::FilterMode::Linear, 43 | min_filter: wgpu::FilterMode::Linear, 44 | mipmap_filter: wgpu::FilterMode::Nearest, 45 | lod_min_clamp: -100.0, 46 | lod_max_clamp: 100.0, 47 | compare: None, 48 | anisotropy_clamp: None, 49 | label: Some("Normal Texture Sampler"), 50 | }); 51 | 52 | Self { 53 | size, 54 | texture, 55 | view, 56 | sampler, 57 | } 58 | } 59 | 60 | pub fn create_ir_texture(device: &wgpu::Device, size: (u32, u32)) -> Self { 61 | let size = wgpu::Extent3d { 62 | width: size.0, 63 | height: size.1, 64 | depth: 1, 65 | }; 66 | let texture = device.create_texture( 67 | &(wgpu::TextureDescriptor { 68 | label: Some("IR Texture"), 69 | size, 70 | mip_level_count: 1, 71 | sample_count: 1, 72 | dimension: wgpu::TextureDimension::D2, 73 | format: wgpu::TextureFormat::R8Unorm, 74 | usage: wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::SAMPLED, 75 | }), 76 | ); 77 | let view = texture.create_view(&wgpu::TextureViewDescriptor { 78 | label: Some("IR Texture View"), 79 | dimension: Some(wgpu::TextureViewDimension::D2), 80 | format: Some(wgpu::TextureFormat::R8Unorm), 81 | aspect: wgpu::TextureAspect::All, 82 | base_mip_level: 0, 83 | level_count: None, 84 | base_array_layer: 0, 85 | array_layer_count: None, 86 | }); 87 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 88 | address_mode_u: wgpu::AddressMode::ClampToEdge, 89 | address_mode_v: wgpu::AddressMode::ClampToEdge, 90 | address_mode_w: wgpu::AddressMode::ClampToEdge, 91 | mag_filter: wgpu::FilterMode::Linear, 92 | min_filter: wgpu::FilterMode::Linear, 93 | mipmap_filter: wgpu::FilterMode::Nearest, 94 | lod_min_clamp: -100.0, 95 | lod_max_clamp: 100.0, 96 | compare: None, 97 | anisotropy_clamp: None, 98 | label: Some("IR Texture Sampler"), 99 | }); 100 | 101 | Self { 102 | texture, 103 | view, 104 | sampler, 105 | size, 106 | } 107 | } 108 | 109 | pub fn create_depth_texture( 110 | device: &wgpu::Device, 111 | sc_desc: &wgpu::SwapChainDescriptor, 112 | sample_count: u32, 113 | label: Option<&str>, 114 | ) -> Self { 115 | let size = wgpu::Extent3d { 116 | width: sc_desc.width, 117 | height: sc_desc.height, 118 | depth: 1, 119 | }; 120 | let texture = device.create_texture( 121 | &(wgpu::TextureDescriptor { 122 | label, 123 | size, 124 | mip_level_count: 1, 125 | sample_count, 126 | dimension: wgpu::TextureDimension::D2, 127 | format: Self::DEPTH_FORMAT, 128 | usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT 129 | | wgpu::TextureUsage::SAMPLED 130 | | wgpu::TextureUsage::COPY_SRC, 131 | }), 132 | ); 133 | let view = texture.create_view(&wgpu::TextureViewDescriptor { 134 | label: Some("IR Texture View"), 135 | dimension: Some(wgpu::TextureViewDimension::D2), 136 | format: Some(Self::DEPTH_FORMAT), 137 | aspect: wgpu::TextureAspect::All, 138 | base_mip_level: 0, 139 | level_count: None, 140 | base_array_layer: 0, 141 | array_layer_count: None, 142 | }); 143 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 144 | address_mode_u: wgpu::AddressMode::ClampToEdge, 145 | address_mode_v: wgpu::AddressMode::ClampToEdge, 146 | address_mode_w: wgpu::AddressMode::ClampToEdge, 147 | mag_filter: wgpu::FilterMode::Linear, 148 | min_filter: wgpu::FilterMode::Linear, 149 | mipmap_filter: wgpu::FilterMode::Nearest, 150 | lod_min_clamp: -100.0, 151 | lod_max_clamp: 100.0, 152 | compare: Some(wgpu::CompareFunction::LessEqual), 153 | anisotropy_clamp: None, 154 | label: Some("IR Texture Sampler"), 155 | }); 156 | 157 | Self { 158 | texture, 159 | view, 160 | sampler, 161 | size, 162 | } 163 | } 164 | 165 | pub fn update( 166 | &mut self, 167 | device: &wgpu::Device, 168 | queue: &mut wgpu::Queue, 169 | texture: image::GrayImage, 170 | ) { 171 | let flat_samples = texture.as_flat_samples(); 172 | let old_size = self.size; 173 | let (width, height) = texture.dimensions(); 174 | if old_size.width != width || old_size.height != height { 175 | *self = Texture::create_ir_texture(device, (width, height)); 176 | } 177 | queue.write_texture( 178 | wgpu::TextureCopyView { 179 | texture: &self.texture, 180 | mip_level: 0, 181 | origin: wgpu::Origin3d::ZERO, 182 | }, 183 | flat_samples.samples, 184 | wgpu::TextureDataLayout { 185 | offset: 0, 186 | bytes_per_row: flat_samples.layout.height_stride as u32, 187 | rows_per_image: 0, 188 | }, 189 | self.size, 190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/mcu/ir_register.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | 4 | #[repr(packed)] 5 | #[derive(Copy, Clone, Default, Eq, PartialEq)] 6 | pub struct Register { 7 | page: u8, 8 | offset: u8, 9 | value: u8, 10 | } 11 | 12 | impl Register { 13 | fn new(address: Address, value: u8) -> Register { 14 | Register { 15 | page: address.address().0, 16 | offset: address.address().1, 17 | value, 18 | } 19 | } 20 | 21 | pub fn decode_raw<'a>( 22 | page: u8, 23 | base_offset: u8, 24 | values: &'a [u8], 25 | ) -> impl Iterator + 'a { 26 | values 27 | .iter() 28 | .enumerate() 29 | .filter_map(move |(offset, value)| { 30 | Address::try_from((page, base_offset + offset as u8)) 31 | .map(|a| Register::new(a, *value)) 32 | .ok() 33 | }) 34 | } 35 | 36 | pub fn page(self) -> u8 { 37 | self.page 38 | } 39 | 40 | pub fn same_address(self, other: Register) -> bool { 41 | self.page == other.page && self.offset == other.offset 42 | } 43 | 44 | // Default value: 320x240 45 | pub fn resolution(resolution: Resolution) -> Register { 46 | Register::new(Resolution, resolution as u8) 47 | } 48 | 49 | // default 200µs, max 600µs 50 | pub fn exposure_us(exposure: u32) -> [Register; 2] { 51 | let raw_exposure = exposure * 31200 / 1000; 52 | [ 53 | Register::new(ExposureLSB, (raw_exposure & 0xff) as u8), 54 | Register::new(ExposureMSB, (raw_exposure >> 8) as u8), 55 | ] 56 | } 57 | 58 | // default: Manual 59 | pub fn exposure_mode(mode: ExposureMode) -> Register { 60 | Register::new(ExposureMode, mode as u8) 61 | } 62 | 63 | // TODO default 0x10 0x0 64 | pub fn digital_gain(gain: u16) -> [Register; 2] { 65 | // todo: check 66 | [ 67 | Register::new(DigitalGainLSB, ((gain & 0x0f) << 4) as u8), 68 | Register::new(DigitalGainMSB, ((gain & 0xf0) >> 4) as u8), 69 | ] 70 | } 71 | 72 | // default value: 0x10 TODO 73 | pub fn ir_leds(leds: Leds) -> Register { 74 | Register::new(IRLeds, leds.0) 75 | } 76 | 77 | // default value: X1 78 | pub fn external_light_filter(filter: ExternalLightFilter) -> Register { 79 | Register::new(ExternalLightFilter, filter as u8) 80 | } 81 | 82 | // default value: 0xc8 83 | pub fn white_pixel_threshold(threshold: u8) -> Register { 84 | Register::new(WhitePixelThreshold, threshold) 85 | } 86 | 87 | // default: 0, 0xe, 1, 1 88 | pub fn leds_intensity(far: u8, near: u8) -> [Register; 2] { 89 | assert_eq!(0, (far | near) & 0xf0); 90 | [ 91 | Register::new(IntensityLedsFar12, far), 92 | Register::new(IntensityLedsNear34, near), 93 | ] 94 | } 95 | 96 | // default: Normal 97 | pub fn flip(side: Flip) -> Register { 98 | Register::new(Flip, side as u8) 99 | } 100 | 101 | // default : enabled 102 | pub fn denoise(enabled: bool) -> Register { 103 | Register::new(Denoise, enabled as u8) 104 | } 105 | 106 | /// default: 0x23 107 | pub fn edge_smoothing_threshold(threshold: u8) -> Register { 108 | Register::new(EdgeSmoothingThreshold, threshold) 109 | } 110 | 111 | // Default: 0x44 112 | pub fn color_interpolation_threshold(threshold: u8) -> Register { 113 | Register::new(ColorInterpolationThreshold, threshold) 114 | } 115 | 116 | // Default value: 0x32 117 | pub fn buffer_update_time(time: u8) -> Register { 118 | Register::new(BufferUpdateTimeLSB, time) 119 | } 120 | 121 | pub fn finish() -> Register { 122 | Register::new(Finish, 1) 123 | } 124 | } 125 | 126 | impl fmt::Debug for Register { 127 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 128 | let mut out = f.debug_struct("ir::Register"); 129 | match Address::try_from((self.page, self.offset)) { 130 | Ok(a) => out.field("name", &a), 131 | Err(_) => out.field( 132 | "address", 133 | &format_args!("0x{:x?}:0x{:x?}", self.page, self.offset), 134 | ), 135 | }; 136 | out.field("value", &format_args!("0x{:x?}", self.value)); 137 | out.finish() 138 | } 139 | } 140 | 141 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 142 | enum Address { 143 | Resolution, 144 | DigitalGainLSB, 145 | DigitalGainMSB, 146 | ExposureLSB, 147 | ExposureMSB, 148 | ExposureMode, 149 | ExternalLightFilter, 150 | WhitePixelThreshold, 151 | IntensityLedsFar12, 152 | IntensityLedsNear34, 153 | Flip, 154 | Denoise, 155 | EdgeSmoothingThreshold, 156 | ColorInterpolationThreshold, 157 | BufferUpdateTimeLSB, 158 | IRLeds, 159 | Finish, 160 | } 161 | use Address::*; 162 | 163 | impl Address { 164 | /// page + offset 165 | fn address(self) -> (u8, u8) { 166 | match self { 167 | BufferUpdateTimeLSB => (0, 0x04), 168 | Finish => (0, 0x07), 169 | ExternalLightFilter => (0, 0x0e), 170 | IRLeds => (0, 0x10), 171 | IntensityLedsFar12 => (0, 0x11), 172 | IntensityLedsNear34 => (0, 0x12), 173 | Flip => (0, 0x2d), 174 | Resolution => (0, 0x2e), 175 | DigitalGainLSB => (1, 0x2e), 176 | DigitalGainMSB => (1, 0x2f), 177 | ExposureLSB => (1, 0x30), 178 | ExposureMSB => (1, 0x31), 179 | ExposureMode => (1, 0x32), 180 | WhitePixelThreshold => (1, 0x43), 181 | Denoise => (1, 0x67), 182 | EdgeSmoothingThreshold => (1, 0x68), 183 | ColorInterpolationThreshold => (1, 0x69), 184 | } 185 | } 186 | } 187 | const ALL_ADDRESSES: &[Address] = &[ 188 | Resolution, 189 | DigitalGainLSB, 190 | DigitalGainMSB, 191 | ExposureLSB, 192 | ExposureMSB, 193 | ExposureMode, 194 | ExternalLightFilter, 195 | WhitePixelThreshold, 196 | IntensityLedsFar12, 197 | IntensityLedsNear34, 198 | Flip, 199 | Denoise, 200 | EdgeSmoothingThreshold, 201 | ColorInterpolationThreshold, 202 | BufferUpdateTimeLSB, 203 | IRLeds, 204 | Finish, 205 | ]; 206 | 207 | impl TryFrom<(u8, u8)> for Address { 208 | type Error = (); 209 | 210 | fn try_from(raw: (u8, u8)) -> Result { 211 | for address in ALL_ADDRESSES { 212 | if address.address() == raw { 213 | return Ok(*address); 214 | } 215 | } 216 | Err(()) 217 | } 218 | } 219 | 220 | #[repr(u8)] 221 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 222 | pub enum Resolution { 223 | /// Full pixel array 224 | R320x240 = 0b0000_0000, 225 | /// Sensor Binning [2 X 2] 226 | R160x120 = 0b0101_0000, 227 | /// Sensor Binning [4 x 2] and Skipping [1 x 2] 228 | R80x60 = 0b0110_0100, 229 | /// Sensor Binning [4 x 2] and Skipping [2 x 4] 230 | R40x30 = 0b0110_1001, 231 | } 232 | 233 | impl Resolution { 234 | pub fn max_fragment_id(self) -> u8 { 235 | match self { 236 | Resolution::R320x240 => 0xff, 237 | Resolution::R160x120 => 0x3f, 238 | Resolution::R80x60 => 0x0f, 239 | Resolution::R40x30 => 0x03, 240 | } 241 | } 242 | 243 | pub fn size(self) -> (u32, u32) { 244 | match self { 245 | Resolution::R320x240 => (320, 240), 246 | Resolution::R160x120 => (160, 120), 247 | Resolution::R80x60 => (80, 60), 248 | Resolution::R40x30 => (40, 30), 249 | } 250 | } 251 | } 252 | 253 | impl Default for Resolution { 254 | fn default() -> Self { 255 | Resolution::R160x120 256 | } 257 | } 258 | 259 | #[repr(u8)] 260 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 261 | pub enum ExposureMode { 262 | Manual = 0, 263 | Max = 1, 264 | } 265 | 266 | #[repr(u8)] 267 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 268 | pub enum ExternalLightFilter { 269 | Off = 0b00, 270 | X1 = 0b11, 271 | } 272 | 273 | #[repr(u8)] 274 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 275 | pub enum Flip { 276 | Normal = 0, 277 | Vertically = 1, 278 | Horizontally = 2, 279 | Both = 3, 280 | } 281 | 282 | bitfield::bitfield! { 283 | #[repr(transparent)] 284 | #[derive(Copy, Clone)] 285 | pub struct Leds(u8); 286 | impl Debug; 287 | pub flashlight, set_flashlight: 0; 288 | pub disable_far_narrow12, set_disable_far_narrow12: 4; 289 | pub disable_near_wide34, set_disable_near_wide34: 5; 290 | pub strobe, set_strobe: 7; 291 | } 292 | -------------------------------------------------------------------------------- /joy-infrared/src/render/ir_compute.rs: -------------------------------------------------------------------------------- 1 | use iced_wgpu::wgpu; 2 | 3 | pub struct IRCompute { 4 | vertex_buffer: wgpu::Buffer, 5 | index_buffer: wgpu::Buffer, 6 | pub texture_binding_layout: wgpu::BindGroupLayout, 7 | texture: Option, 8 | pub normal_texture: super::texture::Texture, 9 | normal_texture_binding_layout: wgpu::BindGroupLayout, 10 | pub texture_binding: Option, 11 | mesh_binding: wgpu::BindGroup, 12 | pipeline: wgpu::ComputePipeline, 13 | } 14 | 15 | impl IRCompute { 16 | pub fn vertices(&self) -> &wgpu::Buffer { 17 | &self.vertex_buffer 18 | } 19 | 20 | pub fn indices(&self) -> &wgpu::Buffer { 21 | &self.index_buffer 22 | } 23 | 24 | pub fn indices_count(&self) -> u32 { 25 | self.texture 26 | .as_ref() 27 | .map(|texture| (texture.size.width - 1) * (texture.size.height - 1) * 6) 28 | .unwrap_or(0) 29 | } 30 | 31 | pub fn new(device: &wgpu::Device, uniform_bind_group_layout: &wgpu::BindGroupLayout) -> Self { 32 | // TODO: remove magic value 33 | let vertex_buffer_size = 320 * 240 * 16 * 3; 34 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { 35 | label: Some("compute vertex buffer"), 36 | size: vertex_buffer_size, 37 | usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::STORAGE, 38 | mapped_at_creation: false, 39 | }); 40 | let index_buffer_size = 320 * 240 * 6 * 4; 41 | let index_buffer = device.create_buffer(&wgpu::BufferDescriptor { 42 | label: Some("compute index buffer"), 43 | size: index_buffer_size, 44 | usage: wgpu::BufferUsage::INDEX | wgpu::BufferUsage::STORAGE, 45 | mapped_at_creation: false, 46 | }); 47 | let mesh_binding_layout = 48 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 49 | entries: &[ 50 | wgpu::BindGroupLayoutEntry { 51 | binding: 0, 52 | visibility: wgpu::ShaderStage::COMPUTE, 53 | ty: wgpu::BindingType::StorageBuffer { 54 | dynamic: false, 55 | readonly: false, 56 | min_binding_size: None, 57 | }, 58 | count: None, 59 | }, 60 | wgpu::BindGroupLayoutEntry { 61 | binding: 1, 62 | visibility: wgpu::ShaderStage::COMPUTE, 63 | ty: wgpu::BindingType::StorageBuffer { 64 | dynamic: false, 65 | readonly: false, 66 | min_binding_size: None, 67 | }, 68 | count: None, 69 | }, 70 | ], 71 | label: Some("static compute binding layout"), 72 | }); 73 | let mesh_binding = device.create_bind_group(&wgpu::BindGroupDescriptor { 74 | layout: &mesh_binding_layout, 75 | entries: &[ 76 | wgpu::BindGroupEntry { 77 | binding: 0, 78 | resource: wgpu::BindingResource::Buffer(vertex_buffer.slice(..)), 79 | }, 80 | wgpu::BindGroupEntry { 81 | binding: 1, 82 | resource: wgpu::BindingResource::Buffer(index_buffer.slice(..)), 83 | }, 84 | ], 85 | label: Some("static compute binding"), 86 | }); 87 | let texture_binding_layout = 88 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 89 | entries: &[ 90 | wgpu::BindGroupLayoutEntry { 91 | binding: 0, 92 | visibility: wgpu::ShaderStage::COMPUTE | wgpu::ShaderStage::FRAGMENT, 93 | ty: wgpu::BindingType::SampledTexture { 94 | dimension: wgpu::TextureViewDimension::D2, 95 | component_type: wgpu::TextureComponentType::Uint, 96 | multisampled: false, 97 | }, 98 | count: None, 99 | }, 100 | wgpu::BindGroupLayoutEntry { 101 | binding: 1, 102 | visibility: wgpu::ShaderStage::COMPUTE | wgpu::ShaderStage::FRAGMENT, 103 | ty: wgpu::BindingType::Sampler { comparison: false }, 104 | count: None, 105 | }, 106 | ], 107 | label: Some("dynamic compute binding layout"), 108 | }); 109 | let normal_texture_binding_layout = 110 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 111 | entries: &[wgpu::BindGroupLayoutEntry { 112 | binding: 0, 113 | visibility: wgpu::ShaderStage::COMPUTE | wgpu::ShaderStage::FRAGMENT, 114 | ty: wgpu::BindingType::StorageTexture { 115 | dimension: wgpu::TextureViewDimension::D2, 116 | readonly: false, 117 | format: wgpu::TextureFormat::Rgba32Float, 118 | }, 119 | count: None, 120 | }], 121 | label: Some("Depth texture binding layout"), 122 | }); 123 | 124 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 125 | bind_group_layouts: &[ 126 | &mesh_binding_layout, 127 | &texture_binding_layout, 128 | &normal_texture_binding_layout, 129 | uniform_bind_group_layout, 130 | ], 131 | push_constant_ranges: &[], 132 | label: Some("IR Compute Pipeline Layout"), 133 | }); 134 | let spirv: &[u32] = vk_shader_macros::include_glsl!("src/render/shaders/compute.comp"); 135 | let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 136 | layout: Some(&pipeline_layout), 137 | compute_stage: wgpu::ProgrammableStageDescriptor { 138 | module: &device.create_shader_module(wgpu::ShaderModuleSource::SpirV(spirv.into())), 139 | entry_point: "main", 140 | }, 141 | label: Some("IR Compute Pipeline"), 142 | }); 143 | Self { 144 | pipeline, 145 | vertex_buffer, 146 | index_buffer, 147 | mesh_binding, 148 | texture_binding_layout, 149 | texture: None, 150 | texture_binding: None, 151 | normal_texture: super::texture::Texture::create_normal_texture(device, (1, 1)), 152 | normal_texture_binding_layout, 153 | } 154 | } 155 | 156 | pub fn push_ir_data( 157 | &mut self, 158 | device: &wgpu::Device, 159 | queue: &mut wgpu::Queue, 160 | encoder: &mut wgpu::CommandEncoder, 161 | uniform_bind_group: &wgpu::BindGroup, 162 | image: image::GrayImage, 163 | ) { 164 | let (width, height) = image.dimensions(); 165 | let texture = self.texture.get_or_insert_with(|| { 166 | super::texture::Texture::create_ir_texture(device, (width, height)) 167 | }); 168 | texture.update(device, queue, image); 169 | 170 | if self.normal_texture.size.width != width || self.normal_texture.size.height != height { 171 | self.normal_texture = 172 | super::texture::Texture::create_normal_texture(device, (width, height)); 173 | } 174 | 175 | let texture_binding = device.create_bind_group(&wgpu::BindGroupDescriptor { 176 | layout: &self.texture_binding_layout, 177 | entries: &[ 178 | wgpu::BindGroupEntry { 179 | binding: 0, 180 | resource: wgpu::BindingResource::TextureView(&texture.view), 181 | }, 182 | wgpu::BindGroupEntry { 183 | binding: 1, 184 | resource: wgpu::BindingResource::Sampler(&texture.sampler), 185 | }, 186 | ], 187 | label: Some("dynamic compute group"), 188 | }); 189 | let normal_texture_binding = device.create_bind_group(&wgpu::BindGroupDescriptor { 190 | layout: &self.normal_texture_binding_layout, 191 | entries: &[wgpu::BindGroupEntry { 192 | binding: 0, 193 | resource: wgpu::BindingResource::TextureView(&self.normal_texture.view), 194 | }], 195 | label: Some("Normal texture bind group"), 196 | }); 197 | 198 | { 199 | let mut cpass = encoder.begin_compute_pass(); 200 | cpass.set_pipeline(&self.pipeline); 201 | cpass.set_bind_group(0, &self.mesh_binding, &[]); 202 | cpass.set_bind_group(1, &texture_binding, &[]); 203 | cpass.set_bind_group(2, &normal_texture_binding, &[]); 204 | cpass.set_bind_group(3, uniform_bind_group, &[]); 205 | cpass.dispatch(width, height, 1); 206 | } 207 | 208 | self.texture_binding = Some(texture_binding); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /trace/reopen-ringfit: -------------------------------------------------------------------------------- 1 | 0:05:38.026963 OutputReport { id: RumbleAndSubcmd, counter: 3, subcmd: SubcommandRequest { mcu_state: Standby } } 2 | 0:05:38.058615 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, mcu_report: MCUReport { type: Some(Empty) } } } 3 | 0:05:38.080612 OutputReport { id: RumbleAndSubcmd, counter: 5, subcmd: SubcommandRequest { mcu_state: Standby } } 4 | 0:05:38.081859 OutputReport { id: RumbleAndSubcmd, counter: 6, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(250), set_mcu_mode: MaybeRingcon } } } 5 | 0:05:38.110195 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, mcu_report: MCUReport { type: Some(Empty) } } } 6 | 0:05:38.350368 OutputReport { id: RumbleAndSubcmd, counter: 8, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(250), set_mcu_mode: MaybeRingcon } } } 7 | 0:05:38.382650 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(32), mcu_report: MCUReport { status: MCUStatus { _unknown: [0, 255], fw_major_version: 0x800, fw_minor_versio 8 | n: 0x1b00, state: Standby } } } } 9 | 0:05:38.410643 OutputReport { id: RumbleAndSubcmd, counter: 9, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(250), set_mcu_mode: MaybeRingcon } } } 10 | 0:05:38.446452 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(32), mcu_report: MCUReport { status: MCUStatus { _unknown: [0, 0], fw_major_version: 0x800, fw_minor_version: 11 | 0x1b00, state: MaybeRingcon } } } } 12 | 0:05:38.455434 OutputReport { id: RumbleAndSubcmd, counter: 10, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(250), set_mcu_mode: MaybeRingcon } } } 13 | 0:05:38.478592 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(32), mcu_report: MCUReport { status: MCUStatus { _unknown: [50, 0], fw_major_version: 0x800, fw_minor_version 14 | : 0x1b00, state: MaybeRingcon } } } } 15 | 0:05:38.509494 OutputReport { id: RumbleAndSubcmd, counter: 11, subcmd: SubcommandRequest { subcommand: MCUCommand { crc: MCUCommandCRC(243), set_ir_mode: MCUIRModeData { ir_mode: IRSensorSleep, no_of_frags: 0, mc 16 | u_fw_version: (0x0, 0x0) } } } } 17 | 0:05:38.541359 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(32), mcu_report: MCUReport { status: MCUStatus { _unknown: [50, 0], fw_major_version: 0x800, fw_minor_version 18 | : 0x1b00, state: MaybeRingcon } } } } 19 | 0:05:38.665682 OutputReport { id: RumbleAndSubcmd, counter: 13, subcmd: SubcommandRequest { subcommand: Unknown0x59, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20 | 0, 0, 0, 0, 0, 0, 0, 0] } } 21 | 0:05:38.723607 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(89), subcommand: Unknown0x59, raw: [0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 23 | 0:05:38.760374 OutputReport { id: RumbleAndSubcmd, counter: 14, subcmd: SubcommandRequest { set_imu_mode: MaybeRingcon } } 24 | 0:05:38.783773 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: SetIMUMode } } 25 | 0:05:38.805552 OutputReport { id: RumbleAndSubcmd, counter: 15, subcmd: SubcommandRequest { subcommand: Unknown0x5c, raw: [6, 3, 37, 6, 0, 0, 0, 0, 236, 153, 172, 227, 28, 0, 0, 0, 67, 65, 91, 42, 211, 87, 0, 0, 4 26 | , 0, 0, 0, 0, 0, 0, 0, 144, 40, 161, 227, 28, 0] } } 27 | 0:05:38.833799 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: Unknown0x5c, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 29 | 0:05:38.850447 OutputReport { id: RumbleAndSubcmd, counter: 0, subcmd: SubcommandRequest { subcommand: Unknown0x5a, raw: [4, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 30 | , 0, 0, 0, 0, 0, 0, 0] } } 31 | 0:05:38.872468 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack, subcommand: Unknown0x5a, raw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 33 | 0:05:38.920633 OutputReport { id: RumbleAndSubcmd, counter: 1, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 5, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34 | 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 35 | 0:05:38.965131 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 37 | 0:05:39.162011 OutputReport { id: RumbleAndSubcmd, counter: 2, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 39 | 0:05:39.260080 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 40 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 41 | 0:05:39.350394 OutputReport { id: RumbleAndSubcmd, counter: 4, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 4, 5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 43 | 0:05:39.378540 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 45 | 0:05:39.404358 OutputReport { id: RumbleAndSubcmd, counter: 5, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 47 | 0:05:39.443512 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 48 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 49 | 0:05:39.460595 OutputReport { id: RumbleAndSubcmd, counter: 7, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 4, 10, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 50 | , 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 51 | 0:05:39.503567 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 20, 0, 0, 0, 0, 244, 22, 0, 0, 230, 5, 0, 0, 243, 11, 0, 0, 234, 52 | 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 53 | 0:05:39.525672 OutputReport { id: RumbleAndSubcmd, counter: 8, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 4, 50, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 54 | , 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 55 | 0:05:39.563756 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 5, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 56 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 57 | 0:05:39.589375 OutputReport { id: RumbleAndSubcmd, counter: 9, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58 | 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 59 | 0:05:39.623653 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 16, 0, 0, 0, 0, 33, 0, 21, 0, 16, 87, 66, 66, 48, 50, 52, 32, 0, 60 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 61 | 0:05:39.655337 OutputReport { id: RumbleAndSubcmd, counter: 10, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 62 | , 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 63 | 0:05:39.698539 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 64 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 65 | 0:05:39.709412 OutputReport { id: RumbleAndSubcmd, counter: 12, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 4, 10, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 67 | 0:05:39.743725 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 20, 0, 0, 0, 0, 244, 22, 0, 0, 230, 5, 0, 0, 243, 11, 0, 0, 234, 68 | 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 69 | 0:05:39.769395 OutputReport { id: RumbleAndSubcmd, counter: 13, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 70 | , 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 71 | 0:05:39.803664 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 8, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 72 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 73 | 0:05:39.835689 OutputReport { id: RumbleAndSubcmd, counter: 14, subcmd: SubcommandRequest { subcommand_id: RawId("0x58"), raw: [4, 4, 26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 75 | 0:05:39.878845 InputReport { report_id: StandardAndSubcmd, subcommand_reply: SubcommandReply { ack: Ack(88), subcommand_id: RawId("0x58"), raw: [0, 20, 0, 0, 0, 0, 254, 202, 134, 0, 254, 202, 134, 0, 68, 12, 174, 76 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } } 77 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/output/report.rs: -------------------------------------------------------------------------------- 1 | //! Structs binary compatible with the HID output reports 2 | //! 3 | //! 4 | 5 | use crate::{ 6 | accessory::AccessoryCommand, 7 | common::*, 8 | imu::{self, IMUMode}, 9 | light, 10 | mcu::{ir::*, *}, 11 | output::RumbleData, 12 | raw_enum, 13 | spi::*, 14 | }; 15 | use std::mem::size_of_val; 16 | 17 | #[repr(u8)] 18 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 19 | pub enum OutputReportId { 20 | RumbleAndSubcmd = 0x01, 21 | MCUFwUpdate = 0x03, 22 | RumbleOnly = 0x10, 23 | RequestMCUData = 0x11, 24 | } 25 | 26 | // Describes a HID report sent to the JoyCon. 27 | // 28 | // ```ignore 29 | // let report = OutputReport::from(SubcommandRequest::request_device_info()); 30 | // write_hid_report(report.as_bytes()); 31 | // ``` 32 | raw_enum! { 33 | #[id: OutputReportId] 34 | #[post_id rumble rumble_mut: Rumble] 35 | #[union: OutputReportUnion] 36 | #[struct: OutputReport] 37 | pub enum OutputReportEnum { 38 | rumble_subcmd rumble_subcmd_mut: RumbleAndSubcmd = SubcommandRequest, 39 | mcu_fw_update mcu_fw_update_mut: MCUFwUpdate = (), 40 | rumble_only rumble_only_mut: RumbleOnly = (), 41 | request_mcu_data request_mcu_data_mut: RequestMCUData = MCURequest 42 | } 43 | } 44 | 45 | #[repr(packed)] 46 | #[derive(Copy, Clone, Debug, Default)] 47 | pub struct Rumble { 48 | pub packet_counter: u8, 49 | pub rumble_data: RumbleData, 50 | } 51 | 52 | impl OutputReport { 53 | pub fn packet_counter(&mut self) -> &mut u8 { 54 | &mut self.rumble.packet_counter 55 | } 56 | 57 | pub fn is_special(&self) -> bool { 58 | self.id != OutputReportId::RumbleOnly 59 | } 60 | 61 | pub fn set_registers(regs: &[ir::Register]) -> (OutputReport, &[ir::Register]) { 62 | let size = regs.len().min(9); 63 | let mut regs_fixed = [ir::Register::default(); 9]; 64 | regs_fixed[..size].copy_from_slice(®s[..size]); 65 | let mcu_cmd = MCUCommand::set_ir_registers(MCURegisters { 66 | len: size as u8, 67 | regs: regs_fixed, 68 | }); 69 | (SubcommandRequest::from(mcu_cmd).into(), ®s[size..]) 70 | } 71 | 72 | fn ir_build(ack_request_packet: IRAckRequestPacket) -> OutputReport { 73 | let mcu_request = MCURequest::from(IRRequest::from(ack_request_packet)); 74 | mcu_request.into() 75 | } 76 | 77 | pub fn ir_resend(packet_id: u8) -> OutputReport { 78 | OutputReport::ir_build(IRAckRequestPacket { 79 | packet_missing: Bool::True.into(), 80 | missed_packet_id: packet_id, 81 | ack_packet_id: 0, 82 | }) 83 | } 84 | 85 | pub fn ir_ack(packet_id: u8) -> OutputReport { 86 | OutputReport::ir_build(IRAckRequestPacket { 87 | packet_missing: Bool::False.into(), 88 | missed_packet_id: 0, 89 | ack_packet_id: packet_id, 90 | }) 91 | } 92 | 93 | pub fn set_rumble(rumble_data: RumbleData) -> OutputReport { 94 | let mut report: OutputReport = OutputReportEnum::RumbleOnly(()).into(); 95 | report.rumble.rumble_data = rumble_data; 96 | report 97 | } 98 | 99 | pub fn byte_size(&self) -> usize { 100 | match self.id.try_into() { 101 | Some(OutputReportId::RumbleAndSubcmd) => 49, 102 | Some(OutputReportId::MCUFwUpdate) => unimplemented!(), 103 | Some(OutputReportId::RumbleOnly) => 10, 104 | Some(OutputReportId::RequestMCUData) => 48, 105 | None => size_of_val(self), 106 | } 107 | } 108 | 109 | pub fn as_bytes(&self) -> &[u8] { 110 | unsafe { std::slice::from_raw_parts(self as *const _ as *const u8, self.byte_size()) } 111 | } 112 | 113 | pub fn as_bytes_mut(&mut self) -> &mut [u8] { 114 | unsafe { std::slice::from_raw_parts_mut(self as *mut _ as *mut u8, size_of_val(self)) } 115 | } 116 | 117 | #[cfg(test)] 118 | pub(crate) unsafe fn as_mcu_request(&self) -> &MCURequest { 119 | &self.u.request_mcu_data 120 | } 121 | 122 | #[cfg(test)] 123 | pub(crate) unsafe fn as_mcu_cmd(&self) -> &MCUCommand { 124 | &self.u.rumble_subcmd.u.set_mcu_conf 125 | } 126 | } 127 | 128 | impl From for OutputReport { 129 | fn from(subcmd: SubcommandRequest) -> Self { 130 | OutputReportEnum::RumbleAndSubcmd(subcmd).into() 131 | } 132 | } 133 | 134 | impl From for OutputReport { 135 | fn from(subcmd: SubcommandRequestEnum) -> Self { 136 | SubcommandRequest::from(subcmd).into() 137 | } 138 | } 139 | 140 | impl From for OutputReport { 141 | fn from(mcu_request: MCURequest) -> Self { 142 | OutputReportEnum::RequestMCUData(mcu_request).into() 143 | } 144 | } 145 | 146 | //normal normal_mut: Normal = NormalInputReport, 147 | raw_enum! { 148 | #[id: SubcommandId] 149 | #[union: SubcommandRequestUnion] 150 | #[struct: SubcommandRequest] 151 | #[raw [u8; 38]] 152 | pub enum SubcommandRequestEnum { 153 | get_only_controller_state get_only_controller_state_mut: GetOnlyControllerState = (), 154 | bluetooth_manual_pairing bluetooth_manual_pairing_mut: BluetoothManualPairing = (), 155 | request_device_info request_device_info_mut: RequestDeviceInfo = (), 156 | set_input_report_mode set_input_report_mode_mut: SetInputReportMode = RawId, 157 | get_trigger_buttons_elapsed_time get_trigger_buttons_elapsed_time_mut: GetTriggerButtonsElapsedTime = (), 158 | set_shipment_mode set_shipment_mode_mut: SetShipmentMode = RawId, 159 | spi_read spi_read_mut: SPIRead = SPIReadRequest, 160 | spi_write spi_write_mut: SPIWrite = SPIWriteRequest, 161 | set_mcu_conf set_mcu_conf_mut: SetMCUConf = MCUCommand, 162 | set_mcu_state set_mcu_state_mut: SetMCUState = RawId, 163 | set_unknown_data set_unknown_data_mut: SetUnknownData = [u8; 38], 164 | set_player_lights set_player_lights_mut: SetPlayerLights = light::PlayerLights, 165 | set_home_light set_home_light_mut: SetHomeLight = light::HomeLight, 166 | set_imu_mode set_imu_mode_mut: SetIMUMode = RawId, 167 | set_imu_sens set_imu_sens_mut: SetIMUSens = imu::Sensitivity, 168 | enable_vibration enable_vibration_mut: EnableVibration = RawId, 169 | maybe_accessory maybe_accessory_mut: MaybeAccessory = AccessoryCommand, 170 | unknown0x59 unknown0x59_mut: Unknown0x59 = (), 171 | unknown0x5a unknown0x5a_mut: Unknown0x5a = [u8; 38], 172 | unknown0x5b unknown0x5b_mut: Unknown0x5b = (), 173 | unknown0x5c unknown0x5c_mut: Unknown0x5c = [u8; 38] 174 | } 175 | } 176 | 177 | impl SubcommandRequest { 178 | pub fn disable_shipment_mode() -> Self { 179 | SubcommandRequestEnum::SetShipmentMode(Bool::False.into()).into() 180 | } 181 | 182 | pub fn subcmd_0x59() -> Self { 183 | SubcommandRequestEnum::Unknown0x59(()).into() 184 | } 185 | 186 | pub fn subcmd_0x5a() -> Self { 187 | SubcommandRequestEnum::Unknown0x5a([ 188 | 4, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 189 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 190 | ]) 191 | .into() 192 | } 193 | 194 | pub fn subcmd_0x5b() -> Self { 195 | SubcommandRequestEnum::Unknown0x5b(()).into() 196 | } 197 | 198 | pub fn subcmd_0x5c_0() -> Self { 199 | SubcommandRequestEnum::Unknown0x5c([ 200 | 0, 0, 150, 227, 28, 0, 0, 0, 236, 153, 172, 227, 28, 0, 0, 0, 243, 130, 241, 89, 46, 201 | 89, 0, 0, 224, 88, 179, 227, 28, 0, 0, 0, 0, 242, 5, 42, 1, 0, 202 | ]) 203 | .into() 204 | } 205 | 206 | pub fn subcmd_0x5c_6() -> Self { 207 | SubcommandRequestEnum::Unknown0x5c([ 208 | 6, 3, 37, 6, 0, 0, 0, 0, 236, 153, 172, 227, 28, 0, 0, 0, 105, 155, 22, 246, 93, 86, 0, 209 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 144, 40, 161, 227, 28, 0, 210 | ]) 211 | .into() 212 | } 213 | } 214 | 215 | impl From for SubcommandRequest { 216 | fn from(mcu_cmd: MCUCommand) -> Self { 217 | SubcommandRequestEnum::SetMCUConf(mcu_cmd).into() 218 | } 219 | } 220 | 221 | impl From for SubcommandRequest { 222 | fn from(accessory_cmd: AccessoryCommand) -> Self { 223 | SubcommandRequestEnum::MaybeAccessory(accessory_cmd).into() 224 | } 225 | } 226 | 227 | impl From for SubcommandRequest { 228 | fn from(imu_sensitivity: crate::imu::Sensitivity) -> Self { 229 | SubcommandRequestEnum::SetIMUSens(imu_sensitivity).into() 230 | } 231 | } 232 | 233 | impl From for SubcommandRequest { 234 | fn from(spi_read: SPIReadRequest) -> Self { 235 | SubcommandRequestEnum::SPIRead(spi_read).into() 236 | } 237 | } 238 | 239 | impl From for SubcommandRequest { 240 | fn from(spi_write: SPIWriteRequest) -> Self { 241 | SubcommandRequestEnum::SPIWrite(spi_write).into() 242 | } 243 | } 244 | 245 | impl From for SubcommandRequest { 246 | fn from(player_lights: light::PlayerLights) -> Self { 247 | SubcommandRequestEnum::SetPlayerLights(player_lights).into() 248 | } 249 | } 250 | 251 | impl From for SubcommandRequest { 252 | fn from(home_light: light::HomeLight) -> Self { 253 | SubcommandRequestEnum::SetHomeLight(home_light).into() 254 | } 255 | } 256 | 257 | #[test] 258 | pub fn check_layout() { 259 | unsafe { 260 | let report = OutputReport::new(); 261 | assert_eq!(2, offset_of(&report, &report.rumble.rumble_data)); 262 | assert_eq!(10, offset_of(&report, &report.u.rumble_subcmd)); 263 | assert_eq!(11, offset_of(&report, report.as_mcu_cmd())); 264 | assert_eq!(49, std::mem::size_of_val(&report)); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /crates/joycon-sys/src/input/report.rs: -------------------------------------------------------------------------------- 1 | //! Structs binary compatible with the HID input reports 2 | //! 3 | //! 4 | 5 | use crate::{accessory::AccessoryResponse, common::*, imu, input::*, mcu::*, raw_enum, spi::*}; 6 | use std::{fmt, mem::size_of_val}; 7 | 8 | raw_enum! { 9 | #[id: InputReportId] 10 | #[union: InputReportUnion] 11 | #[struct: InputReport] 12 | pub enum InputReportEnum { 13 | normal normal_mut: Normal = NormalInputReport, 14 | standard_subcmd standard_subcmd_mut: StandardAndSubcmd = ( 15 | StandardInputReport, 16 | SubcommandReply, 17 | ), 18 | mcu_fw_update mcu_fw_update_mut: MCUFwUpdate = (), 19 | standard_full standard_full_mut: StandardFull = ( 20 | StandardInputReport, 21 | [imu::Frame; 3] 22 | ), 23 | standard_full_mcu standard_full_mcu_mut: StandardFullMCU = ( 24 | StandardInputReport, 25 | [imu::Frame; 3], 26 | MCUReport 27 | ) 28 | } 29 | } 30 | 31 | // Describes a HID report from the JoyCon. 32 | // 33 | // ```ignore 34 | // let mut report = InputReport::new(); 35 | // read_hid_report(buffer.as_bytes_mut()); 36 | // ``` 37 | //pub struct InputReport { 38 | 39 | impl InputReport { 40 | pub fn is_special(&self) -> bool { 41 | self.id != InputReportId::Normal 42 | && self.id != InputReportId::StandardFull 43 | && self 44 | .mcu_report() 45 | .and_then(MCUReport::ir_data) 46 | .map(|_| false) 47 | .unwrap_or(true) 48 | } 49 | 50 | pub fn len(&self) -> usize { 51 | match self.id.try_into() { 52 | Some(InputReportId::Normal) => 12, 53 | Some(InputReportId::StandardAndSubcmd) | Some(InputReportId::StandardFull) => 49, 54 | Some(InputReportId::StandardFullMCU) => 362, 55 | Some(InputReportId::MCUFwUpdate) => unimplemented!(), 56 | None => size_of_val(self), 57 | } 58 | } 59 | 60 | pub fn as_bytes(&self) -> &[u8] { 61 | unsafe { std::slice::from_raw_parts(self as *const _ as *const u8, self.len()) } 62 | } 63 | 64 | pub fn as_bytes_mut(&mut self) -> &mut [u8] { 65 | unsafe { std::slice::from_raw_parts_mut(self as *mut _ as *mut u8, size_of_val(self)) } 66 | } 67 | 68 | pub fn validate(&self) { 69 | match self.id.try_into() { 70 | Some(_) => { 71 | if let Some(rep) = self.subcmd_reply() { 72 | rep.validate() 73 | } 74 | if let Some(rep) = self.mcu_report() { 75 | rep.validate() 76 | } 77 | } 78 | None => panic!("unknown report id {:x?}", self.id), 79 | } 80 | } 81 | 82 | pub fn standard(&self) -> Option<&StandardInputReport> { 83 | if self.id == InputReportId::StandardAndSubcmd 84 | || self.id == InputReportId::StandardFull 85 | || self.id == InputReportId::StandardFullMCU 86 | { 87 | Some(unsafe { &self.u.standard_full.0 }) 88 | } else { 89 | None 90 | } 91 | } 92 | 93 | pub fn subcmd_reply(&self) -> Option<&SubcommandReply> { 94 | self.standard_subcmd().map(|x| &x.1) 95 | } 96 | 97 | pub fn imu_frames(&self) -> Option<&[imu::Frame; 3]> { 98 | if self.id == InputReportId::StandardFull || self.id == InputReportId::StandardFullMCU { 99 | Some(unsafe { &self.u.standard_full.1 }) 100 | } else { 101 | None 102 | } 103 | } 104 | 105 | pub fn mcu_report(&self) -> Option<&MCUReport> { 106 | self.standard_full_mcu().map(|x| &x.2) 107 | } 108 | 109 | #[cfg(test)] 110 | pub(crate) unsafe fn u_mcu_report(&self) -> &MCUReport { 111 | &self.u.standard_full_mcu.2 112 | } 113 | } 114 | 115 | #[repr(packed)] 116 | #[derive(Copy, Clone, Debug, Default)] 117 | pub struct NormalInputReport { 118 | pub buttons: [u8; 2], 119 | pub stick: u8, 120 | _filler: [u8; 8], 121 | } 122 | 123 | #[repr(packed)] 124 | #[derive(Copy, Clone, Debug)] 125 | pub struct StandardInputReport { 126 | pub timer: u8, 127 | pub info: DeviceStatus, 128 | pub buttons: ButtonsStatus, 129 | pub left_stick: Stick, 130 | pub right_stick: Stick, 131 | pub vibrator: u8, 132 | } 133 | 134 | raw_enum! { 135 | #[pre_id ack ack_mut: Ack] 136 | #[id: SubcommandId] 137 | #[union: SubcommandReplyUnion] 138 | #[struct: SubcommandReply] 139 | #[raw [u8; 39]] 140 | pub enum SubcommandReplyEnum { 141 | controller_state controller_state_mut: GetOnlyControllerState = (), 142 | bluetooth_manual_pairing bluetooth_manual_pairing_mut: BluetoothManualPairing = (), 143 | device_info device_info_mut: RequestDeviceInfo = DeviceInfo, 144 | input_report_mode_result input_report_mode_result_mut: SetInputReportMode = (), 145 | trigger_buttons_elapsed_time trigger_buttons_elapsed_time_mut: GetTriggerButtonsElapsedTime = [U16LE; 7], 146 | shipment_mode_result shipment_mode_result_mut: SetShipmentMode = (), 147 | spi_read_result spi_read_result_mut: SPIRead = SPIReadResult, 148 | spi_write_result spi_write_result_mut: SPIWrite = SPIWriteResult, 149 | mcu_report mcu_report_mut: SetMCUConf = MCUReport, 150 | mcu_state_result mcu_state_result_mut: SetMCUState = (), 151 | set_unknown_data set_unknown_data_mut: SetUnknownData = (), 152 | player_lights_result player_lights_result_mut: SetPlayerLights = (), 153 | home_light_result home_light_result_mut: SetHomeLight = (), 154 | imu_mode_result imu_mode_result_mut: SetIMUMode = (), 155 | imu_sens_result imu_sens_result_mut: SetIMUSens = (), 156 | enable_vibration enable_vibration_mut: EnableVibration = (), 157 | maybe_accessory maybe_accessory_mut: MaybeAccessory = AccessoryResponse, 158 | unknown0x59 unknown0x59_mut: Unknown0x59 = (), 159 | unknown0x5a unknown0x5a_mut: Unknown0x5a = (), 160 | unknown0x5b unknown0x5b_mut: Unknown0x5b = (), 161 | unknown0x5c unknown0x5c_mut: Unknown0x5c = () 162 | } 163 | } 164 | 165 | impl SubcommandReply { 166 | pub fn validate(&self) { 167 | assert!( 168 | self.id.try_into().is_some(), 169 | "invalid subcmd id{:?}", 170 | self.id 171 | ) 172 | } 173 | 174 | pub fn is_spi_write_success(&self) -> Option { 175 | self.spi_write_result() 176 | .map(|r| self.ack.is_ok() && r.success()) 177 | } 178 | } 179 | 180 | #[repr(packed)] 181 | #[derive(Copy, Clone, Default, PartialEq, Eq)] 182 | pub struct Ack(u8); 183 | 184 | impl Ack { 185 | pub fn is_ok(self) -> bool { 186 | (self.0 & 0x80) != 0 187 | } 188 | } 189 | 190 | impl fmt::Debug for Ack { 191 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 192 | if self.0 == 0 { 193 | f.debug_tuple("NAck").finish() 194 | } else { 195 | let data = self.0 & 0x7f; 196 | let mut out = f.debug_tuple("Ack"); 197 | if data != 0 { 198 | out.field(&data); 199 | } 200 | out.finish() 201 | } 202 | } 203 | } 204 | 205 | #[repr(packed)] 206 | #[derive(Copy, Clone, Debug)] 207 | pub struct DeviceInfo { 208 | pub firmware_version: FirmwareVersion, 209 | // 1=Left Joy-Con, 2=Right Joy-Con, 3=Pro Controller 210 | pub which_controller: RawId, 211 | // Unknown. Seems to be always 02 212 | _something: u8, 213 | // Big endian 214 | pub mac_address: MACAddress, 215 | // Unknown. Seems to be always 01 216 | _somethingelse: u8, 217 | // bool 218 | pub use_spi_colors: RawId, 219 | } 220 | 221 | #[repr(packed)] 222 | #[derive(Copy, Clone, Debug)] 223 | pub struct FirmwareVersion(pub [u8; 2]); 224 | 225 | impl fmt::Display for FirmwareVersion { 226 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 227 | write!(f, "{}.{}", self.0[0], self.0[1]) 228 | } 229 | } 230 | 231 | #[repr(packed)] 232 | #[derive(Copy, Clone, Debug)] 233 | pub struct MACAddress(pub [u8; 6]); 234 | 235 | impl fmt::Display for MACAddress { 236 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 237 | write!( 238 | f, 239 | "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", 240 | self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5] 241 | ) 242 | } 243 | } 244 | 245 | #[repr(u8)] 246 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, Eq, PartialEq)] 247 | pub enum WhichController { 248 | LeftJoyCon = 1, 249 | RightJoyCon = 2, 250 | ProController = 3, 251 | } 252 | 253 | impl fmt::Display for WhichController { 254 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 255 | write!( 256 | f, 257 | "{}", 258 | match *self { 259 | WhichController::LeftJoyCon => "JoyCon (L)", 260 | WhichController::RightJoyCon => "JoyCon (R)", 261 | WhichController::ProController => "Pro Controller", 262 | } 263 | ) 264 | } 265 | } 266 | 267 | #[repr(u8)] 268 | #[derive(Copy, Clone, Debug, FromPrimitive, ToPrimitive, Eq, PartialEq)] 269 | pub enum UseSPIColors { 270 | No = 0, 271 | WithoutGrip = 1, 272 | IncludingGrip = 2, 273 | } 274 | 275 | #[cfg(test)] 276 | #[test] 277 | fn check_layout() { 278 | unsafe { 279 | let report = InputReport::new(); 280 | assert_eq!(6, offset_of(&report, &report.u.standard_full.0.left_stick)); 281 | assert_eq!(13, offset_of(&report, &report.u.standard_full.1)); 282 | assert_eq!(13, offset_of(&report, &report.u.standard_subcmd.1)); 283 | assert_eq!(15, offset_of(&report, &report.u.standard_subcmd.1.u)); 284 | assert_eq!(49, offset_of(&report, &report.u.standard_full_mcu.2)); 285 | assert_eq!(362, std::mem::size_of_val(&report)); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /crates/dualshock-sys/src/input.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, mem::size_of}; 2 | 3 | use bitfield::bitfield; 4 | use cgmath::{vec2, vec3, Vector2, Vector3}; 5 | 6 | use crate::{ConnectionType, RawId, I16LE}; 7 | 8 | #[repr(packed)] 9 | #[derive(Clone, Copy)] 10 | pub struct InputReport { 11 | id: RawId, 12 | u: InputReportData, 13 | // allows detection of larger reads in conn_type 14 | _padding: u8, 15 | } 16 | 17 | impl InputReport { 18 | pub fn new() -> InputReport { 19 | unsafe { std::mem::zeroed() } 20 | } 21 | 22 | pub fn as_bytes_mut(&mut self) -> &mut [u8] { 23 | unsafe { 24 | std::slice::from_raw_parts_mut(self as *mut _ as *mut u8, std::mem::size_of_val(self)) 25 | } 26 | } 27 | 28 | pub fn bt_simple(&self) -> Option<&BTSimpleReport> { 29 | if self.id == InputReportId::Simple { 30 | Some(unsafe { &self.u.simple }) 31 | } else { 32 | None 33 | } 34 | } 35 | 36 | pub fn bt_full(&self) -> Option<&BTFullReport> { 37 | if self.id == InputReportId::Full { 38 | Some(unsafe { &self.u.complete }) 39 | } else { 40 | None 41 | } 42 | } 43 | 44 | pub fn usb_full(&self) -> Option<&USBReport> { 45 | // USB uses different ids smh... 46 | if self.id == InputReportId::Simple { 47 | Some(unsafe { &self.u.usb }) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | pub fn conn_type(mut nb_read: usize) -> ConnectionType { 54 | // Remove report id 55 | nb_read -= 1; 56 | // TODO: better detection, what about other reports? 57 | if nb_read == size_of::() { 58 | ConnectionType::USB 59 | } else if [size_of::(), size_of::()].contains(&nb_read) { 60 | ConnectionType::Bluetooth 61 | } else { 62 | dbg!(size_of::()); 63 | dbg!(nb_read); 64 | unreachable!() 65 | } 66 | } 67 | } 68 | 69 | impl fmt::Debug for InputReport { 70 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 71 | // TODO: handle bluetooth 72 | match self.id.try_into() { 73 | Some(InputReportId::Simple) => self.bt_simple().fmt(f), 74 | Some(InputReportId::Full) => self.bt_full().fmt(f), 75 | None => unimplemented!(), 76 | } 77 | } 78 | } 79 | 80 | #[repr(u8)] 81 | #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)] 82 | pub enum InputReportId { 83 | Simple = 0x01, 84 | Full = 0x11, 85 | } 86 | 87 | #[repr(packed)] 88 | #[derive(Clone, Copy)] 89 | union InputReportData { 90 | simple: BTSimpleReport, 91 | complete: BTFullReport, 92 | usb: USBReport, 93 | } 94 | 95 | #[repr(packed)] 96 | #[derive(Debug, Clone, Copy)] 97 | pub struct USBReport { 98 | pub full: FullReport, 99 | pub trackpad: USBTrackpad, 100 | _unknown: [u8; 12], 101 | } 102 | 103 | #[repr(packed)] 104 | #[derive(Debug, Clone, Copy)] 105 | pub struct BTSimpleReport { 106 | pub base: SimpleReport, 107 | } 108 | 109 | #[repr(packed)] 110 | #[derive(Debug, Clone, Copy)] 111 | pub struct BTFullReport { 112 | _unknown1: u8, 113 | report_id: u8, 114 | pub full: FullReport, 115 | pub trackpad: BTTrackpad, 116 | _unknown2: [u8; 2], 117 | crc32: [u8; 4], 118 | } 119 | 120 | #[repr(packed)] 121 | #[derive(Debug, Clone, Copy)] 122 | pub struct FullReport { 123 | pub base: SimpleReport, 124 | _unknown_timestamp: [u8; 2], 125 | battery_level: u8, 126 | pub gyro: Gyro, 127 | pub accel: Accel, 128 | _unknown_0x00: [u8; 5], 129 | pub type_: Type, 130 | unknown_0x00_2: [u8; 2], 131 | } 132 | 133 | #[repr(packed)] 134 | #[derive(Debug, Clone, Copy)] 135 | pub struct SimpleReport { 136 | pub left_stick: Stick, 137 | pub right_stick: Stick, 138 | pub buttons: Buttons<[u8; 3]>, 139 | pub left_trigger: Trigger, 140 | pub right_trigger: Trigger, 141 | } 142 | 143 | bitfield! { 144 | #[repr(transparent)] 145 | #[derive(Copy, Clone, Default)] 146 | pub struct Type(u8); 147 | impl Debug; 148 | u8; 149 | pub battery, _: 3, 0; 150 | pub usb, _: 4; 151 | pub mic, _: 5; 152 | pub phone, _: 6; 153 | } 154 | 155 | #[repr(transparent)] 156 | #[derive(Clone, Copy)] 157 | pub struct Trigger(u8); 158 | 159 | impl Trigger { 160 | pub fn normalize(&self) -> f64 { 161 | self.0 as f64 / 255. 162 | } 163 | } 164 | 165 | impl fmt::Debug for Trigger { 166 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 167 | f.debug_tuple("Trigger").field(&self.normalize()).finish() 168 | } 169 | } 170 | 171 | bitfield! { 172 | #[repr(transparent)] 173 | #[derive(Copy, Clone, Default)] 174 | pub struct Buttons([u8]); 175 | impl Debug; 176 | u8; 177 | pub into Dpad, dpad, _: 3, 0; 178 | pub dpad_pressed, _: 3; 179 | pub square, _: 4; 180 | pub cross, _: 5; 181 | pub circle, _: 6; 182 | pub triangle, _: 7; 183 | pub l1, _: 8; 184 | pub r1, _: 9; 185 | pub l2, _: 10; 186 | pub r2, _: 11; 187 | pub share, _: 12; 188 | pub options, _: 13; 189 | pub l3, _: 14; 190 | pub r3, _: 15; 191 | pub ps, _: 16; 192 | pub tpad, _: 17; 193 | pub counter, _: 23, 18; 194 | } 195 | 196 | #[derive(Debug, Clone, Copy)] 197 | pub struct Dpad(u8); 198 | 199 | impl Dpad { 200 | pub fn up(&self) -> bool { 201 | self.0 <= 1 || self.0 == 7 202 | } 203 | 204 | pub fn right(&self) -> bool { 205 | 1 <= self.0 && self.0 <= 3 206 | } 207 | 208 | pub fn down(&self) -> bool { 209 | 3 <= self.0 && self.0 <= 5 210 | } 211 | 212 | pub fn left(&self) -> bool { 213 | 5 <= self.0 && self.0 <= 7 214 | } 215 | } 216 | 217 | impl From for Dpad { 218 | fn from(u: u8) -> Self { 219 | Dpad(u) 220 | } 221 | } 222 | 223 | #[repr(packed)] 224 | #[derive(Debug, Clone, Copy)] 225 | pub struct Gyro { 226 | pitch: I16LE, 227 | yaw: I16LE, 228 | roll: I16LE, 229 | } 230 | 231 | impl Gyro { 232 | pub fn normalize(&self) -> Vector3 { 233 | let factor = 2000. / (2.0_f64.powi(15)); 234 | let pitch = i16::from(self.pitch) as f64 * factor; 235 | let yaw = i16::from(self.yaw) as f64 * factor; 236 | let roll = i16::from(self.roll) as f64 * factor; 237 | vec3(pitch, yaw, roll) 238 | } 239 | } 240 | 241 | #[repr(packed)] 242 | #[derive(Debug, Clone, Copy)] 243 | pub struct Accel { 244 | x: I16LE, 245 | y: I16LE, 246 | z: I16LE, 247 | } 248 | 249 | impl Accel { 250 | pub fn raw(&self) -> Vector3 { 251 | vec3(i16::from(self.x), i16::from(self.y), i16::from(self.z)) 252 | } 253 | 254 | /// Convert to SI units, in G across each axis. 255 | pub fn normalize(&self) -> Vector3 { 256 | self.raw().cast::().unwrap() / 2.0_f64.powi(15) * 4. 257 | } 258 | } 259 | 260 | #[repr(packed)] 261 | #[derive(Clone, Copy)] 262 | pub struct Stick { 263 | x: u8, 264 | y: u8, 265 | } 266 | 267 | impl Stick { 268 | pub fn val(&self) -> (u8, u8) { 269 | (self.x, 255 - self.y) 270 | } 271 | 272 | pub fn normalize(&self) -> Vector2 { 273 | let x = self.x as f64 - 128.; 274 | let y = self.y as f64 - 128.; 275 | vec2(x, -y) / 128. 276 | } 277 | } 278 | 279 | impl fmt::Debug for Stick { 280 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 281 | let s = self.normalize(); 282 | f.debug_tuple("Stick").field(&s.x).field(&s.y).finish() 283 | } 284 | } 285 | 286 | #[repr(packed)] 287 | #[derive(Clone, Copy)] 288 | pub struct BTTrackpad { 289 | len: u8, 290 | packets: [TrackpadPacket; 4], 291 | } 292 | 293 | impl BTTrackpad { 294 | pub fn packets(&self) -> impl Iterator { 295 | self.packets 296 | .iter() 297 | .take(self.len as usize) 298 | .filter(|p| p.fingers().any(|f| f.is_active())) 299 | } 300 | } 301 | 302 | impl fmt::Debug for BTTrackpad { 303 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 304 | f.debug_list().entries(self.packets()).finish() 305 | } 306 | } 307 | 308 | #[repr(packed)] 309 | #[derive(Clone, Copy)] 310 | pub struct USBTrackpad { 311 | _unknown: u8, 312 | packets: [TrackpadPacket; 2], 313 | } 314 | 315 | impl USBTrackpad { 316 | pub fn packets(&self) -> impl Iterator { 317 | self.packets 318 | .iter() 319 | .filter(|p| p.fingers().any(|f| f.is_active())) 320 | } 321 | } 322 | 323 | impl fmt::Debug for USBTrackpad { 324 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 325 | f.debug_list().entries(self.packets()).finish() 326 | } 327 | } 328 | 329 | #[repr(packed)] 330 | #[derive(Debug, Clone, Copy)] 331 | pub struct TrackpadPacket { 332 | counter: u8, 333 | fingers: [Finger; 2], 334 | } 335 | 336 | impl TrackpadPacket { 337 | pub fn fingers(&self) -> impl Iterator { 338 | self.fingers.iter().filter(|f| f.is_active()) 339 | } 340 | } 341 | 342 | #[repr(packed)] 343 | #[derive(Clone, Copy)] 344 | pub struct Finger { 345 | id: u8, 346 | coordinate: FingerCoord, 347 | } 348 | 349 | impl Finger { 350 | pub fn is_active(&self) -> bool { 351 | self.id & 0x80 == 0 352 | } 353 | 354 | pub fn id(&self) -> u8 { 355 | self.id & 0x7F 356 | } 357 | 358 | pub fn coord(&self) -> Vector2 { 359 | self.coordinate.normalize() 360 | } 361 | } 362 | 363 | impl fmt::Debug for Finger { 364 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 365 | if self.is_active() { 366 | f.debug_struct("Finger") 367 | .field("id", &self.id()) 368 | .field("coordinate", &self.coordinate) 369 | .finish() 370 | } else { 371 | f.debug_struct("Finger (none)").finish() 372 | } 373 | } 374 | } 375 | 376 | #[repr(packed)] 377 | #[derive(Clone, Copy)] 378 | pub struct FingerCoord(u8, u8, u8); 379 | 380 | impl FingerCoord { 381 | pub fn raw(&self) -> (u16, u16) { 382 | let (a, b, c) = (self.0 as u16, self.1 as u16, self.2 as u16); 383 | (((b & 0xf) << 8) | a, (c << 4) | ((b & 0xf0) >> 4)) 384 | } 385 | 386 | pub fn normalize(&self) -> Vector2 { 387 | let (x, y) = self.raw(); 388 | vec2(x as f64 / 1919., y as f64 / 942.) 389 | } 390 | } 391 | 392 | impl fmt::Debug for FingerCoord { 393 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 394 | let val = self.raw(); 395 | f.debug_tuple("FingerCoord") 396 | .field(&val.0) 397 | .field(&val.1) 398 | .finish() 399 | } 400 | } 401 | --------------------------------------------------------------------------------