diff --git a/.gitignore b/.gitignore index 6438f1c0..a34615ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ target/ build/ +# Windows build outputs +*.efi +*.exe +*.pdb diff --git a/Cargo.lock b/Cargo.lock index 228dd7f2..465dc6e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,7 +388,7 @@ dependencies = [ [[package]] name = "framework_lib" -version = "0.3.0" +version = "0.4.0" dependencies = [ "built", "clap", @@ -420,7 +420,7 @@ dependencies = [ [[package]] name = "framework_tool" -version = "0.3.0" +version = "0.4.0" dependencies = [ "framework_lib", "static_vcruntime", @@ -428,7 +428,7 @@ dependencies = [ [[package]] name = "framework_uefi" -version = "0.3.0" +version = "0.4.0" dependencies = [ "framework_lib", "log", diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..4f1ae29c --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,303 @@ +# Example usage + +## Check firmware versions + +### Camera (Framework 12, Framework 13, Framework 16) + +Example on Framework 12: + +``` +> framework_tool --versions +[...] +Framework Laptop 12 Webcam Module + Firmware Version: 0.1.6 +``` + +Example on Framework 13: + +``` +> framework_tool --versions +[...] +Laptop Webcam Module (2nd Gen) + Firmware Version: 1.1.1 +``` + +### Touchscreen (Framework 12) + +``` +> framework_tool --versions +[...] +Touchscreen + Firmware Version: v7.0.0.5.0.0.0.0 + USI Protocol: false + MPP Protocol: true +``` + +### Stylus (Framework 12) + +``` +> sudo framework_tool --versions +[...] +Stylus + Serial Number: 28C1A00-12E71DAE + Vendor ID: 32AC (Framework Computer) + Product ID: 002B (Framework Stylus) + Firmware Version: FF.FF +[...] +``` + +### Touchpad (Framework 12, Framework 13, Framework 16) + +``` +> framework_tool --versions +[...] +Touchpad + IC Type: 0239 + Firmware Version: v0E07 +``` + +### Input modules (Framework 16) + +Shows firmware version and location of the modules. + +``` +> framework_tool --versions +[...] +Laptop 16 Numpad + Firmware Version: 0.2.9 + Location: [X] [ ] [ ] [ ] [ ] +Laptop 16 ANSI Keyboard + Firmware Version: 0.2.9 + Location: [ ] [ ] [X] [ ] [ ] +[...] +``` + +``` +> framework_tool --versions +[...] +LED Matrix + Firmware Version: 0.2.0 + Location: [X] [ ] [ ] [ ] [ ] +Laptop 16 ANSI Keyboard + Firmware Version: 0.2.9 + Location: [ ] [x] [ ] [ ] [ ] +LED Matrix + Firmware Version: 0.2.0 + Location: [ ] [ ] [ ] [ ] [x] +[...] +``` + +## Check input deck status + +### On Framework 12 + +``` +> framework_tool --inputdeck +Input Deck + Chassis Closed: true + Power Button Board: Present + Audio Daughterboard: Present + Touchpad: Present +``` + +### On Framework 13 + +``` +> framework_tool --inputdeck +Input Deck + Chassis Closed: true + Audio Daughterboard: Present + Touchpad: Present +``` + +### On Framework 16 + +``` +> framework_tool --inputdeck +Chassis Closed: true +Input Deck State: On +Touchpad present: true +Positions: + Pos 0: GenericC + Pos 1: KeyboardA + Pos 2: Disconnected + Pos 3: Disconnected + Pos 4: GenericC +``` + +## Check temperatures and fan speed + +``` +> sudo ./target/debug/framework_tool --thermal + F75303_Local: 43 C + F75303_CPU: 44 C + F75303_DDR: 39 C + APU: 62 C + Fan Speed: 0 RPM +``` + +## Check sensors (ALS and G-Sensor) + +``` +> sudo ./target/debug/framework_tool --sensors +ALS: 76 Lux +``` + +## Set custom fan duty/RPM + +``` +# Set a target fanduty of 100% (all or just fan ID=0) +> sudo framework_tool --fansetduty 100 +> sudo framework_tool --fansetduty 0 100 +> sudo framework_tool --thermal + F75303_Local: 40 C + F75303_CPU: 41 C + F75303_DDR: 37 C + APU: 42 C + Fan Speed: 7281 RPM + +# Set a target RPM (all or just fan ID=0) +> sudo framework_tool --fansetrpm 3141 +> sudo framework_tool --fansetrpm 0 3141 +> sudo framework_tool --thermal + F75303_Local: 41 C + F75303_CPU: 42 C + F75303_DDR: 37 C + APU: 44 C + Fan Speed: 3171 RPM + +# And back to normal +> sudo framework_tool --autofanctrl +> sudo framework_tool --thermal + F75303_Local: 40 C + F75303_CPU: 40 C + F75303_DDR: 38 C + APU: 42 C + Fan Speed: 0 RPM +``` + +## Check expansion bay (Framework 16) + +``` +> sudo framework_tool --expansion-bay +Expansion Bay + Enabled: true + No fault: true + Door closed: true + Board: DualInterposer + Serial Number: FRAXXXXXXXXXXXXXXX + +## Check charger and battery status (Framework 12/13/16) + +``` +> sudo framework_tool --power +Charger Status + AC is: not connected + Charger Voltage: 17048mV + Charger Current: 0mA + Chg Input Current:384mA + Battery SoC: 93% +Battery Status + AC is: not connected + Battery is: connected + Battery LFCC: 3693 mAh (Last Full Charge Capacity) + Battery Capacity: 3409 mAh + 58.96 Wh + Charge level: 92% + Battery discharging +``` + +Get more information + +``` +> sudo framework_tool --power -vv +Charger Status + AC is: not connected + Charger Voltage: 14824mV + Charger Current: 0mA + Chg Input Current:384mA + Battery SoC: 33% +Battery Status + AC is: not connected + Battery is: connected + Battery LFCC: 4021 mAh (Last Full Charge Capacity) + Battery Capacity: 1300 mAh + 19.267 Wh + Charge level: 32% + Manufacturer: NVT + Model Number: FRANGWA + Serial Number: 038F + Battery Type: LION + Present Voltage: 14.821 V + Present Rate: 943 mA + Design Capacity: 3915 mAh + 60.604 Wh + Design Voltage: 15.480 V + Cycle Count: 64 + Battery discharging +``` + +### Setting a custom charger current limit + +``` +# 1C = normal charging rate +# This means charging from 0 to 100% takes 1 hour +# Set charging rate to 0.8C +> sudo framework_tool --charge-rate-limit 0.8 + +# Limit charge current to the battery to to 2A +# In the output of `framework_tool --power -vv` above you can se "Design Capacity" +# Dividing that by 1h gives you the maximum charging current (1C) +# For example Design Capacity: 3915 mAh => 3915mA +> sudo framework_tool --charge-current-limit 2000 + +# And then plug in a power adapter +> sudo framework_tool --power +Charger Status + AC is: connected + Charger Voltage: 17800mV + Charger Current: 2000mA + 0.51C + Chg Input Current:3084mA + Battery SoC: 87% +Battery Status + AC is: connected + Battery is: connected + Battery LFCC: 3713 mAh (Last Full Charge Capacity) + Battery Capacity: 3215 mAh + 56.953 Wh + Charge level: 86% + Battery charging + +# Remove limit (set rate to 1C) +> sudo framework_tool --charge-rate-limit 1 + +# Back to normal +> sudo framework_tool --power +Charger Status + AC is: connected + Charger Voltage: 17800mV + Charger Current: 2740mA + 0.70C + Chg Input Current:3084mA + Battery SoC: 92% +Battery Status + AC is: connected + Battery is: connected + Battery LFCC: 3713 mAh (Last Full Charge Capacity) + Battery Capacity: 3387 mAh + 60.146 Wh + Charge level: 91% + Battery charging + +# Set charge rate/current limit only if battery is >80% charged +> sudo framework_tool --charge-rate-limit 80 0.8 +> sudo framework_tool --charge-current-limit 80 2000 +``` + +## Stylus (Framework 12) + +``` +> sudo framework_tool --stylus-battery +Stylus Battery Strength: 77% +``` diff --git a/README.md b/README.md index ded23458..0e89f34b 100644 --- a/README.md +++ b/README.md @@ -178,9 +178,10 @@ Options: --dump Dump extracted UX capsule bitmap image to a file --h2o-capsule Parse UEFI Capsule information from binary file --intrusion Show status of intrusion switch - --inputmodules Show status of the input modules (Framework 16 only) + --inputdeck Show status of the input deck --input-deck-mode Set input deck power mode [possible values: auto, off, on] (Framework 16 only) [possible values: auto, off, on] + --expansion-bay Show status of the expansion bay (Framework 16 only) --charge-limit [] Get or set max charge limit --get-gpio diff --git a/completions/bash/framework_tool b/completions/bash/framework_tool index 24f41d46..f96fcc23 100755 --- a/completions/bash/framework_tool +++ b/completions/bash/framework_tool @@ -34,8 +34,8 @@ _framework_tool() { "--flash-ro-ec" "--flash-rw-ec" "--intrusion" - "--inputmodules" - "--input-deck-mode" + "--inputdeck" + "--inputdeck-mode" "--charge-limit" "--get-gpio" "--fp-led-level" @@ -56,7 +56,7 @@ _framework_tool() { ) local devices=("bios" "ec" "pd0" "pd1" "rtm01" "rtm23" "ac-left" "ac-right") - local input_deck_modes=("auto" "off" "on") + local inputdeck_modes=("auto" "off" "on") local console_modes=("recent" "follow") local drivers=("portio" "cros-ec" "windows") local has_mec_options=("true" "false") @@ -71,8 +71,8 @@ _framework_tool() { COMPREPLY=( $(compgen -W "${options[*]}" -- "$current_word") ) elif [[ $prev_word == "--device" ]]; then COMPREPLY=( $(compgen -W "${devices[*]}" -- "$current_word") ) - elif [[ $prev_word == "--input-deck-mode" ]]; then - COMPREPLY=( $(compgen -W "${input_deck_modes[*]}" -- "$current_word") ) + elif [[ $prev_word == "--inputdeck-mode" ]]; then + COMPREPLY=( $(compgen -W "${inputdeck_modes[*]}" -- "$current_word") ) elif [[ $prev_word == "--console" ]]; then COMPREPLY=( $(compgen -W "${console_modes[*]}" -- "$current_word") ) elif [[ $prev_word == "--driver" ]]; then diff --git a/completions/zsh/_framework_tool b/completions/zsh/_framework_tool index 70ea7516..ad6aa668 100644 --- a/completions/zsh/_framework_tool +++ b/completions/zsh/_framework_tool @@ -32,8 +32,8 @@ options=( '--flash-ro-ec[Flash EC with new RO firmware from file]:flash_ro_ec' '--flash-rw-ec[Flash EC with new RW firmware from file]:flash_rw_ec' '--intrusion[Show status of intrusion switch]' - '--inputmodules[Show status of the input modules (Framework 16 only)]' - '--input-deck-mode[Set input deck power mode]:input_deck_mode:(auto off on)' + '--inputdeck[Show status of the input deck]' + '--inputdeck-mode[Set input deck power mode]:inputdeck_mode:(auto off on)' '--charge-limit[Get or set max charge limit]:charge_limit' '--get-gpio[Get GPIO value by name]:get_gpio' '--fp-led-level-gpio[Get or set fingerprint LED brightness level]:fp_led_level:(high medium low ultra-low auto)' diff --git a/framework_lib/Cargo.toml b/framework_lib/Cargo.toml index 27819125..b9a7cc91 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framework_lib" -version = "0.3.0" +version = "0.4.0" edition = "2021" # Minimum Supported Rust Version # Ubuntu 24.04 LTS ships 1.75 diff --git a/framework_lib/src/ccgx/hid.rs b/framework_lib/src/ccgx/hid.rs index e3e14dc0..64dcc748 100644 --- a/framework_lib/src/ccgx/hid.rs +++ b/framework_lib/src/ccgx/hid.rs @@ -256,6 +256,7 @@ pub fn find_devices(api: &HidApi, filter_devs: &[u16], sn: Option<&str>) -> Vec< let usage_page = dev_info.usage_page(); debug!("Found {:X}:{:X} Usage Page: {}", vid, pid, usage_page); + #[cfg(not(target_os = "freebsd"))] let usage_page_filter = usage_page == CCG_USAGE_PAGE; // On FreeBSD it seems we don't get different usage pages // There's just one entry overall diff --git a/framework_lib/src/chromium_ec/command.rs b/framework_lib/src/chromium_ec/command.rs index 0eaa9395..416fd3e0 100644 --- a/framework_lib/src/chromium_ec/command.rs +++ b/framework_lib/src/chromium_ec/command.rs @@ -28,16 +28,21 @@ pub enum EcCommands { /// Erase section of EC flash FlashErase = 0x13, FlashProtect = 0x15, + PwmSetFanTargetRpm = 0x0021, PwmGetKeyboardBacklight = 0x0022, PwmSetKeyboardBacklight = 0x0023, PwmSetFanDuty = 0x0024, PwmSetDuty = 0x0025, PwmGetDuty = 0x0026, SetTabletMode = 0x0031, + AutoFanCtrl = 0x0052, GpioGet = 0x0093, I2cPassthrough = 0x009e, ConsoleSnapshot = 0x0097, ConsoleRead = 0x0098, + ChargeState = 0x00A0, + ChargeCurrentLimit = 0x00A1, + HibernationDelay = 0x00A8, /// List the features supported by the firmware GetFeatures = 0x000D, /// Force reboot, causes host reboot as well @@ -46,6 +51,7 @@ pub enum EcCommands { RebootEc = 0x00D2, /// Get information about PD controller power UsbPdPowerInfo = 0x0103, + AdcRead = 0x0123, RgbKbdSetColor = 0x013A, RgbKbd = 0x013B, diff --git a/framework_lib/src/chromium_ec/commands.rs b/framework_lib/src/chromium_ec/commands.rs index 6bde4170..38cafeb6 100644 --- a/framework_lib/src/chromium_ec/commands.rs +++ b/framework_lib/src/chromium_ec/commands.rs @@ -187,6 +187,63 @@ impl EcRequest for EcRequestPwmGetKeyboardBac } } +#[repr(C, packed)] +pub struct EcRequestPwmSetFanTargetRpmV0 { + /// Duty cycle in percent + pub rpm: u32, +} + +impl EcRequest<()> for EcRequestPwmSetFanTargetRpmV0 { + fn command_id() -> EcCommands { + EcCommands::PwmSetFanTargetRpm + } +} + +pub struct EcRequestPwmSetFanTargetRpmV1 { + /// Fan RPM + pub rpm: u32, + /// Fan index + pub fan_idx: u32, +} + +impl EcRequest<()> for EcRequestPwmSetFanTargetRpmV1 { + fn command_id() -> EcCommands { + EcCommands::PwmSetFanTargetRpm + } + fn command_version() -> u8 { + 1 + } +} + +#[repr(C, packed)] +pub struct EcRequestPwmSetFanDutyV0 { + /// Duty cycle in percent + pub percent: u32, +} + +impl EcRequest<()> for EcRequestPwmSetFanDutyV0 { + fn command_id() -> EcCommands { + EcCommands::PwmSetFanDuty + } +} + +#[repr(C, packed)] +pub struct EcRequestPwmSetFanDutyV1 { + /// Duty cycle in percent + pub percent: u32, + /// Fan index + pub fan_idx: u32, +} + +impl EcRequest<()> for EcRequestPwmSetFanDutyV1 { + fn command_id() -> EcCommands { + EcCommands::PwmSetFanDuty + } + fn command_version() -> u8 { + 1 + } +} + pub const PWM_MAX_DUTY: u16 = 0xFFFF; #[repr(C, packed)] @@ -243,6 +300,30 @@ impl EcRequest<()> for EcRequestSetTabletMode { } } +#[repr(C, packed)] +pub struct EcRequestAutoFanCtrlV0 {} + +impl EcRequest<()> for EcRequestAutoFanCtrlV0 { + fn command_id() -> EcCommands { + EcCommands::AutoFanCtrl + } +} + +#[repr(C, packed)] +pub struct EcRequestAutoFanCtrlV1 { + /// Fan id + pub fan_idx: u8, +} + +impl EcRequest<()> for EcRequestAutoFanCtrlV1 { + fn command_id() -> EcCommands { + EcCommands::AutoFanCtrl + } + fn command_version() -> u8 { + 1 + } +} + #[repr(C, packed)] pub struct EcRequestGpioGetV0 { pub name: [u8; 32], @@ -296,6 +377,87 @@ impl EcRequest<()> for EcRequestConsoleRead { } } +#[repr(u8)] +pub enum ChargeStateCmd { + GetState = 0, + GetParam, + SetParam, + NumCmds, +} + +#[repr(C, packed)] +pub struct EcRequestChargeStateGetV0 { + pub cmd: u8, + pub param: u32, +} + +#[repr(C, packed)] +pub struct EcResponseChargeStateGetV0 { + pub ac: u32, + pub chg_voltage: u32, + pub chg_current: u32, + pub chg_input_current: u32, + pub batt_state_of_charge: u32, +} + +impl EcRequest for EcRequestChargeStateGetV0 { + fn command_id() -> EcCommands { + EcCommands::ChargeState + } + fn command_version() -> u8 { + 0 + } +} + +pub struct EcRequestCurrentLimitV0 { + /// Current limit in mA + pub current: u32, +} + +impl EcRequest<()> for EcRequestCurrentLimitV0 { + fn command_id() -> EcCommands { + EcCommands::ChargeCurrentLimit + } +} + +pub struct EcRequestCurrentLimitV1 { + /// Current limit in mA + pub current: u32, + /// Battery state of charge is the minimum charge percentage at which + /// the battery charge current limit will apply. + /// When not set, the limit will apply regardless of state of charge. + pub battery_soc: u8, +} + +impl EcRequest<()> for EcRequestCurrentLimitV1 { + fn command_id() -> EcCommands { + EcCommands::ChargeCurrentLimit + } + fn command_version() -> u8 { + 1 + } +} + +#[repr(C, packed)] +pub struct EcRequesetHibernationDelay { + /// Seconds in G3 after EC turns off, 0 to read current + pub seconds: u32, +} + +#[repr(C, packed)] +pub struct EcResponseHibernationDelay { + pub time_g3: u32, + pub time_remaining: u32, + /// How long to wait in G3 until turn off + pub hibernation_delay: u32, +} + +impl EcRequest for EcRequesetHibernationDelay { + fn command_id() -> EcCommands { + EcCommands::HibernationDelay + } +} + /// Supported features #[derive(Debug, FromPrimitive)] pub enum EcFeatureCode { @@ -518,6 +680,22 @@ impl EcRequest for EcRequestUsbPdPowerInfo { } } +#[repr(C, packed)] +pub struct EcRequestAdcRead { + /// ADC Channel, specific to each mainboard schematic + pub adc_channel: u8, +} + +pub struct EcResponseAdcRead { + pub adc_value: i32, +} + +impl EcRequest for EcRequestAdcRead { + fn command_id() -> EcCommands { + EcCommands::AdcRead + } +} + // TODO: Actually 128, but if we go above ~80 EC returns REQUEST_TRUNCATED // At least when I use the portio driver pub const EC_RGBKBD_MAX_KEY_COUNT: usize = 64; diff --git a/framework_lib/src/chromium_ec/mod.rs b/framework_lib/src/chromium_ec/mod.rs index acbd5c0c..e0a3094a 100644 --- a/framework_lib/src/chromium_ec/mod.rs +++ b/framework_lib/src/chromium_ec/mod.rs @@ -10,6 +10,7 @@ use crate::ec_binary; use crate::os_specific; +use crate::power; use crate::smbios; #[cfg(feature = "uefi")] use crate::uefi::shell_get_execution_break_flag; @@ -120,6 +121,86 @@ pub enum EcResponseStatus { Busy = 16, } +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub enum Framework12Adc { + MainboardBoardId, + PowerButtonBoardId, + Psys, + AdapterCurrent, + TouchpadBoardId, + AudioBoardId, +} + +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub enum FrameworkHx20Hx30Adc { + AdapterCurrent, + Psys, + BattTemp, + TouchpadBoardId, + MainboardBoardId, + AudioBoardId, +} + +/// So far on all Nuvoton/Zephyr EC based platforms +/// Until at least Framework 13 AMD Ryzen AI 300 +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub enum Framework13Adc { + MainboardBoardId, + Psys, + AdapterCurrent, + TouchpadBoardId, + AudioBoardId, + BattTemp, +} + +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub enum Framework16Adc { + MainboardBoardId, + HubBoardId, + GpuBoardId0, + GpuBoardId1, + AdapterCurrent, + Psys, +} + +/* + * PLATFORM_EC_ADC_RESOLUTION default 10 bit + * + * +------------------+-----------+----------+-------------+---------+----------------------+ + * | BOARD VERSION | voltage | NPC DB V | main board | GPU | Input module | + * +------------------+-----------+----------|-------------+---------+----------------------+ + * | BOARD_VERSION_0 | 0 mV | 100 mV | Unused | | Reserved | + * | BOARD_VERSION_1 | 173 mV | 310 mV | Unused | | Reserved | + * | BOARD_VERSION_2 | 300 mV | 520 mV | Unused | | Reserved | + * | BOARD_VERSION_3 | 430 mV | 720 mV | Unused | | Reserved | + * | BOARD_VERSION_4 | 588 mV | 930 mV | EVT1 | | Reserved | + * | BOARD_VERSION_5 | 783 mV | 1130 mV | Unused | | Reserved | + * | BOARD_VERSION_6 | 905 mV | 1340 mV | Unused | | Reserved | + * | BOARD_VERSION_7 | 1033 mV | 1550 mV | DVT1 | | Reserved | + * | BOARD_VERSION_8 | 1320 mV | 1750 mV | DVT2 | | Generic A size | + * | BOARD_VERSION_9 | 1500 mV | 1960 mV | PVT | | Generic B size | + * | BOARD_VERSION_10 | 1650 mV | 2170 mV | MP | | Generic C size | + * | BOARD_VERSION_11 | 1980 mV | 2370 mV | Unused | RID_0 | 10 Key B size | + * | BOARD_VERSION_12 | 2135 mV | 2580 mV | Unused | RID_0,1 | Keyboard | + * | BOARD_VERSION_13 | 2500 mV | 2780 mV | Unused | RID_0 | Touchpad | + * | BOARD_VERSION_14 | 2706 mV | 2990 mV | Unused | | Reserved | + * | BOARD_VERSION_15 | 2813 mV | 3200 mV | Unused | | Not installed | + * +------------------+-----------+----------+-------------+---------+----------------------+ + */ + +const BOARD_VERSION_COUNT: usize = 16; +const BOARD_VERSION: [i32; BOARD_VERSION_COUNT] = [ + 85, 233, 360, 492, 649, 844, 965, 1094, 1380, 1562, 1710, 2040, 2197, 2557, 2766, 2814, +]; + +const BOARD_VERSION_NPC_DB: [i32; BOARD_VERSION_COUNT] = [ + 100, 311, 521, 721, 931, 1131, 1341, 1551, 1751, 1961, 2171, 2370, 2580, 2780, 2990, 3200, +]; + pub fn has_mec() -> bool { let platform = smbios::get_platform().unwrap(); if let Platform::GenericFramework(_, _, has_mec) = platform { @@ -314,6 +395,42 @@ impl CrosEc { Ok((limits.min_percentage, limits.max_percentage)) } + pub fn set_charge_current_limit(&self, current: u32, battery_soc: Option) -> EcResult<()> { + if let Some(battery_soc) = battery_soc { + let battery_soc = battery_soc as u8; + EcRequestCurrentLimitV1 { + current, + battery_soc, + } + .send_command(self) + } else { + EcRequestCurrentLimitV0 { current }.send_command(self) + } + } + + pub fn set_charge_rate_limit(&self, rate: f32, battery_soc: Option) -> EcResult<()> { + let power_info = power::power_info(self).ok_or(EcError::DeviceError( + "Failed to get battery info".to_string(), + ))?; + let battery = power_info + .battery + .ok_or(EcError::DeviceError("No battery present".to_string()))?; + println!("Requested Rate: {}C", rate); + println!("Design Current: {}mA", battery.design_capacity); + let current = (rate * (battery.design_capacity as f32)) as u32; + println!("Limiting Current to: {}mA", current); + if let Some(battery_soc) = battery_soc { + let battery_soc = battery_soc as u8; + EcRequestCurrentLimitV1 { + current, + battery_soc, + } + .send_command(self) + } else { + EcRequestCurrentLimitV0 { current }.send_command(self) + } + } + pub fn set_fp_led_percentage(&self, percentage: u8) -> EcResult<()> { // Sending bytes manually because the Set command, as opposed to the Get command, // does not return any data @@ -394,6 +511,65 @@ impl CrosEc { Ok(InputDeckStatus::from(status)) } + pub fn print_fw12_inputdeck_status(&self) -> EcResult<()> { + let intrusion = self.get_intrusion_status()?; + let pwrbtn = self.read_board_id(Framework12Adc::PowerButtonBoardId as u8)?; + let audio = self.read_board_id(Framework12Adc::AudioBoardId as u8)?; + let tp = self.read_board_id(Framework12Adc::TouchpadBoardId as u8)?; + + let is_present = |p| if p { "Present" } else { "Missing" }; + + println!("Input Deck"); + println!(" Chassis Closed: {}", intrusion.currently_open); + println!(" Power Button Board: {}", is_present(pwrbtn.is_some())); + println!(" Audio Daughterboard: {}", is_present(audio.is_some())); + println!(" Touchpad: {}", is_present(tp.is_some())); + + Ok(()) + } + + pub fn print_fw13_inputdeck_status(&self) -> EcResult<()> { + let intrusion = self.get_intrusion_status()?; + + let (audio, tp) = match smbios::get_platform() { + Some(Platform::IntelGen11) + | Some(Platform::IntelGen12) + | Some(Platform::IntelGen13) => ( + self.read_board_id(FrameworkHx20Hx30Adc::AudioBoardId as u8)?, + self.read_board_id(FrameworkHx20Hx30Adc::TouchpadBoardId as u8)?, + ), + + _ => ( + self.read_board_id_npc_db(Framework13Adc::AudioBoardId as u8)?, + self.read_board_id_npc_db(Framework13Adc::TouchpadBoardId as u8)?, + ), + }; + + let is_present = |p| if p { "Present" } else { "Missing" }; + + println!("Input Deck"); + println!(" Chassis Closed: {}", intrusion.currently_open); + println!(" Audio Daughterboard: {}", is_present(audio.is_some())); + println!(" Touchpad: {}", is_present(tp.is_some())); + + Ok(()) + } + + pub fn print_fw16_inputdeck_status(&self) -> EcResult<()> { + let intrusion = self.get_intrusion_status()?; + let status = self.get_input_deck_status()?; + println!("Chassis Closed: {}", intrusion.currently_open); + println!("Input Deck State: {:?}", status.state); + println!("Touchpad present: {}", status.touchpad_present); + println!("Positions:"); + println!(" Pos 0: {:?}", status.top_row.pos0); + println!(" Pos 1: {:?}", status.top_row.pos1); + println!(" Pos 2: {:?}", status.top_row.pos2); + println!(" Pos 3: {:?}", status.top_row.pos3); + println!(" Pos 4: {:?}", status.top_row.pos4); + Ok(()) + } + pub fn set_input_deck_mode(&self, mode: DeckStateMode) -> EcResult { let status = EcRequestDeckState { mode }.send_command(self)?; @@ -426,6 +602,33 @@ impl CrosEc { Ok((kblight.duty / (PWM_MAX_DUTY / 100)) as u8) } + pub fn fan_set_rpm(&self, fan: Option, rpm: u32) -> EcResult<()> { + if let Some(fan_idx) = fan { + EcRequestPwmSetFanTargetRpmV1 { rpm, fan_idx }.send_command(self) + } else { + EcRequestPwmSetFanTargetRpmV0 { rpm }.send_command(self) + } + } + + pub fn fan_set_duty(&self, fan: Option, percent: u32) -> EcResult<()> { + if percent > 100 { + return Err(EcError::DeviceError("Fan duty must be <= 100".to_string())); + } + if let Some(fan_idx) = fan { + EcRequestPwmSetFanDutyV1 { fan_idx, percent }.send_command(self) + } else { + EcRequestPwmSetFanDutyV0 { percent }.send_command(self) + } + } + + pub fn autofanctrl(&self, fan: Option) -> EcResult<()> { + if let Some(fan_idx) = fan { + EcRequestAutoFanCtrlV1 { fan_idx }.send_command(self) + } else { + EcRequestAutoFanCtrlV0 {}.send_command(self) + } + } + /// Set tablet mode pub fn set_tablet_mode(&self, mode: TabletModeOverride) { let mode = mode as u8; @@ -790,6 +993,27 @@ impl CrosEc { res } + pub fn check_bay_status(&self) -> EcResult<()> { + println!("Expansion Bay"); + + let info = EcRequestExpansionBayStatus {}.send_command(self)?; + println!(" Enabled: {}", info.module_enabled()); + println!(" No fault: {}", !info.module_fault()); + println!(" Door closed: {}", info.hatch_switch_closed()); + match info.expansion_bay_board() { + Ok(board) => println!(" Board: {:?}", board), + Err(err) => println!(" Board: {:?}", err), + } + + if let Ok(sn) = self.get_gpu_serial() { + println!(" Serial Number: {}", sn); + } else { + println!(" Serial Number: Unknown"); + } + + Ok(()) + } + /// Get the GPU Serial /// pub fn get_gpu_serial(&self) -> EcResult { @@ -820,24 +1044,26 @@ impl CrosEc { Ok(result.valid) } - /// Requests recent console output from EC and constantly asks for more + /// Requests console output from EC and constantly asks for more /// Prints the output and returns it when an error is encountered - pub fn console_read(&self) -> EcResult { - let mut console = String::new(); + pub fn console_read(&self) -> EcResult<()> { + EcRequestConsoleSnapshot {}.send_command(self)?; + let mut cmd = EcRequestConsoleRead { - subcmd: ConsoleReadSubCommand::ConsoleReadRecent as u8, + subcmd: ConsoleReadSubCommand::ConsoleReadNext as u8, }; - - EcRequestConsoleSnapshot {}.send_command(self)?; loop { match cmd.send_command_vec(self) { Ok(data) => { - // EC Buffer is empty. We can wait a bit and see if there's more - // Can't run it too quickly, otherwise the commands might fail - if data.is_empty() { - trace!("Empty EC response"); - println!("---"); - os_specific::sleep(1_000_000); // 1s + // EC Buffer is empty. That means we've read everything from the snapshot + // The windows crosecbus driver returns all NULL instead of empty response + if data.is_empty() || data.iter().all(|x| *x == 0) { + debug!("Empty EC response. Stopping console read"); + // Don't read too fast, wait a second before reading more + os_specific::sleep(1_000_000); + EcRequestConsoleSnapshot {}.send_command(self)?; + cmd.subcmd = ConsoleReadSubCommand::ConsoleReadRecent as u8; + continue; } let utf8 = std::str::from_utf8(&data).unwrap(); @@ -846,40 +1072,99 @@ impl CrosEc { .replace(['\0'], ""); print!("{}", ascii); - console.push_str(ascii.as_str()); } Err(err) => { error!("Err: {:?}", err); - return Ok(console); - //return Err(err) + return Err(err); } }; - cmd.subcmd = ConsoleReadSubCommand::ConsoleReadNext as u8; // Need to explicitly handle CTRL-C termination on UEFI Shell #[cfg(feature = "uefi")] if shell_get_execution_break_flag() { - return Ok(console); + return Ok(()); } } } + /// Read all of EC console buffer and return it pub fn console_read_one(&self) -> EcResult { EcRequestConsoleSnapshot {}.send_command(self)?; - let data = EcRequestConsoleRead { - subcmd: ConsoleReadSubCommand::ConsoleReadRecent as u8, + + let mut console = String::new(); + let cmd = EcRequestConsoleRead { + subcmd: ConsoleReadSubCommand::ConsoleReadNext as u8, + }; + loop { + match cmd.send_command_vec(self) { + Ok(data) => { + // EC Buffer is empty. That means we've read everything + // The windows crosecbus driver returns all NULL instead of empty response + if data.is_empty() || data.iter().all(|x| *x == 0) { + debug!("Empty EC response. Stopping console read"); + return Ok(console); + } + + let utf8 = std::str::from_utf8(&data).unwrap(); + let ascii = utf8 + .replace(|c: char| !c.is_ascii(), "") + .replace(['\0'], ""); + + console.push_str(ascii.as_str()); + } + Err(err) => { + error!("Err: {:?}", err); + return Err(err); + } + }; + } + } + + pub fn get_charge_state(&self, power_info: &power::PowerInfo) -> EcResult<()> { + let res = EcRequestChargeStateGetV0 { + cmd: ChargeStateCmd::GetState as u8, + param: 0, } - .send_command_vec(self)?; - let utf8 = std::str::from_utf8(&data).unwrap(); - let ascii = utf8 - .replace(|c: char| !c.is_ascii(), "") - .replace(['\0'], ""); - Ok(ascii) + .send_command(self)?; + println!("Charger Status"); + println!( + " AC is: {}", + if res.ac == 1 { + "connected" + } else { + "not connected" + } + ); + println!(" Charger Voltage: {}mV", { res.chg_voltage }); + println!(" Charger Current: {}mA", { res.chg_current }); + if let Some(battery) = &power_info.battery { + let charge_rate = (res.chg_current as f32) / (battery.design_capacity as f32); + println!(" {:.2}C", charge_rate); + } + println!(" Chg Input Current:{}mA", { res.chg_input_current }); + println!(" Battery SoC: {}%", { res.batt_state_of_charge }); + + Ok(()) + } + + pub fn set_ec_hib_delay(&self, seconds: u32) -> EcResult<()> { + EcRequesetHibernationDelay { seconds }.send_command(self)?; + Ok(()) + } + + pub fn get_ec_hib_delay(&self) -> EcResult { + let res = EcRequesetHibernationDelay { seconds: 0 }.send_command(self)?; + debug!("Time in G3: {:?}", { res.time_g3 }); + debug!("Time remaining: {:?}", { res.time_remaining }); + println!("EC Hibernation Delay: {:?}s", { res.hibernation_delay }); + Ok(res.hibernation_delay) } /// Check features supported by the firmware pub fn get_features(&self) -> EcResult<()> { let data = EcRequestGetFeatures {}.send_command(self)?; + println!(" ID | Name | Enabled?"); + println!(" -- | --------------------------- | --------"); for i in 0..64 { let byte = i / 32; let bit = i % 32; @@ -887,7 +1172,8 @@ impl CrosEc { let feat: Option = FromPrimitive::from_usize(i); if let Some(feat) = feat { - println!("{:>2}: {:>5} {:?}", i, val, feat); + let name = format!("{:?}", feat); + println!(" {:>2} | {:<27} | {:>5}", i, name, val); } } @@ -953,6 +1239,50 @@ impl CrosEc { Ok(res.val == 1) } + pub fn adc_read(&self, adc_channel: u8) -> EcResult { + let res = EcRequestAdcRead { adc_channel }.send_command(self)?; + Ok(res.adc_value) + } + + fn read_board_id(&self, channel: u8) -> EcResult> { + self.read_board_id_raw(channel, BOARD_VERSION) + } + fn read_board_id_npc_db(&self, channel: u8) -> EcResult> { + self.read_board_id_raw(channel, BOARD_VERSION_NPC_DB) + } + + fn read_board_id_raw( + &self, + channel: u8, + table: [i32; BOARD_VERSION_COUNT], + ) -> EcResult> { + let mv = self.adc_read(channel)?; + if mv < 0 { + return Err(EcError::DeviceError(format!( + "Failed to read ADC channel {}", + channel + ))); + } + + debug!("ADC Channel {} - Measured {}mv", channel, mv); + for (board_id, board_id_res) in table.iter().enumerate() { + if mv < *board_id_res { + debug!("ADC Channel {} - Board ID {}", channel, board_id); + // 15 is not present, less than 2 is undefined + return Ok(if board_id == 15 || board_id < 2 { + None + } else { + Some(board_id as u8) + }); + } + } + + Err(EcError::DeviceError(format!( + "Unknown board id. ADC mv: {}", + mv + ))) + } + pub fn rgbkbd_set_color(&self, start_key: u8, colors: Vec) -> EcResult<()> { for (chunk, colors) in colors.chunks(EC_RGBKBD_MAX_KEY_COUNT).enumerate() { let mut request = EcRequestRgbKbdSetColor { diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index f75f23ef..3c81c755 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -57,6 +57,20 @@ struct ClapCli { #[arg(long)] sensors: bool, + /// Set fan duty cycle (0-100%) + #[clap(num_args=..=2)] + #[arg(long)] + fansetduty: Vec, + + /// Set fan RPM (limited by EC fan table max RPM) + #[clap(num_args=..=2)] + #[arg(long)] + fansetrpm: Vec, + + /// Turn on automatic fan speed control + #[arg(long)] + autofanctrl: bool, + /// Show information about USB-C PD ports #[arg(long)] pdports: bool, @@ -127,16 +141,30 @@ struct ClapCli { /// Show status of the input modules (Framework 16 only) #[arg(long)] - inputmodules: bool, + inputdeck: bool, /// Set input deck power mode [possible values: auto, off, on] (Framework 16 only) #[arg(long)] - input_deck_mode: Option, + inputdeck_mode: Option, + + /// Show status of the expansion bay (Framework 16 only) + #[arg(long)] + expansion_bay: bool, /// Get or set max charge limit #[arg(long)] charge_limit: Option>, + /// Get or set max charge current limit + #[arg(long)] + #[clap(num_args = ..2)] + charge_current_limit: Vec, + + /// Get or set max charge current limit + #[arg(long)] + #[clap(num_args = ..2)] + charge_rate_limit: Vec, + /// Get GPIO value by name #[arg(long)] get_gpio: Option, @@ -170,6 +198,11 @@ struct ClapCli { #[arg(long)] touchscreen_enable: Option, + /// Check stylus battery level (USI 2.0 stylus only) + #[clap(value_enum)] + #[arg(long)] + stylus_battery: bool, + /// Get EC console, choose whether recent or to follow the output #[clap(value_enum)] #[arg(long)] @@ -180,6 +213,11 @@ struct ClapCli { #[arg(long)] reboot_ec: Option, + /// Get or set EC hibernate delay (S5 to G3) + #[clap(value_enum)] + #[arg(long)] + ec_hib_delay: Option>, + /// Hash a file of arbitrary data #[arg(long)] hash: Option, @@ -277,6 +315,29 @@ pub fn parse(args: &[String]) -> Cli { // Checked by clap _ => unreachable!(), }; + let fansetduty = match args.fansetduty.len() { + 2 => Some((Some(args.fansetduty[0]), args.fansetduty[1])), + 1 => Some((None, args.fansetduty[0])), + _ => None, + }; + let fansetrpm = match args.fansetrpm.len() { + 2 => Some((Some(args.fansetrpm[0]), args.fansetrpm[1])), + 1 => Some((None, args.fansetrpm[0])), + _ => None, + }; + let charge_current_limit = match args.charge_current_limit.len() { + 2 => Some(( + args.charge_current_limit[0], + Some(args.charge_current_limit[1]), + )), + 1 => Some((args.charge_current_limit[0], None)), + _ => None, + }; + let charge_rate_limit = match args.charge_rate_limit.len() { + 2 => Some((args.charge_rate_limit[0], Some(args.charge_rate_limit[1]))), + 1 => Some((args.charge_rate_limit[0], None)), + _ => None, + }; Cli { verbosity: args.verbosity.log_level_filter(), @@ -289,6 +350,9 @@ pub fn parse(args: &[String]) -> Cli { power: args.power, thermal: args.thermal, sensors: args.sensors, + fansetduty, + fansetrpm, + autofanctrl: args.autofanctrl, pdports: args.pdports, pd_info: args.pd_info, dp_hdmi_info: args.dp_hdmi_info, @@ -323,9 +387,12 @@ pub fn parse(args: &[String]) -> Cli { .flash_rw_ec .map(|x| x.into_os_string().into_string().unwrap()), intrusion: args.intrusion, - inputmodules: args.inputmodules, - input_deck_mode: args.input_deck_mode, + inputdeck: args.inputdeck, + inputdeck_mode: args.inputdeck_mode, + expansion_bay: args.expansion_bay, charge_limit: args.charge_limit, + charge_current_limit, + charge_rate_limit, get_gpio: args.get_gpio, fp_led_level: args.fp_led_level, fp_brightness: args.fp_brightness, @@ -333,8 +400,10 @@ pub fn parse(args: &[String]) -> Cli { rgbkbd: args.rgbkbd, tablet_mode: args.tablet_mode, touchscreen_enable: args.touchscreen_enable, + stylus_battery: args.stylus_battery, console: args.console, reboot_ec: args.reboot_ec, + ec_hib_delay: args.ec_hib_delay, hash: args.hash.map(|x| x.into_os_string().into_string().unwrap()), driver: args.driver, pd_addrs, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 6a73d655..87b08e3f 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -46,6 +46,8 @@ use crate::chromium_ec::{EcError, EcResult}; use crate::csme; use crate::ec_binary; use crate::esrt; +#[cfg(feature = "rusb")] +use crate::inputmodule::check_inputmodule_version; use crate::power; use crate::smbios; use crate::smbios::ConfigDigit0; @@ -57,7 +59,7 @@ use crate::touchscreen; #[cfg(feature = "uefi")] use crate::uefi::enable_page_break; use crate::util; -use crate::util::{Config, Platform}; +use crate::util::{Config, Platform, PlatformFamily}; #[cfg(feature = "hidapi")] use hidapi::HidApi; use sha2::{Digest, Sha256, Sha384, Sha512}; @@ -148,6 +150,9 @@ pub struct Cli { pub power: bool, pub thermal: bool, pub sensors: bool, + pub fansetduty: Option<(Option, u32)>, + pub fansetrpm: Option<(Option, u32)>, + pub autofanctrl: bool, pub pdports: bool, pub privacy: bool, pub pd_info: bool, @@ -166,9 +171,12 @@ pub struct Cli { pub driver: Option, pub test: bool, pub intrusion: bool, - pub inputmodules: bool, - pub input_deck_mode: Option, + pub inputdeck: bool, + pub inputdeck_mode: Option, + pub expansion_bay: bool, pub charge_limit: Option>, + pub charge_current_limit: Option<(u32, Option)>, + pub charge_rate_limit: Option<(f32, Option)>, pub get_gpio: Option, pub fp_led_level: Option>, pub fp_brightness: Option>, @@ -176,8 +184,10 @@ pub struct Cli { pub rgbkbd: Vec, pub tablet_mode: Option, pub touchscreen_enable: Option, + pub stylus_battery: bool, pub console: Option, pub reboot_ec: Option, + pub ec_hib_delay: Option>, pub hash: Option, pub pd_addrs: Option<(u16, u16)>, pub pd_ports: Option<(u8, u8)>, @@ -328,6 +338,18 @@ fn active_mode(mode: &FwMode, reference: FwMode) -> &'static str { } } +#[cfg(feature = "hidapi")] +fn print_stylus_battery_level() { + loop { + if let Some(level) = touchscreen::get_battery_level() { + println!("Stylus Battery Strength: {}%", level); + return; + } else { + debug!("Stylus Battery Strength: Unknown"); + } + } +} + fn print_versions(ec: &CrosEc) { println!("UEFI BIOS"); if let Some(smbios) = get_smbios() { @@ -420,50 +442,46 @@ fn print_versions(ec: &CrosEc) { println!(" Unknown") } - println!("Retimers"); - let mut found_retimer = false; + let has_retimer = matches!( + smbios::get_platform(), + Some(Platform::IntelGen11) + | Some(Platform::IntelGen12) + | Some(Platform::IntelGen13) + | Some(Platform::IntelCoreUltra1) + ); + let mut left_retimer: Option = None; + let mut right_retimer: Option = None; if let Some(esrt) = esrt::get_esrt() { for entry in &esrt.entries { - match entry.fw_class { - esrt::TGL_RETIMER01_GUID - | esrt::TGL_RETIMER23_GUID - | esrt::ADL_RETIMER01_GUID - | esrt::ADL_RETIMER23_GUID - | esrt::RPL_RETIMER01_GUID - | esrt::RPL_RETIMER23_GUID - | esrt::MTL_RETIMER01_GUID - | esrt::MTL_RETIMER23_GUID => { - if !found_retimer { - found_retimer = true; - } - } - _ => {} - } match entry.fw_class { esrt::TGL_RETIMER01_GUID | esrt::ADL_RETIMER01_GUID | esrt::RPL_RETIMER01_GUID | esrt::MTL_RETIMER01_GUID => { - println!( - " Left: 0x{:X} ({})", - entry.fw_version, entry.fw_version - ); + left_retimer = Some(entry.fw_version); } esrt::TGL_RETIMER23_GUID | esrt::ADL_RETIMER23_GUID | esrt::RPL_RETIMER23_GUID | esrt::MTL_RETIMER23_GUID => { - println!( - " Right: 0x{:X} ({})", - entry.fw_version, entry.fw_version - ); + right_retimer = Some(entry.fw_version); } _ => {} } } } - if !found_retimer { - println!(" Unknown"); + if has_retimer { + println!("Retimers"); + if let Some(fw_version) = left_retimer { + println!(" Left: 0x{:X} ({})", fw_version, fw_version); + } + if let Some(fw_version) = right_retimer { + println!(" Right: 0x{:X} ({})", fw_version, fw_version); + } + if left_retimer.is_none() && right_retimer.is_none() { + // This means there's a bug, we should've found one but didn't + println!(" Unknown"); + } } #[cfg(feature = "linux")] @@ -481,11 +499,16 @@ fn print_versions(ec: &CrosEc) { #[cfg(feature = "rusb")] let _ignore_err = check_camera_version(); + #[cfg(feature = "rusb")] + let _ignore_err = check_inputmodule_version(); + #[cfg(feature = "hidapi")] let _ignore_err = print_touchpad_fw_ver(); #[cfg(feature = "hidapi")] - let _ignore_err = touchscreen::print_fw_ver(); + if let Some(Platform::Framework12IntelGen13) = smbios::get_platform() { + let _ignore_err = touchscreen::print_fw_ver(); + } } fn print_esrt() { @@ -738,25 +761,27 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } else { println!(" Unable to tell"); } - } else if args.inputmodules { - println!("Input Module Status:"); - if let Some(status) = print_err(ec.get_input_deck_status()) { - println!("Input Deck State: {:?}", status.state); - println!("Touchpad present: {:?}", status.touchpad_present); - println!("Positions:"); - println!(" Pos 0: {:?}", status.top_row.pos0); - println!(" Pos 1: {:?}", status.top_row.pos1); - println!(" Pos 2: {:?}", status.top_row.pos2); - println!(" Pos 3: {:?}", status.top_row.pos3); - println!(" Pos 4: {:?}", status.top_row.pos4); - } else { - println!(" Unable to tell"); - } - } else if let Some(mode) = &args.input_deck_mode { + } else if args.inputdeck { + let res = match smbios::get_platform().and_then(Platform::which_family) { + Some(PlatformFamily::Framework12) => ec.print_fw12_inputdeck_status(), + Some(PlatformFamily::Framework13) => ec.print_fw13_inputdeck_status(), + Some(PlatformFamily::Framework16) => ec.print_fw16_inputdeck_status(), + _ => Ok(()), + }; + print_err(res); + } else if let Some(mode) = &args.inputdeck_mode { println!("Set mode to: {:?}", mode); ec.set_input_deck_mode((*mode).into()).unwrap(); + } else if args.expansion_bay { + if let Err(err) = ec.check_bay_status() { + error!("{:?}", err); + } } else if let Some(maybe_limit) = args.charge_limit { print_err(handle_charge_limit(&ec, maybe_limit)); + } else if let Some((limit, soc)) = args.charge_current_limit { + print_err(ec.set_charge_current_limit(limit, soc)); + } else if let Some((limit, soc)) = args.charge_rate_limit { + print_err(ec.set_charge_rate_limit(limit, soc)); } else if let Some(gpio_name) = &args.get_gpio { print!("Getting GPIO value {}: ", gpio_name); if let Ok(value) = ec.get_gpio(gpio_name) { @@ -805,6 +830,11 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { if touchscreen::enable_touch(*_enable).is_none() { error!("Failed to enable/disable touch"); } + } else if args.stylus_battery { + #[cfg(feature = "hidapi")] + print_stylus_battery_level(); + #[cfg(not(feature = "hidapi"))] + error!("Not build with hidapi feature"); } else if let Some(console_arg) = &args.console { match console_arg { ConsoleArg::Follow => { @@ -839,6 +869,11 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { Err(err) => println!("Failed: {:?}", err), }, } + } else if let Some(delay) = &args.ec_hib_delay { + if let Some(delay) = delay { + print_err(ec.set_ec_hib_delay(*delay)); + } + print_err(ec.get_ec_hib_delay()); } else if args.test { println!("Self-Test"); let result = selftest(&ec); @@ -852,6 +887,12 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { power::print_thermal(&ec); } else if args.sensors { power::print_sensors(&ec); + } else if let Some((fan, percent)) = args.fansetduty { + print_err(ec.fan_set_duty(fan, percent)); + } else if let Some((fan, rpm)) = args.fansetrpm { + print_err(ec.fan_set_rpm(fan, rpm)); + } else if args.autofanctrl { + print_err(ec.autofanctrl(None)); } else if args.pdports { power::get_and_print_pd_info(&ec); } else if args.info { @@ -1048,6 +1089,9 @@ Options: --power Show current power status (battery and AC) --thermal Print thermal information (Temperatures and Fan speed) --sensors Print sensor information (ALS, G-Sensor) + --fansetduty Set fan duty cycle (0-100%) + --fansetrpm Set fan RPM (limited by EC fan table max RPM) + --autofanctrl Turn on automatic fan speed control --pdports Show information about USB-C PD ports --info Show info from SMBIOS (Only on UEFI) --pd-info Show details about the PD controllers @@ -1063,9 +1107,11 @@ Options: --flash-rw-ec Flash EC with new firmware from file --reboot-ec Control EC RO/RW jump [possible values: reboot, jump-ro, jump-rw, cancel-jump, disable-jump] --intrusion Show status of intrusion switch - --inputmodules Show status of the input modules (Framework 16 only) - --input-deck-mode Set input deck power mode [possible values: auto, off, on] (Framework 16 only) + --inputdeck Show status of the input deck + --inputdeck-mode Set input deck power mode [possible values: auto, off, on] (Framework 16 only) + --expansion-bay Show status of the expansion bay (Framework 16 only) --charge-limit [] Get or set battery charge limit (Percentage number as arg, e.g. '100') + --charge-current-limit [] Get or set battery current charge limit (Percentage number as arg, e.g. '100') --get-gpio Get GPIO value by name --fp-led-level [] Get or set fingerprint LED brightness level [possible values: high, medium, low] --fp-brightness []Get or set fingerprint LED brightness percentage diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 54a79fc2..626f0bd0 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -65,6 +65,9 @@ pub fn parse(args: &[String]) -> Cli { power: false, thermal: false, sensors: false, + fansetduty: None, + fansetrpm: None, + autofanctrl: false, pdports: false, pd_info: false, dp_hdmi_info: false, @@ -81,9 +84,12 @@ pub fn parse(args: &[String]) -> Cli { dump: None, h2o_capsule: None, intrusion: false, - inputmodules: false, - input_deck_mode: None, + inputdeck: false, + inputdeck_mode: None, + expansion_bay: false, charge_limit: None, + charge_current_limit: None, + charge_rate_limit: None, get_gpio: None, fp_led_level: None, fp_brightness: None, @@ -91,8 +97,10 @@ pub fn parse(args: &[String]) -> Cli { rgbkbd: vec![], tablet_mode: None, touchscreen_enable: None, + stylus_battery: false, console: None, reboot_ec: None, + ec_hib_delay: None, hash: None, // This is the only driver that works on UEFI driver: Some(CrosEcDriverType::Portio), @@ -148,6 +156,67 @@ pub fn parse(args: &[String]) -> Cli { } else if arg == "--sensors" { cli.sensors = true; found_an_option = true; + } else if arg == "--fansetduty" { + cli.fansetduty = if args.len() > i + 2 { + let fan_idx = args[i + 1].parse::(); + let duty = args[i + 2].parse::(); + if let (Ok(fan_idx), Ok(duty)) = (fan_idx, duty) { + Some((Some(fan_idx), duty)) + } else { + println!( + "Invalid values for --fansetduty: '{} {}'. Must be u32 integers.", + args[i + 1], + args[i + 2] + ); + None + } + } else if args.len() > i + 1 { + if let Ok(duty) = args[i + 1].parse::() { + Some((None, duty)) + } else { + println!( + "Invalid values for --fansetduty: '{}'. Must be 0-100.", + args[i + 1], + ); + None + } + } else { + println!("--fansetduty requires one or two. [fan id] [duty] or [duty]"); + None + }; + found_an_option = true; + } else if arg == "--fansetrpm" { + cli.fansetrpm = if args.len() > i + 2 { + let fan_idx = args[i + 1].parse::(); + let rpm = args[i + 2].parse::(); + if let (Ok(fan_idx), Ok(rpm)) = (fan_idx, rpm) { + Some((Some(fan_idx), rpm)) + } else { + println!( + "Invalid values for --fansetrpm: '{} {}'. Must be u32 integers.", + args[i + 1], + args[i + 2] + ); + None + } + } else if args.len() > i + 1 { + if let Ok(rpm) = args[i + 1].parse::() { + Some((None, rpm)) + } else { + println!( + "Invalid values for --fansetrpm: '{}'. Must be an integer.", + args[i + 1], + ); + None + } + } else { + println!("--fansetrpm requires one or two. [fan id] [rpm] or [rpm]"); + None + }; + found_an_option = true; + } else if arg == "--autofanctrol" { + cli.autofanctrl = true; + found_an_option = true; } else if arg == "--pdports" { cli.pdports = true; found_an_option = true; @@ -160,29 +229,32 @@ pub fn parse(args: &[String]) -> Cli { } else if arg == "--intrusion" { cli.intrusion = true; found_an_option = true; - } else if arg == "--inputmodules" { - cli.inputmodules = true; + } else if arg == "--inputdeck" { + cli.inputdeck = true; found_an_option = true; - } else if arg == "--input-deck-mode" { - cli.input_deck_mode = if args.len() > i + 1 { - let input_deck_mode = &args[i + 1]; - if input_deck_mode == "auto" { + } else if arg == "--inputdeck-mode" { + cli.inputdeck_mode = if args.len() > i + 1 { + let inputdeck_mode = &args[i + 1]; + if inputdeck_mode == "auto" { Some(InputDeckModeArg::Auto) - } else if input_deck_mode == "off" { + } else if inputdeck_mode == "off" { Some(InputDeckModeArg::Off) - } else if input_deck_mode == "on" { + } else if inputdeck_mode == "on" { Some(InputDeckModeArg::On) } else { - println!("Invalid value for --input-deck-mode: {}", input_deck_mode); + println!("Invalid value for --inputdeck-mode: {}", inputdeck_mode); None } } else { println!( - "Need to provide a value for --input-deck-mode. Either `auto`, `off`, or `on`" + "Need to provide a value for --inputdeck-mode. Either `auto`, `off`, or `on`" ); None }; found_an_option = true; + } else if arg == "--expansion-bay" { + cli.expansion_bay = true; + found_an_option = true; } else if arg == "--charge-limit" { cli.charge_limit = if args.len() > i + 1 { if let Ok(percent) = args[i + 1].parse::() { @@ -198,6 +270,64 @@ pub fn parse(args: &[String]) -> Cli { Some(None) }; found_an_option = true; + } else if arg == "--charge-current-limit" { + cli.charge_current_limit = if args.len() > i + 2 { + let limit = args[i + 1].parse::(); + let soc = args[i + 2].parse::(); + if let (Ok(limit), Ok(soc)) = (limit, soc) { + Some((limit, Some(soc))) + } else { + println!( + "Invalid values for --charge-current-limit: '{} {}'. Must be u32 integers.", + args[i + 1], + args[i + 2] + ); + None + } + } else if args.len() > i + 1 { + if let Ok(limit) = args[i + 1].parse::() { + Some((limit, None)) + } else { + println!( + "Invalid values for --charge-current-limit: '{}'. Must be an integer.", + args[i + 1], + ); + None + } + } else { + println!("--charge-current-limit requires one or two. [limit] [soc] or [limit]"); + None + }; + found_an_option = true; + } else if arg == "--charge-rate-limit" { + cli.charge_rate_limit = if args.len() > i + 2 { + let limit = args[i + 1].parse::(); + let soc = args[i + 2].parse::(); + if let (Ok(limit), Ok(soc)) = (limit, soc) { + Some((limit, Some(soc))) + } else { + println!( + "Invalid values for --charge-rate-limit: '{} {}'. Must be u32 integers.", + args[i + 1], + args[i + 2] + ); + None + } + } else if args.len() > i + 1 { + if let Ok(limit) = args[i + 1].parse::() { + Some((limit, None)) + } else { + println!( + "Invalid values for --charge-rate-limit: '{}'. Must be an integer.", + args[i + 1], + ); + None + } + } else { + println!("--charge-rate-limit requires one or two. [limit] [soc] or [limit]"); + None + }; + found_an_option = true; } else if arg == "--get-gpio" { cli.get_gpio = if args.len() > i + 1 { Some(args[i + 1].clone()) @@ -333,6 +463,23 @@ pub fn parse(args: &[String]) -> Cli { None }; found_an_option = true; + } else if arg == "--reboot-ec" { + cli.ec_hib_delay = if args.len() > i + 1 { + if let Ok(delay) = args[i + 1].parse::() { + if delay == 0 { + println!("Invalid value for --ec-hib-delay: {}. Must be >0", delay); + None + } else { + Some(Some(delay)) + } + } else { + println!("Invalid value for --fp-brightness. Must be amount in seconds >0"); + None + } + } else { + Some(None) + }; + found_an_option = true; } else if arg == "-t" || arg == "--test" { cli.test = true; found_an_option = true; diff --git a/framework_lib/src/inputmodule.rs b/framework_lib/src/inputmodule.rs new file mode 100644 index 00000000..4e280065 --- /dev/null +++ b/framework_lib/src/inputmodule.rs @@ -0,0 +1,62 @@ +pub const FRAMEWORK_VID: u16 = 0x32AC; +pub const LEDMATRIX_PID: u16 = 0x0020; +pub const FRAMEWORK16_INPUTMODULE_PIDS: [u16; 6] = [ + 0x0012, // Keyboard White Backlight ANSI + 0x0013, // Keyboard RGB Backlight Numpad + 0x0014, // Keyboard White Backlight Numpad + 0x0018, // Keyboard White Backlight ISO + 0x0019, // Keyboard White Backlight JIS + LEDMATRIX_PID, +]; + +/// Get and print the firmware version of the camera +pub fn check_inputmodule_version() -> Result<(), rusb::Error> { + for dev in rusb::devices().unwrap().iter() { + let dev_descriptor = dev.device_descriptor().unwrap(); + let vid = dev_descriptor.vendor_id(); + let pid = dev_descriptor.product_id(); + if vid != FRAMEWORK_VID || !FRAMEWORK16_INPUTMODULE_PIDS.contains(&pid) { + trace!("Skipping {:04X}:{:04X}", vid, pid); + continue; + } + + // I'm not sure why, but the LED Matrix can't be opened with this code + if pid == LEDMATRIX_PID { + println!("LED Matrix"); + } else { + debug!("Opening {:04X}:{:04X}", vid, pid); + let handle = dev.open().unwrap(); + + let dev_descriptor = dev.device_descriptor()?; + let i_product = dev_descriptor + .product_string_index() + .and_then(|x| handle.read_string_descriptor_ascii(x).ok()); + println!("{}", i_product.unwrap_or_default()); + } + println!(" Firmware Version: {}", dev_descriptor.device_version()); + + debug!("Address: {:?}", dev.address()); + debug!("Bus Number: {:?}", dev.bus_number()); + debug!("Port Number: {:?}", dev.port_number()); + debug!("Port Numbers: {:?}", dev.port_numbers()); + let port_numbers = dev.port_numbers(); + let location = if let Ok(port_numbers) = port_numbers { + if port_numbers.len() == 2 { + match (port_numbers[0], port_numbers[1]) { + (4, 2) => "[X] [ ] [ ] [ ] [ ]", + (4, 3) => "[ ] [X] [ ] [ ] [ ]", + (3, 1) => "[ ] [ ] [X] [ ] [ ]", + (3, 2) => "[ ] [ ] [ ] [X] [ ]", + (3, 3) => "[ ] [ ] [ ] [ ] [X]", + _ => "Unknown", + } + } else { + "Unknown" + } + } else { + "Unknown" + }; + println!(" Location: {}", location); + } + Ok(()) +} diff --git a/framework_lib/src/lib.rs b/framework_lib/src/lib.rs index b0c92e4d..a237326a 100644 --- a/framework_lib/src/lib.rs +++ b/framework_lib/src/lib.rs @@ -16,6 +16,8 @@ extern crate log; pub mod audio_card; #[cfg(feature = "rusb")] pub mod camera; +#[cfg(feature = "rusb")] +pub mod inputmodule; #[cfg(feature = "hidapi")] pub mod touchpad; #[cfg(any(feature = "hidapi", feature = "windows"))] diff --git a/framework_lib/src/power.rs b/framework_lib/src/power.rs index d69dda24..bb3e9ea4 100644 --- a/framework_lib/src/power.rs +++ b/framework_lib/src/power.rs @@ -71,6 +71,11 @@ const EC_BATT_FLAG_DISCHARGING: u8 = 0x04; const EC_BATT_FLAG_CHARGING: u8 = 0x08; const EC_BATT_FLAG_LEVEL_CRITICAL: u8 = 0x10; +const EC_FAN_SPEED_ENTRIES: usize = 4; +/// Used on old EC firmware (before 2023) +const EC_FAN_SPEED_STALLED_DEPRECATED: u16 = 0xFFFE; +const EC_FAN_SPEED_NOT_PRESENT: u16 = 0xFFFF; + #[derive(Debug)] enum TempSensor { Ok(u8), @@ -373,8 +378,30 @@ pub fn print_thermal(ec: &CrosEc) { } } - let fan0 = u16::from_le_bytes([fans[0], fans[1]]); - println!(" Fan Speed: {:>4} RPM", fan0); + for i in 0..EC_FAN_SPEED_ENTRIES { + let fan = u16::from_le_bytes([fans[i * 2], fans[1 + i * 2]]); + if fan == EC_FAN_SPEED_STALLED_DEPRECATED { + println!(" Fan Speed: {:>4} RPM (Stalled)", fan); + } else if fan == EC_FAN_SPEED_NOT_PRESENT { + info!(" Fan Speed: Not present"); + } else { + println!(" Fan Speed: {:>4} RPM", fan); + } + } +} + +pub fn get_fan_num(ec: &CrosEc) -> EcResult { + let fans = ec.read_memory(EC_MEMMAP_FAN, 0x08).unwrap(); + + let mut count = 0; + for i in 0..EC_FAN_SPEED_ENTRIES { + let fan = u16::from_le_bytes([fans[i * 2], fans[1 + i * 2]]); + if fan == EC_FAN_SPEED_NOT_PRESENT { + continue; + } + count += 1; + } + Ok(count) } // TODO: Use Result @@ -445,6 +472,7 @@ pub fn is_standalone(ec: &CrosEc) -> bool { pub fn get_and_print_power_info(ec: &CrosEc) -> i32 { if let Some(power_info) = power_info(ec) { + print_err_ref(&ec.get_charge_state(&power_info)); print_battery_information(&power_info); if let Some(_battery) = &power_info.battery { return 0; @@ -454,6 +482,7 @@ pub fn get_and_print_power_info(ec: &CrosEc) -> i32 { } fn print_battery_information(power_info: &PowerInfo) { + println!("Battery Status"); print!(" AC is: "); if power_info.ac_present { println!("connected"); diff --git a/framework_lib/src/touchpad.rs b/framework_lib/src/touchpad.rs index 72bc30c8..e767c7e6 100644 --- a/framework_lib/src/touchpad.rs +++ b/framework_lib/src/touchpad.rs @@ -101,9 +101,7 @@ pub fn print_touchpad_fw_ver() -> Result<(), HidError> { #[cfg(target_os = "linux")] debug!(" HID Version {:04X}", hid_ver); #[cfg(not(target_os = "linux"))] - if ver != format!("{:04X}", hid_ver) { - println!(" HID Version v{:04X}", hid_ver); - } else if log_enabled!(Level::Debug) { + if ver != format!("{:04X}", hid_ver) || log_enabled!(Level::Debug) { println!(" HID Version v{:04X}", hid_ver); } diff --git a/framework_lib/src/touchscreen.rs b/framework_lib/src/touchscreen.rs index c155c173..b06965a7 100644 --- a/framework_lib/src/touchscreen.rs +++ b/framework_lib/src/touchscreen.rs @@ -5,16 +5,24 @@ use crate::touchscreen_win; pub const ILI_VID: u16 = 0x222A; pub const ILI_PID: u16 = 0x5539; +const VENDOR_USAGE_PAGE: u16 = 0xFF00; pub const USI_BITMAP: u8 = 1 << 1; pub const MPP_BITMAP: u8 = 1 << 2; +const REPORT_ID_FIRMWARE: u8 = 0x27; +const REPORT_ID_USI_VER: u8 = 0x28; + struct HidapiTouchScreen { device: HidDevice, } impl TouchScreen for HidapiTouchScreen { - fn open_device() -> Option { - debug!("Looking for touchscreen HID device"); + fn open_device(target_up: u16, skip: u8) -> Option { + debug!( + "Looking for touchscreen HID device {:X} {}", + target_up, skip + ); + let mut skip = skip; match HidApi::new() { Ok(api) => { for dev_info in api.device_list() { @@ -29,7 +37,7 @@ impl TouchScreen for HidapiTouchScreen { " Found {:04X}:{:04X} (Usage Page {:04X})", vid, pid, usage_page ); - if usage_page != 0xFF00 { + if usage_page != target_up { debug!(" Skipping usage page. Expected {:04X}", 0xFF00); continue; } @@ -40,6 +48,10 @@ impl TouchScreen for HidapiTouchScreen { debug!(" Found matching touchscreen HID device"); debug!(" Path: {:?}", dev_info.path()); debug!(" IC Type: {:04X}", pid); + if skip > 0 { + skip -= 1; + continue; + } // Unwrapping because if we can enumerate it, we should be able to open it let device = dev_info.open_device(&api).unwrap(); @@ -97,16 +109,86 @@ impl TouchScreen for HidapiTouchScreen { debug!(" Read buf: {:X?}", buf); Some(buf[msg_len..msg_len + read_len].to_vec()) } + + fn get_battery_status(&self) -> Option { + let mut msg = [0u8; 0x40]; + msg[0] = 0x0D; + self.device.read(&mut msg).ok()?; + // println!(" Tip Switch {}%", msg[12]); + // println!(" Barrell Switch: {}%", msg[12]); + // println!(" Eraser: {}%", msg[12]); + // println!(" Invert: {}%", msg[12]); + // println!(" In Range: {}%", msg[12]); + // println!(" 2nd Barrel Switch:{}%", msg[12]); + // println!(" X {}%", msg[12]); + // println!(" Y {}%", msg[12]); + // println!(" Tip Pressure: {}%", msg[12]); + // println!(" X Tilt: {}%", msg[12]); + // println!(" Y Tilt: {}%", msg[12]); + debug!(" Battery Strength: {}%", msg[12]); + debug!( + " Barrel Pressure: {}", + u16::from_le_bytes([msg[13], msg[14]]) + ); + debug!(" Transducer Index: {}", msg[15]); + + if msg[12] == 0 { + None + } else { + Some(msg[12]) + } + } + + fn get_stylus_fw(&self) -> Option<()> { + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_USI_VER; + self.device.get_feature_report(&mut msg).ok()?; + let usi_major = msg[2]; + let usi_minor = msg[3]; + debug!("USI version (Major.Minor): {}.{}", usi_major, usi_minor); + + if usi_major != 2 || usi_minor != 0 { + // Probably not USI mode + return None; + } + + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_FIRMWARE; + self.device.get_feature_report(&mut msg).ok()?; + let sn_low = u32::from_le_bytes([msg[2], msg[3], msg[4], msg[5]]); + let sn_high = u32::from_le_bytes([msg[6], msg[7], msg[8], msg[9]]); + let vid = u16::from_le_bytes([msg[14], msg[15]]); + let vendor = if vid == 0x32AC { + " (Framework Computer)" + } else { + "" + }; + let pid = u16::from_le_bytes([msg[16], msg[17]]); + let product = if pid == 0x002B { + " (Framework Stylus)" + } else { + "" + }; + println!("Stylus"); + println!(" Serial Number: {:X}-{:X}", sn_high, sn_low); + debug!(" Redundant SN {:X?}", &msg[10..14]); + println!(" Vendor ID: {:04X}{}", vid, vendor); + println!(" Product ID: {:04X}{}", pid, product); + println!(" Firmware Version: {:02X}.{:02X}", &msg[18], msg[19]); + + Some(()) + } } pub trait TouchScreen { - fn open_device() -> Option + fn open_device(usage_page: u16, skip: u8) -> Option where Self: std::marker::Sized; fn send_message(&self, message_id: u8, read_len: usize, data: Vec) -> Option>; fn check_fw_version(&self) -> Option<()> { println!("Touchscreen"); + let res = self.send_message(0x42, 3, vec![0])?; let ver = res .iter() @@ -135,22 +217,44 @@ pub trait TouchScreen { self.send_message(0x38, 0, vec![!enable as u8, 0x00])?; Some(()) } + + fn get_stylus_fw(&self) -> Option<()>; + fn get_battery_status(&self) -> Option; +} + +pub fn get_battery_level() -> Option { + for skip in 0..5 { + if let Some(device) = HidapiTouchScreen::open_device(0x000D, skip) { + if let Some(level) = device.get_battery_status() { + return Some(level); + } + } + } + None } pub fn print_fw_ver() -> Option<()> { + for skip in 0..5 { + if let Some(device) = HidapiTouchScreen::open_device(0x000D, skip) { + if device.get_stylus_fw().is_some() { + break; + } + } + } + #[cfg(target_os = "windows")] - let device = touchscreen_win::NativeWinTouchScreen::open_device()?; + let device = touchscreen_win::NativeWinTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; #[cfg(not(target_os = "windows"))] - let device = HidapiTouchScreen::open_device()?; + let device = HidapiTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; device.check_fw_version() } pub fn enable_touch(enable: bool) -> Option<()> { #[cfg(target_os = "windows")] - let device = touchscreen_win::NativeWinTouchScreen::open_device()?; + let device = touchscreen_win::NativeWinTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; #[cfg(not(target_os = "windows"))] - let device = HidapiTouchScreen::open_device()?; + let device = HidapiTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; device.enable_touch(enable) } diff --git a/framework_lib/src/touchscreen_win.rs b/framework_lib/src/touchscreen_win.rs index 2a1be801..f5fd42a0 100644 --- a/framework_lib/src/touchscreen_win.rs +++ b/framework_lib/src/touchscreen_win.rs @@ -1,4 +1,7 @@ -use crate::touchscreen::TouchScreen; +use hidapi::HidApi; +use std::path::Path; + +use crate::touchscreen::{TouchScreen, ILI_PID, ILI_VID}; #[allow(unused_imports)] use windows::{ core::*, @@ -13,41 +16,86 @@ use windows::{ }, }; +const REPORT_ID_FIRMWARE: u8 = 0x27; +const REPORT_ID_USI_VER: u8 = 0x28; + pub struct NativeWinTouchScreen { handle: HANDLE, } impl TouchScreen for NativeWinTouchScreen { - fn open_device() -> Option { - // TODO: I don't know if this might be different on other systems - // Should enumerate and find the right one - // See: https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/finding-and-opening-a-hid-collection - let path = - w!(r"\\?\HID#ILIT2901&Col03#5&357cbf85&0&0002#{4d1e55b2-f16f-11cf-88cb-001111000030}"); - - let res = unsafe { - CreateFileW( - path, - FILE_GENERIC_WRITE.0 | FILE_GENERIC_READ.0, - FILE_SHARE_READ | FILE_SHARE_WRITE, - None, - OPEN_EXISTING, - // hidapi-rs is using FILE_FLAG_OVERLAPPED but it doesn't look like we need that - FILE_FLAGS_AND_ATTRIBUTES(0), - None, - ) - }; - let handle = match res { - Ok(h) => h, - Err(err) => { - error!("Failed to open device {:?}", err); - return None; + fn open_device(target_up: u16, skip: u8) -> Option { + debug!( + "Looking for touchscreen HID device {:X} {}", + target_up, skip + ); + let mut skip = skip; + match HidApi::new() { + Ok(api) => { + for dev_info in api.device_list() { + let vid = dev_info.vendor_id(); + let pid = dev_info.product_id(); + let usage_page = dev_info.usage_page(); + if vid != ILI_VID { + trace!(" Skipping VID:PID. Expected {:04X}:*", ILI_VID); + continue; + } + debug!( + " Found {:04X}:{:04X} (Usage Page {:04X})", + vid, pid, usage_page + ); + if usage_page != target_up { + debug!(" Skipping usage page. Expected {:04X}", 0xFF00); + continue; + } + if pid != ILI_PID { + debug!(" Warning: PID is {:04X}, expected {:04X}", pid, ILI_PID); + } + + debug!(" Found matching touchscreen HID device"); + debug!(" Path: {:?}", dev_info.path()); + debug!(" IC Type: {:04X}", pid); + if skip > 0 { + skip -= 1; + continue; + } + + // TODO: Enumerate with windows + // Should enumerate and find the right one + // See: https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/finding-and-opening-a-hid-collection + let path = dev_info.path().to_str().unwrap(); + + let res = unsafe { + CreateFileW( + &HSTRING::from(Path::new(path)), + FILE_GENERIC_WRITE.0 | FILE_GENERIC_READ.0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + // hidapi-rs is using FILE_FLAG_OVERLAPPED but it doesn't look like we need that + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + ) + }; + let handle = match res { + Ok(h) => h, + Err(err) => { + error!("Failed to open device {:?}", err); + return None; + } + }; + + debug!("Opened {:?}", path); + + return Some(NativeWinTouchScreen { handle }); + } + } + Err(e) => { + error!("Failed to open hidapi. Error: {e}"); } }; - debug!("Opened {:?}", path); - - Some(NativeWinTouchScreen { handle }) + None } fn send_message(&self, message_id: u8, read_len: usize, data: Vec) -> Option> { @@ -98,4 +146,28 @@ impl TouchScreen for NativeWinTouchScreen { Some(buf[msg_len..msg_len + read_len].to_vec()) } + + fn get_stylus_fw(&self) -> Option<()> { + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_FIRMWARE; + unsafe { + let success = HidD_GetFeature(self.handle, msg.as_mut_ptr() as _, msg.len() as u32); + debug!(" Success: {}", success); + } + println!("Stylus firmware: {:X?}", msg); + + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_USI_VER; + unsafe { + let success = HidD_GetFeature(self.handle, msg.as_mut_ptr() as _, msg.len() as u32); + debug!(" Success: {}", success); + } + println!("USI Version: {:X?}", msg); + + None + } + fn get_battery_status(&self) -> Option { + error!("Get stylus battery status not supported on Windows"); + None + } } diff --git a/framework_lib/src/util.rs b/framework_lib/src/util.rs index 3fa8f505..cfb97368 100644 --- a/framework_lib/src/util.rs +++ b/framework_lib/src/util.rs @@ -40,6 +40,31 @@ pub enum Platform { GenericFramework((u16, u16), (u8, u8), bool), } +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum PlatformFamily { + Framework12, + Framework13, + Framework16, + FrameworkDesktop, +} + +impl Platform { + pub fn which_family(self) -> Option { + match self { + Platform::Framework12IntelGen13 => Some(PlatformFamily::Framework12), + Platform::IntelGen11 + | Platform::IntelGen12 + | Platform::IntelGen13 + | Platform::IntelCoreUltra1 + | Platform::Framework13Amd7080 + | Platform::Framework13AmdAi300 => Some(PlatformFamily::Framework13), + Platform::Framework16Amd7080 => Some(PlatformFamily::Framework16), + Platform::FrameworkDesktopAmdAiMax300 => Some(PlatformFamily::FrameworkDesktop), + Platform::GenericFramework(..) => None, + } + } +} + #[derive(Debug)] pub struct Config { // TODO: Actually set and read this diff --git a/framework_tool/Cargo.toml b/framework_tool/Cargo.toml index d5a8958d..ecef1fb0 100644 --- a/framework_tool/Cargo.toml +++ b/framework_tool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framework_tool" -version = "0.3.0" +version = "0.4.0" edition = "2021" [features] diff --git a/framework_uefi/Cargo.toml b/framework_uefi/Cargo.toml index 8b7755c0..13d6e0b5 100644 --- a/framework_uefi/Cargo.toml +++ b/framework_uefi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framework_uefi" -version = "0.3.0" +version = "0.4.0" edition = "2021" # Minimum Supported Rust Version rust-version = "1.74" diff --git a/framework_uefi/README.md b/framework_uefi/README.md new file mode 100644 index 00000000..19816a93 --- /dev/null +++ b/framework_uefi/README.md @@ -0,0 +1,4 @@ +## Building + +Currently recommended on Linux via: `make`. +Or (on Windows) via: `cargo rustc --target x86_64-unknown-uefi --release -- --emit link=framework_uefi/boot.efi` diff --git a/support-matrices.md b/support-matrices.md index 4a4396ed..6c468160 100644 --- a/support-matrices.md +++ b/support-matrices.md @@ -37,6 +37,6 @@ | `--pd-info` | PD Communication | All | | `--privacy` | EC Communication | All | | `--intrusion` | EC Communication | All | -| `--inputmodules` | EC Communication | Framework 16 | +| `--inputdeck` | EC Communication | Framework 16 | | `--console` | EC Communication | All | -| `--kblight` | EC Communication | All, except FL16 | \ No newline at end of file +| `--kblight` | EC Communication | All, except FL16 | pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy