first commit
7
src-tauri/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
25
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "razer-linux-desktop"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "razer_linux_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
10
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
397
src-tauri/src/backend/button_mapping.rs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
fn decode_button_mapping(
|
||||
profile: String,
|
||||
button: String,
|
||||
hypershift: bool,
|
||||
data: &[u8],
|
||||
) -> Result<ButtonMappingState, String> {
|
||||
let fn_class = data.first().copied().unwrap_or(0);
|
||||
let fn_len = data
|
||||
.get(1)
|
||||
.copied()
|
||||
.unwrap_or(data.len().saturating_sub(2) as u8) as usize;
|
||||
let payload_start = data.len().min(2);
|
||||
let payload_end = (2 + fn_len.min(5)).min(data.len());
|
||||
let fn_value = &data[payload_start..payload_end];
|
||||
let decoded = match fn_class {
|
||||
0x00 => Ok(("disabled".to_string(), json!({}))),
|
||||
0x01 => Ok((
|
||||
"mouse".to_string(),
|
||||
json!({"fn": decode_fn_mouse(*fn_value.first().ok_or_else(|| "Mouse mapping payload too short".to_string())?)?, "double_click": false}),
|
||||
)),
|
||||
0x0b => Ok((
|
||||
"mouse".to_string(),
|
||||
json!({"fn": decode_fn_mouse(*fn_value.first().ok_or_else(|| "Mouse mapping payload too short".to_string())?)?, "double_click": true}),
|
||||
)),
|
||||
0x0e => Ok((
|
||||
"mouse".to_string(),
|
||||
json!({
|
||||
"fn": decode_fn_mouse(*fn_value.first().ok_or_else(|| "Turbo mouse mapping payload too short".to_string())?)?,
|
||||
"turbo": u16::from_be_bytes([
|
||||
*fn_value.get(1).ok_or_else(|| "Turbo mouse mapping payload too short".to_string())?,
|
||||
*fn_value.get(2).ok_or_else(|| "Turbo mouse mapping payload too short".to_string())?,
|
||||
])
|
||||
}),
|
||||
)),
|
||||
0x02 => Ok((
|
||||
"keyboard".to_string(),
|
||||
json!({
|
||||
"modifier": decode_modifiers(*fn_value.first().ok_or_else(|| "Keyboard mapping payload too short".to_string())?),
|
||||
"key": *fn_value.get(1).ok_or_else(|| "Keyboard mapping payload too short".to_string())?
|
||||
}),
|
||||
)),
|
||||
0x0d => Ok((
|
||||
"keyboard".to_string(),
|
||||
json!({
|
||||
"modifier": decode_modifiers(*fn_value.first().ok_or_else(|| "Turbo keyboard mapping payload too short".to_string())?),
|
||||
"key": *fn_value.get(1).ok_or_else(|| "Turbo keyboard mapping payload too short".to_string())?,
|
||||
"turbo": u16::from_be_bytes([
|
||||
*fn_value.get(2).ok_or_else(|| "Turbo keyboard mapping payload too short".to_string())?,
|
||||
*fn_value.get(3).ok_or_else(|| "Turbo keyboard mapping payload too short".to_string())?,
|
||||
])
|
||||
}),
|
||||
)),
|
||||
0x03 | 0x04 | 0x05 | 0x0f => Ok((
|
||||
"macro".to_string(),
|
||||
json!({
|
||||
"mode": decode_macro_mode(fn_class)?,
|
||||
"macro_id": u16::from_be_bytes([
|
||||
*fn_value.first().ok_or_else(|| "Macro mapping payload too short".to_string())?,
|
||||
*fn_value.get(1).ok_or_else(|| "Macro mapping payload too short".to_string())?,
|
||||
]),
|
||||
"times": if fn_class == 0x03 { fn_value.get(2).copied().unwrap_or(1) } else { 1 }
|
||||
}),
|
||||
)),
|
||||
0x06 => decode_dpi_switch(fn_value).map(|payload| ("dpi_switch".to_string(), payload)),
|
||||
0x07 => decode_profile_switch(fn_value).map(|payload| ("profile_switch".to_string(), payload)),
|
||||
0x09 => Ok((
|
||||
"system".to_string(),
|
||||
json!({"fn": decode_system_flags(*fn_value.first().ok_or_else(|| "System mapping payload too short".to_string())?)}),
|
||||
)),
|
||||
0x0a => Ok((
|
||||
"consumer".to_string(),
|
||||
json!({"fn": u16::from_be_bytes([
|
||||
*fn_value.first().ok_or_else(|| "Consumer mapping payload too short".to_string())?,
|
||||
*fn_value.get(1).ok_or_else(|| "Consumer mapping payload too short".to_string())?,
|
||||
])}),
|
||||
)),
|
||||
0x0c => Ok((
|
||||
"hypershift_toggle".to_string(),
|
||||
json!({"fn": *fn_value.first().ok_or_else(|| "Hypershift toggle payload too short".to_string())?}),
|
||||
)),
|
||||
0x12 => Ok((
|
||||
"scroll_mode_toggle".to_string(),
|
||||
json!({"fn": *fn_value.first().ok_or_else(|| "Scroll mode toggle payload too short".to_string())?}),
|
||||
)),
|
||||
_ => Ok(("custom".to_string(), json!({"fn_class": fn_class, "fn_value": fn_value}))),
|
||||
};
|
||||
let (category, payload) = decoded.unwrap_or_else(|_| {
|
||||
(
|
||||
"custom".to_string(),
|
||||
json!({"fn_class": fn_class, "fn_value": fn_value}),
|
||||
)
|
||||
});
|
||||
|
||||
Ok(ButtonMappingState {
|
||||
profile,
|
||||
button,
|
||||
hypershift,
|
||||
category,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_button_mapping(mapping: &ButtonMappingState) -> Result<[u8; 7], String> {
|
||||
let mut out = [0u8; 7];
|
||||
let payload = mapping
|
||||
.payload
|
||||
.as_object()
|
||||
.ok_or_else(|| "Button mapping payload must be an object".to_string())?;
|
||||
|
||||
let (fn_class, fn_value) = match mapping.category.as_str() {
|
||||
"disabled" => (0x00, vec![]),
|
||||
"mouse" => {
|
||||
let fn_button = fn_mouse_value(get_string(payload, "fn")?)?;
|
||||
if payload.get("turbo").is_some() && !payload["turbo"].is_null() {
|
||||
let turbo = get_u16(payload, "turbo")?;
|
||||
(0x0e, vec![fn_button, (turbo >> 8) as u8, turbo as u8])
|
||||
} else if payload
|
||||
.get("double_click")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
(0x0b, vec![fn_button])
|
||||
} else {
|
||||
(0x01, vec![fn_button])
|
||||
}
|
||||
}
|
||||
"keyboard" => {
|
||||
let modifier = encode_modifiers(payload.get("modifier"))?;
|
||||
let key = get_u8(payload, "key")?;
|
||||
if payload.get("turbo").is_some() && !payload["turbo"].is_null() {
|
||||
let turbo = get_u16(payload, "turbo")?;
|
||||
(0x0d, vec![modifier, key, (turbo >> 8) as u8, turbo as u8])
|
||||
} else {
|
||||
(0x02, vec![modifier, key])
|
||||
}
|
||||
}
|
||||
"macro" => {
|
||||
let fn_class = encode_macro_mode(get_string(payload, "mode")?)?;
|
||||
let macro_id = get_u16(payload, "macro_id")?;
|
||||
let mut value = vec![(macro_id >> 8) as u8, macro_id as u8];
|
||||
if fn_class == 0x03 {
|
||||
value.push(get_u8(payload, "times").unwrap_or(1));
|
||||
}
|
||||
(fn_class, value)
|
||||
}
|
||||
"dpi_switch" => encode_dpi_switch(payload)?,
|
||||
"profile_switch" => encode_profile_switch(payload)?,
|
||||
"system" => (0x09, vec![encode_system_flags(payload.get("fn"))?]),
|
||||
"consumer" => {
|
||||
let code = get_u16(payload, "fn")?;
|
||||
(0x0a, vec![(code >> 8) as u8, code as u8])
|
||||
}
|
||||
"hypershift_toggle" => (0x0c, vec![get_u8(payload, "fn").unwrap_or(1)]),
|
||||
"scroll_mode_toggle" => (0x12, vec![get_u8(payload, "fn").unwrap_or(1)]),
|
||||
"custom" => {
|
||||
let fn_class = get_u8(payload, "fn_class")?;
|
||||
let fn_value = payload
|
||||
.get("fn_value")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| "custom.fn_value must be an array".to_string())?
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.filter(|value| *value <= 255)
|
||||
.map(|value| value as u8)
|
||||
.ok_or_else(|| "custom.fn_value must contain bytes".to_string())
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
(fn_class, fn_value)
|
||||
}
|
||||
other => return Err(format!("Unsupported button mapping category '{other}'")),
|
||||
};
|
||||
|
||||
if fn_value.len() > 5 {
|
||||
return Err("Button mapping payload is too large".to_string());
|
||||
}
|
||||
out[0] = fn_class;
|
||||
out[1] = fn_value.len() as u8;
|
||||
out[2..2 + fn_value.len()].copy_from_slice(&fn_value);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn decode_fn_mouse(value: u8) -> Result<&'static str, String> {
|
||||
match value {
|
||||
0x01 => Ok("left"),
|
||||
0x02 => Ok("right"),
|
||||
0x03 => Ok("middle"),
|
||||
0x04 => Ok("backward"),
|
||||
0x05 => Ok("forward"),
|
||||
0x09 => Ok("wheel_up"),
|
||||
0x0a => Ok("wheel_down"),
|
||||
0x68 => Ok("wheel_left"),
|
||||
0x69 => Ok("wheel_right"),
|
||||
_ => Err(format!("Unknown mouse function value {value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_modifiers(value: u8) -> Vec<&'static str> {
|
||||
let mut out = Vec::new();
|
||||
for (flag, name) in [
|
||||
(0x01, "left_control"),
|
||||
(0x02, "left_shift"),
|
||||
(0x04, "left_alt"),
|
||||
(0x08, "left_gui"),
|
||||
(0x10, "right_control"),
|
||||
(0x20, "right_shift"),
|
||||
(0x40, "right_alt"),
|
||||
(0x80, "right_gui"),
|
||||
] {
|
||||
if value & flag != 0 {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_modifiers(value: Option<&Value>) -> Result<u8, String> {
|
||||
let mut out = 0u8;
|
||||
let Some(values) = value else { return Ok(out) };
|
||||
let Some(values) = values.as_array() else { return Err("modifier must be an array".to_string()) };
|
||||
for item in values {
|
||||
out |= match item.as_str().ok_or_else(|| "modifier entries must be strings".to_string())? {
|
||||
"left_control" => 0x01,
|
||||
"left_shift" => 0x02,
|
||||
"left_alt" => 0x04,
|
||||
"left_gui" => 0x08,
|
||||
"right_control" => 0x10,
|
||||
"right_shift" => 0x20,
|
||||
"right_alt" => 0x40,
|
||||
"right_gui" => 0x80,
|
||||
other => return Err(format!("Unknown modifier '{other}'")),
|
||||
};
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn decode_macro_mode(value: u8) -> Result<&'static str, String> {
|
||||
match value {
|
||||
0x03 => Ok("macro_fixed"),
|
||||
0x04 => Ok("macro_hold"),
|
||||
0x05 => Ok("macro_toggle"),
|
||||
0x0f => Ok("macro_sequence"),
|
||||
_ => Err(format!("Unknown macro mode {value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_macro_mode(value: &str) -> Result<u8, String> {
|
||||
match value {
|
||||
"macro_fixed" => Ok(0x03),
|
||||
"macro_hold" => Ok(0x04),
|
||||
"macro_toggle" => Ok(0x05),
|
||||
"macro_sequence" => Ok(0x0f),
|
||||
_ => Err(format!("Unknown macro mode '{value}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_dpi_switch(value: &[u8]) -> Result<Value, String> {
|
||||
Ok(match value.len() {
|
||||
1 => json!({ "fn": decode_dpi_switch_op(value[0])? }),
|
||||
2 => json!({ "fn": decode_dpi_switch_op(value[0])?, "stage": value[1] }),
|
||||
5 => json!({ "fn": decode_dpi_switch_op(value[0])?, "dpi": [u16::from_be_bytes([value[1], value[2]]), u16::from_be_bytes([value[3], value[4]])] }),
|
||||
_ => return Err("Unsupported DPI switch payload".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_dpi_switch(payload: &serde_json::Map<String, Value>) -> Result<(u8, Vec<u8>), String> {
|
||||
let op = get_string(payload, "fn")?;
|
||||
let op_value = encode_dpi_switch_op(op)?;
|
||||
Ok(match op {
|
||||
"fixed" => (0x06, vec![op_value, get_u8(payload, "stage")?]),
|
||||
"aim" => {
|
||||
let dpi = payload
|
||||
.get("dpi")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| "dpi_switch.dpi must be an array".to_string())?;
|
||||
let x = dpi.first().and_then(Value::as_u64).ok_or_else(|| "dpi_switch.dpi[0] missing".to_string())? as u16;
|
||||
let y = dpi.get(1).and_then(Value::as_u64).ok_or_else(|| "dpi_switch.dpi[1] missing".to_string())? as u16;
|
||||
(0x06, vec![op_value, (x >> 8) as u8, x as u8, (y >> 8) as u8, y as u8])
|
||||
}
|
||||
_ => (0x06, vec![op_value]),
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_dpi_switch_op(value: u8) -> Result<&'static str, String> {
|
||||
match value {
|
||||
0x01 => Ok("next"),
|
||||
0x02 => Ok("prev"),
|
||||
0x03 => Ok("fixed"),
|
||||
0x05 => Ok("aim"),
|
||||
0x06 => Ok("next_loop"),
|
||||
0x07 => Ok("prev_loop"),
|
||||
_ => Err(format!("Unknown DPI switch op {value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_dpi_switch_op(value: &str) -> Result<u8, String> {
|
||||
match value {
|
||||
"next" => Ok(0x01),
|
||||
"prev" => Ok(0x02),
|
||||
"fixed" => Ok(0x03),
|
||||
"aim" => Ok(0x05),
|
||||
"next_loop" => Ok(0x06),
|
||||
"prev_loop" => Ok(0x07),
|
||||
_ => Err(format!("Unknown DPI switch op '{value}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_profile_switch(value: &[u8]) -> Result<Value, String> {
|
||||
Ok(match value.len() {
|
||||
1 => json!({ "fn": decode_profile_switch_op(value[0])? }),
|
||||
2 => json!({ "fn": decode_profile_switch_op(value[0])?, "profile": profile_name(value[1])? }),
|
||||
_ => return Err("Unsupported profile switch payload".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_profile_switch(payload: &serde_json::Map<String, Value>) -> Result<(u8, Vec<u8>), String> {
|
||||
let op = get_string(payload, "fn")?;
|
||||
let op_value = encode_profile_switch_op(op)?;
|
||||
Ok(if op == "fixed" {
|
||||
(0x07, vec![op_value, profile_value(get_string(payload, "profile")?)?])
|
||||
} else {
|
||||
(0x07, vec![op_value])
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_profile_switch_op(value: u8) -> Result<&'static str, String> {
|
||||
match value {
|
||||
0x01 => Ok("next"),
|
||||
0x02 => Ok("prev"),
|
||||
0x03 => Ok("fixed"),
|
||||
0x04 => Ok("next_loop"),
|
||||
0x05 => Ok("prev_loop"),
|
||||
_ => Err(format!("Unknown profile switch op {value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_profile_switch_op(value: &str) -> Result<u8, String> {
|
||||
match value {
|
||||
"next" => Ok(0x01),
|
||||
"prev" => Ok(0x02),
|
||||
"fixed" => Ok(0x03),
|
||||
"next_loop" => Ok(0x04),
|
||||
"prev_loop" => Ok(0x05),
|
||||
_ => Err(format!("Unknown profile switch op '{value}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_system_flags(value: u8) -> Vec<&'static str> {
|
||||
let mut out = Vec::new();
|
||||
for (flag, name) in [(0x01, "power_down"), (0x02, "sleep"), (0x04, "wake_up")] {
|
||||
if value & flag != 0 {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_system_flags(value: Option<&Value>) -> Result<u8, String> {
|
||||
let Some(values) = value else { return Ok(0) };
|
||||
let Some(values) = values.as_array() else { return Err("system.fn must be an array".to_string()) };
|
||||
let mut out = 0u8;
|
||||
for item in values {
|
||||
out |= match item.as_str().ok_or_else(|| "system.fn entries must be strings".to_string())? {
|
||||
"power_down" => 0x01,
|
||||
"sleep" => 0x02,
|
||||
"wake_up" => 0x04,
|
||||
other => return Err(format!("Unknown system function '{other}'")),
|
||||
};
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn get_string<'a>(payload: &'a serde_json::Map<String, Value>, key: &str) -> Result<&'a str, String> {
|
||||
payload
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("Missing string field '{key}'"))
|
||||
}
|
||||
|
||||
fn get_u8(payload: &serde_json::Map<String, Value>, key: &str) -> Result<u8, String> {
|
||||
payload
|
||||
.get(key)
|
||||
.and_then(Value::as_u64)
|
||||
.filter(|value| *value <= 255)
|
||||
.map(|value| value as u8)
|
||||
.ok_or_else(|| format!("Missing byte field '{key}'"))
|
||||
}
|
||||
|
||||
fn get_u16(payload: &serde_json::Map<String, Value>, key: &str) -> Result<u16, String> {
|
||||
payload
|
||||
.get(key)
|
||||
.and_then(Value::as_u64)
|
||||
.filter(|value| *value <= 65535)
|
||||
.map(|value| value as u16)
|
||||
.ok_or_else(|| format!("Missing u16 field '{key}'"))
|
||||
}
|
||||
440
src-tauri/src/backend/commands.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
#[tauri::command]
|
||||
fn list_supported_devices() -> Result<Vec<DeviceSummary>, String> {
|
||||
discover_devices().map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn connect_device(path: String, state: tauri::State<'_, AppState>) -> Result<DeviceState, String> {
|
||||
let discovered = discover_devices().map_err(|err| err.to_string())?;
|
||||
let selected = discovered
|
||||
.iter()
|
||||
.find(|device| device.path == path)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("No supported Razer device found at {path}"))?;
|
||||
let mut candidates = connection_candidates(&discovered, &selected);
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for summary in candidates.drain(..) {
|
||||
match try_connect_summary(summary.clone(), state.logs.clone()) {
|
||||
Ok((device, snapshot)) => {
|
||||
*state
|
||||
.connected
|
||||
.lock()
|
||||
.map_err(|_| "Device state lock was poisoned".to_string())? = Some(device);
|
||||
return Ok(snapshot);
|
||||
}
|
||||
Err(error) => failures.push(format!("{}: {error}", summary.path)),
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Could not talk to the selected Razer device on any matching hidraw node. Tried: {}",
|
||||
failures.join("; ")
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn refresh_device_state(
|
||||
profile: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
with_device(&state, |device| device.snapshot(&profile))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_scroll_mode(
|
||||
profile: String,
|
||||
mode: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_scroll_mode(profile_value(&profile)?, scroll_mode_value(&mode)?)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_scroll_acceleration(
|
||||
profile: String,
|
||||
enabled: bool,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_bool_command(0x0216, profile_value(&profile)?, enabled)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_scroll_smart_reel(
|
||||
profile: String,
|
||||
enabled: bool,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_bool_command(0x0217, profile_value(&profile)?, enabled)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_polling_rate(
|
||||
profile: String,
|
||||
polling_rate_ms: u8,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
if polling_rate_ms == 0 {
|
||||
return Err("Polling rate delay must be at least 1 ms".to_string());
|
||||
}
|
||||
|
||||
with_device(&state, |device| {
|
||||
device.sr_with(0x000e, &[profile_value(&profile)?, polling_rate_ms], 0)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_dpi_xy(
|
||||
profile: String,
|
||||
x: u16,
|
||||
y: u16,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
if !(100..=25600).contains(&x) || !(100..=25600).contains(&y) {
|
||||
return Err("DPI must be between 100 and 25600".to_string());
|
||||
}
|
||||
|
||||
with_device(&state, |device| {
|
||||
let mut args = Vec::with_capacity(7);
|
||||
args.push(profile_value(&profile)?);
|
||||
args.extend_from_slice(&x.to_be_bytes());
|
||||
args.extend_from_slice(&y.to_be_bytes());
|
||||
args.extend_from_slice(&[0, 0]);
|
||||
device.sr_with(0x0405, &args, 0)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_dpi_stages(
|
||||
profile: String,
|
||||
dpi_stages: Vec<[u16; 2]>,
|
||||
active_stage: u8,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
if dpi_stages.is_empty() || dpi_stages.len() > 5 {
|
||||
return Err("DPI stage count must be between 1 and 5".to_string());
|
||||
}
|
||||
if active_stage == 0 || usize::from(active_stage) > dpi_stages.len() {
|
||||
return Err("Active DPI stage must point to an existing stage".to_string());
|
||||
}
|
||||
if dpi_stages
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|dpi| !(100..=25600).contains(dpi))
|
||||
{
|
||||
return Err("All DPI stages must be between 100 and 25600".to_string());
|
||||
}
|
||||
|
||||
with_device(&state, |device| {
|
||||
let mut args = Vec::with_capacity(38);
|
||||
args.push(profile_value(&profile)?);
|
||||
args.push(active_stage);
|
||||
args.push(dpi_stages.len() as u8);
|
||||
for (index, [x, y]) in dpi_stages.iter().copied().enumerate() {
|
||||
args.push(index as u8);
|
||||
args.extend_from_slice(&x.to_be_bytes());
|
||||
args.extend_from_slice(&y.to_be_bytes());
|
||||
args.extend_from_slice(&[0, 0]);
|
||||
}
|
||||
while args.len() < 38 {
|
||||
args.extend_from_slice(&[0; 7]);
|
||||
}
|
||||
device.sr_with(0x0406, &args, 0)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_profile(
|
||||
profile: String,
|
||||
current_profile: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
let profile = profile_value(&profile)?;
|
||||
with_device(&state, |device| {
|
||||
device.sr_with(0x0502, &[profile], 0)?;
|
||||
device.snapshot(¤t_profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_profile(
|
||||
profile: String,
|
||||
next_profile: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
let profile = profile_value(&profile)?;
|
||||
with_device(&state, |device| {
|
||||
device.sr_with(0x0503, &[profile], 0)?;
|
||||
device.snapshot(&next_profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_led_state(profile: String, state: tauri::State<'_, AppState>) -> Result<LedState, String> {
|
||||
with_device(&state, |device| device.led_snapshot(&profile))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_led_effect(
|
||||
profile: String,
|
||||
region: String,
|
||||
effect: String,
|
||||
mode: u8,
|
||||
speed: u8,
|
||||
colors: Vec<[u8; 3]>,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<LedState, String> {
|
||||
with_device(&state, |device| {
|
||||
let profile = profile_value(&profile)?;
|
||||
let region = led_region_value(®ion)?;
|
||||
let effect = led_effect_value(&effect)?;
|
||||
let color_count = colors.len();
|
||||
if color_count > 2 {
|
||||
return Err("At most 2 LED colors are supported in this port".to_string());
|
||||
}
|
||||
let mut args = Vec::with_capacity(6 + color_count * 3);
|
||||
args.push(profile);
|
||||
args.push(region);
|
||||
args.push(effect);
|
||||
args.push(mode);
|
||||
args.push(speed);
|
||||
args.push(color_count as u8);
|
||||
for color in colors {
|
||||
args.extend_from_slice(&color);
|
||||
}
|
||||
device.sr_with(0x0f02, &args, 0)?;
|
||||
device.led_snapshot(&profile_name(profile)?.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_led_brightness(
|
||||
profile: String,
|
||||
region: String,
|
||||
brightness: u8,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<LedState, String> {
|
||||
with_device(&state, |device| {
|
||||
let profile = profile_value(&profile)?;
|
||||
let region = led_region_value(®ion)?;
|
||||
device.sr_with(0x0f04, &[profile, region, brightness], 0)?;
|
||||
device.led_snapshot(&profile_name(profile)?.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn apply_led_to_all_regions(
|
||||
profile: String,
|
||||
region_state: LedRegionState,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<LedState, String> {
|
||||
with_device(&state, |device| {
|
||||
let profile_value = profile_value(&profile)?;
|
||||
let effect_value = led_effect_value(®ion_state.effect)?;
|
||||
for region in ["wheel", "logo", "strip"] {
|
||||
let region_value = led_region_value(region)?;
|
||||
let mut effect_args = Vec::with_capacity(6 + region_state.colors.len() * 3);
|
||||
effect_args.push(profile_value);
|
||||
effect_args.push(region_value);
|
||||
effect_args.push(effect_value);
|
||||
effect_args.push(region_state.mode);
|
||||
effect_args.push(region_state.speed);
|
||||
effect_args.push(region_state.colors.len() as u8);
|
||||
for color in ®ion_state.colors {
|
||||
effect_args.extend_from_slice(color);
|
||||
}
|
||||
device.sr_with(0x0f02, &effect_args, 0)?;
|
||||
device.sr_with(0x0f04, &[profile_value, region_value, region_state.brightness], 0)?;
|
||||
}
|
||||
device.led_snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_button_mapping(
|
||||
profile: String,
|
||||
button: String,
|
||||
hypershift: bool,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ButtonMappingState, String> {
|
||||
with_device(&state, |device| device.get_button_mapping_state(&profile, &button, hypershift))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_button_mapping(
|
||||
mapping: ButtonMappingState,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ButtonMappingState, String> {
|
||||
with_device(&state, |device| device.set_button_mapping_state(mapping))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_profile_config(
|
||||
profile: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ProfileConfigBundle, String> {
|
||||
with_device(&state, |device| device.export_profile_config(&profile))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn import_profile_config(
|
||||
profile: String,
|
||||
bundle: ProfileConfigBundle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<DeviceState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.import_profile_config(&profile, bundle)?;
|
||||
device.snapshot(&profile)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_macros(state: tauri::State<'_, AppState>) -> Result<Vec<u16>, String> {
|
||||
with_device(&state, |device| device.get_macro_list())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_macro_definition(
|
||||
macro_id: u16,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<MacroDefinition, String> {
|
||||
with_device(&state, |device| device.get_macro_definition(macro_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_macro_definition(
|
||||
macro_id: u16,
|
||||
definition: MacroDefinition,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<Vec<u16>, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_macro_definition(macro_id, definition)?;
|
||||
device.get_macro_list()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_macro(macro_id: u16, state: tauri::State<'_, AppState>) -> Result<Vec<u16>, String> {
|
||||
with_device(&state, |device| {
|
||||
device.delete_macro(macro_id)?;
|
||||
device.get_macro_list()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn reset_macro_flash(state: tauri::State<'_, AppState>) -> Result<Vec<u16>, String> {
|
||||
with_device(&state, |device| {
|
||||
device.reset_macro_flash()?;
|
||||
device.get_macro_list()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_debug_logs(state: tauri::State<'_, AppState>) -> Result<Vec<DebugLogEntry>, String> {
|
||||
state
|
||||
.logs
|
||||
.lock()
|
||||
.map(|logs| logs.clone())
|
||||
.map_err(|_| "Debug log lock was poisoned".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn clear_debug_logs(state: tauri::State<'_, AppState>) -> Result<(), String> {
|
||||
state
|
||||
.logs
|
||||
.lock()
|
||||
.map(|mut logs| logs.clear())
|
||||
.map_err(|_| "Debug log lock was poisoned".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_sensor_state(state: tauri::State<'_, AppState>) -> Result<SensorState, String> {
|
||||
with_device(&state, |device| device.sensor_snapshot())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_sensor_lift_mode(
|
||||
lift_mode: String,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<SensorState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_sensor_lift_mode(&lift_mode)?;
|
||||
device.sensor_snapshot()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_sensor_calibration(state: tauri::State<'_, AppState>) -> Result<SensorState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.start_sensor_calibration()?;
|
||||
device.sensor_snapshot()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_sensor_calibration(state: tauri::State<'_, AppState>) -> Result<SensorState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.stop_sensor_calibration()?;
|
||||
device.sensor_snapshot()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_sensor_params(
|
||||
param_a: Vec<u8>,
|
||||
param_b: Vec<u8>,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<SensorState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.set_sensor_params(¶m_a, ¶m_b)?;
|
||||
device.sensor_snapshot()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_info_state(state: tauri::State<'_, AppState>) -> Result<InfoState, String> {
|
||||
with_device(&state, |device| device.info_snapshot())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn send_raw_command(
|
||||
command: u16,
|
||||
args: Vec<u8>,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
with_device(&state, |device| Ok(device.sr_with(command, &args, 0)?.to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn reset_flash(state: tauri::State<'_, AppState>) -> Result<InfoState, String> {
|
||||
with_device(&state, |device| {
|
||||
device.reset_macro_flash()?;
|
||||
device.info_snapshot()
|
||||
})
|
||||
}
|
||||
|
||||
fn with_device<T>(
|
||||
state: &tauri::State<'_, AppState>,
|
||||
f: impl FnOnce(&mut ConnectedDevice) -> Result<T, String>,
|
||||
) -> Result<T, String> {
|
||||
let mut guard = state
|
||||
.connected
|
||||
.lock()
|
||||
.map_err(|_| "Device state lock was poisoned".to_string())?;
|
||||
let device = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| "No device is connected".to_string())?;
|
||||
f(device)
|
||||
}
|
||||
4
src-tauri/src/backend/device.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
include!("device_parts/core.rs");
|
||||
include!("device_parts/profile_led_button.rs");
|
||||
include!("device_parts/macro.rs");
|
||||
include!("device_parts/sensor_io.rs");
|
||||
196
src-tauri/src/backend/device_parts/core.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
impl ConnectedDevice {
|
||||
fn snapshot(&mut self, profile: &str) -> Result<DeviceState, String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let serial = self.get_serial()?;
|
||||
let firmware = self.get_firmware_version()?;
|
||||
let profiles = self
|
||||
.get_profile_list()
|
||||
.unwrap_or_else(|_| vec!["direct".to_string()]);
|
||||
let basic = BasicSettings {
|
||||
profile: profile.to_string(),
|
||||
scroll_mode: self.get_scroll_mode(profile_value)?,
|
||||
scroll_acceleration: self.get_bool_command(0x0296, profile_value)?,
|
||||
scroll_smart_reel: self.get_bool_command(0x0297, profile_value)?,
|
||||
polling_rate_ms: self.get_polling_rate(profile_value)?,
|
||||
dpi_xy: self.get_dpi_xy(profile_value)?,
|
||||
dpi_stages: self.get_dpi_stages(profile_value)?.0,
|
||||
active_dpi_stage: self.get_dpi_stages(profile_value)?.1,
|
||||
};
|
||||
|
||||
Ok(DeviceState {
|
||||
device: self.summary.clone(),
|
||||
serial,
|
||||
firmware,
|
||||
profiles,
|
||||
basic,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_serial(&mut self) -> Result<String, String> {
|
||||
let response = self.sr_with(0x0082, &[0; 16], 0)?;
|
||||
let serial = response[..16]
|
||||
.split(|byte| *byte == 0)
|
||||
.next()
|
||||
.unwrap_or_default();
|
||||
Ok(String::from_utf8_lossy(serial).to_string())
|
||||
}
|
||||
|
||||
fn get_firmware_version(&mut self) -> Result<String, String> {
|
||||
let response = self.sr_with(0x0081, &[0; 4], 0)?;
|
||||
Ok(format!(
|
||||
"{}.{}.{}.{}",
|
||||
response[0], response[1], response[2], response[3]
|
||||
))
|
||||
}
|
||||
|
||||
fn get_profile_list(&mut self) -> Result<Vec<String>, String> {
|
||||
let count = self.sr_with(0x0580, &[0], 0)?[0] as usize;
|
||||
let mut args = vec![0; count + 1];
|
||||
let response = self.sr_with(0x0581, &args, 0)?;
|
||||
args.clear();
|
||||
|
||||
let mut profiles = vec!["direct".to_string()];
|
||||
for profile in response.iter().skip(1).take(count) {
|
||||
if let Ok(name) = profile_name(*profile) {
|
||||
if !profiles.iter().any(|existing| existing == name) {
|
||||
profiles.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
fn get_scroll_mode(&mut self, profile: u8) -> Result<String, String> {
|
||||
let response = self.sr_with(0x0294, &[profile, 0], 0)?;
|
||||
match response[1] {
|
||||
0 => Ok("tactile".to_string()),
|
||||
1 => Ok("freespin".to_string()),
|
||||
value => Err(format!("Unknown scroll mode value {value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_scroll_mode(&mut self, profile: u8, mode: u8) -> Result<(), String> {
|
||||
self.sr_with(0x0214, &[profile, mode], 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn get_bool_command(&mut self, command: u16, profile: u8) -> Result<bool, String> {
|
||||
Ok(self.sr_with(command, &[profile, 0], 0)?[1] != 0)
|
||||
}
|
||||
|
||||
fn set_bool_command(&mut self, command: u16, profile: u8, enabled: bool) -> Result<(), String> {
|
||||
self.sr_with(command, &[profile, u8::from(enabled)], 0)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn get_polling_rate(&mut self, profile: u8) -> Result<u8, String> {
|
||||
Ok(self.sr_with(0x008e, &[profile, 0], 0)?[1])
|
||||
}
|
||||
|
||||
fn get_dpi_xy(&mut self, profile: u8) -> Result<[u16; 2], String> {
|
||||
let response = self.sr_with(0x0485, &[profile, 0, 0, 0, 0, 0, 0], 0)?;
|
||||
Ok([
|
||||
u16::from_be_bytes([response[1], response[2]]),
|
||||
u16::from_be_bytes([response[3], response[4]]),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_dpi_stages(&mut self, profile: u8) -> Result<(Vec<[u16; 2]>, u8), String> {
|
||||
let mut args = vec![0; 38];
|
||||
args[0] = profile;
|
||||
let response = self.sr_with(0x0486, &args, 0)?;
|
||||
let active_stage = response[1];
|
||||
let stage_count = response[2].min(5) as usize;
|
||||
let stages = (0..stage_count)
|
||||
.map(|index| {
|
||||
let offset = 3 + (index * 7);
|
||||
[
|
||||
u16::from_be_bytes([response[offset + 1], response[offset + 2]]),
|
||||
u16::from_be_bytes([response[offset + 3], response[offset + 4]]),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
Ok((stages, active_stage))
|
||||
}
|
||||
|
||||
fn sr_with(&mut self, command: u16, args: &[u8], wait_power: u8) -> Result<[u8; 80], String> {
|
||||
if args.len() > 80 {
|
||||
return Err("Razer reports support at most 80 argument bytes".to_string());
|
||||
}
|
||||
|
||||
let mut report = [0u8; 90];
|
||||
report[0] = 0;
|
||||
report[1] = 0x1f;
|
||||
report[4] = 0;
|
||||
report[5] = args.len() as u8;
|
||||
report[6] = (command >> 8) as u8;
|
||||
report[7] = command as u8;
|
||||
report[8..8 + args.len()].copy_from_slice(args);
|
||||
report[88] = report[2..88].iter().fold(0, |crc, byte| crc ^ byte);
|
||||
self.append_log(
|
||||
"tx",
|
||||
format!("cmd=0x{command:04x} wait_power={wait_power} report={}", bytes_to_hex_compact(&report)),
|
||||
);
|
||||
|
||||
if let Err(error) = self.send_feature_report(&report) {
|
||||
self.append_log("error", format!("send cmd=0x{command:04x} failed: {error}"));
|
||||
return Err(error);
|
||||
}
|
||||
for attempt in 0..(15 * (usize::from(wait_power) + 1)) {
|
||||
sleep(Duration::from_millis(10 * (attempt as u64 + 1)));
|
||||
let response = match self.get_feature_report() {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
self.append_log("error", format!("recv cmd=0x{command:04x} failed: {error}"));
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
self.append_log(
|
||||
"rx",
|
||||
format!("cmd=0x{command:04x} attempt={} report={}", attempt + 1, bytes_to_hex_compact(&response)),
|
||||
);
|
||||
if response[6] != report[6] || response[7] != report[7] {
|
||||
self.append_log(
|
||||
"warn",
|
||||
format!(
|
||||
"cmd=0x{command:04x} got different reply command=0x{:02x}{:02x}",
|
||||
response[6], response[7]
|
||||
),
|
||||
);
|
||||
return Err(
|
||||
"Device replied to a different command; close other software using it"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
match response[0] {
|
||||
2 => {
|
||||
let mut arguments = [0u8; 80];
|
||||
arguments.copy_from_slice(&response[8..88]);
|
||||
self.append_log("info", format!("cmd=0x{command:04x} completed"));
|
||||
return Ok(arguments);
|
||||
}
|
||||
1 => continue,
|
||||
3 => {
|
||||
self.append_log("error", format!("cmd=0x{command:04x} failure status"));
|
||||
return Err("Device reported command failure".to_string());
|
||||
}
|
||||
4 => {
|
||||
self.append_log("error", format!("cmd=0x{command:04x} timeout status"));
|
||||
return Err("Device reported command timeout".to_string());
|
||||
}
|
||||
5 => {
|
||||
self.append_log("warn", format!("cmd=0x{command:04x} unsupported"));
|
||||
return Err("Device does not support this command".to_string());
|
||||
}
|
||||
status => {
|
||||
self.append_log("error", format!("cmd=0x{command:04x} unexpected status {status}"));
|
||||
return Err(format!("Device returned unexpected status {status}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.append_log("error", format!("cmd=0x{command:04x} timed out waiting for response"));
|
||||
Err("Timed out waiting for device response".to_string())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
157
src-tauri/src/backend/device_parts/macro.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
impl ConnectedDevice {
|
||||
fn get_macro_count(&mut self) -> Result<u16, String> {
|
||||
Ok(u16::from_be_bytes(self.sr_with(0x0680, &[0, 0], 0)?[..2].try_into().unwrap()))
|
||||
}
|
||||
|
||||
fn get_macro_list(&mut self) -> Result<Vec<u16>, String> {
|
||||
let mut macros = Vec::new();
|
||||
let total = usize::from(self.get_macro_count()?);
|
||||
while macros.len() < total {
|
||||
let offset = u16::try_from(macros.len()).map_err(|_| "Macro list offset overflow".to_string())?;
|
||||
let mut args = Vec::with_capacity(68);
|
||||
args.extend_from_slice(&offset.to_be_bytes());
|
||||
args.extend_from_slice(&0u16.to_be_bytes());
|
||||
args.extend_from_slice(&[0; 64]);
|
||||
let response = self.sr_with(0x068b, &args, 0)?;
|
||||
let size = u16::from_be_bytes([response[0], response[1]]) as usize;
|
||||
let take = size.saturating_sub(macros.len()).min(32);
|
||||
for index in 0..take {
|
||||
let offset = 2 + index * 2;
|
||||
macros.push(u16::from_be_bytes([response[offset], response[offset + 1]]));
|
||||
}
|
||||
if take == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(macros)
|
||||
}
|
||||
|
||||
fn get_macro_info(&mut self, macro_id: u16) -> Result<Vec<u8>, String> {
|
||||
let mut data = Vec::new();
|
||||
loop {
|
||||
let mut args = Vec::with_capacity(70);
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&(data.len() as u16).to_be_bytes());
|
||||
args.extend_from_slice(&0u16.to_be_bytes());
|
||||
args.extend_from_slice(&[0; 64]);
|
||||
let response = self.sr_with(0x068c, &args, 1)?;
|
||||
let total_size = u16::from_be_bytes([response[2], response[3]]) as usize;
|
||||
let chunk = &response[4..68];
|
||||
let remaining = total_size.saturating_sub(data.len());
|
||||
let take = remaining.min(chunk.len());
|
||||
data.extend_from_slice(&chunk[..take]);
|
||||
if data.len() >= total_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn set_macro_info(&mut self, macro_id: u16, data: &[u8]) -> Result<(), String> {
|
||||
let total_size = u16::try_from(data.len()).map_err(|_| "Macro info is too large to write".to_string())?;
|
||||
let mut offset = 0usize;
|
||||
while offset < data.len() {
|
||||
let chunk = &data[offset..(offset + 64).min(data.len())];
|
||||
let mut args = Vec::with_capacity(6 + chunk.len());
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&(offset as u16).to_be_bytes());
|
||||
args.extend_from_slice(&total_size.to_be_bytes());
|
||||
args.extend_from_slice(chunk);
|
||||
self.sr_with(0x060c, &args, 1)?;
|
||||
offset += chunk.len();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_macro_size(&mut self, macro_id: u16) -> Result<u32, String> {
|
||||
let mut args = Vec::with_capacity(6);
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&[0; 4]);
|
||||
let response = self.sr_with(0x0688, &args, 0)?;
|
||||
Ok(u32::from_be_bytes([response[2], response[3], response[4], response[5]]))
|
||||
}
|
||||
|
||||
fn set_macro_size(&mut self, macro_id: u16, size: u32) -> Result<(), String> {
|
||||
let mut args = Vec::with_capacity(6);
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&size.to_be_bytes());
|
||||
self.sr_with(0x0608, &args, 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn get_macro_function(&mut self, macro_id: u16) -> Result<Vec<u8>, String> {
|
||||
let size = self.get_macro_size(macro_id)? as usize;
|
||||
let mut data = Vec::with_capacity(size);
|
||||
while data.len() < size {
|
||||
let offset = u32::try_from(data.len()).map_err(|_| "Macro function offset overflow".to_string())?;
|
||||
let mut args = Vec::with_capacity(71);
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&offset.to_be_bytes());
|
||||
args.push(64);
|
||||
args.extend_from_slice(&[0; 64]);
|
||||
let response = self.sr_with(0x0689, &args, 1)?;
|
||||
let chunk = &response[..64.min(size - data.len())];
|
||||
data.extend_from_slice(chunk);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn set_macro_function_bytes(&mut self, macro_id: u16, data: &[u8]) -> Result<(), String> {
|
||||
let total_size = u32::try_from(data.len()).map_err(|_| "Macro function is too large to write".to_string())?;
|
||||
self.set_macro_size(macro_id, total_size)?;
|
||||
let mut offset = 0usize;
|
||||
while offset < data.len() {
|
||||
let chunk = &data[offset..(offset + 64).min(data.len())];
|
||||
let mut args = Vec::with_capacity(7 + chunk.len());
|
||||
args.extend_from_slice(¯o_id.to_be_bytes());
|
||||
args.extend_from_slice(&(offset as u32).to_be_bytes());
|
||||
args.push(chunk.len() as u8);
|
||||
args.extend_from_slice(chunk);
|
||||
self.sr_with(0x0609, &args, 1)?;
|
||||
offset += chunk.len();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_macro_definition(&mut self, macro_id: u16) -> Result<MacroDefinition, String> {
|
||||
let macro_info = self.get_macro_info(macro_id).ok();
|
||||
let macro_function = self.get_macro_function(macro_id)?;
|
||||
Ok(MacroDefinition {
|
||||
macro_id,
|
||||
macro_info_hex: macro_info.map(|bytes| bytes_to_hex_compact(&bytes)),
|
||||
operations: decode_macro_operations(¯o_function)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_macro_definition(
|
||||
&mut self,
|
||||
macro_id: u16,
|
||||
definition: MacroDefinition,
|
||||
) -> Result<(), String> {
|
||||
let operation_bytes = encode_macro_operations(&definition.operations)?;
|
||||
self.delete_macro(macro_id).ok();
|
||||
self.set_macro_function_bytes(macro_id, &operation_bytes)?;
|
||||
if let Some(info_hex) = definition.macro_info_hex {
|
||||
self.set_macro_info(macro_id, &parse_hex_bytes(&info_hex)?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_macro(&mut self, macro_id: u16) -> Result<(), String> {
|
||||
self.sr_with(0x0603, ¯o_id.to_be_bytes(), 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn reset_macro_flash(&mut self) -> Result<(), String> {
|
||||
let request = [0x00, 0x00, 0x00, 0x02, 0x00, 0x00];
|
||||
self.sr_with(0x060a, &request, 0)?;
|
||||
for _ in 0..20 {
|
||||
let response = self.sr_with(0x068a, &request, 0)?;
|
||||
if response[..6] == [0x00, 0x00, 0x02, 0x02, 0x00, 0x00] {
|
||||
return Ok(());
|
||||
}
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
Err("Resetting macro flash took too long".to_string())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
226
src-tauri/src/backend/device_parts/profile_led_button.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
impl ConnectedDevice {
|
||||
fn led_snapshot(&mut self, profile: &str) -> Result<LedState, String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let mut regions = Vec::new();
|
||||
for (region_name, region_value) in [("wheel", 0x01), ("logo", 0x04), ("strip", 0x0a)] {
|
||||
let (effect, mode, speed, colors) = self.get_led_effect(profile_value, region_value)?;
|
||||
let brightness = self.get_led_brightness(profile_value, region_value)?;
|
||||
regions.push(LedRegionState {
|
||||
region: region_name.to_string(),
|
||||
effect,
|
||||
mode,
|
||||
speed,
|
||||
colors,
|
||||
brightness,
|
||||
});
|
||||
}
|
||||
Ok(LedState {
|
||||
profile: profile.to_string(),
|
||||
regions,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_led_effect(
|
||||
&mut self,
|
||||
profile: u8,
|
||||
region: u8,
|
||||
) -> Result<(String, u8, u8, Vec<[u8; 3]>), String> {
|
||||
let response = self.sr_with(0x0f82, &[profile, region, 0, 0, 0, 0, 0, 0, 0, 0], 0)?;
|
||||
let effect = led_effect_name(response[2])?.to_string();
|
||||
let mode = response[3];
|
||||
let speed = response[4];
|
||||
let color_count = response[5] as usize;
|
||||
let mut colors = Vec::new();
|
||||
for index in 0..color_count.min(2) {
|
||||
let offset = 6 + index * 3;
|
||||
colors.push([response[offset], response[offset + 1], response[offset + 2]]);
|
||||
}
|
||||
Ok((effect, mode, speed, colors))
|
||||
}
|
||||
|
||||
fn get_led_brightness(&mut self, profile: u8, region: u8) -> Result<u8, String> {
|
||||
Ok(self.sr_with(0x0f84, &[profile, region, 0], 0)?[2])
|
||||
}
|
||||
|
||||
fn get_button_mapping_state(
|
||||
&mut self,
|
||||
profile: &str,
|
||||
button: &str,
|
||||
hypershift: bool,
|
||||
) -> Result<ButtonMappingState, String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let button_value = button_value(button)?;
|
||||
let hypershift_value = u8::from(hypershift);
|
||||
let response = self.sr_with(
|
||||
0x028c,
|
||||
&[profile_value, button_value, hypershift_value, 0, 0, 0, 0, 0, 0, 0],
|
||||
0,
|
||||
)?;
|
||||
decode_button_mapping(profile.to_string(), button.to_string(), hypershift, &response[3..10])
|
||||
}
|
||||
|
||||
fn set_button_mapping_state(
|
||||
&mut self,
|
||||
mapping: ButtonMappingState,
|
||||
) -> Result<ButtonMappingState, String> {
|
||||
let profile_value = profile_value(&mapping.profile)?;
|
||||
let button_value = button_value(&mapping.button)?;
|
||||
let hypershift_value = u8::from(mapping.hypershift);
|
||||
let function_bytes = encode_button_mapping(&mapping)?;
|
||||
let mut args = Vec::with_capacity(10);
|
||||
args.extend_from_slice(&[profile_value, button_value, hypershift_value]);
|
||||
args.extend_from_slice(&function_bytes);
|
||||
self.sr_with(0x020c, &args, 0)?;
|
||||
self.get_button_mapping_state(&mapping.profile, &mapping.button, mapping.hypershift)
|
||||
}
|
||||
|
||||
fn get_profile_info(&mut self, profile: &str) -> Result<Vec<u8>, String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let mut data = Vec::new();
|
||||
loop {
|
||||
let mut args = Vec::with_capacity(69);
|
||||
args.push(profile_value);
|
||||
args.extend_from_slice(&(data.len() as u16).to_be_bytes());
|
||||
args.extend_from_slice(&0u16.to_be_bytes());
|
||||
args.extend_from_slice(&[0; 64]);
|
||||
let response = self.sr_with(0x0588, &args, 1)?;
|
||||
let total_size = u16::from_be_bytes([response[1], response[2]]) as usize;
|
||||
let chunk = &response[3..67];
|
||||
let remaining = total_size.saturating_sub(data.len());
|
||||
let take = remaining.min(chunk.len());
|
||||
data.extend_from_slice(&chunk[..take]);
|
||||
if data.len() >= total_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn set_profile_info(&mut self, profile: &str, data: &[u8]) -> Result<(), String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let total_size = u16::try_from(data.len())
|
||||
.map_err(|_| "Profile info is too large to write".to_string())?;
|
||||
let mut offset = 0usize;
|
||||
while offset < data.len() {
|
||||
let chunk = &data[offset..(offset + 64).min(data.len())];
|
||||
let mut args = Vec::with_capacity(5 + chunk.len());
|
||||
args.push(profile_value);
|
||||
args.extend_from_slice(&(offset as u16).to_be_bytes());
|
||||
args.extend_from_slice(&total_size.to_be_bytes());
|
||||
args.extend_from_slice(chunk);
|
||||
self.sr_with(0x0508, &args, 1)?;
|
||||
offset += chunk.len();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export_profile_config(&mut self, profile: &str) -> Result<ProfileConfigBundle, String> {
|
||||
let basic = self.snapshot(profile)?.basic;
|
||||
let button_mappings = all_button_names()
|
||||
.iter()
|
||||
.flat_map(|button| [false, true].into_iter().map(move |hypershift| (*button, hypershift)))
|
||||
.map(|(button, hypershift)| {
|
||||
self.get_button_mapping_state(profile, button, hypershift)
|
||||
.unwrap_or_else(|error| {
|
||||
self.append_log(
|
||||
"warn",
|
||||
format!(
|
||||
"profile export fallback for {button} hypershift={} on profile {profile}: {error}",
|
||||
hypershift
|
||||
),
|
||||
);
|
||||
ButtonMappingState {
|
||||
profile: profile.to_string(),
|
||||
button: button.to_string(),
|
||||
hypershift,
|
||||
category: "custom".to_string(),
|
||||
payload: json!({
|
||||
"fn_class": 0,
|
||||
"fn_value": [],
|
||||
"read_error": error
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let led = self.led_snapshot(profile)?;
|
||||
let profile_info_hex = self
|
||||
.get_profile_info(profile)
|
||||
.ok()
|
||||
.map(|bytes| bytes_to_hex_compact(&bytes));
|
||||
|
||||
Ok(ProfileConfigBundle {
|
||||
basic,
|
||||
button_mappings,
|
||||
led,
|
||||
profile_info_hex,
|
||||
})
|
||||
}
|
||||
|
||||
fn import_profile_config(
|
||||
&mut self,
|
||||
profile: &str,
|
||||
bundle: ProfileConfigBundle,
|
||||
) -> Result<(), String> {
|
||||
let profile_value = profile_value(profile)?;
|
||||
let basic = bundle.basic;
|
||||
|
||||
self.set_scroll_mode(profile_value, scroll_mode_value(&basic.scroll_mode)?)?;
|
||||
self.set_bool_command(0x0216, profile_value, basic.scroll_acceleration)?;
|
||||
self.set_bool_command(0x0217, profile_value, basic.scroll_smart_reel)?;
|
||||
self.sr_with(0x000e, &[profile_value, basic.polling_rate_ms], 0)?;
|
||||
|
||||
let mut dpi_args = Vec::with_capacity(7);
|
||||
dpi_args.push(profile_value);
|
||||
dpi_args.extend_from_slice(&basic.dpi_xy[0].to_be_bytes());
|
||||
dpi_args.extend_from_slice(&basic.dpi_xy[1].to_be_bytes());
|
||||
dpi_args.extend_from_slice(&[0, 0]);
|
||||
self.sr_with(0x0405, &dpi_args, 0)?;
|
||||
|
||||
let mut stage_args = Vec::with_capacity(38);
|
||||
stage_args.push(profile_value);
|
||||
stage_args.push(basic.active_dpi_stage);
|
||||
stage_args.push(basic.dpi_stages.len() as u8);
|
||||
for (index, [x, y]) in basic.dpi_stages.iter().copied().enumerate() {
|
||||
stage_args.push(index as u8);
|
||||
stage_args.extend_from_slice(&x.to_be_bytes());
|
||||
stage_args.extend_from_slice(&y.to_be_bytes());
|
||||
stage_args.extend_from_slice(&[0, 0]);
|
||||
}
|
||||
while stage_args.len() < 38 {
|
||||
stage_args.extend_from_slice(&[0; 7]);
|
||||
}
|
||||
self.sr_with(0x0406, &stage_args, 0)?;
|
||||
|
||||
for mut mapping in bundle.button_mappings {
|
||||
mapping.profile = profile.to_string();
|
||||
self.set_button_mapping_state(mapping)?;
|
||||
}
|
||||
|
||||
for region in bundle.led.regions {
|
||||
let region_value = led_region_value(®ion.region)?;
|
||||
let effect_value = led_effect_value(®ion.effect)?;
|
||||
let mut effect_args = Vec::with_capacity(6 + region.colors.len() * 3);
|
||||
effect_args.push(profile_value);
|
||||
effect_args.push(region_value);
|
||||
effect_args.push(effect_value);
|
||||
effect_args.push(region.mode);
|
||||
effect_args.push(region.speed);
|
||||
effect_args.push(region.colors.len() as u8);
|
||||
for color in ®ion.colors {
|
||||
effect_args.extend_from_slice(color);
|
||||
}
|
||||
self.sr_with(0x0f02, &effect_args, 0)?;
|
||||
self.sr_with(0x0f04, &[profile_value, region_value, region.brightness], 0)?;
|
||||
}
|
||||
|
||||
if let Some(profile_info_hex) = bundle.profile_info_hex {
|
||||
let bytes = parse_hex_bytes(&profile_info_hex)?;
|
||||
self.set_profile_info(profile, &bytes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
187
src-tauri/src/backend/device_parts/sensor_io.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
impl ConnectedDevice {
|
||||
fn get_device_mode(&mut self) -> Result<(u8, u8), String> {
|
||||
let response = self.sr_with(0x0084, &[0, 0], 0)?;
|
||||
Ok((response[0], response[1]))
|
||||
}
|
||||
|
||||
fn set_device_mode(&mut self, mode: &str, param: u8) -> Result<(), String> {
|
||||
self.sr_with(0x0004, &[device_mode_value(mode)?, param], 0)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn get_sensor_state_enabled(&mut self) -> Result<bool, String> {
|
||||
let response = self.sr_with(0x0b83, &[0x00, 0x04, 0x00], 0)?;
|
||||
Ok(response[2] != 0)
|
||||
}
|
||||
|
||||
fn set_sensor_state_enabled(&mut self, enabled: bool) -> Result<(), String> {
|
||||
self.sr_with(0x0b03, &[0x00, 0x04, u8::from(enabled)], 0)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn set_sensor_calibration(&mut self, enabled: bool) -> Result<(), String> {
|
||||
self.sr_with(0x0b09, &[0x00, 0x04, 0x00, u8::from(enabled)], 0)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn get_sensor_lift_mode(&mut self) -> Result<String, String> {
|
||||
let response = self.sr_with(0x0b8b, &[0x00, 0x04, 0x00, 0x00], 0)?;
|
||||
let value = u16::from_be_bytes([response[2], response[3]]);
|
||||
Ok(lift_mode_name(value)?.to_string())
|
||||
}
|
||||
|
||||
fn set_sensor_lift_mode(&mut self, lift_mode: &str) -> Result<(), String> {
|
||||
let value = lift_mode_value(lift_mode)?;
|
||||
let smart_mode = lift_mode.starts_with("sym_") || lift_mode.starts_with("asym_");
|
||||
self.set_sensor_state_enabled(!smart_mode)?;
|
||||
self.sr_with(0x0b0b, &[0x00, 0x04, (value >> 8) as u8, value as u8], 0)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn get_sensor_lift_config(&mut self) -> Result<Vec<u8>, String> {
|
||||
let response = self.sr_with(0x0b85, &[0x00, 0x04, 0, 0, 0, 0, 0, 0, 0, 0], 0)?;
|
||||
Ok(response[2..10].to_vec())
|
||||
}
|
||||
|
||||
fn set_sensor_lift_config(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 8 {
|
||||
return Err("Sensor config A must contain exactly 8 bytes".to_string());
|
||||
}
|
||||
let mut args = vec![0x00, 0x04];
|
||||
args.extend_from_slice(data);
|
||||
self.sr_with(0x0b05, &args, 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn get_sensor_lift_config_b(&mut self) -> Result<Vec<u8>, String> {
|
||||
let response = self.sr_with(0x0b8c, &[0x00, 0x04, 0, 0, 0, 0, 0], 0)?;
|
||||
Ok(response[2..7].to_vec())
|
||||
}
|
||||
|
||||
fn set_sensor_lift_config_b(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 5 {
|
||||
return Err("Sensor config B must contain exactly 5 bytes".to_string());
|
||||
}
|
||||
let mut args = vec![0x00, 0x04];
|
||||
args.extend_from_slice(data);
|
||||
self.sr_with(0x0b0c, &args, 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn get_sensor_lift_config_a(&mut self) -> Result<Vec<u8>, String> {
|
||||
let response = self.sr_with(0x0b8d, &[0x00, 0x04, 0, 0, 0, 0, 0, 0, 0, 0], 0)?;
|
||||
Ok(response[2..10].to_vec())
|
||||
}
|
||||
|
||||
fn set_sensor_lift_config_a(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 8 {
|
||||
return Err("Sensor config A must contain exactly 8 bytes".to_string());
|
||||
}
|
||||
let mut args = vec![0x00, 0x04];
|
||||
args.extend_from_slice(data);
|
||||
self.sr_with(0x0b0d, &args, 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn sensor_snapshot(&mut self) -> Result<SensorState, String> {
|
||||
let lift_mode = self.get_sensor_lift_mode()?;
|
||||
let sensor_enabled = self.get_sensor_state_enabled()?;
|
||||
let device_mode = device_mode_name(self.get_device_mode()?.0)?.to_string();
|
||||
let retrieved_calib = self.get_sensor_lift_config()?;
|
||||
let (param_a, param_b) = if lift_mode.starts_with("config2") || lift_mode.starts_with("calib2") {
|
||||
(self.get_sensor_lift_config_a()?, self.get_sensor_lift_config_b()?)
|
||||
} else {
|
||||
(self.get_sensor_lift_config()?, Vec::new())
|
||||
};
|
||||
Ok(SensorState {
|
||||
lift_mode,
|
||||
sensor_enabled,
|
||||
device_mode,
|
||||
retrieved_calib,
|
||||
param_a,
|
||||
param_b,
|
||||
})
|
||||
}
|
||||
|
||||
fn start_sensor_calibration(&mut self) -> Result<(), String> {
|
||||
self.set_device_mode("driver", 0)?;
|
||||
self.set_sensor_state_enabled(true)?;
|
||||
self.set_sensor_lift_mode("calib1")?;
|
||||
self.set_sensor_calibration(false)
|
||||
}
|
||||
|
||||
fn stop_sensor_calibration(&mut self) -> Result<(), String> {
|
||||
self.set_sensor_calibration(true)?;
|
||||
self.set_sensor_state_enabled(false)?;
|
||||
self.set_device_mode("normal", 0)
|
||||
}
|
||||
|
||||
fn set_sensor_params(&mut self, param_a: &[u8], param_b: &[u8]) -> Result<(), String> {
|
||||
if param_b.is_empty() {
|
||||
self.set_sensor_lift_config(param_a)
|
||||
} else {
|
||||
self.set_sensor_lift_config_a(param_a)?;
|
||||
self.set_sensor_lift_config_b(param_b)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flash_usage(&mut self) -> Result<(u16, u32, u32, u32), String> {
|
||||
let response = self.sr_with(0x068e, &[0; 14], 0)?;
|
||||
Ok((
|
||||
u16::from_be_bytes([response[0], response[1]]),
|
||||
u32::from_be_bytes([response[2], response[3], response[4], response[5]]),
|
||||
u32::from_be_bytes([response[6], response[7], response[8], response[9]]),
|
||||
u32::from_be_bytes([response[10], response[11], response[12], response[13]]),
|
||||
))
|
||||
}
|
||||
|
||||
fn info_snapshot(&mut self) -> Result<InfoState, String> {
|
||||
let (_, flash_total, flash_free, flash_recycled) = self.get_flash_usage()?;
|
||||
Ok(InfoState {
|
||||
flash_total,
|
||||
flash_free,
|
||||
flash_recycled,
|
||||
macro_count: self.get_macro_count()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn append_log(&self, level: &str, message: String) {
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
if let Ok(mut logs) = self.logs.lock() {
|
||||
logs.push(DebugLogEntry {
|
||||
timestamp_ms,
|
||||
level: level.to_string(),
|
||||
message,
|
||||
});
|
||||
if logs.len() > 500 {
|
||||
let drop_count = logs.len() - 500;
|
||||
logs.drain(0..drop_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_feature_report(&self, report: &[u8; 90]) -> Result<(), String> {
|
||||
let mut buffer = [0u8; 91];
|
||||
buffer[1..].copy_from_slice(report);
|
||||
hidraw_ioctl(
|
||||
self.file.as_raw_fd(),
|
||||
hid_iocsfeature(buffer.len()),
|
||||
&mut buffer,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("Failed to send feature report: {err}"))
|
||||
}
|
||||
|
||||
fn get_feature_report(&self) -> Result<[u8; 90], String> {
|
||||
let mut buffer = [0u8; 91];
|
||||
hidraw_ioctl(
|
||||
self.file.as_raw_fd(),
|
||||
hid_iocgfeature(buffer.len()),
|
||||
&mut buffer,
|
||||
)
|
||||
.map_err(|err| format!("Failed to read feature report: {err}"))?;
|
||||
let mut report = [0u8; 90];
|
||||
report.copy_from_slice(&buffer[1..]);
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
166
src-tauri/src/backend/discovery.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
fn try_connect_summary(
|
||||
summary: DeviceSummary,
|
||||
logs: std::sync::Arc<Mutex<Vec<DebugLogEntry>>>,
|
||||
) -> Result<(ConnectedDevice, DeviceState), String> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&summary.path)
|
||||
.map_err(|err| format!("Could not open. Check hidraw permissions or udev rules: {err}"))?;
|
||||
|
||||
let mut device = ConnectedDevice { file, summary, logs };
|
||||
let snapshot = device.snapshot("direct")?;
|
||||
Ok((device, snapshot))
|
||||
}
|
||||
|
||||
fn connection_candidates(
|
||||
discovered: &[DeviceSummary],
|
||||
selected: &DeviceSummary,
|
||||
) -> Vec<DeviceSummary> {
|
||||
let mut candidates: Vec<DeviceSummary> = discovered
|
||||
.iter()
|
||||
.filter(|device| {
|
||||
device.vendor_id == selected.vendor_id
|
||||
&& device.product_id == selected.product_id
|
||||
&& device.probe_group == selected.probe_group
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
candidates.sort_by_key(|device| {
|
||||
(
|
||||
device.path != selected.path,
|
||||
interface_probe_priority(device.interface_number),
|
||||
device.path.clone(),
|
||||
)
|
||||
});
|
||||
candidates
|
||||
}
|
||||
|
||||
fn interface_probe_priority(interface_number: Option<u8>) -> u8 {
|
||||
match interface_number {
|
||||
Some(3) => 0,
|
||||
Some(2) => 1,
|
||||
Some(1) => 2,
|
||||
Some(0) => 3,
|
||||
Some(other) => 4 + other,
|
||||
None => u8::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_devices() -> io::Result<Vec<DeviceSummary>> {
|
||||
let mut devices = Vec::new();
|
||||
let hidraw_root = Path::new("/sys/class/hidraw");
|
||||
if !hidraw_root.exists() {
|
||||
return Ok(devices);
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(hidraw_root)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let Some((vendor_id, product_id, product_name, serial)) =
|
||||
read_hidraw_identity(&entry.path())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(supported) = SUPPORTED_DEVICES
|
||||
.iter()
|
||||
.find(|device| device.product_id == product_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let interface_number = read_interface_number(&entry.path())?;
|
||||
let probe_group = read_probe_group(&entry.path())?;
|
||||
let manufacturer = read_ancestor_file(&entry.path(), "manufacturer")?;
|
||||
let product_usb_string = read_ancestor_file(&entry.path(), "product")?;
|
||||
|
||||
devices.push(DeviceSummary {
|
||||
path: format!("/dev/{name}"),
|
||||
vendor_id,
|
||||
product_id,
|
||||
product_name: product_usb_string.unwrap_or(product_name),
|
||||
manufacturer,
|
||||
serial,
|
||||
interface_number,
|
||||
supported_name: supported.name.to_string(),
|
||||
probe_group,
|
||||
});
|
||||
}
|
||||
|
||||
devices.sort_by_key(|device| {
|
||||
(
|
||||
device.probe_group.clone(),
|
||||
interface_probe_priority(device.interface_number),
|
||||
device.path.clone(),
|
||||
)
|
||||
});
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn read_hidraw_identity(
|
||||
hidraw_path: &Path,
|
||||
) -> io::Result<Option<(u16, u16, String, Option<String>)>> {
|
||||
let uevent = fs::read_to_string(hidraw_path.join("device/uevent"))?;
|
||||
let mut vendor_id = None;
|
||||
let mut product_id = None;
|
||||
let mut product_name = None;
|
||||
let mut serial = None;
|
||||
|
||||
for line in uevent.lines() {
|
||||
if let Some(value) = line.strip_prefix("HID_ID=") {
|
||||
let parts: Vec<&str> = value.split(':').collect();
|
||||
if parts.len() == 3 {
|
||||
vendor_id = u16::from_str_radix(parts[1].trim_start_matches("0000"), 16).ok();
|
||||
product_id = u16::from_str_radix(parts[2].trim_start_matches("0000"), 16).ok();
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("HID_NAME=") {
|
||||
product_name = Some(value.to_string());
|
||||
} else if let Some(value) = line.strip_prefix("HID_UNIQ=") {
|
||||
if !value.is_empty() {
|
||||
serial = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (vendor_id, product_id) {
|
||||
(Some(RAZER_VENDOR_ID), Some(product_id)) => Ok(Some((
|
||||
RAZER_VENDOR_ID,
|
||||
product_id,
|
||||
product_name.unwrap_or_else(|| "Razer HID device".to_string()),
|
||||
serial,
|
||||
))),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_interface_number(hidraw_path: &Path) -> io::Result<Option<u8>> {
|
||||
let Some(value) = read_ancestor_file(hidraw_path, "bInterfaceNumber")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(u8::from_str_radix(value.trim(), 16).ok())
|
||||
}
|
||||
|
||||
fn read_probe_group(hidraw_path: &Path) -> io::Result<String> {
|
||||
let canonical = fs::canonicalize(hidraw_path)?;
|
||||
let interface_path = canonical
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "hidraw device has no parent"))?;
|
||||
let usb_device = interface_path.parent().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::NotFound, "HID interface has no USB parent")
|
||||
})?;
|
||||
Ok(usb_device.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn read_ancestor_file(path: &Path, file_name: &str) -> io::Result<Option<String>> {
|
||||
let mut current = fs::canonicalize(path)?;
|
||||
for _ in 0..8 {
|
||||
let candidate = current.join(file_name);
|
||||
if candidate.exists() {
|
||||
return Ok(Some(fs::read_to_string(candidate)?.trim().to_string()));
|
||||
}
|
||||
if !current.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
33
src-tauri/src/backend/ioctl.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
fn hidraw_ioctl(fd: libc::c_int, request: libc::c_ulong, buffer: &mut [u8]) -> io::Result<i32> {
|
||||
let result = unsafe { libc::ioctl(fd, request, buffer.as_mut_ptr()) };
|
||||
if result < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn hid_iocsfeature(len: usize) -> libc::c_ulong {
|
||||
ioc(IOC_READ | IOC_WRITE, b'H', 0x06, len)
|
||||
}
|
||||
|
||||
fn hid_iocgfeature(len: usize) -> libc::c_ulong {
|
||||
ioc(IOC_READ | IOC_WRITE, b'H', 0x07, len)
|
||||
}
|
||||
|
||||
const IOC_NRBITS: libc::c_ulong = 8;
|
||||
const IOC_TYPEBITS: libc::c_ulong = 8;
|
||||
const IOC_SIZEBITS: libc::c_ulong = 14;
|
||||
const IOC_NRSHIFT: libc::c_ulong = 0;
|
||||
const IOC_TYPESHIFT: libc::c_ulong = IOC_NRSHIFT + IOC_NRBITS;
|
||||
const IOC_SIZESHIFT: libc::c_ulong = IOC_TYPESHIFT + IOC_TYPEBITS;
|
||||
const IOC_DIRSHIFT: libc::c_ulong = IOC_SIZESHIFT + IOC_SIZEBITS;
|
||||
const IOC_WRITE: libc::c_ulong = 1;
|
||||
const IOC_READ: libc::c_ulong = 2;
|
||||
|
||||
fn ioc(dir: libc::c_ulong, ty: u8, nr: u8, size: usize) -> libc::c_ulong {
|
||||
(dir << IOC_DIRSHIFT)
|
||||
| ((ty as libc::c_ulong) << IOC_TYPESHIFT)
|
||||
| ((nr as libc::c_ulong) << IOC_NRSHIFT)
|
||||
| ((size as libc::c_ulong) << IOC_SIZESHIFT)
|
||||
}
|
||||
183
src-tauri/src/backend/macro_ops.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
const MACRO_OP_KEYBOARD_DOWN: u8 = 0x01;
|
||||
const MACRO_OP_KEYBOARD_UP: u8 = 0x02;
|
||||
const MACRO_OP_SYSTEM_A: u8 = 0x03;
|
||||
const MACRO_OP_SYSTEM_B: u8 = 0x04;
|
||||
const MACRO_OP_CONSUMER_A: u8 = 0x05;
|
||||
const MACRO_OP_CONSUMER_B: u8 = 0x06;
|
||||
const MACRO_OP_MOUSE_BUTTON: u8 = 0x08;
|
||||
const MACRO_OP_MOUSE_WHEEL: u8 = 0x0a;
|
||||
const MACRO_OP_DELAY_1: u8 = 0x11;
|
||||
const MACRO_OP_DELAY_2: u8 = 0x12;
|
||||
|
||||
fn decode_macro_operations(data: &[u8]) -> Result<Vec<MacroOperationState>, String> {
|
||||
let mut operations = Vec::new();
|
||||
let mut offset = 0usize;
|
||||
while offset < data.len() {
|
||||
let op_type = data[offset];
|
||||
offset += 1;
|
||||
let value_len = match op_type {
|
||||
MACRO_OP_KEYBOARD_DOWN
|
||||
| MACRO_OP_KEYBOARD_UP
|
||||
| MACRO_OP_SYSTEM_A
|
||||
| MACRO_OP_SYSTEM_B
|
||||
| MACRO_OP_MOUSE_BUTTON
|
||||
| MACRO_OP_MOUSE_WHEEL
|
||||
| MACRO_OP_DELAY_1 => 1,
|
||||
MACRO_OP_CONSUMER_A | MACRO_OP_CONSUMER_B | MACRO_OP_DELAY_2 => 2,
|
||||
other => return Err(format!("Unsupported macro op type {other:#04x}")),
|
||||
};
|
||||
if offset + value_len > data.len() {
|
||||
return Err("Macro operation payload is truncated".to_string());
|
||||
}
|
||||
let value = &data[offset..offset + value_len];
|
||||
offset += value_len;
|
||||
|
||||
let operation = match op_type {
|
||||
MACRO_OP_KEYBOARD_DOWN => MacroOperationState {
|
||||
category: "keyboard".to_string(),
|
||||
payload: json!({"key": value[0], "is_up": false}),
|
||||
},
|
||||
MACRO_OP_KEYBOARD_UP => MacroOperationState {
|
||||
category: "keyboard".to_string(),
|
||||
payload: json!({"key": value[0], "is_up": true}),
|
||||
},
|
||||
MACRO_OP_SYSTEM_A => MacroOperationState {
|
||||
category: "system".to_string(),
|
||||
payload: json!({"key": value[0], "is_b": false}),
|
||||
},
|
||||
MACRO_OP_SYSTEM_B => MacroOperationState {
|
||||
category: "system".to_string(),
|
||||
payload: json!({"key": value[0], "is_b": true}),
|
||||
},
|
||||
MACRO_OP_CONSUMER_A => MacroOperationState {
|
||||
category: "consumer".to_string(),
|
||||
payload: json!({"key": u16::from_be_bytes([value[0], value[1]]), "is_b": false}),
|
||||
},
|
||||
MACRO_OP_CONSUMER_B => MacroOperationState {
|
||||
category: "consumer".to_string(),
|
||||
payload: json!({"key": u16::from_be_bytes([value[0], value[1]]), "is_b": true}),
|
||||
},
|
||||
MACRO_OP_MOUSE_BUTTON => MacroOperationState {
|
||||
category: "mouse_button".to_string(),
|
||||
payload: json!({"button": decode_macro_mouse_buttons(value[0])}),
|
||||
},
|
||||
MACRO_OP_MOUSE_WHEEL => MacroOperationState {
|
||||
category: "mouse_wheel".to_string(),
|
||||
payload: json!({"value": i8::from_be_bytes([value[0]])}),
|
||||
},
|
||||
MACRO_OP_DELAY_1 => MacroOperationState {
|
||||
category: "delay".to_string(),
|
||||
payload: json!({"value": value[0]}),
|
||||
},
|
||||
MACRO_OP_DELAY_2 => MacroOperationState {
|
||||
category: "delay".to_string(),
|
||||
payload: json!({"value": u16::from_be_bytes([value[0], value[1]])}),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
operations.push(operation);
|
||||
}
|
||||
Ok(operations)
|
||||
}
|
||||
|
||||
fn encode_macro_operations(operations: &[MacroOperationState]) -> Result<Vec<u8>, String> {
|
||||
let mut out = Vec::new();
|
||||
for operation in operations {
|
||||
let payload = operation
|
||||
.payload
|
||||
.as_object()
|
||||
.ok_or_else(|| "Macro operation payload must be an object".to_string())?;
|
||||
match operation.category.as_str() {
|
||||
"keyboard" => {
|
||||
out.push(if payload.get("is_up").and_then(Value::as_bool).unwrap_or(false) {
|
||||
MACRO_OP_KEYBOARD_UP
|
||||
} else {
|
||||
MACRO_OP_KEYBOARD_DOWN
|
||||
});
|
||||
out.push(get_u8(payload, "key")?);
|
||||
}
|
||||
"system" => {
|
||||
out.push(if payload.get("is_b").and_then(Value::as_bool).unwrap_or(false) {
|
||||
MACRO_OP_SYSTEM_B
|
||||
} else {
|
||||
MACRO_OP_SYSTEM_A
|
||||
});
|
||||
out.push(get_u8(payload, "key")?);
|
||||
}
|
||||
"consumer" => {
|
||||
let key = get_u16(payload, "key")?;
|
||||
out.push(if payload.get("is_b").and_then(Value::as_bool).unwrap_or(false) {
|
||||
MACRO_OP_CONSUMER_B
|
||||
} else {
|
||||
MACRO_OP_CONSUMER_A
|
||||
});
|
||||
out.extend_from_slice(&key.to_be_bytes());
|
||||
}
|
||||
"mouse_button" => {
|
||||
out.push(MACRO_OP_MOUSE_BUTTON);
|
||||
out.push(encode_macro_mouse_buttons(get_string(payload, "button")?)?);
|
||||
}
|
||||
"mouse_wheel" => {
|
||||
out.push(MACRO_OP_MOUSE_WHEEL);
|
||||
let value = payload
|
||||
.get("value")
|
||||
.and_then(Value::as_i64)
|
||||
.filter(|value| (-128..=127).contains(value))
|
||||
.ok_or_else(|| "mouse_wheel.value must be between -128 and 127".to_string())?;
|
||||
out.push((value as i8) as u8);
|
||||
}
|
||||
"delay" => {
|
||||
let value = payload
|
||||
.get("value")
|
||||
.and_then(Value::as_u64)
|
||||
.filter(|value| *value <= u16::MAX as u64)
|
||||
.ok_or_else(|| "delay.value must be between 0 and 65535".to_string())?
|
||||
as u16;
|
||||
if value < 0x100 {
|
||||
out.push(MACRO_OP_DELAY_1);
|
||||
out.push(value as u8);
|
||||
} else {
|
||||
out.push(MACRO_OP_DELAY_2);
|
||||
out.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
}
|
||||
other => return Err(format!("Unsupported macro operation category '{other}'")),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn decode_macro_mouse_buttons(value: u8) -> String {
|
||||
if value == 0 {
|
||||
return "NONE".to_string();
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
for (flag, name) in [
|
||||
(0x01, "LEFT"),
|
||||
(0x02, "RIGHT"),
|
||||
(0x04, "MIDDLE"),
|
||||
(0x08, "BACKWARD"),
|
||||
(0x10, "FORWARD"),
|
||||
] {
|
||||
if value & flag != 0 {
|
||||
flags.push(name);
|
||||
}
|
||||
}
|
||||
flags.join("|")
|
||||
}
|
||||
|
||||
fn encode_macro_mouse_buttons(value: &str) -> Result<u8, String> {
|
||||
let mut flags = 0u8;
|
||||
for part in value.split('|').map(str::trim).filter(|part| !part.is_empty()) {
|
||||
flags |= match part {
|
||||
"NONE" => 0x00,
|
||||
"LEFT" => 0x01,
|
||||
"RIGHT" => 0x02,
|
||||
"MIDDLE" => 0x04,
|
||||
"BACKWARD" => 0x08,
|
||||
"FORWARD" => 0x10,
|
||||
other => return Err(format!("Unknown macro mouse button flag '{other}'")),
|
||||
};
|
||||
}
|
||||
Ok(flags)
|
||||
}
|
||||
142
src-tauri/src/backend/models.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#[derive(Clone, Copy)]
|
||||
struct SupportedDevice {
|
||||
product_id: u16,
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeviceSummary {
|
||||
path: String,
|
||||
vendor_id: u16,
|
||||
product_id: u16,
|
||||
product_name: String,
|
||||
manufacturer: Option<String>,
|
||||
serial: Option<String>,
|
||||
interface_number: Option<u8>,
|
||||
supported_name: String,
|
||||
probe_group: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BasicSettings {
|
||||
profile: String,
|
||||
scroll_mode: String,
|
||||
scroll_acceleration: bool,
|
||||
scroll_smart_reel: bool,
|
||||
polling_rate_ms: u8,
|
||||
dpi_xy: [u16; 2],
|
||||
dpi_stages: Vec<[u16; 2]>,
|
||||
active_dpi_stage: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeviceState {
|
||||
device: DeviceSummary,
|
||||
serial: String,
|
||||
firmware: String,
|
||||
profiles: Vec<String>,
|
||||
basic: BasicSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LedRegionState {
|
||||
region: String,
|
||||
effect: String,
|
||||
mode: u8,
|
||||
speed: u8,
|
||||
colors: Vec<[u8; 3]>,
|
||||
brightness: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LedState {
|
||||
profile: String,
|
||||
regions: Vec<LedRegionState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ButtonMappingState {
|
||||
profile: String,
|
||||
button: String,
|
||||
hypershift: bool,
|
||||
category: String,
|
||||
payload: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProfileConfigBundle {
|
||||
basic: BasicSettings,
|
||||
button_mappings: Vec<ButtonMappingState>,
|
||||
led: LedState,
|
||||
profile_info_hex: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MacroOperationState {
|
||||
category: String,
|
||||
payload: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MacroDefinition {
|
||||
macro_id: u16,
|
||||
macro_info_hex: Option<String>,
|
||||
operations: Vec<MacroOperationState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SensorState {
|
||||
lift_mode: String,
|
||||
sensor_enabled: bool,
|
||||
device_mode: String,
|
||||
retrieved_calib: Vec<u8>,
|
||||
param_a: Vec<u8>,
|
||||
param_b: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InfoState {
|
||||
flash_total: u32,
|
||||
flash_free: u32,
|
||||
flash_recycled: u32,
|
||||
macro_count: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DebugLogEntry {
|
||||
timestamp_ms: u64,
|
||||
level: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
connected: Mutex<Option<ConnectedDevice>>,
|
||||
logs: std::sync::Arc<Mutex<Vec<DebugLogEntry>>>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connected: Mutex::new(None),
|
||||
logs: std::sync::Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectedDevice {
|
||||
summary: DeviceSummary,
|
||||
file: File,
|
||||
logs: std::sync::Arc<Mutex<Vec<DebugLogEntry>>>,
|
||||
}
|
||||
187
src-tauri/src/backend/value_maps.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
fn profile_value(profile: &str) -> Result<u8, String> {
|
||||
match profile {
|
||||
"direct" => Ok(0x00),
|
||||
"white" | "default" => Ok(0x01),
|
||||
"red" => Ok(0x02),
|
||||
"green" => Ok(0x03),
|
||||
"blue" => Ok(0x04),
|
||||
"cyan" => Ok(0x05),
|
||||
_ => Err(format!("Unsupported profile '{profile}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_name(profile: u8) -> Result<&'static str, String> {
|
||||
match profile {
|
||||
0x00 => Ok("direct"),
|
||||
0x01 => Ok("white"),
|
||||
0x02 => Ok("red"),
|
||||
0x03 => Ok("green"),
|
||||
0x04 => Ok("blue"),
|
||||
0x05 => Ok("cyan"),
|
||||
_ => Err(format!("Unknown profile value {profile}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_mode_value(mode: &str) -> Result<u8, String> {
|
||||
match mode {
|
||||
"tactile" => Ok(0),
|
||||
"freespin" => Ok(1),
|
||||
_ => Err(format!("Unsupported scroll mode '{mode}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn led_region_value(region: &str) -> Result<u8, String> {
|
||||
match region {
|
||||
"all" => Ok(0x00),
|
||||
"wheel" => Ok(0x01),
|
||||
"logo" => Ok(0x04),
|
||||
"strip" => Ok(0x0a),
|
||||
_ => Err(format!("Unsupported LED region '{region}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn led_effect_value(effect: &str) -> Result<u8, String> {
|
||||
match effect {
|
||||
"off" | "disabled" => Ok(0x00),
|
||||
"static" => Ok(0x01),
|
||||
"spectrum" => Ok(0x03),
|
||||
"wave" => Ok(0x04),
|
||||
"custom" => Ok(0x08),
|
||||
_ => Err(format!("Unsupported LED effect '{effect}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn led_effect_name(effect: u8) -> Result<&'static str, String> {
|
||||
match effect {
|
||||
0x00 => Ok("off"),
|
||||
0x01 => Ok("static"),
|
||||
0x03 => Ok("spectrum"),
|
||||
0x04 => Ok("wave"),
|
||||
0x08 => Ok("custom"),
|
||||
_ => Err(format!("Unknown LED effect value {effect}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn button_value(button: &str) -> Result<u8, String> {
|
||||
match button {
|
||||
"left" => Ok(0x01),
|
||||
"right" => Ok(0x02),
|
||||
"middle" => Ok(0x03),
|
||||
"backward" => Ok(0x04),
|
||||
"forward" => Ok(0x05),
|
||||
"wheel_up" => Ok(0x09),
|
||||
"wheel_down" => Ok(0x0a),
|
||||
"bottom" => Ok(0x0e),
|
||||
"aim" => Ok(0x0f),
|
||||
"wheel_left" => Ok(0x34),
|
||||
"wheel_right" => Ok(0x35),
|
||||
"middle_backward" => Ok(0x60),
|
||||
"middle_forward" => Ok(0x6a),
|
||||
_ => Err(format!("Unsupported button '{button}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn all_button_names() -> &'static [&'static str] {
|
||||
&[
|
||||
"aim",
|
||||
"left",
|
||||
"middle",
|
||||
"right",
|
||||
"forward",
|
||||
"wheel_up",
|
||||
"middle_forward",
|
||||
"wheel_left",
|
||||
"backward",
|
||||
"wheel_down",
|
||||
"middle_backward",
|
||||
"wheel_right",
|
||||
"bottom",
|
||||
]
|
||||
}
|
||||
|
||||
fn bytes_to_hex_compact(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
fn parse_hex_bytes(input: &str) -> Result<Vec<u8>, String> {
|
||||
let compact = input.chars().filter(|ch| !ch.is_whitespace()).collect::<String>();
|
||||
if compact.len() % 2 != 0 {
|
||||
return Err("Hex input must contain an even number of characters".to_string());
|
||||
}
|
||||
(0..compact.len())
|
||||
.step_by(2)
|
||||
.map(|index| {
|
||||
u8::from_str_radix(&compact[index..index + 2], 16)
|
||||
.map_err(|_| format!("Invalid hex byte '{}'", &compact[index..index + 2]))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn device_mode_name(mode: u8) -> Result<&'static str, String> {
|
||||
match mode {
|
||||
0x00 => Ok("normal"),
|
||||
0x01 => Ok("bootloader"),
|
||||
0x02 => Ok("test"),
|
||||
0x03 => Ok("driver"),
|
||||
_ => Err(format!("Unknown device mode {mode}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn device_mode_value(mode: &str) -> Result<u8, String> {
|
||||
match mode {
|
||||
"normal" => Ok(0x00),
|
||||
"bootloader" => Ok(0x01),
|
||||
"test" => Ok(0x02),
|
||||
"driver" => Ok(0x03),
|
||||
_ => Err(format!("Unknown device mode '{mode}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn lift_mode_name(value: u16) -> Result<&'static str, String> {
|
||||
match value {
|
||||
0x0000 => Ok("none"),
|
||||
0x0100 => Ok("sym_1"),
|
||||
0x0101 => Ok("sym_2"),
|
||||
0x0102 => Ok("sym_3"),
|
||||
0x0200 => Ok("asym_12"),
|
||||
0x0201 => Ok("asym_13"),
|
||||
0x0202 => Ok("asym_23"),
|
||||
0x0300 => Ok("config1"),
|
||||
0x0400 => Ok("config2"),
|
||||
0x0500 => Ok("calib1"),
|
||||
0x0600 => Ok("calib2"),
|
||||
_ => Err(format!("Unknown lift mode 0x{value:04x}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn lift_mode_value(value: &str) -> Result<u16, String> {
|
||||
match value {
|
||||
"none" => Ok(0x0000),
|
||||
"sym_1" => Ok(0x0100),
|
||||
"sym_2" => Ok(0x0101),
|
||||
"sym_3" => Ok(0x0102),
|
||||
"asym_12" => Ok(0x0200),
|
||||
"asym_13" => Ok(0x0201),
|
||||
"asym_23" => Ok(0x0202),
|
||||
"config1" => Ok(0x0300),
|
||||
"config2" => Ok(0x0400),
|
||||
"calib1" => Ok(0x0500),
|
||||
"calib2" => Ok(0x0600),
|
||||
_ => Err(format!("Unknown lift mode '{value}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn fn_mouse_value(button: &str) -> Result<u8, String> {
|
||||
match button {
|
||||
"left" => Ok(0x01),
|
||||
"right" => Ok(0x02),
|
||||
"middle" => Ok(0x03),
|
||||
"backward" => Ok(0x04),
|
||||
"forward" => Ok(0x05),
|
||||
"wheel_up" => Ok(0x09),
|
||||
"wheel_down" => Ok(0x0a),
|
||||
"wheel_left" => Ok(0x68),
|
||||
"wheel_right" => Ok(0x69),
|
||||
_ => Err(format!("Unsupported mouse function '{button}'")),
|
||||
}
|
||||
}
|
||||
81
src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io,
|
||||
os::fd::AsRawFd,
|
||||
path::Path,
|
||||
sync::Mutex,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const RAZER_VENDOR_ID: u16 = 0x1532;
|
||||
const SUPPORTED_DEVICES: &[SupportedDevice] = &[
|
||||
SupportedDevice {
|
||||
product_id: 0x0099,
|
||||
name: "Razer Basilisk V3",
|
||||
},
|
||||
SupportedDevice {
|
||||
product_id: 0x00aa,
|
||||
name: "Razer Basilisk V3 Pro (wired)",
|
||||
},
|
||||
SupportedDevice {
|
||||
product_id: 0x00ab,
|
||||
name: "Razer Basilisk V3 Pro (wireless)",
|
||||
},
|
||||
];
|
||||
|
||||
include!("backend/models.rs");
|
||||
include!("backend/commands.rs");
|
||||
include!("backend/device.rs");
|
||||
include!("backend/discovery.rs");
|
||||
include!("backend/value_maps.rs");
|
||||
include!("backend/button_mapping.rs");
|
||||
include!("backend/macro_ops.rs");
|
||||
include!("backend/ioctl.rs");
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(AppState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_supported_devices,
|
||||
connect_device,
|
||||
refresh_device_state,
|
||||
set_scroll_mode,
|
||||
set_scroll_acceleration,
|
||||
set_scroll_smart_reel,
|
||||
set_polling_rate,
|
||||
set_dpi_xy,
|
||||
set_dpi_stages,
|
||||
create_profile,
|
||||
delete_profile,
|
||||
get_led_state,
|
||||
set_led_effect,
|
||||
set_led_brightness,
|
||||
apply_led_to_all_regions,
|
||||
get_button_mapping,
|
||||
set_button_mapping,
|
||||
export_profile_config,
|
||||
import_profile_config,
|
||||
list_macros,
|
||||
get_macro_definition,
|
||||
set_macro_definition,
|
||||
delete_macro,
|
||||
reset_macro_flash,
|
||||
get_debug_logs,
|
||||
clear_debug_logs,
|
||||
get_sensor_state,
|
||||
set_sensor_lift_mode,
|
||||
start_sensor_calibration,
|
||||
stop_sensor_calibration,
|
||||
set_sensor_params,
|
||||
get_info_state,
|
||||
send_raw_command,
|
||||
reset_flash,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
razer_linux_desktop_lib::run()
|
||||
}
|
||||
36
src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "razer-linux-desktop",
|
||||
"version": "0.1.0",
|
||||
"identifier": "one.daichendt.razer-linux-desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "NO_COLOR=false trunk serve",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "NO_COLOR=false trunk build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "razer-linux-desktop",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["appimage"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||