Initial Commit
This commit is contained in:
13 files changed
+1267
No files matched your search
+133
@@ -0,0 +1,133 @@
|
||||
pkg/
|
||||
*.tar
|
||||
|
||||
### VisualStudioCode template
|
||||
.vscode/*
|
||||
#!.vscode/settings.json
|
||||
#!.vscode/tasks.json
|
||||
#!.vscode/launch.json
|
||||
#!.vscode/extensions.json
|
||||
#!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### Rust template
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Linux template
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "gk1000_controller"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
hidapi = "2.6.5"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org>
|
||||
@@ -0,0 +1,7 @@
|
||||
extern crate hidapi;
|
||||
mod constants;
|
||||
pub mod error;
|
||||
pub mod wrapper;
|
||||
|
||||
pub use error::HidWrapperError;
|
||||
pub use wrapper::HidWrapper;
|
||||
@@ -0,0 +1,4 @@
|
||||
pub const VENDOR_ID: u16 = 0x0c45;
|
||||
pub const PRODUCT_ID: u16 = 0x8006;
|
||||
pub const MAX_SEND_REPORT_LEN: usize = 65;
|
||||
pub const GET_REPORT_REPORT: [u8; MAX_SEND_REPORT_LEN] = [0u8; MAX_SEND_REPORT_LEN];
|
||||
@@ -0,0 +1,40 @@
|
||||
use hidapi::HidError;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HidWrapperError {
|
||||
#[allow(dead_code)]
|
||||
HidApiError(HidError),
|
||||
NoHidDeviceError,
|
||||
}
|
||||
|
||||
impl Display for HidWrapperError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HidWrapperError::HidApiError(err) => {
|
||||
write!(f, "Hid Api Error: {err}")
|
||||
}
|
||||
HidWrapperError::NoHidDeviceError => {
|
||||
write!(f, "No Hid Device Found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HidWrapperError {}
|
||||
|
||||
impl From<HidError> for HidWrapperError {
|
||||
fn from(error: HidError) -> Self {
|
||||
match error {
|
||||
HidError::HidApiError { ref message } => {
|
||||
if message == "No HID devices with requested VID/PID found in the system." {
|
||||
HidWrapperError::NoHidDeviceError
|
||||
} else {
|
||||
HidWrapperError::HidApiError(error)
|
||||
}
|
||||
}
|
||||
_ => HidWrapperError::HidApiError(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use crate::hid_wrapper::constants::{
|
||||
GET_REPORT_REPORT, MAX_SEND_REPORT_LEN, PRODUCT_ID, VENDOR_ID,
|
||||
};
|
||||
use crate::hid_wrapper::error::HidWrapperError;
|
||||
use hidapi::{HidApi, HidDevice};
|
||||
|
||||
pub struct HidWrapper {
|
||||
hid_api: HidApi,
|
||||
hid_device: Option<HidDevice>,
|
||||
}
|
||||
|
||||
impl Default for HidWrapper {
|
||||
/// Makes a new instance of HidWrapper (No shit Sherlock)
|
||||
///
|
||||
/// # Panics
|
||||
/// If ```HidWrapperError::HidApiError``` is returned from new()
|
||||
fn default() -> Self {
|
||||
match Self::new() {
|
||||
Ok(hid_wrapper) => hid_wrapper,
|
||||
Err(err) => panic!("hid wrapper Default() failed to create new HidWrapper: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HidWrapper {
|
||||
/// Makes a new instance of HidWrapper (No shit Sherlock)
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
pub fn new() -> Result<HidWrapper, HidWrapperError> {
|
||||
Ok(HidWrapper {
|
||||
hid_api: match HidApi::new() {
|
||||
Ok(hidapi) => hidapi,
|
||||
Err(err) => return Err(HidWrapperError::from(err)),
|
||||
},
|
||||
hid_device: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_device_connected(&mut self) -> Result<bool, HidWrapperError> {
|
||||
match self.hid_api.reset_devices() {
|
||||
Ok(_) => {}
|
||||
Err(err) => return Err(HidWrapperError::from(err)),
|
||||
};
|
||||
match self.hid_api.add_devices(VENDOR_ID, PRODUCT_ID) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return Err(HidWrapperError::from(err)),
|
||||
};
|
||||
Ok(self.hid_api.device_list().count() >= 1)
|
||||
}
|
||||
|
||||
/// Opens a device
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
pub fn open_device(&mut self) -> Result<(), HidWrapperError> {
|
||||
self.hid_device = match self.hid_api.open(VENDOR_ID, PRODUCT_ID) {
|
||||
Ok(hid_device) => Some(hid_device),
|
||||
Err(err) => return Err(HidWrapperError::from(err)),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drops the device
|
||||
///
|
||||
pub fn close_device(&mut self) {
|
||||
self.hid_device = None;
|
||||
}
|
||||
|
||||
/// Sends a set report (65 bytes len)
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
pub fn send_report(&mut self, data: &[u8]) -> Result<(), HidWrapperError> {
|
||||
match &self.hid_device {
|
||||
Some(hid_device) => {
|
||||
let mut out = [0; MAX_SEND_REPORT_LEN];
|
||||
out[1..1 + data.len()].copy_from_slice(data);
|
||||
match hid_device.send_feature_report(&out) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(HidWrapperError::from(err)),
|
||||
}
|
||||
}
|
||||
None => Err(HidWrapperError::NoHidDeviceError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a get report (64 bytes len)
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
pub fn get_report(&mut self) -> Result<usize, HidWrapperError> {
|
||||
match &self.hid_device {
|
||||
Some(hid_device) => {
|
||||
let mut request = GET_REPORT_REPORT;
|
||||
request[1] = 64;
|
||||
match hid_device.get_feature_report(&mut request) {
|
||||
Ok(usize) => Ok(usize),
|
||||
Err(err) => Err(HidWrapperError::from(err)),
|
||||
}
|
||||
}
|
||||
None => Err(HidWrapperError::NoHidDeviceError),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod hid_wrapper;
|
||||
mod rgb_controller;
|
||||
|
||||
pub use hid_wrapper::HidWrapperError;
|
||||
pub use rgb_controller::controller::RGBController;
|
||||
pub use rgb_controller::effects::Effects;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod controller;
|
||||
pub mod effect_info;
|
||||
pub mod effects;
|
||||
@@ -0,0 +1,695 @@
|
||||
use crate::Effects;
|
||||
use crate::HidWrapperError;
|
||||
use crate::hid_wrapper::HidWrapper;
|
||||
|
||||
pub struct RGBController {
|
||||
hid_wrapper: HidWrapper,
|
||||
}
|
||||
|
||||
impl Default for RGBController {
|
||||
/// Makes a new instance of HidWrapper (No shit Sherlock)
|
||||
///
|
||||
/// # Panics
|
||||
/// if ```HidWrapperError::HidApiError``` is returned from new()
|
||||
fn default() -> Self {
|
||||
match Self::new() {
|
||||
Ok(hid_wrapper) => hid_wrapper,
|
||||
Err(err) => panic!("RGBController Default() failed to create new RGBController: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RGBController {
|
||||
/// Makes a new instance of HidWrapper (No shit Sherlock)
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
pub fn new() -> Result<RGBController, HidWrapperError> {
|
||||
Ok(RGBController {
|
||||
hid_wrapper: HidWrapper::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_device_connected(&mut self) -> Result<bool, HidWrapperError> {
|
||||
self.hid_wrapper.is_device_connected()
|
||||
}
|
||||
|
||||
/// Starts the RGB conversation
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
fn prepare_device(&mut self) -> Result<(), HidWrapperError> {
|
||||
self.hid_wrapper.send_report(&[0x04, 0xab])?;
|
||||
self.hid_wrapper.get_report()?;
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x26, 0x36, 0xf2, 0x27, 0xb7, 0x2a, 0x56, 0x77, 0x98, 0x59, 0x9c, 0x6d, 0x9f, 0xc6,
|
||||
0xb, 0x44, 0xec, 0xa8, 0x47, 0x21, 0xe2, 0x65, 0x76, 0xa3, 0x70, 0xbe, 0xa2, 0xbb,
|
||||
0x95, 0x47, 0x5e, 0xbe, 0x23, 0x1b, 0xad, 0xb8, 0xc3, 0xcd, 0x1f, 0x4, 0x39, 0xa2,
|
||||
0x3d, 0x19, 0xe9, 0xfd, 0x27, 0xa7, 0x9a, 0x1a, 0x7c, 0xe3, 0x7b, 0xff, 0x7a, 0x1e,
|
||||
0x63, 0xb0, 0x67, 0x9d, 0x5d, 0xa3, 0x2f, 0xa5,
|
||||
])?;
|
||||
|
||||
self.hid_wrapper.send_report(&[0x04, 0x02])?;
|
||||
self.hid_wrapper.get_report()?;
|
||||
|
||||
self.hid_wrapper
|
||||
.send_report(&[0x04, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12])?;
|
||||
self.hid_wrapper.get_report()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Some middle bytes IDK what to name ts
|
||||
/// Is a required part of the RGB conversation
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
fn mid(&mut self) -> Result<(), HidWrapperError> {
|
||||
//TODO: Rename func and possibly change packets depending on mode
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x1, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x2, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x3, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x4, 0x5a, 0xff, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
])?;
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x5, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x6, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x7, 0x5a, 0xff, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x8, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
])?;
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x9, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0xa, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0xb, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0xc, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
])?;
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
0xd, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0xe, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0xf, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x10, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
])?;
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x11, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x12, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x13, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x1, 0x10, 0xc, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
0x80, 0xff, 0xff, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0xaa, 0x55,
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets colors for each key (only if StaticPerKey effect is set) base on a 2d array
|
||||
/// Is a required part of the RGB conversation
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
fn custom_keys(&mut self, keymap: &[[u8; 3]; 144]) -> Result<(), HidWrapperError> {
|
||||
for i in 0..9 {
|
||||
//Magic num 9 == len of keymap / 16
|
||||
self.hid_wrapper.send_report(&[
|
||||
0x80,
|
||||
keymap[i * 16][0],
|
||||
keymap[i * 16][1],
|
||||
keymap[i * 16][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 1][0],
|
||||
keymap[i * 16 + 1][1],
|
||||
keymap[i * 16 + 1][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 2][0],
|
||||
keymap[i * 16 + 2][1],
|
||||
keymap[i * 16 + 2][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 3][0],
|
||||
keymap[i * 16 + 3][1],
|
||||
keymap[i * 16 + 3][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 4][0],
|
||||
keymap[i * 16 + 4][1],
|
||||
keymap[i * 16 + 4][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 5][0],
|
||||
keymap[i * 16 + 5][1],
|
||||
keymap[i * 16 + 5][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 6][0],
|
||||
keymap[i * 16 + 6][1],
|
||||
keymap[i * 16 + 6][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 7][0],
|
||||
keymap[i * 16 + 7][1],
|
||||
keymap[i * 16 + 7][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 8][0],
|
||||
keymap[i * 16 + 8][1],
|
||||
keymap[i * 16 + 8][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 9][0],
|
||||
keymap[i * 16 + 9][1],
|
||||
keymap[i * 16 + 9][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 10][0],
|
||||
keymap[i * 16 + 10][1],
|
||||
keymap[i * 16 + 10][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 11][0],
|
||||
keymap[i * 16 + 11][1],
|
||||
keymap[i * 16 + 11][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 12][0],
|
||||
keymap[i * 16 + 12][1],
|
||||
keymap[i * 16 + 12][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 13][0],
|
||||
keymap[i * 16 + 13][1],
|
||||
keymap[i * 16 + 13][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 14][0],
|
||||
keymap[i * 16 + 14][1],
|
||||
keymap[i * 16 + 14][2],
|
||||
0x80,
|
||||
keymap[i * 16 + 15][0],
|
||||
keymap[i * 16 + 15][1],
|
||||
keymap[i * 16 + 15][2],
|
||||
])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies an effect to the device
|
||||
/// Is a required part of the RGB conversation
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
fn apply_to_device(&mut self) -> Result<(), HidWrapperError> {
|
||||
self.hid_wrapper.send_report(&[0x04, 0x02])?;
|
||||
self.hid_wrapper.get_report()?;
|
||||
|
||||
self.hid_wrapper.send_report(&[0x04, 0xf0])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets an effect
|
||||
///
|
||||
/// # Errors
|
||||
/// ```HidWrapperError::HidApiError```
|
||||
/// ```HidWrapperError::NoHidDeviceError```
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn set_effect(
|
||||
&mut self,
|
||||
effect: Effects,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
full_color: bool,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
direction: u8,
|
||||
keymap: Option<[[u8; 3]; 144]>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.hid_wrapper.open_device()?;
|
||||
|
||||
self.prepare_device()?;
|
||||
|
||||
self.mid()?;
|
||||
|
||||
self.hid_wrapper.send_report(&[])?;
|
||||
self.hid_wrapper.send_report(&[])?;
|
||||
self.hid_wrapper.send_report(&[])?;
|
||||
|
||||
match keymap {
|
||||
Some(map) => self.custom_keys(&map)?,
|
||||
None => self.custom_keys(&[[0, 255, 0]; 144])?,
|
||||
}
|
||||
|
||||
self.hid_wrapper.send_report(&[
|
||||
effect.to_u8(),
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
u8::from(full_color),
|
||||
brightness.clamp(0, 16),
|
||||
speed.clamp(1, 16),
|
||||
direction.clamp(0, 3),
|
||||
0,
|
||||
0,
|
||||
0xaa,
|
||||
0x55,
|
||||
])?;
|
||||
|
||||
self.apply_to_device()?;
|
||||
|
||||
self.hid_wrapper.close_device();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_static_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Static,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
1,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_single_off_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::SingleOff,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_single_on_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::SingleOn,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_glittering_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Glittering,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_rain_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Rain,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_colorful_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Colorful,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_breath_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Breath,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_spectrum_effect(
|
||||
&mut self,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Spectrum,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_centrifugal_wave_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::CentrifugalWave,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn set_vertical_wave_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
direction: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::VerticalWave,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
direction.clamp(2, 3),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn set_horizontal_wave_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
direction: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::HorizontalWave,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
direction.clamp(0, 1),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_rotating_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Rotating,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_explosion_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Explosion,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_launch_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Launch,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_ripples_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Ripples,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_snake_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Snake,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_pulse_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Pulse,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_tilt_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Tilt,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_shuttle_effect(
|
||||
&mut self,
|
||||
red: u8,
|
||||
green: u8,
|
||||
blue: u8,
|
||||
brightness: u8,
|
||||
speed: u8,
|
||||
full_color: Option<bool>,
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::Shuttle,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
full_color.unwrap_or(false),
|
||||
brightness,
|
||||
speed,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_static_per_key_effect(
|
||||
&mut self,
|
||||
brightness: u8,
|
||||
keymap: [[u8; 3]; 144],
|
||||
) -> Result<(), HidWrapperError> {
|
||||
self.set_effect(
|
||||
Effects::StaticPerKey,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
brightness,
|
||||
1,
|
||||
0,
|
||||
Some(keymap),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use crate::Effects;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct EffectInfo<'a> {
|
||||
name: &'a str,
|
||||
effect: Effects,
|
||||
can_set_color: bool,
|
||||
can_set_speed: bool,
|
||||
can_set_direction: DirectionPolicy,
|
||||
can_set_full_color: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum DirectionPolicy {
|
||||
None,
|
||||
Horizontal01,
|
||||
Vertical23,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const EFFECTS: [EffectInfo; 20] = [
|
||||
EffectInfo {
|
||||
name: "Static",
|
||||
effect: Effects::Static,
|
||||
can_set_color: true,
|
||||
can_set_speed: false,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "SingleOn",
|
||||
effect: Effects::SingleOn,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "SingleOff",
|
||||
effect: Effects::SingleOff,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Glittering",
|
||||
effect: Effects::Glittering,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Rain",
|
||||
effect: Effects::Rain,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Colorful",
|
||||
effect: Effects::Colorful,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Breath",
|
||||
effect: Effects::Breath,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Spectrum",
|
||||
effect: Effects::Spectrum,
|
||||
can_set_color: false,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: false,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "CentrifugalWave",
|
||||
effect: Effects::CentrifugalWave,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "VerticalWave",
|
||||
effect: Effects::VerticalWave,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::Vertical23,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "HorizontalWave",
|
||||
effect: Effects::HorizontalWave,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::Horizontal01,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Rotating",
|
||||
effect: Effects::Rotating,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Explosion",
|
||||
effect: Effects::Explosion,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Launch",
|
||||
effect: Effects::Launch,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Ripples",
|
||||
effect: Effects::Ripples,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Snake",
|
||||
effect: Effects::Snake,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Pulse",
|
||||
effect: Effects::Pulse,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Tilt",
|
||||
effect: Effects::Tilt,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "Shuttle",
|
||||
effect: Effects::Shuttle,
|
||||
can_set_color: true,
|
||||
can_set_speed: true,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: true,
|
||||
},
|
||||
EffectInfo {
|
||||
name: "StaticPerKey",
|
||||
effect: Effects::StaticPerKey,
|
||||
can_set_color: false,
|
||||
can_set_speed: false,
|
||||
can_set_direction: DirectionPolicy::None,
|
||||
can_set_full_color: false,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,57 @@
|
||||
use Effects::{
|
||||
Breath, CentrifugalWave, Colorful, Explosion, Glittering, HorizontalWave, Launch, Pulse, Rain,
|
||||
Ripples, Rotating, Shuttle, SingleOff, SingleOn, Snake, Spectrum, Static, StaticPerKey, Tilt,
|
||||
VerticalWave,
|
||||
};
|
||||
|
||||
/// Effects of the keyboard
|
||||
pub enum Effects {
|
||||
Static,
|
||||
SingleOn,
|
||||
SingleOff,
|
||||
Glittering,
|
||||
Rain,
|
||||
Colorful,
|
||||
Breath,
|
||||
Spectrum,
|
||||
CentrifugalWave,
|
||||
VerticalWave,
|
||||
HorizontalWave,
|
||||
Rotating,
|
||||
Explosion,
|
||||
Launch,
|
||||
Ripples,
|
||||
Snake,
|
||||
Pulse,
|
||||
Tilt,
|
||||
Shuttle,
|
||||
StaticPerKey,
|
||||
}
|
||||
|
||||
impl Effects {
|
||||
/// Returns an u8 of the effect used for communication with the device
|
||||
pub fn to_u8(&self) -> u8 {
|
||||
match self {
|
||||
Static => 1,
|
||||
SingleOn => 2,
|
||||
SingleOff => 3,
|
||||
Glittering => 4,
|
||||
Rain => 5,
|
||||
Colorful => 6,
|
||||
Breath => 7,
|
||||
Spectrum => 8,
|
||||
CentrifugalWave => 9,
|
||||
VerticalWave => 10,
|
||||
HorizontalWave => 11,
|
||||
Rotating => 12,
|
||||
Explosion => 13,
|
||||
Launch => 14,
|
||||
Ripples => 15,
|
||||
Snake => 16,
|
||||
Pulse => 17,
|
||||
Tilt => 18,
|
||||
Shuttle => 19,
|
||||
StaticPerKey => 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user