use log::warn; use serde::{Deserialize, Serialize}; #[cfg(feature = "dbus")] use zbus::zvariant::Type; /// The `LedCode` used in setting up keyboard layouts is important because it /// determines the idexing for an RGB value in the final USB packets (for /// per-key addressable keyboards). #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum LedCode { VolUp, VolDown, MicMute, #[default] RogApp, RogFan, Esc, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Del, Tilde, N1, N2, N3, N4, N5, N6, N7, N8, N9, N0, Hyphen, Equals, Backspace, /// For keyboards where the backspace button has 3 LED Backspace3_1, Backspace3_2, Backspace3_3, Home, Tab, Q, W, E, R, T, Y, U, I, O, P, LBracket, RBracket, BackSlash, PgUp, Caps, A, S, D, F, G, H, J, K, L, SemiColon, Quote, Return, /// For keyboards where the return button has 3 LED Return3_1, Return3_2, Return3_3, PgDn, LShift, /// For keyboards where the left shift button has 3 LED LShift3_1, LShift3_2, LShift3_3, Z, X, C, V, B, N, M, Comma, Period, FwdSlash, Star, NumPadDel, NumPadPlus, NumPadEnter, NumPadPause, NumPadPrtSc, NumPadHome, NumLock, Rshift, Rshift3_1, Rshift3_2, Rshift3_3, End, LCtrl, LFn, Meta, LAlt, Spacebar, /// For keyboards where the spacebar button has 5 LED Spacebar5_1, Spacebar5_2, Spacebar5_3, Spacebar5_4, Spacebar5_5, Pause, RAlt, PrtSc, RCtrl, Up, Down, Left, Right, RFn, MediaPlay, MediaStop, MediaNext, MediaPrev, LidLogo, LidLeft, LidRight, /// Used by per-key and multizoned LightbarRight, /// Used by per-key and multizoned LightbarRightCorner, /// Used by per-key and multizoned LightbarRightBottom, /// Used by per-key and multizoned LightbarLeftBottom, /// Used by per-key and multizoned LightbarLeftCorner, /// Used by per-key and multizoned LightbarLeft, /// Use if the keyboard supports only a single zone. This zone uses the same /// packet data as the `Zoned*` below SingleZone, /// Use if the keyboard supports 4 zones, this is the left zone ZonedKbLeft, /// Use if the keyboard supports 4 zones, this is the left-center zone ZonedKbLeftMid, /// Use if the keyboard supports 4 zones, this is the right-center zone ZonedKbRightMid, /// Use if the keyboard supports 4 zones, this is the right zone ZonedKbRight, /// To be ignored by effects Spacing, /// To be ignored by effects Blocking, } impl LedCode { pub fn is_placeholder(&self) -> bool { matches!(self, Self::Spacing | Self::Blocking) } pub fn is_keyboard_zone(&self) -> bool { matches!( self, Self::ZonedKbLeft | Self::ZonedKbLeftMid | Self::ZonedKbRightMid | Self::ZonedKbRight ) } pub fn is_lightbar_zone(&self) -> bool { matches!( self, Self::LightbarLeft | Self::LightbarLeftCorner | Self::LightbarLeftBottom | Self::LightbarRightBottom | Self::LightbarRightCorner | Self::LightbarRight ) } } /// Represents the per-key raw USB packets pub type UsbPackets = Vec>; /// A `UsbPackets` contains all data to change the full set of keyboard /// key colours individually. /// /// Each row of the internal array is a full HID packet that can be sent /// to the keyboard EC. One row controls one group of keys, these keys are not /// necessarily all on the same row of the keyboard, with some splitting between /// two rows. #[cfg_attr(feature = "dbus", derive(Type))] #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LedUsbPackets { /// The packet data used to send data to the USB keyboard usb_packets: UsbPackets, /// Wether or not this packet collection is zoned. The determines which /// starting bytes are used and what the indexing is for lightbar RGB /// colours zoned: bool, } impl Default for LedUsbPackets { fn default() -> Self { Self::new_per_key() } } impl LedUsbPackets { /// Set up a series of per-key packets. This includes setting all the /// required starting bytes per packet, but does not set any colours. /// /// These packets will not work with per-zone keyboards pub fn new_per_key() -> Self { let mut set = vec![vec![0u8; 64]; 11]; // set[0].copy_from_slice(&KeyColourArray::get_init_msg()); for (count, row) in set.iter_mut().enumerate() { row[0] = 0x5d; // Report ID row[1] = 0xbc; // Mode = custom??, 0xb3 is builtin row[2] = 0x00; row[3] = 0x01; // ?? row[4] = 0x01; // ??, 4,5,6 are normally RGB for builtin mode colours row[5] = 0x01; // ?? row[6] = (count as u8) << 4; // Key group if count == 10 { row[7] = 0x08; // 0b00001000 } else { row[7] = 0x10; // 0b00010000 addressing? flips for group a0 } row[8] = 0x00; } Self { usb_packets: set, zoned: false, } } /// Create new zoned packets. Although the result is a nested `Vec` only the /// first vector is available. The final packet is slightly different /// for single-zoned compared to multizoned. /// /// This packet will not work with per-key keyboards /// /// Wireshark captures show: /// ```ignore /// 5d,bc,01,01,00,00,00,00,00,ff,00,00, RED, single zone /// 5d,bc,01,01,04,00,00,00,00,ff,00,00, RED, multizone /// ``` pub fn new_zoned(multizoned: bool) -> Self { let mut pkt = vec![0u8; 64]; pkt[0] = 0x5d; // Report ID pkt[1] = 0xbc; // Mode = custom??, 0xb3 is builtin pkt[2] = 0x01; pkt[3] = 0x01; // ?? if !multizoned { pkt[4] = 0x00; // This doesn't actually seem to matter on this // keyboard? } else { pkt[4] = 0x04; // ??, 4,5,6 are normally RGB for builtin mode // colours } Self { usb_packets: vec![pkt], zoned: true, } } /// Initialise and clear the keyboard for custom effects, this must be done /// for every time mode switches from builtin to custom #[inline] pub const fn get_init_msg() -> [u8; 64] { let mut init = [0u8; 64]; init[0] = 0x5d; // Report ID init[1] = 0xbc; // Mode = custom??, 0xb3 is builtin init } /// Set the RGB colour of an `LedCode` #[inline] pub fn set(&mut self, key: LedCode, r: u8, g: u8, b: u8) { if let Some(c) = self.rgb_for_led_code(key) { c[0] = r; c[1] = g; c[2] = b; } } /// Indexes in to `UsbPackets` at the correct row and column /// to set a series of three bytes to the chosen R,G,B values /// /// Indexing is different for `zoned` and assumes that only one packet is /// generated for all the zones fn rgb_for_led_code(&mut self, led_code: LedCode) -> Option<&mut [u8]> { let zoned = self.zoned; // Tuples are indexes in to array #[allow(clippy::match_same_arms)] let (row, col) = match led_code { LedCode::VolDown => (0, 15), LedCode::VolUp => (0, 18), LedCode::MicMute => (0, 21), LedCode::RogApp => (0, 24), // LedCode::Esc => (1, 24), LedCode::F1 => (1, 30), LedCode::F2 => (1, 33), LedCode::F3 => (1, 36), LedCode::F4 => (1, 39), LedCode::F5 => (1, 45), LedCode::F6 => (1, 48), LedCode::F7 => (1, 51), LedCode::F8 => (1, 54), // LedCode::F9 => (2, 12), LedCode::F10 => (2, 15), LedCode::F11 => (2, 18), LedCode::F12 => (2, 21), LedCode::Del => (2, 24), LedCode::Tilde => (2, 39), LedCode::N1 => (2, 42), LedCode::N2 => (2, 45), LedCode::N3 => (2, 48), LedCode::N4 => (2, 51), LedCode::N5 => (2, 54), // LedCode::N6 => (3, 9), LedCode::N7 => (3, 12), LedCode::N8 => (3, 15), LedCode::N9 => (3, 18), LedCode::N0 => (3, 21), LedCode::Hyphen => (3, 24), LedCode::Equals => (3, 27), LedCode::Backspace3_1 => (3, 30), LedCode::Backspace3_2 => (3, 33), LedCode::Backspace3_3 => (3, 36), LedCode::Home => (3, 39), LedCode::Tab => (3, 54), // LedCode::Q => (4, 9), LedCode::W => (4, 12), LedCode::E => (4, 15), LedCode::R => (4, 18), LedCode::T => (4, 21), LedCode::Y => (4, 24), LedCode::U => (4, 27), LedCode::I => (4, 30), LedCode::O => (4, 33), LedCode::P => (4, 36), LedCode::LBracket => (4, 39), LedCode::RBracket => (4, 42), LedCode::BackSlash => (4, 45), LedCode::PgUp => (4, 54), // LedCode::Caps => (5, 21), LedCode::A => (5, 24), LedCode::S => (5, 27), LedCode::D => (5, 30), LedCode::F => (5, 33), LedCode::G => (5, 36), LedCode::H => (5, 39), LedCode::J => (5, 42), LedCode::K => (5, 45), LedCode::L => (5, 48), LedCode::SemiColon => (5, 51), LedCode::Quote => (5, 54), // LedCode::Return => (6, 9), LedCode::Return3_1 => (6, 12), LedCode::Return3_2 => (6, 15), LedCode::Return3_3 => (6, 18), LedCode::PgDn => (6, 21), LedCode::LShift => (6, 36), // TODO: Find correct locations LedCode::LShift3_1 => (6, 36), LedCode::LShift3_2 => (6, 36), LedCode::LShift3_3 => (6, 36), LedCode::Z => (6, 42), LedCode::X => (6, 45), LedCode::C => (6, 48), LedCode::V => (6, 51), LedCode::B => (6, 54), // LedCode::N => (7, 9), LedCode::M => (7, 12), LedCode::Comma => (7, 15), LedCode::Period => (7, 18), LedCode::FwdSlash => (7, 21), LedCode::Rshift => (7, 24), LedCode::Rshift3_1 => (7, 27), LedCode::Rshift3_2 => (7, 30), LedCode::Rshift3_3 => (7, 33), LedCode::End => (7, 36), LedCode::LCtrl => (7, 51), LedCode::LFn => (7, 54), // LedCode::Meta => (8, 9), LedCode::LAlt => (8, 12), LedCode::Spacebar5_1 => (8, 15), LedCode::Spacebar5_2 => (8, 18), LedCode::Spacebar5_3 => (8, 21), LedCode::Spacebar5_4 => (8, 24), LedCode::Spacebar5_5 => (8, 27), LedCode::RAlt => (8, 30), LedCode::PrtSc => (8, 33), LedCode::RCtrl => (8, 36), LedCode::Up => (8, 42), LedCode::RFn => (8, 51), // LedCode::Left => (9, 54), // LedCode::Down => (10, 9), LedCode::Right => (10, 12), LedCode::LidLogo => (11, 9), LedCode::LidLeft => (11, 36), LedCode::LidRight => (11, 39), // LedCode::SingleZone | LedCode::ZonedKbLeft => (0, 9), LedCode::ZonedKbLeftMid => (0, 12), LedCode::ZonedKbRightMid => (0, 15), LedCode::ZonedKbRight => (0, 18), LedCode::LightbarRight => if zoned {(0, 27)} else { (11, 15)}, LedCode::LightbarRightCorner => if zoned {(0, 30)} else {(11, 18)}, LedCode::LightbarRightBottom => if zoned {(0, 33)} else{(11, 21)}, LedCode::LightbarLeftBottom => if zoned {(0, 36)} else{(11, 24)}, LedCode::LightbarLeftCorner => if zoned {(0, 39)} else{(11, 27)}, LedCode::LightbarLeft => if zoned {(0, 42)} else{(11, 30)}, // LedCode::Spacing | LedCode::Blocking // TODO: the addressing of the following | LedCode::MediaPlay | LedCode::MediaStop | LedCode::MediaPrev | LedCode::MediaNext | LedCode::Pause | LedCode::NumLock | LedCode::Star | LedCode::NumPadDel | LedCode::NumPadPlus | LedCode::NumPadEnter | LedCode::NumPadPause | LedCode::NumPadPrtSc | LedCode::NumPadHome | LedCode::RogFan | LedCode::Spacebar | LedCode::Backspace => return None, }; if self.zoned && row > 0 { warn!( "LedCode {led_code:?} for zoned is not correct or out of Zone range. Setting to 0", ); return None; } Some(&mut self.usb_packets[row][col..=col + 2]) } #[inline] pub fn get(&self) -> UsbPackets { self.usb_packets.clone() } #[inline] pub fn get_ref(&self) -> &UsbPackets { &self.usb_packets } #[inline] pub fn get_mut(&mut self) -> &mut UsbPackets { &mut self.usb_packets } } impl From for UsbPackets { fn from(k: LedUsbPackets) -> Self { k.usb_packets } } #[cfg(test)] mod tests { use crate::advanced::{LedCode, LedUsbPackets, UsbPackets}; macro_rules! colour_check_zoned { ($zone:expr, $pkt_idx_start:expr) => { let mut zone = LedUsbPackets::new_zoned(true); let c = zone.rgb_for_led_code($zone).unwrap(); c[0] = 255; c[1] = 255; c[2] = 255; let pkt: UsbPackets = zone.into(); assert_eq!(pkt[0][$pkt_idx_start], 0xff); assert_eq!(pkt[0][$pkt_idx_start + 1], 0xff); assert_eq!(pkt[0][$pkt_idx_start + 2], 0xff); }; } #[test] fn zone_to_packet_check() { let zone = LedUsbPackets::new_zoned(true); let pkt: UsbPackets = zone.into(); assert_eq!(pkt[0][0], 0x5d); assert_eq!(pkt[0][1], 0xbc); assert_eq!(pkt[0][2], 0x01); assert_eq!(pkt[0][3], 0x01); assert_eq!(pkt[0][4], 0x04); colour_check_zoned!(LedCode::ZonedKbLeft, 9); colour_check_zoned!(LedCode::ZonedKbLeftMid, 12); colour_check_zoned!(LedCode::ZonedKbRightMid, 15); colour_check_zoned!(LedCode::ZonedKbRight, 18); colour_check_zoned!(LedCode::LightbarRight, 27); colour_check_zoned!(LedCode::LightbarRightCorner, 30); colour_check_zoned!(LedCode::LightbarRightBottom, 33); colour_check_zoned!(LedCode::LightbarLeftBottom, 36); colour_check_zoned!(LedCode::LightbarLeftCorner, 39); colour_check_zoned!(LedCode::LightbarLeft, 42); } #[test] fn perkey_to_packet_check() { let per_key = LedUsbPackets::new_per_key(); let pkt: UsbPackets = per_key.into(); assert_eq!(pkt[0][0], 0x5d); assert_eq!(pkt[0][1], 0xbc); assert_eq!(pkt[0][2], 0x00); assert_eq!(pkt[0][3], 0x01); assert_eq!(pkt[0][4], 0x01); assert_eq!(pkt[0][5], 0x01); let mut per_key = LedUsbPackets::new_per_key(); let c = per_key.rgb_for_led_code(LedCode::D).unwrap(); c[0] = 255; c[1] = 255; c[2] = 255; let c = per_key.rgb_for_led_code(LedCode::O).unwrap(); c[0] = 255; c[1] = 255; c[2] = 255; let c = per_key.rgb_for_led_code(LedCode::N0).unwrap(); c[0] = 255; c[1] = 255; c[2] = 255; let c = per_key.rgb_for_led_code(LedCode::M).unwrap(); c[0] = 255; c[1] = 255; c[2] = 255; let pkt: UsbPackets = per_key.into(); assert_eq!(pkt[5][30], 0xff); // D, red assert_eq!(pkt[5][31], 0xff); // D assert_eq!(pkt[5][32], 0xff); // D assert_eq!(pkt[5][33], 0x00); // D assert_eq!(pkt[4][33], 0xff); // O, red assert_eq!(pkt[4][34], 0xff); // O assert_eq!(pkt[4][35], 0xff); // O assert_eq!(pkt[4][36], 0x00); // O assert_eq!(pkt[7][12], 0xff); // M, red assert_eq!(pkt[7][13], 0xff); // M assert_eq!(pkt[7][14], 0xff); // M assert_eq!(pkt[7][15], 0x00); // M } }