diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..30da14fd --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +use devenv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdadf0df..32037c69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,14 @@ jobs: - name: Setup Rust toolchain run: | rm rust-toolchain.toml - rustup toolchain install 1.75.0-x86_64-unknown-freebsd + rustup target add x86_64-unknown-freebsd rustup show - name: Install cross compilation tool run: cargo install cross - name: Build FreeBSD tool - run: cross build --target=x86_64-unknown-freebsd --no-default-features --features cross_freebsd + run: cross build --target=x86_64-unknown-freebsd --no-default-features -p framework_lib - name: Upload FreeBSD App uses: actions/upload-artifact@v4 @@ -35,7 +35,7 @@ jobs: path: target/x86_64-unknown-freebsd/debug/framework_tool build: - name: Build Linux and UEFI + name: Build Linux runs-on: ubuntu-24.04 env: CARGO_NET_GIT_FETCH_WITH_CLI: true @@ -62,24 +62,60 @@ jobs: - name: Upload Linux App uses: actions/upload-artifact@v4 with: - name: framework_tool + name: framework_tool_debug path: target/debug/framework_tool + - name: Build Linux tool (Release) + run: cargo build -p framework_tool --release + + - name: Upload Linux App + uses: actions/upload-artifact@v4 + with: + name: framework_tool + path: target/release/framework_tool + + - name: Make sure nothing changed (e.g. Cargo.lock) + run: git diff --exit-code + + build-uefi: + name: Build UEFI + runs-on: ubuntu-24.04 + env: + CARGO_NET_GIT_FETCH_WITH_CLI: true + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + - name: Build UEFI application (no ESP) - run: make -C framework_uefi build/x86_64-unknown-uefi/boot.efi + run: | + make -C framework_uefi build/x86_64-unknown-uefi/boot.efi + mv framework_uefi/build/x86_64-unknown-uefi/boot.efi framework_tool_full.efi + make -C framework_uefi FEATURES=readonly build/x86_64-unknown-uefi/boot.efi + cp framework_uefi/build/x86_64-unknown-uefi/boot.efi framework_tool.efi - name: Upload UEFI App uses: actions/upload-artifact@v4 with: - name: framework.efi - path: framework_uefi/build/x86_64-unknown-uefi/boot.efi + name: framework_efi.zip + path: ./*.efi - - name: Install mtools to build ESP (Linux) - run: sudo apt-get install -y mtools + - name: Install mtools to build ESP and ISO (Linux) + run: sudo apt-get install -y mtools genisoimage - name: Build ESP (Linux) run: make -C framework_uefi + - name: Build ISO (Linux) + run: make -C framework_uefi iso + + - name: Upload UEFI App ISO + uses: actions/upload-artifact@v4 + with: + name: UEFI-Shell-fwk.iso + path: framework_uefi/build/x86_64-unknown-uefi/UEFI-Shell-fwk.iso + build-windows: name: Build Windows runs-on: windows-2022 @@ -91,20 +127,24 @@ jobs: - name: Setup Rust toolchain run: rustup show + # Build debug library first to fail fast - name: Build library (Windows) - run: cargo build -p framework_lib --no-default-features --features "windows" + run: cargo build -p framework_lib - name: Build Windows tool - run: cargo build -p framework_tool --no-default-features --features "windows" + run: | + cargo build -p framework_tool + cargo build -p framework_tool --release - name: Check if Windows tool can start - run: cargo run --no-default-features --features "windows" -- --help + run: cargo run -- --help --release + # Upload release build so that vcruntime is statically linked - name: Upload Windows App uses: actions/upload-artifact@v4 with: name: framework_tool.exe - path: target/debug/framework_tool.exe + path: target/release/framework_tool.exe test: diff --git a/.gitignore b/.gitignore index 6438f1c0..5e33b24d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,18 @@ target/ build/ +# Windows build outputs +*.efi +*.exe +*.pdb + +# Devenv +.devenv* +devenv.local.nix +# Nix +result* + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/Cargo.lock b/Cargo.lock index f2cb24fc..dfee2ca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ checksum = "031718ddb8f78aa5def78a09e90defe30151d1f6c672f937af4dd916429ed996" dependencies = [ "semver", "serde", - "toml", + "toml 0.5.11", "url", ] @@ -175,6 +175,15 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-num" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822c4000301ac390e65995c62207501e3ef800a1fc441df913a5e8e4dc374816" +dependencies = [ + "num-traits", +] + [[package]] name = "clap-verbosity-flag" version = "2.2.1" @@ -206,7 +215,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -302,7 +311,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -319,7 +328,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -345,6 +354,20 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "embed-resource" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.22", + "vswhom", + "winreg", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -368,6 +391,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -379,13 +408,14 @@ dependencies = [ [[package]] name = "framework_lib" -version = "0.2.1" +version = "0.4.5" dependencies = [ "built", "clap", + "clap-num", "clap-verbosity-flag", "env_logger", - "guid_macros", + "guid-create", "hidapi", "lazy_static", "libc", @@ -405,20 +435,24 @@ dependencies = [ "uefi", "uefi-services", "windows 0.59.0", + "windows-version", "wmi", ] [[package]] name = "framework_tool" -version = "0.2.1" +version = "0.4.5" dependencies = [ + "embed-resource", "framework_lib", "static_vcruntime", + "winapi", + "winresource", ] [[package]] name = "framework_uefi" -version = "0.2.1" +version = "0.4.5" dependencies = [ "framework_lib", "log", @@ -482,7 +516,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -548,14 +582,20 @@ dependencies = [ ] [[package]] -name = "guid_macros" -version = "0.11.0" +name = "guid-create" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c332e3cc6970b85f68ff39438fdb87b5c9e27a0260d720d7d550701d9964baa0" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", + "winapi", ] +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + [[package]] name = "heck" version = "0.5.0" @@ -564,9 +604,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hidapi" -version = "2.6.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e58251020fe88fe0dae5ebcc1be92b4995214af84725b375d08354d0311c23c" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" dependencies = [ "cc", "cfg-if", @@ -615,6 +655,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "io-kit-sys" version = "0.1.0" @@ -826,7 +876,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -908,9 +958,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -937,9 +987,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -955,9 +1005,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -967,9 +1017,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -978,9 +1028,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rusb" @@ -1059,6 +1109,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1135,9 +1194,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.13" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -1155,22 +1214,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] @@ -1197,6 +1256,47 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "typenum" version = "1.16.0" @@ -1302,6 +1402,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "wasm-bindgen" version = "0.2.84" @@ -1396,61 +1516,29 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-implement 0.52.0", - "windows-interface 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ - "windows-core 0.59.0", + "windows-core", "windows-targets 0.53.0", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.0", + "windows-implement", + "windows-interface", "windows-result", "windows-strings", "windows-targets 0.53.0", ] -[[package]] -name = "windows-implement" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", -] - [[package]] name = "windows-implement" version = "0.59.0" @@ -1459,30 +1547,25 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] name = "windows-interface" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.98", ] [[package]] -name = "windows-interface" -version = "0.59.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -1567,6 +1650,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" @@ -1705,16 +1797,46 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winresource" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4a67c78ee5782c0c1cb41bebc7e12c6e79644daa1650ebbc1de5d5b08593f7" +dependencies = [ + "toml 0.8.22", + "version_check", +] + [[package]] name = "wmi" -version = "0.13.3" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a" +checksum = "58078b4e28f04064dae68f6e11a6b93133c83b88dfd5ae16738ded4942db6544" dependencies = [ "chrono", "futures", "log", "serde", "thiserror", - "windows 0.52.0", + "windows 0.59.0", + "windows-core", ] diff --git a/Cargo.toml b/Cargo.toml index 41f48e81..d8108e1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,6 @@ members = [ "framework_uefi", # Catchall library that we'll probably want to split up further "framework_lib", - # Fork of https://github.com/rust-osdev/uefi-rs/blob/main/uefi-macros - # To avoid pulling in UEFI dependencies when building for an OS - "guid_macros", ] # Don't build UEFI by default. Needs special cargo invocation diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..557d8349 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,690 @@ +# Example usage + +Built-in help: + +``` +> framework_tool +Swiss army knife for Framework laptops + +Usage: framework_tool [OPTIONS] + +Options: + --flash-gpu-descriptor + + -v, --verbose... + Increase logging verbosity + -q, --quiet... + Decrease logging verbosity + --versions + List current firmware versions + --version + Show tool version information (Add -vv for more details) + --features + Show features support by the firmware + --esrt + Display the UEFI ESRT table + --device + [possible values: bios, ec, pd0, pd1, rtm01, rtm23, ac-left, ac-right] + --compare-version + + --power + Show current power status of battery and AC (Add -vv for more details) + --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 + --pd-reset + Reset a specific PD controller (for debugging only) + --pd-disable + Disable all ports on a specific PD controller (for debugging only) + --pd-enable + Enable all ports on a specific PD controller (for debugging only) + --dp-hdmi-info + Show details about connected DP or HDMI Expansion Cards + --dp-hdmi-update + Update the DisplayPort or HDMI Expansion Card + --audio-card-info + Show details about connected Audio Expansion Cards (Needs root privileges) + --privacy + Show privacy switch statuses (camera and microphone) + --pd-bin + Parse versions from PD firmware binary file + --ec-bin + Parse versions from EC firmware binary file + --capsule + Parse UEFI Capsule information from binary file + --dump + Dump extracted UX capsule bitmap image to a file + --h2o-capsule + Parse UEFI Capsule information from binary file + --dump-ec-flash + Dump EC flash contents + --flash-ec + Flash EC (RO+RW) with new firmware from file - may render your hardware unbootable! + --flash-ro-ec + Flash EC with new RO firmware from file - may render your hardware unbootable! + --flash-rw-ec + Flash EC with new RW firmware from file + --intrusion + Show status of intrusion switch + --inputdeck + Show status of the input modules (Framework 16 only) + --inputdeck-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 + --charge-current-limit ... + Set max charge current limit + --charge-rate-limit ... + Set max charge current limit + --get-gpio [] + Get GPIO value by name or all, if no name provided + --fp-led-level [] + Get or set fingerprint LED brightness level [possible values: high, medium, low, ultra-low, auto] + --fp-brightness [] + Get or set fingerprint LED brightness percentage + --kblight [] + Set keyboard backlight percentage or get, if no value provided + --remap-key + Remap a key by changing the scancode + --rgbkbd ... + Set the color of to . Multiple colors for adjacent keys can be set at once. [ ...] Example: 0 0xFF000 0x00FF00 0x0000FF + --tablet-mode + Set tablet mode override [possible values: auto, tablet, laptop] + --touchscreen-enable + Enable/disable touchscreen [possible values: true, false] + --stylus-battery + Check stylus battery level (USI 2.0 stylus only) + --console + Get EC console, choose whether recent or to follow the output [possible values: recent, follow] + --reboot-ec + Control EC RO/RW jump [possible values: reboot, jump-ro, jump-rw, cancel-jump, disable-jump] + --ec-hib-delay [] + Get or set EC hibernate delay (S5 to G3) + --hash + Hash a file of arbitrary data + --driver + Select which driver is used. By default portio is used [possible values: portio, cros-ec, windows] + --pd-addrs + Specify I2C addresses of the PD chips (Advanced) + --pd-ports + Specify I2C ports of the PD chips (Advanced) + -t, --test + Run self-test to check if interaction with EC is possible + -f, --force + Force execution of an unsafe command - may render your hardware unbootable! + --dry-run + Simulate execution of a command (e.g. --flash-ec) + --flash-gpu-descriptor-file + File to write to the gpu EEPROM + --dump-gpu-descriptor-file + File to dump the gpu EEPROM to + -h, --help + Print help +``` + +## Check firmware versions + +### BIOS (Mainboard, UEFI, EC, PD, Retimer) + +Example on Framework 13 AMD Ryzen AI 300 Series: + +``` +> framework_tool --versions +Mainboard Hardware + Type: Laptop 13 (AMD Ryzen AI 300 Series) + Revision: MassProduction +UEFI BIOS + Version: 03.00 + Release Date: 03/10/2025 +EC Firmware + Build version: lilac-3.0.0-1541dc6 2025-05-05 11:31:24 zoid@localhost + Current image: RO +PD Controllers + Right (01): 0.0.0E (MainFw) + Left (23): 0.0.0E (MainFw) +[...] +``` + +Example on Framework 13 Intel Core Ultra Series 1: + +``` +> framework_tool --versions +Mainboard Hardware + Type: Laptop 13 (AMD Ryzen AI 300 Series) + Revision: MassProduction +UEFI BIOS + Version: 03.03 + Release Date: 10/07/2024 +EC Firmware + Build version: marigold-3.0.3-278d300 2024-10-04 03:03:58 marigold1@ip-172-26-3-226 + Current image: RO +PD Controllers + Right (01): 0.0.08 (MainFw) + Left (23): 0.0.08 (MainFw) +[...] +``` + +### 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 + Protocols: USI +``` + +### 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 + 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] +[...] +``` + +### DisplayPort or HDMI Expansion Card + +``` +> framework_tool --dp-hdmi-info +DisplayPort Expansion Card + Serial Number: 11AD1D0030123F17142C0B00 + Active Firmware: 101 (3.0.11.065) + Inactive Firmware: 008 (3.0.11.008) + Operating Mode: MainFw (#2) + +# Or +> framework_tool --versions +[...] +DisplayPort Expansion Card + Active Firmware: 101 (3.0.11.065) + Inactive Firmware: 008 (3.0.11.008) + Operating Mode: MainFw (#2) +``` + +### CSME Version (Linux on Intel systems) + +``` +> framework_tool --versions +[...] +CSME + Firmware Version: 0:16.1.32.2473 +[...] +``` + +### Firmware Version using ESRT (BIOS, Retimer, CSME) + +All systems have at least an entry for BIOS. Intel systems also have CSME and some Retimers. + +Example on Framework 13 Intel Core Ultra Series 1: + +``` +> sudo framework_tool --esrt +ESRT Table + ResourceCount: 4 + ResourceCountMax: 4 + ResourceVersion: 1 +ESRT Entry 0 + GUID: BDFFCE36-809C-4FA6-AECC-54536922F0E0 + GUID: MtlRetimer23 + Type: DeviceFirmware + Version: 0x270 (624) + Min FW Version: 0x0 (0) + Capsule Flags: 0x0 + Last Attempt Version: 0x270 (624) + Last Attempt Status: Success +ESRT Entry 1 + GUID: 32D8D677-EEBC-4947-8F8A-0693A45240E5 + GUID: MtlCsme + Type: DeviceFirmware + Version: 0x85D (2141) + Min FW Version: 0x3E8 (1000) + Capsule Flags: 0x0 + Last Attempt Version: 0x0 (0) + Last Attempt Status: Success +ESRT Entry 2 + GUID: C57FD615-2AC9-4154-BF34-4DC715344408 + GUID: MtlRetimer01 + Type: DeviceFirmware + Version: 0x270 (624) + Min FW Version: 0x0 (0) + Capsule Flags: 0x0 + Last Attempt Version: 0x270 (624) + Last Attempt Status: Success +ESRT Entry 3 + GUID: 72CECB9B-2B37-5EC2-A9FF-C739AABAADF3 + GUID: MtlBios + Type: SystemFirmware + Version: 0x303 (771) + Min FW Version: 0x303 (771) + Capsule Flags: 0x0 + Last Attempt Version: 0x303 (771) + Last Attempt Status: Success +``` + +## 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 +SLEEP# GPIO high: true +Positions: + Pos 0: GenericC + Pos 1: KeyboardA + Pos 2: Disconnected + Pos 3: Disconnected + Pos 4: GenericC +``` + +## Check temperatures and fan speed + +``` +> sudo framework_tool --thermal + F75303_Local: 43 C + F75303_CPU: 44 C + F75303_DDR: 39 C + APU: 62 C + Fan Speed: 0 RPM +``` + +## Check sensors + +### Ambient Light (Framework 13, Framework 16) + +``` +> sudo framework_tool --sensors +ALS: 76 Lux +``` + +### Accelerometer (Framework 12) + +``` +> sudo framework_tool --sensors +Accelerometers: + Lid Angle: 118 Deg + Lid Sensor: X=+0.00G Y=+0.86G, Z=+0.53G + Base Sensor: X=-0.03G Y=-0.07G, Z=+1.02G +``` + +## 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 + Config: Pcie4x2 + Vendor: SsdHolder + Expansion Bay EEPROM + Valid: true + HW Version: 8.0 +``` + +Add `-vv` for more verbose details. + +## 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 0.8 80 +> sudo framework_tool --charge-current-limit 2000 80 +``` + +## EC Console + +``` +# Get recent EC console logs and watch for more +> framework_tool.exe --console follow +[53694.741000 Battery 62% (Display 61.1 %) / 3h:18 to empty] +[53715.010000 Battery 62% (Display 61.0 %) / 3h:21 to empty] +[53734.281200 Battery 62% (Display 60.9 %) / 3h:18 to empty] +[53738.037200 Battery 61% (Display 60.9 %) / 3h:6 to empty] +[53752.301500 Battery 61% (Display 60.8 %) / 3h:15 to empty] +``` + +## Keyboard backlight + +``` +# Check current keyboard backlight brightness +> framework_tool.exe --kblight +Keyboard backlight: 5% + +# Set keyboard backlight brightness +# Off +> framework_tool.exe --kblight 0 +# 20% +> framework_tool.exe --kblight 20 +``` + +## Fingerprint/Powerbutton brightness + +On Framework 13 and Framework 16 the power button has an integrated fingerprint reader, hence the name. +On Framework 12 it does not, but the same command can be used. + +``` +# Check the current brightness +> framework_tool --fp-brightness +Fingerprint LED Brightness + Requested: Auto + Brightness: 55% + +# Set it to a custom perfentage +> framework_tool --fp-brightness 42 +Fingerprint LED Brightness + Requested: Custom + Brightness: 42% + +# Set to a specific level (like the BIOS setting does) +> framework_tool --fp-led-level high +Fingerprint LED Brightness + Requested: High + Brightness: 55% + +# Set it back to auto +> framework_tool --fp-led-level auto +Fingerprint LED Brightness + Requested: Auto + Brightness: 15% +``` + +## RGB LED (Framework Desktop) + +``` +# To set three LEDs to red, green, blue +sudo framework_tool --rgbkbd 0 0xFF0000 0x00FF00 0x0000FF + +# To clear 8 LEDs +sudo framework_tool --rgbkbd 0 0 0 0 0 0 0 0 0 + +# Just turn the 3rd LED red +sudo framework_tool --rgbkbd 2 0xFF0000 +``` + +## Stylus (Framework 12) + +``` +> sudo framework_tool --stylus-battery +Stylus Battery Strength: 77% +``` + +## Remap keyboard + +Note that the keyboard matrix on Framework 12 and Framework 13 are +different. +The scancodes are the same. + +- Left-Ctrl 0x0014 +- Left-Alt 0x0014 +- Tab 0x0058 + +### Framework 12 + +``` +# Remap capslock key as left-ctrl +> framework_tool --remap-key 6 15 0x0014 + +# Swap left-ctrl and alt +> framework_tool --remap-key 1 14 0x0011 +> framework_tool --remap-key 6 13 0x0014 +``` + +### Framework 13 + +``` +# Remap capslock key as left-ctrl +> framework_tool --remap-key 4 4 0x0014 + +# Swap left-ctrl and alt +> framework_tool --remap-key 1 12 0x0011 +> framework_tool --remap-key 1 3 0x0014 +``` + +### Framework 16 + +It's not controlled by the EC, use https://keyboard.frame.work. + +## Advanced commands + +Mostly for debugging firmware. + +See [EXAMPLES_ADVANCED.md](EXAMPLES_ADVANCED.md) diff --git a/EXAMPLES_ADVANCED.md b/EXAMPLES_ADVANCED.md new file mode 100644 index 00000000..a350df39 --- /dev/null +++ b/EXAMPLES_ADVANCED.md @@ -0,0 +1,274 @@ +# Advanced debugging + +## Verbosity + +To debug, increase the verbosity from the commandline with `-v`. +The verbosity levels are: + +| Commandline | Level | +|-------------|--------| +| `-q` | No log | +| None | Error | +| `-v` | Warn | +| `-vv` | Info | +| `-vvv` | Debug | +| `-vvvv` | Trace | + +For example it is useful to check which EC driver is used: + +``` +> framework_tool --kblight -vvv +[DEBUG] Chromium EC Driver: CrosEc +[DEBUG] send_command(command=0x22, ver=0, data_len=0) +Keyboard backlight: 0% + +> framework_tool --driver portio --kblight -vvv +[DEBUG] Chromium EC Driver: Portio +[DEBUG] send_command(command=0x22, ver=0, data_len=0) +Keyboard backlight: 0% +``` + +## PD + +### Check PD state + +Example on Framework 13 AMD Ryzen AI 300 + +``` +> sudo framework_tool.exe --pd-info +Right / Ports 01 + Silicon ID: 0x3580 + Mode: MainFw + Flash Row Size: 256 B + Ports Enabled: 0, 1 + Bootloader Version: Base: 3.6.0.009, App: 0.0.01 + FW1 (Backup) Version: Base: 3.7.0.197, App: 0.0.0B + FW2 (Main) Version: Base: 3.7.0.197, App: 0.0.0B +Left / Ports 23 + Silicon ID: 0x3580 + Mode: MainFw + Flash Row Size: 256 B + Ports Enabled: 0, 1 + Bootloader Version: Base: 3.6.0.009, App: 0.0.01 + FW1 (Backup) Version: Base: 3.7.0.197, App: 0.0.0B + FW2 (Main) Version: Base: 3.7.0.197, App: 0.0.0B +``` + +### Disable/enable/reset PD + +``` +# Disable all ports on PD 0 +> sudo framework_tool --pd-disable 0 + +# Reset PD 0 (enables all ports again) +> sudo framework_tool --pd-reset 0 + +# Or enable all ports on PD 0 without resetting it +> sudo framework_tool --pd-enable 0 +``` + +## Check EFI Resource Table + +On Framework Desktop: + +``` +> sudo framework_tool --esrt +ESRT Table + ResourceCount: 1 + ResourceCountMax: 1 + ResourceVersion: 1 +ESRT Entry 0 + GUID: EB68DBAE-3AEF-5077-92AE-9016D1F0C856 + GUID: DesktopAmdAi300Bios + Type: SystemFirmware + Version: 0x204 (516) + Min FW Version: 0x100 (256) + Capsule Flags: 0x0 + Last Attempt Version: 0x108 (264) + Last Attempt Status: Success +``` + +## Manually overriding tablet mode status + +If you have a suspicion that the embedded controller does not control tablet +mode correctly based on Hall and G-Sensor, you can manually force a mode. + +This may also be useful if you want to use the touchpad and keyboard while the +lid is folded back - for example if you're using an external display only (Cyberdeck). +In this case you can force laptop mode. + +Tablet mode: +- Sets a GPIO connected to the touchpad to disable it +- Stops the EC from sending keypresses to the CPU + +``` +# Force tablet mode to disable touchpad and keyboard +> framework_tool --tablet-mode tablet + +# Force laptop mode to always keep touchpad and keyboard enabled +> framework_tool --tablet-mode laptop + +# Let the EC handle tablet mode automatically based on sensors +> framework_tool --tablet-mode auto +``` + +## Flashing EC firmware + +**IMPORTANT** Flashing EC firmware yourself is not recommended. It may render +your hardware unbootable. Please update your firmware using the official BIOS +update methods (Windows .exe, LVFS/FWUPD, EFI updater)! + +This command has not been thoroughly tested on all Framework Computer systems + +``` +# Simulate flashing RW (to see which blocks are updated) +> framework_tool --flash-rw-ec ec.bin --dry-run + +# Actually flash RW +> framework_tool --flash-rw-ec ec.bin + +# Boot into EC RW firmware (will crash your OS and reboot immediately) +# EC will boot back into RO if the system turned off for 30s +> framework_tool --reboot-ec jump-rw +``` + +## Flashing Expansion Bay EEPROM (Framework 16) + +This will render your dGPU unsuable if you flash the wrong file! +It's intended for advanced users who build their own expansion bay module. +The I2C address of the EEPROM is hardcoded to 0x50. + +``` +# Dump current descriptor (e.g. for backup) +> framework_tool --dump-gpu-descriptor-file foo.bin +Dumping to foo.bin +Wrote 153 bytes to foo.bin + +# Update just the serial number +> framework_tool --flash_gpu_descriptor GPU FRAKMQCP41500ASSY1 +> framework_tool --flash_gpu_descriptor 13 FRAKMQCP41500ASSY1 +> framework_tool --flash_gpu_descriptor 0x0D FRAKMQCP41500ASSY1 + +# Update everything from a file +> framework_tool --flash-gpu-descriptor-file pcie_4x2.bin +``` + +## Analyzing binaries + +### EC + +Note that only since Framework 13 Intel Core Ultra (and later) the version number embedded in the ED binary is meaningful. As you can see below, in this example on Intel Core 12th/13th Gen (hx30) it's always 0.0.1. +The commit hash though is accurate and reflects the git commit it was built from. + +``` +> framework-tool --ec--bin ec.bin +File + Size: 524288 B + Size: 512 KB +EC + Version: hx30_v0.0.1-7a61a89 + RollbackVer: 0 + Platform: hx30 + Version: 0.0.1 + Commit: 7a61a89 + Size: 2868 B + Size: 2 KB +``` + +### PD + +``` +> framework_tool --pd-bin pd-0.1.14.bin +File + Size: 65536 B + Size: 64 KB +FW 1 + Silicon ID: 0x3000 + Version: 0.1.14 + Row size: 128 B + Start Row: 22 + Rows: 95 + Size: 12160 B + Size: 11 KB +FW 2 + Silicon ID: 0x3000 + Version: 0.1.14 + Row size: 128 B + Start Row: 118 + Rows: 381 + Size: 48768 B + Size: 47 KB +``` + +### UEFI Capsule + +``` +> framework_tool --capsule retimer23.cap +File + Size: 2232676 B + Size: 2180 KB +Capsule Header + Capsule GUID: (ba2e4e6e, 3b0c, 4f25, [8a,59,4c,55,3f,c8,6e,a2]) + Header size: 28 B + Flags: 0x50000 + Persist across reset (0x10000) + Initiate reset (0x40000) + Capsule Size: 2232676 B + Capsule Size: 2180 KB + Type: Framework Retimer23 (Right) +``` + +## Version Check + +Check if the firmware version is what you expect, returns exit code 0 on +succcess, 1 on failure. + +``` +# Check which devices it's available for +> ./framework_tool --device + [possible values: bios, ec, pd0, pd1, rtm01, rtm23, ac-left, ac-right] + +For more information try '--help' + +# Successful compare +> ./framework_tool --device bios --compare-version 03.01 +Target Version "03.01" +Comparing BIOS version "03.01" +Compared version: 0 +> echo $? +0 + +# Failed compare +> ./framework_tool --device bios --compare-version 03.00 + Finished dev [unoptimized + debuginfo] target(s) in 0.05s +Target Version "03.00" +Comparing BIOS version "03.01" +Compared version: 1 +Error: "Fail" + +> echo $? +1 +``` + +On UEFI Shell: + +``` +# Check if AC is attached on left side +Shell> fs0:framework_tool.efi --device ac-left --compare-version 1 +Target Version "1" +Comparing AcLeft "1" +Comparison Result: 0 +# It is +Shell> echo %lasterror% +0x0 + +# Check if AC is attached on right side +Shell> fs0:framework_tool.efi --device ac-right --compare-version 1 +Target Version "1" +Comparing AcLeft "0" +Comparison Result: 1 + +# It is not +Shell> echo %lasterror% +0x1 +``` diff --git a/README.md b/README.md index f4927fc0..033a86d8 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,36 @@ Rust libraries and tools to interact with the system. The tool works on Linux, Windows and the UEFI shell. -Download it from the latest [GH Actions](https://github.com/FrameworkComputer/framework-system/actions?query=branch%3Amain) run on the main branch. -Most features are supported on every "OS". See below for details. +Most features are supported on every "OS". + +You can find lots of examples in [EXAMPLES.md](./EXAMPLES.md). + +## Installation + +### Linux + +- [NixOS](https://github.com/NixOS/nixpkgs/blob/nixos-25.05/pkgs/by-name/fr/framework-tool/package.nix) + - `nix-shell -p framework-tool` +- [ArchLinux](https://archlinux.org/packages/extra/x86_64/framework-system/) + - `pacman -Sy framework-system` +- [Bazzite](https://github.com/ublue-os/bazzite/pull/3026) +- Others + - Build from source + - Or download [latest binary](https://github.com/FrameworkComputer/framework-system/releases/latest/download/framework_tool) +- ChromeOS + - Build from source + +### Windows + +``` +winget install FrameworkComputer.framework_tool +``` + +### FreeBSD + +``` +sudo pkg install framework-system +``` ## Features @@ -13,11 +41,33 @@ see the [Support Matrices](support-matrices.md). ###### Operating System Support -- [x] OS Tool (`framework_tool`) - - [x] Tested on Linux - - [x] Tested on Windows - - [x] Tested on FreeBSD -- [x] UEFI Shell tool (`framework_uefi`) +The following operating environments are supported. + +- Linux +- Windows +- UEFI +- FreeBSD + +Most functionality depends communication with the EC. +For Linux and Windows there are dedicated drivers. +On UEFI and FreeBSD raw port I/O is used - on Linux this can also be used as a fallback, if the driver is not available or not working. + +| | Port I/O | Linux | Windows | +|---------------------|----------| ------|---------| +| Framework 12 | | | | +| Intel Core 12th Gen | Yes | [6.12](https://github.com/torvalds/linux/commit/62be134abf4250474a7a694837064bc783d2b291) | Yes | +| Framework 13 | | | | +| Intel Core 11th Gen | Yes | [6.11](https://github.com/torvalds/linux/commit/04ca0a51f1e63bd553fd4af8e9af0fe094fa4f0a) | Not yet | +| Intel Core 12th Gen | Yes | [6.13](https://github.com/torvalds/linux/commit/dcd59d0d7d51b2a4b768fc132b0d74a97dfd6d6a) | Not yet | +| Intel Core 13th Gen | Yes | [6.13](https://github.com/torvalds/linux/commit/dcd59d0d7d51b2a4b768fc132b0d74a97dfd6d6a) | Not yet | +| AMD Ryzen 7040 | Yes | [6.10](https://github.com/torvalds/linux/commit/c8f460d991df93d87de01a96b783cad5a2da9616) | BIOS 3.16+ | +| Intel Core Ultra S1 | Yes | [6.12](https://github.com/torvalds/linux/commit/62be134abf4250474a7a694837064bc783d2b291) | BIOS 3.06+ | +| AMD Ryzen AI 300 | Yes | [6.12](https://github.com/torvalds/linux/commit/62be134abf4250474a7a694837064bc783d2b291) | Yes | +| Framework 16 | | | | +| AMD Ryzen 7040 | Yes | [6.10](https://github.com/torvalds/linux/commit/c8f460d991df93d87de01a96b783cad5a2da9616) | BIOS 3.06+ | +| AMD Ryzen AI 300 | Yes | [6.10](https://github.com/torvalds/linux/commit/c8f460d991df93d87de01a96b783cad5a2da9616) | Yes | +| Framework Desktop | | | | +| AMD Ryzen AI Max | Yes | [6.15](https://github.com/torvalds/linux/commit/d83c45aeec9b223fe6db4175e9d1c4f5699cc37a) | Yes | ###### Firmware Information @@ -25,15 +75,14 @@ see the [Support Matrices](support-matrices.md). - [x] ESRT table (UEFI, Linux, FreeBSD only) (`--esrt`) - [x] SMBIOS - [x] Get firmware version from binary file - - [x] Legacy EC (Intel 13th Gen and earlier) (`--ec-bin`) - - [x] Zephyr EC (AMD) (`--ec-bin`) + - [x] EC (Legacy and Zephyr based) (`--ec-bin`) - [x] CCG5 PD (11th Gen TigerLake) (`--pd-bin`) - - [x] CCG6 PD (12th Gen AlderLake) (`--pd-bin`) - - [x] CCG8 PD (Framework 16) (`--pd-bin`) - - [x] HO2 BIOS Capsule (`--ho2-capsule`) + - [x] CCG6 PD (Intel systems, Framework Desktop) (`--pd-bin`) + - [x] CCG8 PD (AMD Laptops) (`--pd-bin`) + - [x] H2O BIOS Capsule (`--h2o-capsule`) - [x] BIOS Version - [x] EC Version - - [x] CCG5/CCG6 PD Version + - [x] CCG5/CCG6/CCG8 PD Version - [x] UEFI Capsule (`--capsule`) - [x] Parse metadata from capsule binary - [x] Determine type (GUID) of capsule binary @@ -41,9 +90,11 @@ see the [Support Matrices](support-matrices.md). - [x] Get firmware version from system (`--versions`) - [x] BIOS - [x] EC - - [x] PD + - [x] PD Controller - [x] ME (Only on Linux) - [x] Retimer + - [x] Touchpad (Linux, Windows, FreeBSD, not UEFI) + - [x] Touchscreen (Linux, Windows, FreeBSD, not UEFI) - [x] Get Expansion Card Firmware (Not on UEFI so far) - [x] HDMI Expansion Card (`--dp-hdmi-info`) - [x] DisplayPort Expansion Card (`--dp-hdmi-info`) @@ -53,16 +104,6 @@ see the [Support Matrices](support-matrices.md). - [x] DisplayPort Expansion Card (`--dp-hdmi-update`) - [ ] Audio Expansion Card -###### Firmware Update - -Note: Use fwupd. - -- [ ] Flash firmware - - [ ] BIOS - - [ ] EC - - [ ] PD - - [ ] Expansion Cards - ###### System Status All of these need EC communication support in order to work. @@ -78,29 +119,51 @@ All of these need EC communication support in order to work. - [x] Get and set keyboard brightness (`--kblight`) - [x] Get and set battery charge limit (`--charge-limit`) -- [x] Get and set fingerprint LED brightness (`--fp-brightness`) +- [x] Get and set fingerprint LED brightness (`--fp-brightness`, `--fp-led-level`) +- [x] Override tablet mode, instead of follow G-Sensor and hall sensor (`--tablet-mode`) +- [x] Disable/Enable touchscreen (`--touchscreen-enable`) ###### Communication with Embedded Controller +- [x] Framework Laptop 12 (Intel 13th Gen) - [x] Framework Laptop 13 (Intel 11-13th Gen) -- [x] Framework Laptop 13 (AMD Ryzen) -- [x] Framework Laptop 16 (AMD Ryzen) +- [x] Framework Laptop 13 (AMD Ryzen 7080) +- [x] Framework Laptop 13 (AMD Ryzen AI 300) +- [x] Framework Laptop 16 (AMD Ryzen 7080) +- [x] Framework Laptop 16 (AMD Ryzen AI 300) +- [x] Framework Desktop (AMD Ryzen AI Max 300) - [x] Port I/O communication on Linux -- [x] Port I/O communication on UEFI +- [x] Port I/O communication in UEFI +- [x] Port I/O communication on FreeBSD - [x] Using `cros_ec` driver in Linux kernel +- [x] Using [Framework EC Windows driver](https://github.com/FrameworkComputer/crosecbus) based on [coolstar's](https://github.com/coolstar/crosecbus) - [x] Using [DHowett's Windows CrosEC driver](https://github.com/DHowett/FrameworkWindowsUtils) -## Prerequisites +## Building -Only [Rustup](https://rustup.rs/) is needed. Based on `rust-toolchain.toml` it -will install the right toolchain and version for this project. +### Dependencies -## Building +[Rustup](https://rustup.rs/) is convenient for setting up the right Rust version. +Based on `rust-toolchain.toml` it will install the right toolchain and version for this project. MSRV (Minimum Supported Rust Version): -- 1.61 for Linux/Windows -- 1.68 for UEFI +- 1.74 for Linux/Windows +- 1.74 for UEFI + +System dependencies + +``` +# NixOS +nix-shell --run fish -p cargo systemd udev hidapi pkg-config +direnv shell + +# Fedora +sudo dnf install systemd-devel hidapi-devel + +# FreeBSD +sudo pkg install hidapi +``` ```sh # Running linter @@ -128,241 +191,20 @@ make -C framework_uefi ls -l framework_uefi/build/x86_64-unknown-uefi/boot.efi ``` -Building on Windows or in general with fewer features: - -```ps1 -# Because we're fetching a private dependency from git, it might be necessary -# to force cargo to use the git commandline. In powershell run: -$env:CARGO_NET_GIT_FETCH_WITH_CLI='true' - -# Build the library and tool -cargo build --no-default-features --features "windows" +## Install local package -# Running the tool -cargo run --no-default-features --features "windows" ``` - -Cross compile from Linux to FreeBSD: - -```sh -# One time, install cross tool -cargo install cross - -# Make sure docker is started as well -sudo systemctl start docker - -# Build -cross build --target=x86_64-unknown-freebsd --no-default-features --features unix +> cargo install --path framework_tool +> which framework_tool +/home/zoid/.cargo/bin/framework_tool ``` ## Running -Run without any arguments to see the help: - -``` -> cargo run -Swiss army knife for Framework laptops - -Usage: framework_tool [OPTIONS] - -Options: - -v, --verbose... More output per occurrence - -q, --quiet... Less output per occurrence - --versions List current firmware versions version - --esrt Display the UEFI ESRT table - --power Show current power status (battery and AC) - --pdports Show information about USB-C PD ports - --info Show info from SMBIOS (Only on UEFI) - --pd-info Show details about the PD controllers - --dp-hdmi-info Show details about connected DP or HDMI Expansion Cards - --dp-hdmi-update Update the DisplayPort or HDMI Expansion Card - --audio-card-info Show details about connected Audio Expansion Cards (Needs root privileges) - --privacy Show privacy switch statuses (camera and microphone) - --pd-bin Parse versions from PD firmware binary file - --ec-bin Parse versions from EC firmware binary file - --capsule Parse UEFI Capsule information from binary file - --dump Dump extracted UX capsule bitmap image to a file - --ho2-capsule Parse UEFI Capsule information from binary file - --intrusion Show status of intrusion switch - --inputmodules Show status of the input modules (Framework 16 only) - --kblight [] Set keyboard backlight percentage or get, if no value provided - --console Get EC console, choose whether recent or to follow the output [possible values: recent, follow] - --driver Select which driver is used. By default portio is used [possible values: portio, cros-ec, windows] - -t, --test Run self-test to check if interaction with EC is possible - -h, --help Print help information -``` +Run without any arguments to see the help. Many actions require root. First build with cargo and then run the binary with sudo: ```sh cargo build && sudo ./target/debug/framework_tool ``` - -Dumping version information from firmware binaries: - -``` -# Dumping PD FW Binary Information: -> cargo run -q -- --pd-bin pd-0.1.14.bin -File - Size: 65536 B - Size: 64 KB -FW 1 - Silicon ID: 0x3000 - Version: 0.1.14 - Row size: 128 B - Start Row: 22 - Rows: 95 - Size: 12160 B - Size: 11 KB -FW 2 - Silicon ID: 0x3000 - Version: 0.1.14 - Row size: 128 B - Start Row: 118 - Rows: 381 - Size: 48768 B - Size: 47 KB - -# Dumping EC FW Binary Information -> cargo run -q -- --ec--bin ec.bin -File - Size: 524288 B - Size: 512 KB -EC - Version: hx30_v0.0.1-7a61a89 - RollbackVer: 0 - Platform: hx30 - Version: 0.0.1 - Commit: 7a61a89 - Size: 2868 B - Size: 2 KB - -# Dumping Capsule Binary Information: -> cargo run -q -- --capsule retimer23.cap -File - Size: 2232676 B - Size: 2180 KB -Capsule Header - Capsule GUID: (ba2e4e6e, 3b0c, 4f25, [8a,59,4c,55,3f,c8,6e,a2]) - Header size: 28 B - Flags: 0x50000 - Persist across reset (0x10000) - Initiate reset (0x40000) - Capsule Size: 2232676 B - Capsule Size: 2180 KB - Type: Framework Retimer23 (Right) -``` - -###### Running on Windows -Windows does not ship with a Chrome EC driver. However there is an open-source implementation that this tool can take advantage of. -The project is hosted on GitHub and you can download pre-built binaries -[there](https://github.com/DHowett/FrameworkWindowsUtils/releases). - -The driver is not signed by Microsoft, so you will have to enable testsigning. - -##### Running on ChromeOS - -The application can run on ChromeOS but most commands rely on custom host -commands that we built into the EC firmware of non-Chromebook Framework laptops. -In theory you could add those patches to the Chromebook platform, build your -own EC firmware and flash it. - -## Tests - -- [x] Basic unit tests -- [x] Test parsing real binaries - -## Version Check - -Check if the firmware version is what you expect, returns exit code 0 on -succcess, 1 on failure. - -``` -# Check which devices it's available for -> ./framework_tool --device - [possible values: bios, ec, pd0, pd1, rtm01, rtm23, ac-left, ac-right] - -For more information try '--help' - -# Successful compare -> ./framework_tool --device bios --compare-version 03.01 -Target Version "03.01" -Comparing BIOS version "03.01" -Compared version: 0 -> echo $? -0 - -# Failed compare -> ./framework_tool --device bios --compare-version 03.00 - Finished dev [unoptimized + debuginfo] target(s) in 0.05s -Target Version "03.00" -Comparing BIOS version "03.01" -Compared version: 1 -Error: "Fail" - -> echo $? -1 -``` - -On UEFI Shell: - -``` -# Check if AC is attached on left side -Shell> fs0:framework_tool.efi --device ac-left --compare-version 1 -Target Version "1" -Comparing AcLeft "1" -Comparison Result: 0 -# It is -Shell> echo %lasterror% -0x0 - -# Check if AC is attached on right side -Shell> fs0:framework_tool.efi --device ac-right --compare-version 1 -Target Version "1" -Comparing AcLeft "0" -Comparison Result: 1 - -# It is not -Shell> echo %lasterror% -0x1 -``` - -## Debugging - -To debug, increase the verbosity from the commandline with `-v`. -The verbosity levels are: - -| Commandline | Level | -|-------------|--------| -| `-q` | No log | -| None | Error | -| `-v` | Warn | -| `-vv` | Info | -| `-vvv` | Debug | -| `-vvvv` | Trace | - -For example it is useful to check which EC driver is used: - -``` -> framework_tool --kblight -vvv -[DEBUG] Chromium EC Driver: CrosEc -[DEBUG] send_command(command=0x22, ver=0, data_len=0) -Keyboard backlight: 0% - -> framework_tool --driver portio --kblight -vvv -[DEBUG] Chromium EC Driver: Portio -[DEBUG] send_command(command=0x22, ver=0, data_len=0) -Keyboard backlight: 0% -``` - -## FreeBSD - -``` -sudo pkg install hidapi - -# Build the library and tool -cargo build --no-default-features --features freebsd - -# Running the tool -cargo run --no-default-features --features freebsd -``` diff --git a/completions/bash/framework_tool b/completions/bash/framework_tool new file mode 100755 index 00000000..7031b8d5 --- /dev/null +++ b/completions/bash/framework_tool @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# Bash completion for framework_tool + +_framework_tool() { + local options + options=( + "--flash-gpu-descriptor" + "-v" "--verbose" + "-q" "--quiet" + "--versions" + "--version" + "--features" + "--esrt" + "--device" + "--compare-version" + "--power" + "--thermal" + "--sensors" + "--pdports" + "--info" + "--pd-info" + "--dp-hdmi-info" + "--dp-hdmi-update" + "--audio-card-info" + "--privacy" + "--pd-bin" + "--ec-bin" + "--capsule" + "--dump" + "--ho2-capsule" + "--dump-ec-flash" + "--flash-ec" + "--flash-ro-ec" + "--flash-rw-ec" + "--intrusion" + "--inputdeck" + "--inputdeck-mode" + "--charge-limit" + "--get-gpio" + "--fp-led-level" + "--fp-brightness" + "--kblight" + "--rgbkbd" + "--tablet-mode" + "--touchscreen-enable" + "--console" + "--reboot-ec" + "--hash" + "--driver" + "--pd-addrs" + "--pd-ports" + "-t" "--test" + "-h" "--help" + ) + + local devices=("bios" "ec" "pd0" "pd1" "rtm01" "rtm23" "ac-left" "ac-right") + local inputdeck_modes=("auto" "off" "on") + local console_modes=("recent" "follow") + local drivers=("portio" "cros-ec" "windows") + local brightness_options=("high" "medium" "low" "ultra-low") + + local current_word prev_word + current_word="${COMP_WORDS[COMP_CWORD]}" + prev_word="${COMP_WORDS[COMP_CWORD-1]}" + + # Handle options + if [[ $COMP_CWORD -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${options[*]}" -- "$current_word") ) + elif [[ $prev_word == "--device" ]]; then + COMPREPLY=( $(compgen -W "${devices[*]}" -- "$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 + COMPREPLY=( $(compgen -W "${drivers[*]}" -- "$current_word") ) + elif [[ $prev_word == "--fp-brightness" ]]; then + COMPREPLY=( $(compgen -W "${brightness_options[*]}" -- "$current_word") ) + fi + + return 0 +} + +complete -F _framework_tool framework_tool diff --git a/completions/zsh/_framework_tool b/completions/zsh/_framework_tool new file mode 100644 index 00000000..2d473c8f --- /dev/null +++ b/completions/zsh/_framework_tool @@ -0,0 +1,55 @@ +#compdef framework_tool + +local -a options + +options=( + '--flash-gpu-descriptor[]' + '-v[Increase logging verbosity]' + '-q[Decrease logging verbosity]' + '--versions[Show current firmware versions]' + '--version[Show tool version information (Add -vv for more details)]' + '--features[Show features supported by the firmware]' + '--esrt[Display the UEFI ESRT table]' + '--device[Specify device type]:device:(bios ec pd0 pd1 rtm01 rtm23 ac-left ac-right)' + '--compare-version[Specify version to compare]:compare_version' + '--power[Show current power status of battery and AC (Add -vv for more details)]' + '--thermal[Print thermal information (Temperatures and Fan speed)]' + '--sensors[Print sensor information (ALS, G-Sensor)]' + '--pdports[Show information about USB-C PD ports]' + '--info[Show info from SMBIOS (Only on UEFI)]' + '--pd-info[Show details about the PD controllers]' + '--dp-hdmi-info[Show details about connected DP or HDMI Expansion Cards]' + '--dp-hdmi-update[Update the DisplayPort or HDMI Expansion Card]:update_bin' + '--audio-card-info[Show details about connected Audio Expansion Cards (Needs root privileges)]' + '--privacy[Show privacy switch statuses (camera and microphone)]' + '--pd-bin[Parse versions from PD firmware binary file]:pd_bin' + '--ec-bin[Parse versions from EC firmware binary file]:ec_bin' + '--capsule[Parse UEFI Capsule information from binary file]:capsule' + '--dump[Dump extracted UX capsule bitmap image to a file]:dump' + '--ho2-capsule[Parse UEFI Capsule information from binary file]:ho2_capsule' + '--dump-ec-flash[Dump EC flash contents]:dump_ec_flash' + '--flash-ec[Flash EC with new firmware from file]:flash_ec' + '--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]' + '--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)' + '--fp-brightness[Get or set fingerprint LED brightness]:fp_brightness' + '--kblight[Set keyboard backlight percentage or get, if no value provided]:kblight' + '--rgbkbd[Set the color of to .]' + '--tablet-mode[Set tablet mode override]:tablet_mode:(auto tablet laptop)' + '--touchscreen-enable[Enable/disable touchscreen]:touchscreen_enable:(true false)' + '--console[Get EC console, choose whether recent or to follow the output]:console:(recent follow)' + '--reboot-ec[Control EC RO/RW jump]:reboot_ec:(reboot jump-ro jump-rw cancel-jump disable-jump)' + '--hash[Hash a file of arbitrary data]:hash' + '--driver[Select which driver is used]:driver:(portio cros-ec windows)' + '--pd-addrs[Specify I2C addresses of the PD chips (Advanced)]:pd_addrs' + '--pd-ports[Specify I2C ports of the PD chips (Advanced)]:pd_ports' + '-t[Run self-test to check if interaction with EC is possible]' + '-h[Print help]' +) + +_arguments -s -S $options diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 00000000..bd0c7fbf --- /dev/null +++ b/devenv.lock @@ -0,0 +1,140 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1747543288, + "owner": "cachix", + "repo": "devenv", + "rev": "3a8a52386bde1cf14fc2f4c4df80f91417348480", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1747392669, + "owner": "nix-community", + "repo": "fenix", + "rev": "c3c27e603b0d9b5aac8a16236586696338856fbb", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747372754, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1746807397, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "fenix": "fenix", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1747557850, + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "e464ff8c755c6e12540a45b83274ec4de4829191", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 00000000..e84d1529 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,18 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + packages = with pkgs; [ + systemd # libudev + # For UEFI building and testing + parted + gnumake + qemu + ]; + + languages.rust = { + enable = true; + targets = [ "x86_64-unknown-uefi" ]; + # https://devenv.sh/reference/options/#languagesrustchannel + channel = "stable"; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 00000000..1d180750 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,8 @@ +inputs: + fenix: + url: github:nix-community/fenix + inputs: + nixpkgs: + follows: nixpkgs + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling diff --git a/framework_lib/Cargo.toml b/framework_lib/Cargo.toml index 3dadedf9..62e0e5d8 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -1,6 +1,11 @@ [package] name = "framework_lib" -version = "0.2.1" +version = "0.4.5" +description = "Library to control Framework Computer systems" +homepage = "https://github.com/FrameworkComputer/framework-system" +repository = "https://github.com/FrameworkComputer/framework-system" +readme = "README.md" +license = "BSD-3-Clause" edition = "2021" # Minimum Supported Rust Version # Ubuntu 24.04 LTS ships 1.75 @@ -8,38 +13,11 @@ rust-version = "1.74" build = "build.rs" [features] -default = ["linux"] -# Linux/FreeBSD -unix = ["std", "raw_pio", "smbios", "dep:nix", "dep:libc"] -linux = ["unix", "linux_pio", "cros_ec_driver", "hidapi", "rusb"] -freebsd = ["unix", "freebsd_pio", "hidapi", "rusb"] -# hidapi and rusb don't seem to build in the cross container at the moment -cross_freebsd = ["unix", "freebsd_pio"] -# Windows does not have the cros_ec driver nor raw port I/O access to userspace -windows = ["std", "smbios", "dep:windows", "win_driver", "raw_pio", "hidapi", "rusb", "dep:wmi"] -smbios = ["dep:smbios-lib"] -std = ["dep:clap", "dep:clap-verbosity-flag", "dep:env_logger", "smbios-lib?/std"] +default = ["hidapi", "rusb"] +readonly = [ ] rusb = ["dep:rusb"] hidapi = ["dep:hidapi"] -uefi = [ - "dep:plain", "raw_pio", "smbios", "lazy_static/spin_no_std", "dep:uefi", "dep:uefi-services", - # Otherwise I get: `LLVM ERROR: Do not know how to split the result of this operator!` - # Seems to be a Ruset/LLVM bug when SSE is enabled. - # See: https://github.com/rust-lang/rust/issues/61721 - "sha2/force-soft" -] - -# EC communication via Port I/O on FreeBSD -freebsd_pio = ["redox_hwio/std"] -# EC communication via Port I/O on Linux -linux_pio = ["dep:libc", "redox_hwio/std"] -# EC communication via raw Port I/O (e.g. UEFI or other ring 0 code) -raw_pio = [] -# EC communication via cros_ec driver on Linux -cros_ec_driver = [] - -# Chromium EC driver by DHowett -win_driver = [] +uefi = [ "lazy_static/spin_no_std" ] [build-dependencies] built = { version = "0.5", features = ["chrono", "git2"] } @@ -47,41 +25,44 @@ built = { version = "0.5", features = ["chrono", "git2"] } [dependencies] lazy_static = "1.4.0" sha2 = { version = "0.10.8", default-features = false, features = [ "force-soft" ] } -regex = { version = "1.10.6", default-features = false } -redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd", default-features = false } -libc = { version = "0.2.155", optional = true } -clap = { version = "4.5", features = ["derive"], optional = true } -clap-verbosity-flag = { version = "2.2.1", optional = true } -nix = { version = "0.29.0", features = ["ioctl", "user"], optional = true } +regex = { version = "1.11.1", default-features = false } num = { version = "0.4", default-features = false } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false } -env_logger = { version = "0.11", optional = true } log = { version = "0.4", default-features = true } -uefi = { version = "0.20", features = ["alloc"], optional = true } -uefi-services = { version = "0.17", optional = true } -plain = { version = "0.2.3", optional = true } -spin = { version = "0.9.8", optional = false } -# hidapi 2.6.2 needs nightly -# See: https://github.com/ruabmbua/hidapi-rs/pull/158 -hidapi = { version = "=2.6.1", optional = true } -rusb = { version = "0.9.4", optional = true } +spin = { version = "0.9.8" } no-std-compat = { version = "0.4.1", features = [ "alloc" ] } -guid_macros = { path = "../guid_macros" } -wmi = { version = "0.13.3", optional = true } +hidapi = { version = "2.6.3", features = [ "windows-native" ], optional = true } +rusb = { version = "0.9.4", optional = true } +guid-create = { version = "0.5.0", default-features = false } + +[target.'cfg(target_os = "uefi")'.dependencies] +uefi = { version = "0.20", features = ["alloc"] } +uefi-services = "0.17" +plain = "0.2.3" +redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd", default-features = false } +smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std", default-features = false } + +[target.'cfg(windows)'.dependencies] +wmi = "0.15.0" +smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std" } +env_logger = "0.11" +clap = { version = "4.5", features = ["derive", "cargo"] } +clap-num = { version = "1.2.0" } +clap-verbosity-flag = { version = "2.2.1" } +windows-version = "0.1.4" -[dependencies.smbios-lib] -git = "https://github.com/FrameworkComputer/smbios-lib.git" -branch = "no-std" -optional = true -default-features = false -# Local development -#path = "../../smbios-lib" -# After my changes are upstreamed -#version = "0.9.0" +[target.'cfg(unix)'.dependencies] +libc = "0.2.155" +nix = { version = "0.29.0", features = ["ioctl", "user"] } +redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd" } +smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std" } +env_logger = "0.11" +clap = { version = "4.5", features = ["derive", "cargo"] } +clap-num = { version = "1.2.0" } +clap-verbosity-flag = { version = "2.2.1" } -[dependencies.windows] -optional = true +[target.'cfg(windows)'.dependencies.windows] version = "0.59.0" features = [ "Win32_Foundation", @@ -90,5 +71,12 @@ features = [ "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_SystemServices", + # For HID devices + "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_HumanInterfaceDevice", + "Win32_Devices_Properties", + "Win32_Storage_EnhancedStorage", + "Win32_System_Threading", + "Win32_UI_Shell_PropertiesSystem" ] diff --git a/framework_lib/src/audio_card.rs b/framework_lib/src/audio_card.rs index 8647791d..3baa33ca 100644 --- a/framework_lib/src/audio_card.rs +++ b/framework_lib/src/audio_card.rs @@ -15,7 +15,7 @@ enum CapeCommand { GetVersion = 0x0103, } -#[repr(packed)] +#[repr(C, packed)] #[derive(Clone, Copy)] struct CapeMessage { _len: i16, @@ -25,7 +25,7 @@ struct CapeMessage { data: [u32; CAPE_DATA_LEN], } -#[repr(packed)] +#[repr(C, packed)] #[derive(Clone, Copy)] struct HidCapeMessage { _report_id: u16, diff --git a/framework_lib/src/camera.rs b/framework_lib/src/camera.rs new file mode 100644 index 00000000..c99d9915 --- /dev/null +++ b/framework_lib/src/camera.rs @@ -0,0 +1,30 @@ +pub const FRAMEWORK_VID: u16 = 0x32AC; +pub const FRAMEWORK13_16_2ND_GEN_PID: u16 = 0x001C; +pub const FRAMEWORK12_PID: u16 = 0x001D; + +/// Get and print the firmware version of the camera +pub fn check_camera_version() -> Result<(), rusb::Error> { + for dev in rusb::devices().unwrap().iter() { + let dev_descriptor = dev.device_descriptor().unwrap(); + if dev_descriptor.vendor_id() != FRAMEWORK_VID + || (dev_descriptor.product_id() != FRAMEWORK13_16_2ND_GEN_PID + && dev_descriptor.product_id() != FRAMEWORK12_PID) + { + debug!( + "Skipping {:04X}:{:04X}", + dev_descriptor.vendor_id(), + dev_descriptor.product_id() + ); + continue; + } + 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()); + } + Ok(()) +} diff --git a/framework_lib/src/capsule.rs b/framework_lib/src/capsule.rs index 9e6a35bc..8ded6493 100644 --- a/framework_lib/src/capsule.rs +++ b/framework_lib/src/capsule.rs @@ -11,21 +11,17 @@ use std::prelude::v1::*; use core::prelude::rust_2021::derive; -#[cfg(all(not(feature = "uefi"), feature = "std"))] +use guid_create::CGuid; +#[cfg(not(feature = "uefi"))] use std::fs::File; -#[cfg(all(not(feature = "uefi"), feature = "std"))] -use std::io::prelude::*; - #[cfg(not(feature = "uefi"))] -use crate::guid::Guid; -#[cfg(feature = "uefi")] -use uefi::Guid; +use std::io::prelude::*; #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct EfiCapsuleHeader { /// A GUID that defines the contents of a capsule. - pub capsule_guid: Guid, + pub capsule_guid: CGuid, /// The size of the capsule header. This may be larger than the size of /// the EFI_CAPSULE_HEADER since CapsuleGuid may imply @@ -180,7 +176,7 @@ pub fn dump_winux_image(data: &[u8], header: &DisplayCapsule, filename: &str) { let image = &data[header_len..image_size]; - #[cfg(all(not(feature = "uefi"), feature = "std"))] + #[cfg(not(feature = "uefi"))] { let mut file = File::create(filename).unwrap(); file.write_all(image).unwrap(); @@ -209,14 +205,14 @@ mod tests { let data = fs::read(capsule_path).unwrap(); let cap = parse_capsule_header(&data).unwrap(); let expected_header = EfiCapsuleHeader { - capsule_guid: esrt::WINUX_GUID, + capsule_guid: CGuid::from(esrt::WINUX_GUID), header_size: 28, flags: 65536, capsule_image_size: 676898, }; assert_eq!(cap, expected_header); - assert_eq!(cap.capsule_guid, esrt::WINUX_GUID); + assert_eq!(cap.capsule_guid, CGuid::from(esrt::WINUX_GUID)); let ux_header = parse_ux_header(&data); assert_eq!( ux_header, diff --git a/framework_lib/src/capsule_content.rs b/framework_lib/src/capsule_content.rs index ca9d9ec6..81199bda 100644 --- a/framework_lib/src/capsule_content.rs +++ b/framework_lib/src/capsule_content.rs @@ -8,7 +8,7 @@ use crate::alloc::string::ToString; use alloc::string::String; use core::convert::TryInto; -use crate::ccgx::binary::{CCG5_PD_LEN, CCG6_PD_LEN}; +use crate::ccgx::binary::{CCG5_PD_LEN, CCG6_PD_LEN, CCG8_PD_LEN}; use crate::ec_binary::EC_LEN; use crate::util; @@ -29,13 +29,14 @@ pub fn find_bios_version(data: &[u8]) -> Option { let needle = b"$BVDT"; let found = util::find_sequence(data, needle)?; + // One of: GFW30, HFW3T, HFW30, IFR30, KFM30, JFP30, LFK30, IFGA3, IFGP6, LFR20, LFSP0 let platform_offset = found + 0xA + needle.len() - 1; - let platform = std::str::from_utf8(&data[platform_offset..platform_offset + 4]) + let platform = std::str::from_utf8(&data[platform_offset..platform_offset + 5]) .map(|x| x.to_string()) .ok()?; - let ver_offset = found + 0xE + needle.len() - 1; - let version = std::str::from_utf8(&data[ver_offset..ver_offset + 4]) + let ver_offset = found + 0x10 + needle.len() - 1; + let version = std::str::from_utf8(&data[ver_offset..ver_offset + 5]) .map(|x| x.to_string()) .ok()?; @@ -43,12 +44,10 @@ pub fn find_bios_version(data: &[u8]) -> Option { } pub fn find_ec_in_bios_cap(data: &[u8]) -> Option<&[u8]> { - let needle = b"_IFLASH_EC_IMG_"; - let found_iflash = util::find_sequence(data, needle)?; - // The actual EC binary is a few bytes after `_IFLASH_EC_IMG_`. - // Just earch for the first 4 bytes that seem to appear in all EC images. - let found = util::find_sequence(&data[found_iflash..], &[0x10, 0x00, 0x00, 0xf7])?; - Some(&data[found_iflash + found..found_iflash + found + EC_LEN]) + let needle = b"$_IFLASH_EC_IMG_"; + let found = util::find_sequence(data, needle)?; + let ec_offset = found + 0x9 + needle.len() - 1; + Some(&data[ec_offset..ec_offset + EC_LEN]) } pub fn find_pd_in_bios_cap(data: &[u8]) -> Option<&[u8]> { @@ -57,10 +56,13 @@ pub fn find_pd_in_bios_cap(data: &[u8]) -> Option<&[u8]> { // they're the same version let ccg5_needle = &[0x00, 0x20, 0x00, 0x20, 0x11, 0x00]; let ccg6_needle = &[0x00, 0x40, 0x00, 0x20, 0x11, 0x00]; + let ccg8_needle = &[0x00, 0x80, 0x00, 0x20, 0xAD, 0x0C]; if let Some(found_pd1) = util::find_sequence(data, ccg5_needle) { Some(&data[found_pd1..found_pd1 + CCG5_PD_LEN]) } else if let Some(found_pd1) = util::find_sequence(data, ccg6_needle) { Some(&data[found_pd1..found_pd1 + CCG6_PD_LEN]) + } else if let Some(found_pd1) = util::find_sequence(data, ccg8_needle) { + Some(&data[found_pd1..found_pd1 + CCG8_PD_LEN]) } else { None } diff --git a/framework_lib/src/ccgx/binary.rs b/framework_lib/src/ccgx/binary.rs index 30d7b33d..cb324221 100644 --- a/framework_lib/src/ccgx/binary.rs +++ b/framework_lib/src/ccgx/binary.rs @@ -44,7 +44,7 @@ const FW_VERSION_OFFSET: usize = 0xE0; const SMALL_ROW: usize = 0x80; const LARGE_ROW: usize = 0x100; -#[repr(packed)] +#[repr(C, packed)] #[derive(Debug, Copy, Clone)] struct VersionInfo { base_version: u32, @@ -53,8 +53,9 @@ struct VersionInfo { silicon_family: u16, } -pub const CCG5_PD_LEN: usize = 0x2_0000; -pub const CCG6_PD_LEN: usize = 0x2_0000; +pub const CCG5_PD_LEN: usize = 0x20_000; +pub const CCG6_PD_LEN: usize = 0x20_000; +pub const CCG8_PD_LEN: usize = 0x40_000; /// Information about all the firmware in a PD binary file /// @@ -105,7 +106,7 @@ fn read_metadata( let buffer = read_256_bytes(file_buffer, metadata_offset, flash_row_size)?; match ccgx { SiliconId::Ccg3 => parse_metadata_ccg3(&buffer), - SiliconId::Ccg5 | SiliconId::Ccg6 => parse_metadata_cyacd(&buffer), + SiliconId::Ccg5 | SiliconId::Ccg6Adl | SiliconId::Ccg6 => parse_metadata_cyacd(&buffer), SiliconId::Ccg8 => parse_metadata_cyacd2(&buffer) .map(|(fw_row_start, fw_size)| (fw_row_start / (flash_row_size as u32), fw_size)), } @@ -171,6 +172,7 @@ pub fn read_versions(file_buffer: &[u8], ccgx: SiliconId) -> Option (SMALL_ROW, 0x03FF, 0x03FE), SiliconId::Ccg5 => (LARGE_ROW, FW1_METADATA_ROW, FW2_METADATA_ROW_CCG5), + SiliconId::Ccg6Adl => (SMALL_ROW, FW1_METADATA_ROW, FW2_METADATA_ROW_CCG6), SiliconId::Ccg6 => (SMALL_ROW, FW1_METADATA_ROW, FW2_METADATA_ROW_CCG6), SiliconId::Ccg8 => (LARGE_ROW, FW1_METADATA_ROW_CCG8, FW2_METADATA_ROW_CCG8), }; diff --git a/framework_lib/src/ccgx/device.rs b/framework_lib/src/ccgx/device.rs index 432db3b6..4e25b883 100644 --- a/framework_lib/src/ccgx/device.rs +++ b/framework_lib/src/ccgx/device.rs @@ -3,78 +3,147 @@ //! The current implementation talks to them by tunneling I2C through EC host commands. use alloc::format; -use alloc::string::ToString; -use alloc::vec; use alloc::vec::Vec; #[cfg(feature = "uefi")] use core::prelude::rust_2021::derive; use crate::ccgx::{AppVersion, BaseVersion, ControllerVersion}; -use crate::chromium_ec::command::EcCommands; -use crate::chromium_ec::{CrosEc, CrosEcDriver, EcError, EcResult}; -use crate::util::{self, assert_win_len, Config, Platform}; -use std::mem::size_of; +use crate::chromium_ec::i2c_passthrough::*; +use crate::chromium_ec::{CrosEc, EcError, EcResult}; +use crate::util::{assert_win_len, Config, Platform}; use super::*; -/// Maximum transfer size for one I2C transaction supported by the chip -const MAX_I2C_CHUNK: usize = 128; +const _HPI_FLASH_ENTER_SIGNATURE: char = 'P'; +const _HPI_JUMP_TO_ALT_SIGNATURE: char = 'A'; +const _HPI_JUMP_TO_BOOT_SIGNATURE: char = 'J'; +const HPI_RESET_SIGNATURE: char = 'R'; +const _HPI_FLASH_RW_SIGNATURE: char = 'F'; +const HPI_RESET_DEV_CMD: u8 = 1; +const _HPI_FLASH_READ_CMD: u8 = 0; +const _HPI_FLASH_WRITE_CMD: u8 = 1; +#[derive(Debug, Copy, Clone)] enum ControlRegisters { DeviceMode = 0, SiliconId = 2, // Two bytes long, First LSB, then MSB + _InterruptStatus = 0x06, + _JumpToBoot = 0x07, + ResetRequest = 0x08, + _FlashmodeEnter = 0x0A, + _ValidateFw = 0x0B, + _FlashSignature = 0x0C, BootLoaderVersion = 0x10, Firmware1Version = 0x18, Firmware2Version = 0x20, + PdPortsEnable = 0x2C, + _ResponseType = 0x7E, + _FlashRwMem = 0x0200, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum PdPort { - Left01, - Right23, + Right01, + Left23, + Back, } impl PdPort { /// SMBUS/I2C Address - fn i2c_address(&self) -> u16 { + fn i2c_address(&self) -> EcResult { let config = Config::get(); let platform = &(*config).as_ref().unwrap().platform; + let unsupported = Err(EcError::DeviceError( + "Controller does not exist on this platform".to_string(), + )); - match (platform, self) { - (Platform::GenericFramework((left, _), _, _), PdPort::Left01) => *left, - (Platform::GenericFramework((_, right), _, _), PdPort::Right23) => *right, + Ok(match (platform, self) { + (Platform::GenericFramework((left, _, _), _), PdPort::Right01) => *left, + (Platform::GenericFramework((_, right, _), _), PdPort::Left23) => *right, + (Platform::GenericFramework((_, _, back), _), PdPort::Back) => *back, // Framework AMD Platforms (CCG8) - (Platform::Framework13Amd | Platform::Framework16, PdPort::Left01) => 0x42, - (Platform::Framework13Amd | Platform::Framework16, PdPort::Right23) => 0x40, + ( + Platform::Framework13Amd7080 + | Platform::Framework13AmdAi300 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300, + PdPort::Right01, + ) => 0x42, + ( + Platform::Framework13Amd7080 + | Platform::Framework13AmdAi300 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300, + PdPort::Left23, + ) => 0x40, + (Platform::Framework16Amd7080 | Platform::Framework16AmdAi300, PdPort::Back) => 0x42, + (Platform::FrameworkDesktopAmdAiMax300, PdPort::Back) => 0x08, + (Platform::FrameworkDesktopAmdAiMax300, _) => unsupported?, // Framework Intel Platforms (CCG5 and CCG6) - (_, PdPort::Left01) => 0x08, - (_, PdPort::Right23) => 0x40, - } + ( + Platform::Framework12IntelGen13 + | Platform::IntelGen11 + | Platform::IntelGen12 + | Platform::IntelGen13 + | Platform::IntelCoreUltra1, + PdPort::Right01, + ) => 0x08, + ( + Platform::Framework12IntelGen13 + | Platform::IntelGen11 + | Platform::IntelGen12 + | Platform::IntelGen13 + | Platform::IntelCoreUltra1, + PdPort::Left23, + ) => 0x40, + (Platform::UnknownSystem, _) => { + Err(EcError::DeviceError("Unsupported platform".to_string()))? + } + (_, PdPort::Back) => unsupported?, + }) } /// I2C port on the EC fn i2c_port(&self) -> EcResult { let config = Config::get(); let platform = &(*config).as_ref().unwrap().platform; + let unsupported = Err(EcError::DeviceError(format!( + "Controller {:?}, does not exist on {:?}", + self, platform + ))); Ok(match (platform, self) { - (Platform::GenericFramework(_, (left, _), _), PdPort::Left01) => *left, - (Platform::GenericFramework(_, (_, right), _), PdPort::Right23) => *right, + (Platform::GenericFramework(_, (left, _, _)), PdPort::Right01) => *left, + (Platform::GenericFramework(_, (_, right, _)), PdPort::Left23) => *right, + (Platform::GenericFramework(_, (_, _, back)), PdPort::Back) => *back, (Platform::IntelGen11, _) => 6, - (Platform::IntelGen12 | Platform::IntelGen13, PdPort::Left01) => 6, - (Platform::IntelGen12 | Platform::IntelGen13, PdPort::Right23) => 7, + (Platform::IntelGen12 | Platform::IntelGen13, PdPort::Right01) => 6, + (Platform::IntelGen12 | Platform::IntelGen13, PdPort::Left23) => 7, ( - Platform::Framework13Amd | Platform::Framework16 | Platform::IntelCoreUltra1, - PdPort::Left01, + Platform::Framework13Amd7080 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300 + | Platform::IntelCoreUltra1 + | Platform::Framework13AmdAi300 + | Platform::Framework12IntelGen13, + PdPort::Right01, ) => 1, ( - Platform::Framework13Amd | Platform::Framework16 | Platform::IntelCoreUltra1, - PdPort::Right23, + Platform::Framework13Amd7080 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300 + | Platform::IntelCoreUltra1 + | Platform::Framework13AmdAi300 + | Platform::Framework12IntelGen13, + PdPort::Left23, ) => 2, - // (_, _) => Err(EcError::DeviceError(format!( - // "Unsupported platform: {:?} {:?}", - // platform, self - // )))?, + (Platform::Framework16Amd7080 | Platform::Framework16AmdAi300, PdPort::Back) => 5, + (Platform::FrameworkDesktopAmdAiMax300, PdPort::Back) => 1, + (Platform::FrameworkDesktopAmdAiMax300, _) => unsupported?, + (Platform::UnknownSystem, _) => { + Err(EcError::DeviceError("Unsupported platform".to_string()))? + } + (_, PdPort::Back) => unsupported?, }) } } @@ -84,59 +153,7 @@ pub struct PdController { ec: CrosEc, } -fn passthrough_offset(dev_index: u16) -> u16 { - dev_index * 0x4000 -} - -#[repr(C, packed)] -struct EcParamsI2cPassthruMsg { - /// Slave address and flags - addr_and_flags: u16, - transfer_len: u16, -} - -#[repr(C, packed)] -struct EcParamsI2cPassthru { - port: u8, - /// How many messages - messages: u8, - msg: [EcParamsI2cPassthruMsg; 0], -} - -#[repr(C, packed)] -struct _EcI2cPassthruResponse { - i2c_status: u8, - /// How many messages - messages: u8, - data: [u8; 0], -} - -struct EcI2cPassthruResponse { - i2c_status: u8, // TODO: Can probably use enum - data: Vec, -} - -impl EcI2cPassthruResponse { - fn is_successful(&self) -> EcResult<()> { - if self.i2c_status & 1 > 0 { - return Err(EcError::DeviceError( - "I2C Transfer not acknowledged".to_string(), - )); - } - if self.i2c_status & (1 << 1) > 0 { - return Err(EcError::DeviceError("I2C Transfer timeout".to_string())); - } - // I'm not aware of any other errors, but there might be. - // But I don't think multiple errors can be indicated at the same time - assert_eq!(self.i2c_status, 0); - Ok(()) - } -} - -/// Indicate that it's a read, not a write -const I2C_READ_FLAG: u16 = 1 << 15; - -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum FwMode { BootLoader = 0, /// Backup CCGX firmware (No 1) @@ -172,63 +189,35 @@ impl PdController { pub fn new(port: PdPort, ec: CrosEc) -> Self { PdController { port, ec } } - /// Wrapped with support for dev id - /// TODO: Should move into chromium_ec module - /// TODO: Must not call CrosEc::new() otherwise the driver isn't configurable! - fn send_ec_command(&self, code: u16, dev_index: u16, data: &[u8]) -> EcResult> { - let command_id = code + passthrough_offset(dev_index); - self.ec.send_command(command_id, 0, data) - } fn i2c_read(&self, addr: u16, len: u16) -> EcResult { trace!( "I2C passthrough from I2C Port {} to I2C Addr {}", - self.port.i2c_port().unwrap(), - self.port.i2c_address() + self.port.i2c_port()?, + self.port.i2c_address()? ); - trace!("i2c_read(addr: {}, len: {})", addr, len); - if usize::from(len) > MAX_I2C_CHUNK { - return EcResult::Err(EcError::DeviceError(format!( - "i2c_read too long. Must be <128, is: {}", - len - ))); - } - let addr_bytes = u16::to_le_bytes(addr); - let messages = vec![ - EcParamsI2cPassthruMsg { - addr_and_flags: self.port.i2c_address(), - transfer_len: addr_bytes.len() as u16, - }, - EcParamsI2cPassthruMsg { - addr_and_flags: self.port.i2c_address() + I2C_READ_FLAG, - transfer_len: len, // How much to read - }, - ]; - let msgs_len = size_of::() * messages.len(); - let msgs_buffer: &[u8] = unsafe { util::any_vec_as_u8_slice(&messages) }; - - let params = EcParamsI2cPassthru { - port: self.port.i2c_port()?, - messages: messages.len() as u8, - msg: [], // Messages are copied right after this struct - }; - let params_len = size_of::(); - let params_buffer: &[u8] = unsafe { util::any_as_u8_slice(¶ms) }; - - let mut buffer: Vec = vec![0; params_len + msgs_len + addr_bytes.len()]; - buffer[0..params_len].copy_from_slice(params_buffer); - buffer[params_len..params_len + msgs_len].copy_from_slice(msgs_buffer); - buffer[params_len + msgs_len..].copy_from_slice(&addr_bytes); - - let data = self.send_ec_command(EcCommands::I2cPassthrough as u16, 0, &buffer)?; - let res: _EcI2cPassthruResponse = unsafe { std::ptr::read(data.as_ptr() as *const _) }; - let res_data = &data[size_of::<_EcI2cPassthruResponse>()..]; - // TODO: Seems to be either one, non-deterministically - debug_assert!(res.messages as usize == messages.len() || res.messages == 0); - Ok(EcI2cPassthruResponse { - i2c_status: res.i2c_status, - data: res_data.to_vec(), - }) + i2c_read( + &self.ec, + self.port.i2c_port()?, + self.port.i2c_address()?, + addr, + len, + ) + } + + pub fn i2c_write(&self, addr: u16, data: &[u8]) -> EcResult { + trace!( + "I2C passthrough from I2C Port {} to I2C Addr {}", + self.port.i2c_port()?, + self.port.i2c_address()? + ); + i2c_write( + &self.ec, + self.port.i2c_port()?, + self.port.i2c_address()?, + addr, + data, + ) } fn ccgx_read(&self, reg: ControlRegisters, len: u16) -> EcResult> { @@ -253,6 +242,35 @@ impl PdController { Ok(data) } + fn ccgx_write(&self, reg: ControlRegisters, data: &[u8]) -> EcResult<()> { + let addr = reg as u16; + trace!( + "ccgx_write(reg: {:?}, addr: {}, data.len(): {}", + reg, + addr, + data.len() + ); + let mut data_written = 0; + + while data_written < data.len() { + let chunk_len = std::cmp::min(MAX_I2C_CHUNK, data.len()); + let buffer = &data[data_written..data_written + chunk_len]; + let offset = addr + data_written as u16; + + let i2c_response = self.i2c_write(offset, buffer)?; + if let Err(EcError::DeviceError(err)) = i2c_response.is_successful() { + return Err(EcError::DeviceError(format!( + "I2C write was not successful: {:?}", + err + ))); + } + + data_written += chunk_len; + } + + Ok(()) + } + pub fn get_silicon_id(&self) -> EcResult { let data = self.ccgx_read(ControlRegisters::SiliconId, 2)?; assert_win_len(data.len(), 2); @@ -344,4 +362,24 @@ impl PdController { base_ver, app_ver ); } + + pub fn reset_device(&self) -> EcResult<()> { + self.ccgx_write( + ControlRegisters::ResetRequest, + &[HPI_RESET_SIGNATURE as u8, HPI_RESET_DEV_CMD], + )?; + Ok(()) + } + + pub fn enable_ports(&self, enable: bool) -> EcResult<()> { + let mask = if enable { 0b11 } else { 0b00 }; + self.ccgx_write(ControlRegisters::PdPortsEnable, &[mask])?; + Ok(()) + } + + pub fn get_port_status(&self) -> EcResult { + let data = self.ccgx_read(ControlRegisters::PdPortsEnable, 1)?; + assert_win_len(data.len(), 1); + Ok(data[0]) + } } diff --git a/framework_lib/src/ccgx/hid.rs b/framework_lib/src/ccgx/hid.rs index cc46111b..c36aeebb 100644 --- a/framework_lib/src/ccgx/hid.rs +++ b/framework_lib/src/ccgx/hid.rs @@ -25,7 +25,7 @@ const FW2_START: usize = 0x0200; const FW1_METADATA: usize = 0x03FF; const FW2_METADATA: usize = 0x03FE; -#[repr(packed)] +#[repr(C, packed)] #[derive(Debug, Copy, Clone)] struct HidFirmwareInfo { report_id: u8, @@ -132,10 +132,10 @@ fn get_fw_info(device: &HidDevice) -> HidFirmwareInfo { decode_fw_info(&buf) } -pub fn check_ccg_fw_version(device: &HidDevice) { +pub fn check_ccg_fw_version(device: &HidDevice, verbose: bool) { magic_unlock(device); let info = get_fw_info(device); - print_fw_info(&info); + print_fw_info(&info, verbose); } fn decode_fw_info(buf: &[u8]) -> HidFirmwareInfo { @@ -152,13 +152,13 @@ fn decode_fw_info(buf: &[u8]) -> HidFirmwareInfo { info } -fn print_fw_info(info: &HidFirmwareInfo) { +fn print_fw_info(info: &HidFirmwareInfo, verbose: bool) { assert_eq!(info.report_id, ReportIdCmd::E0Read as u8); info!(" Signature: {:X?}", info.signature); // Something's totally off if the signature is invalid if info.signature != [b'C', b'Y'] { - println!("Firmware Signature is invalid."); + error!("Firmware Signature is invalid."); return; } @@ -219,23 +219,33 @@ fn print_fw_info(info: &HidFirmwareInfo) { FwMode::BackupFw => (base_version_1, image_1_valid, base_version_2, image_2_valid), }; - println!( - " Active Firmware: {:03} ({}){}", - active_ver.build_number, - active_ver, - if active_valid { "" } else { " - INVALID!" } - ); - println!( - " Inactive Firmware: {:03} ({}){}", - inactive_ver.build_number, - inactive_ver, - if inactive_valid { "" } else { " - INVALID!" } - ); - println!( - " Operating Mode: {:?} (#{})", - FwMode::try_from(info.operating_mode).unwrap(), - info.operating_mode - ); + if verbose || active_ver != inactive_ver { + println!( + " Active Firmware: {:03} ({}){}", + active_ver.build_number, + active_ver, + if active_valid { "" } else { " - INVALID!" } + ); + println!( + " Inactive Firmware: {:03} ({}){}", + inactive_ver.build_number, + inactive_ver, + if inactive_valid { "" } else { " - INVALID!" } + ); + println!( + " Operating Mode: {:?} (#{})", + FwMode::try_from(info.operating_mode).unwrap(), + info.operating_mode + ); + } else { + println!( + " Active Firmware: {:03} ({}, {:?}){}", + active_ver.build_number, + active_ver, + FwMode::try_from(info.operating_mode).unwrap(), + if active_valid { "" } else { " - INVALID!" } + ); + } } /// Turn CCG3 Expansion Card VID/PID into their name @@ -256,6 +266,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 @@ -331,7 +342,7 @@ pub fn flash_firmware(fw_binary: &[u8]) { magic_unlock(&device); let info = get_fw_info(&device); println!("Before Updating"); - print_fw_info(&info); + print_fw_info(&info, true); println!("Updating..."); match info.operating_mode { @@ -368,7 +379,7 @@ pub fn flash_firmware(fw_binary: &[u8]) { wait_to_reappear(&mut api, &filter_devs, sn).expect("Device did not reappear"); println!("After Updating"); - print_fw_info(&info); + print_fw_info(&info, true); } } diff --git a/framework_lib/src/ccgx/mod.rs b/framework_lib/src/ccgx/mod.rs index 8c8f89d2..4495cc7d 100644 --- a/framework_lib/src/ccgx/mod.rs +++ b/framework_lib/src/ccgx/mod.rs @@ -3,6 +3,8 @@ use alloc::format; use alloc::string::String; use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; #[cfg(feature = "uefi")] use core::prelude::rust_2021::derive; use num_derive::FromPrimitive; @@ -30,7 +32,7 @@ const CCG3_METADATA_OFFSET: usize = 0x40; const METADATA_MAGIC: u16 = u16::from_le_bytes([b'Y', b'C']); // CY (Cypress) const CCG8_METADATA_MAGIC: u16 = u16::from_le_bytes([b'F', b'I']); // IF (Infineon) -#[repr(packed)] +#[repr(C, packed)] #[derive(Debug, Copy, Clone)] struct CyAcdMetadata { /// Offset 00: Single Byte FW Checksum @@ -64,7 +66,7 @@ struct CyAcdMetadata { } // TODO: Would be nice to check the checksums -#[repr(packed)] +#[repr(C, packed)] #[derive(Debug, Copy, Clone)] struct CyAcd2Metadata { /// Offset 00: App Firmware Start @@ -103,11 +105,12 @@ struct CyAcd2Metadata { pub enum SiliconId { Ccg3 = 0x1D00, Ccg5 = 0x2100, - Ccg6 = 0x3000, + Ccg6Adl = 0x3000, + Ccg6 = 0x30A0, Ccg8 = 0x3580, } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub struct BaseVersion { /// Major part of the version. X of X.Y.Z.BB pub major: u8, @@ -151,7 +154,7 @@ impl From for BaseVersion { } } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub enum Application { Notebook, Monitor, @@ -159,7 +162,7 @@ pub enum Application { Invalid, } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub struct AppVersion { pub application: Application, /// Major part of the version. X of X.Y.Z @@ -234,23 +237,32 @@ impl ControllerFirmwares { } #[derive(Debug, PartialEq)] -pub struct PdVersions { - pub controller01: ControllerFirmwares, - pub controller23: ControllerFirmwares, +pub enum PdVersions { + RightLeft((ControllerFirmwares, ControllerFirmwares)), + Single(ControllerFirmwares), + Many(Vec), } /// Same as PdVersions but only the main FW #[derive(Debug)] -pub struct MainPdVersions { - pub controller01: ControllerVersion, - pub controller23: ControllerVersion, +pub enum MainPdVersions { + RightLeft((ControllerVersion, ControllerVersion)), + Single(ControllerVersion), + Many(Vec), } pub fn get_pd_controller_versions(ec: &CrosEc) -> EcResult { - Ok(PdVersions { - controller01: PdController::new(PdPort::Left01, ec.clone()).get_fw_versions()?, - controller23: PdController::new(PdPort::Right23, ec.clone()).get_fw_versions()?, - }) + let pd01 = PdController::new(PdPort::Right01, ec.clone()).get_fw_versions(); + let pd23 = PdController::new(PdPort::Left23, ec.clone()).get_fw_versions(); + let pd_back = PdController::new(PdPort::Back, ec.clone()).get_fw_versions(); + + match (pd01, pd23, pd_back) { + (Err(_), Err(_), Ok(pd_back)) => Ok(PdVersions::Single(pd_back)), + (Ok(pd01), Ok(pd23), Err(_)) => Ok(PdVersions::RightLeft((pd01, pd23))), + (Ok(pd01), Ok(pd23), Ok(pd_back)) => Ok(PdVersions::Many(vec![pd01, pd23, pd_back])), + (Err(err), _, _) => Err(err), + (_, Err(err), _) => Err(err), + } } fn parse_metadata_ccg3(buffer: &[u8]) -> Option<(u32, u32)> { @@ -297,3 +309,42 @@ fn parse_metadata_cyacd2(buffer: &[u8]) -> Option<(u32, u32)> { None } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + // Make sure deriving does what I expect, properly comparing with multiple fields + fn derive_ord() { + let v0_0_0 = AppVersion { + application: Application::Notebook, + major: 0, + minor: 0, + circuit: 0, + }; + let v1_0_1 = AppVersion { + application: Application::Notebook, + major: 1, + minor: 0, + circuit: 1, + }; + let v0_1_0 = AppVersion { + application: Application::Notebook, + major: 0, + minor: 1, + circuit: 0, + }; + let v1_1_1 = AppVersion { + application: Application::Notebook, + major: 1, + minor: 1, + circuit: 1, + }; + assert_eq!(v0_0_0, v0_0_0.clone()); + assert!(v0_0_0 < v1_0_1); + assert!(v0_1_0 < v1_0_1); + assert!(v1_0_1 < v1_1_1); + assert!(v1_1_1 > v1_0_1); + } +} diff --git a/framework_lib/src/chromium_ec/command.rs b/framework_lib/src/chromium_ec/command.rs index a0205780..608f29bc 100644 --- a/framework_lib/src/chromium_ec/command.rs +++ b/framework_lib/src/chromium_ec/command.rs @@ -28,29 +28,44 @@ pub enum EcCommands { /// Erase section of EC flash FlashErase = 0x13, FlashProtect = 0x15, + PwmSetFanTargetRpm = 0x0021, PwmGetKeyboardBacklight = 0x0022, PwmSetKeyboardBacklight = 0x0023, PwmSetFanDuty = 0x0024, PwmSetDuty = 0x0025, + MotionSense = 0x002B, PwmGetDuty = 0x0026, - GpioGet = 0x93, - I2cPassthrough = 0x9e, - ConsoleSnapshot = 0x97, - ConsoleRead = 0x98, + 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 = 0x0D, + GetFeatures = 0x000D, /// Force reboot, causes host reboot as well - Reboot = 0xD1, + Reboot = 0x00D1, /// Control EC boot - RebootEc = 0xD2, + RebootEc = 0x00D2, /// Get information about PD controller power - UsbPdPowerInfo = 0x103, + UsbPdPowerInfo = 0x0103, + AdcRead = 0x0123, + ApReset = 0x0125, + LocateChip = 0x0126, + RebootApOnG3 = 0x0127, + RgbKbdSetColor = 0x013A, + RgbKbd = 0x013B, // Framework specific commands /// Configure the behavior of the flash notify FlashNotified = 0x3E01, /// Change charge limit ChargeLimitControl = 0x3E03, + DisablePs2Emulation = 0x3E08, + UpdateKeyboardMatrix = 0x3E0C, /// Get/Set Fingerprint LED brightness FpLedLevelControl = 0x3E0E, /// Get information about the current chassis open/close status @@ -85,6 +100,11 @@ pub enum EcCommands { ExpansionBayStatus = 0x3E1B, /// Get hardware diagnostics GetHwDiag = 0x3E1C, + /// Get gpu bay serial + GetGpuSerial = 0x3E1D, + GetGpuPcie = 0x3E1E, + /// Set gpu bay serial and program structure + ProgramGpuEeprom = 0x3E1F, } pub trait EcRequest { @@ -160,9 +180,9 @@ pub trait EcRequestRaw { { let response = self.send_command_vec_extra(ec, extra_data)?; // TODO: The Windows driver seems to return 20 more bytes than expected - #[cfg(feature = "win_driver")] + #[cfg(windows)] let expected = response.len() != std::mem::size_of::() + 20; - #[cfg(not(feature = "win_driver"))] + #[cfg(not(windows))] let expected = response.len() != std::mem::size_of::(); if expected { return Err(EcError::DeviceError(format!( diff --git a/framework_lib/src/chromium_ec/commands.rs b/framework_lib/src/chromium_ec/commands.rs index 82057165..48763c06 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)] @@ -225,6 +282,176 @@ impl EcRequest for EcRequestPwmGetDuty { } } +#[repr(u8)] +pub enum MotionSenseCmd { + Dump = 0, + Info = 1, +} + +#[repr(C, packed)] +pub struct EcRequestMotionSenseDump { + /// MotionSenseCmd::Dump + pub cmd: u8, + /// Maximal number of sensor the host is expecting. + /// 0 means the host is only interested in the number + /// of sensors controlled by the EC. + pub max_sensor_count: u8, +} + +#[repr(C, packed)] +pub struct EcResponseMotionSenseDump { + /// Flags representing the motion sensor module + pub module_flags: u8, + + /// Number of sensors managed directly by the EC + pub sensor_count: u8, + + /// Sensor data is truncated if response_max is too small + /// for holding all the data. + pub sensor: [u8; 0], +} + +impl EcRequest for EcRequestMotionSenseDump { + fn command_id() -> EcCommands { + EcCommands::MotionSense + } + fn command_version() -> u8 { + 1 + } +} + +#[derive(Debug, FromPrimitive, PartialEq)] +pub enum MotionSenseType { + Accel = 0, + Gyro = 1, + Mag = 2, + Prox = 3, + Light = 4, + Activity = 5, + Baro = 6, + Sync = 7, + LightRgb = 8, +} + +#[derive(Debug, FromPrimitive)] +pub enum MotionSenseLocation { + Base = 0, + Lid = 1, + Camera = 2, +} + +#[derive(Debug, FromPrimitive)] +pub enum MotionSenseChip { + Kxcj9 = 0, + Lsm6ds0 = 1, + Bmi160 = 2, + Si1141 = 3, + Si1142 = 4, + Si1143 = 5, + Kx022 = 6, + L3gd20h = 7, + Bma255 = 8, + Bmp280 = 9, + Opt3001 = 10, + Bh1730 = 11, + Gpio = 12, + Lis2dh = 13, + Lsm6dsm = 14, + Lis2de = 15, + Lis2mdl = 16, + Lsm6ds3 = 17, + Lsm6dso = 18, + Lng2dm = 19, + Tcs3400 = 20, + Lis2dw12 = 21, + Lis2dwl = 22, + Lis2ds = 23, + Bmi260 = 24, + Icm426xx = 25, + Icm42607 = 26, + Bma422 = 27, + Bmi323 = 28, + Bmi220 = 29, + Cm32183 = 30, + Veml3328 = 31, +} + +#[repr(C, packed)] +pub struct EcRequestMotionSenseInfo { + /// MotionSenseCmd::Info + pub cmd: u8, + /// Sensor index + pub sensor_num: u8, +} + +#[repr(C)] +pub struct EcResponseMotionSenseInfo { + /// See enum MotionSenseInfo + pub sensor_type: u8, + /// See enum MotionSenseLocation + pub location: u8, + /// See enum MotionSenseChip + pub chip: u8, +} + +#[derive(Debug)] +pub struct MotionSenseInfo { + pub sensor_type: MotionSenseType, + pub location: MotionSenseLocation, + pub chip: MotionSenseChip, +} + +impl EcRequest for EcRequestMotionSenseInfo { + fn command_id() -> EcCommands { + EcCommands::MotionSense + } + fn command_version() -> u8 { + 1 + } +} + +pub enum TabletModeOverride { + Default = 0, + ForceTablet = 1, + ForceClamshell = 2, +} + +#[repr(C, packed)] +pub struct EcRequestSetTabletMode { + /// See TabletModeOverride + pub mode: u8, +} + +impl EcRequest<()> for EcRequestSetTabletMode { + fn command_id() -> EcCommands { + EcCommands::SetTabletMode + } +} + +#[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], @@ -244,6 +471,61 @@ impl EcRequest for EcRequestGpioGetV0 { } } +pub enum GpioGetSubCommand { + ByName = 0, + Count = 1, + Info = 2, +} + +#[repr(C, packed)] +pub struct EcRequestGpioGetV1Count { + pub subcmd: u8, +} + +#[repr(C, packed)] +pub struct EcRequestGpioGetV1ByName { + pub subcmd: u8, + pub name: [u8; 32], +} + +#[repr(C, packed)] +pub struct EcRequestGpioGetV1Info { + pub subcmd: u8, + pub index: u8, +} + +#[repr(C)] +pub struct EcResponseGpioGetV1Info { + pub val: u8, + pub name: [u8; 32], + pub flags: u32, +} + +impl EcRequest for EcRequestGpioGetV1Count { + fn command_id() -> EcCommands { + EcCommands::GpioGet + } + fn command_version() -> u8 { + 1 + } +} +impl EcRequest for EcRequestGpioGetV1ByName { + fn command_id() -> EcCommands { + EcCommands::GpioGet + } + fn command_version() -> u8 { + 1 + } +} +impl EcRequest for EcRequestGpioGetV1Info { + fn command_id() -> EcCommands { + EcCommands::GpioGet + } + fn command_version() -> u8 { + 1 + } +} + #[repr(C, packed)] pub struct EcRequestReboot {} @@ -278,6 +560,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 { @@ -500,6 +863,89 @@ 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 + } +} + +#[repr(C)] +pub struct EcRequestApReset {} + +impl EcRequest<()> for EcRequestApReset { + fn command_id() -> EcCommands { + EcCommands::ApReset + } +} + +#[repr(C)] +pub struct EcRequestRebootApOnG3V0 {} + +impl EcRequest<()> for EcRequestRebootApOnG3V0 { + fn command_id() -> EcCommands { + EcCommands::RebootApOnG3 + } + fn command_version() -> u8 { + 0 + } +} + +#[repr(C)] +pub struct EcRequestRebootApOnG3V1 { + /// Delay in seconds after entering G3 state + pub delay: u32, +} + +impl EcRequest<()> for EcRequestRebootApOnG3V1 { + fn command_id() -> EcCommands { + EcCommands::RebootApOnG3 + } + fn command_version() -> u8 { + 1 + } +} + +// 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; + +#[repr(C, packed)] +#[derive(Default, Clone, Copy, Debug)] +pub struct RgbS { + pub r: u8, + pub g: u8, + pub b: u8, +} + +#[repr(C, packed)] +pub struct EcRequestRgbKbdSetColor { + /// Specifies the starting key ID whose color is being changed + pub start_key: u8, + /// Specifies # of elements in color + pub length: u8, + /// RGB color data array of length up to MAX_KEY_COUNT + pub color: [RgbS; EC_RGBKBD_MAX_KEY_COUNT], +} + +#[repr(C, packed)] +pub struct EcResponseRgbKbdSetColor {} + +impl EcRequest for EcRequestRgbKbdSetColor { + fn command_id() -> EcCommands { + EcCommands::RgbKbdSetColor + } +} + // --- Framework Specific commands --- #[repr(C, packed)] @@ -517,6 +963,31 @@ impl EcRequest for EcRequestFlashNotify { } } +#[repr(C, packed)] +pub struct KeyboardMatrixMap { + pub row: u8, + pub col: u8, + pub scanset: u16, +} +#[repr(C, packed)] +pub struct EcRequestUpdateKeyboardMatrix { + pub num_items: u32, + pub write: u32, + pub scan_update: [KeyboardMatrixMap; 1], +} +#[repr(C, packed)] +pub struct EcResponseUpdateKeyboardMatrix { + pub num_items: u32, + pub write: u32, + pub scan_update: [KeyboardMatrixMap; 32], +} + +impl EcRequest for EcRequestUpdateKeyboardMatrix { + fn command_id() -> EcCommands { + EcCommands::UpdateKeyboardMatrix + } +} + #[repr(C, packed)] pub struct EcRequestChassisOpenCheck {} @@ -552,18 +1023,38 @@ impl EcRequest for EcRequestChassisIntrusionC } #[repr(C, packed)] -pub struct EcRequestReadPdVersion {} +pub struct EcRequestReadPdVersionV0 {} #[repr(C, packed)] -pub struct _EcResponseReadPdVersion { +pub struct _EcResponseReadPdVersionV0 { pub controller01: [u8; 8], pub controller23: [u8; 8], } -impl EcRequest<_EcResponseReadPdVersion> for EcRequestReadPdVersion { +impl EcRequest<_EcResponseReadPdVersionV0> for EcRequestReadPdVersionV0 { fn command_id() -> EcCommands { EcCommands::ReadPdVersion } + fn command_version() -> u8 { + 0 + } +} + +#[repr(C, packed)] +pub struct EcRequestReadPdVersionV1 {} +#[repr(C, packed)] +pub struct _EcResponseReadPdVersionV1 { + pub pd_chip_count: u8, + pub pd_controllers: [u8; 0], +} + +impl EcRequest<_EcResponseReadPdVersionV1> for EcRequestReadPdVersionV1 { + fn command_id() -> EcCommands { + EcCommands::ReadPdVersion + } + fn command_version() -> u8 { + 1 + } } #[repr(C, packed)] @@ -754,6 +1245,7 @@ pub struct EcRequestGetHwDiag {} pub struct EcResponseGetHwDiag { pub hw_diag: u32, pub bios_complete: u8, + pub device_complete: u8, } impl EcResponseGetHwDiag { @@ -820,16 +1312,31 @@ impl EcRequest for EcRequestChargeLimitControl { /// TODO: Use this pub const EC_CHARGE_LIMIT_RESTORE: u8 = 0x7F; +#[repr(C, packed)] +pub struct EcRequestDisablePs2Emulation { + pub disable: u8, +} + +impl EcRequest<()> for EcRequestDisablePs2Emulation { + fn command_id() -> EcCommands { + EcCommands::DisablePs2Emulation + } +} + #[repr(u8)] #[derive(Debug, FromPrimitive)] pub enum FpLedBrightnessLevel { High = 0, Medium = 1, Low = 2, + UltraLow = 3, + /// Custom: Only get, never set + Custom = 0xFE, + Auto = 0xFF, } #[repr(C, packed)] -pub struct EcRequestFpLedLevelControl { +pub struct EcRequestFpLedLevelControlV0 { /// See enum FpLedBrightnessLevel pub set_level: u8, /// Boolean. >1 to get the level @@ -838,12 +1345,125 @@ pub struct EcRequestFpLedLevelControl { #[repr(C, packed)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct EcResponseFpLedLevelControl { +pub struct EcResponseFpLedLevelControlV0 { + /// Current brightness, 1-100% + pub percentage: u8, +} + +impl EcRequest for EcRequestFpLedLevelControlV0 { + fn command_id() -> EcCommands { + EcCommands::FpLedLevelControl + } + fn command_version() -> u8 { + 0 + } +} + +#[repr(C, packed)] +pub struct EcRequestFpLedLevelControlV1 { + /// Percentage 1-100 + pub set_percentage: u8, + /// Boolean. >1 to get the level + pub get_level: u8, +} + +#[repr(C, packed)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct EcResponseFpLedLevelControlV1 { + /// Current brightness, 1-100% + pub percentage: u8, + /// Requested level. See enum FpLedBrightnessLevel pub level: u8, } -impl EcRequest for EcRequestFpLedLevelControl { +impl EcRequest for EcRequestFpLedLevelControlV1 { fn command_id() -> EcCommands { EcCommands::FpLedLevelControl } + fn command_version() -> u8 { + 1 + } +} + +#[repr(C, packed)] +pub struct EcRequestGetGpuSerial { + pub idx: u8, +} + +#[repr(C, packed)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct EcResponseGetGpuSerial { + pub idx: u8, + pub valid: u8, + pub serial: [u8; 20], +} + +impl EcRequest for EcRequestGetGpuSerial { + fn command_id() -> EcCommands { + EcCommands::GetGpuSerial + } +} + +#[repr(C, packed)] +pub struct EcRequestGetGpuPcie {} + +#[repr(u8)] +#[derive(Debug, FromPrimitive)] +pub enum GpuPcieConfig { + /// PCIe 8x1 + Pcie8x1 = 0, + /// PCIe 4x1 + Pcie4x1 = 1, + /// PCIe 4x2 + Pcie4x2 = 2, +} + +#[repr(u8)] +#[derive(Debug, FromPrimitive)] +pub enum GpuVendor { + Initializing = 0x00, + FanOnly = 0x01, + GpuAmdR23M = 0x02, + SsdHolder = 0x03, + PcieAccessory = 0x4, +} + +#[repr(C, packed)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct EcResponseGetGpuPcie { + pub gpu_pcie_config: u8, + pub gpu_vendor: u8, +} + +impl EcRequest for EcRequestGetGpuPcie { + fn command_id() -> EcCommands { + EcCommands::GetGpuPcie + } +} + +#[repr(u8)] +pub enum SetGpuSerialMagic { + /// 7700S config magic value + WriteGPUConfig = 0x0D, + /// SSD config magic value + WriteSSDConfig = 0x55, +} + +#[repr(C, packed)] +pub struct EcRequestSetGpuSerial { + pub magic: u8, + pub idx: u8, + pub serial: [u8; 20], +} + +#[repr(C, packed)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct EcResponseSetGpuSerial { + pub valid: u8, +} + +impl EcRequest for EcRequestSetGpuSerial { + fn command_id() -> EcCommands { + EcCommands::ProgramGpuEeprom + } } diff --git a/framework_lib/src/chromium_ec/cros_ec.rs b/framework_lib/src/chromium_ec/cros_ec.rs index a3b4928a..372c45ff 100644 --- a/framework_lib/src/chromium_ec/cros_ec.rs +++ b/framework_lib/src/chromium_ec/cros_ec.rs @@ -55,7 +55,7 @@ struct CrosEcCommandV2 { data: [u8; IN_SIZE], } -const DEV_PATH: &str = "/dev/cros_ec"; +pub const DEV_PATH: &str = "/dev/cros_ec"; lazy_static! { static ref CROS_EC_FD: Arc>> = Arc::new(Mutex::new(None)); diff --git a/framework_lib/src/chromium_ec/i2c_passthrough.rs b/framework_lib/src/chromium_ec/i2c_passthrough.rs new file mode 100644 index 00000000..03cb974e --- /dev/null +++ b/framework_lib/src/chromium_ec/i2c_passthrough.rs @@ -0,0 +1,164 @@ +use crate::chromium_ec::command::EcCommands; +use crate::chromium_ec::{CrosEc, CrosEcDriver, EcError, EcResult}; +use crate::util; +use alloc::format; +use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; +use std::mem::size_of; + +/// Maximum transfer size for one I2C transaction supported by the chip +pub const MAX_I2C_CHUNK: usize = 128; + +#[repr(C, packed)] +pub struct EcParamsI2cPassthruMsg { + /// Slave address and flags + addr_and_flags: u16, + transfer_len: u16, +} + +#[repr(C, packed)] +pub struct EcParamsI2cPassthru { + port: u8, + /// How many messages + messages: u8, + msg: [EcParamsI2cPassthruMsg; 0], +} + +#[repr(C, packed)] +struct _EcI2cPassthruResponse { + i2c_status: u8, + /// How many messages + messages: u8, + data: [u8; 0], +} + +#[derive(Debug)] +pub struct EcI2cPassthruResponse { + pub i2c_status: u8, // TODO: Can probably use enum + pub data: Vec, +} + +impl EcI2cPassthruResponse { + pub fn is_successful(&self) -> EcResult<()> { + if self.i2c_status & 1 > 0 { + return Err(EcError::DeviceError( + "I2C Transfer not acknowledged".to_string(), + )); + } + if self.i2c_status & (1 << 1) > 0 { + return Err(EcError::DeviceError("I2C Transfer timeout".to_string())); + } + // I'm not aware of any other errors, but there might be. + // But I don't think multiple errors can be indicated at the same time + assert_eq!(self.i2c_status, 0); + Ok(()) + } +} + +/// Indicate that it's a read, not a write +const I2C_READ_FLAG: u16 = 1 << 15; + +pub fn i2c_read( + ec: &CrosEc, + i2c_port: u8, + i2c_addr: u16, + addr: u16, + len: u16, +) -> EcResult { + trace!( + "i2c_read(i2c_port: 0x{:X}, i2c_addr: 0x{:X}, addr: 0x{:X}, len: 0x{:X})", + i2c_port, + i2c_addr, + addr, + len + ); + if usize::from(len) > MAX_I2C_CHUNK { + return EcResult::Err(EcError::DeviceError(format!( + "i2c_read too long. Must be <128, is: {}", + len + ))); + } + let addr_bytes = u16::to_le_bytes(addr); + let messages = vec![ + EcParamsI2cPassthruMsg { + addr_and_flags: i2c_addr, + transfer_len: addr_bytes.len() as u16, + }, + EcParamsI2cPassthruMsg { + addr_and_flags: i2c_addr + I2C_READ_FLAG, + transfer_len: len, // How much to read + }, + ]; + let msgs_len = size_of::() * messages.len(); + let msgs_buffer: &[u8] = unsafe { util::any_vec_as_u8_slice(&messages) }; + + let params = EcParamsI2cPassthru { + port: i2c_port, + messages: messages.len() as u8, + msg: [], // Messages are copied right after this struct + }; + let params_len = size_of::(); + let params_buffer: &[u8] = unsafe { util::any_as_u8_slice(¶ms) }; + + let mut buffer: Vec = vec![0; params_len + msgs_len + addr_bytes.len()]; + buffer[0..params_len].copy_from_slice(params_buffer); + buffer[params_len..params_len + msgs_len].copy_from_slice(msgs_buffer); + buffer[params_len + msgs_len..].copy_from_slice(&addr_bytes); + + let data = ec.send_command(EcCommands::I2cPassthrough as u16, 0, &buffer)?; + let res: _EcI2cPassthruResponse = unsafe { std::ptr::read(data.as_ptr() as *const _) }; + let res_data = &data[size_of::<_EcI2cPassthruResponse>()..]; + // TODO: Seems to be either one, non-deterministically + debug_assert!(res.messages as usize == messages.len() || res.messages == 0); + Ok(EcI2cPassthruResponse { + i2c_status: res.i2c_status, + data: res_data.to_vec(), + }) +} + +pub fn i2c_write( + ec: &CrosEc, + i2c_port: u8, + i2c_addr: u16, + addr: u16, + data: &[u8], +) -> EcResult { + trace!( + " i2c_write(addr: {}, len: {}, data: {:?})", + addr, + data.len(), + data + ); + let addr_bytes = [addr as u8, (addr >> 8) as u8]; + let messages = vec![EcParamsI2cPassthruMsg { + addr_and_flags: i2c_addr, + transfer_len: (addr_bytes.len() + data.len()) as u16, + }]; + let msgs_len = size_of::() * messages.len(); + let msgs_buffer: &[u8] = unsafe { util::any_vec_as_u8_slice(&messages) }; + + let params = EcParamsI2cPassthru { + port: i2c_port, + messages: messages.len() as u8, + msg: [], // Messages are copied right after this struct + }; + let params_len = size_of::(); + let params_buffer: &[u8] = unsafe { util::any_as_u8_slice(¶ms) }; + + let mut buffer: Vec = vec![0; params_len + msgs_len + addr_bytes.len() + data.len()]; + buffer[0..params_len].copy_from_slice(params_buffer); + buffer[params_len..params_len + msgs_len].copy_from_slice(msgs_buffer); + buffer[params_len + msgs_len..params_len + msgs_len + addr_bytes.len()] + .copy_from_slice(&addr_bytes); + buffer[params_len + msgs_len + addr_bytes.len()..].copy_from_slice(data); + + let data = ec.send_command(EcCommands::I2cPassthrough as u16, 0, &buffer)?; + let res: _EcI2cPassthruResponse = unsafe { std::ptr::read(data.as_ptr() as *const _) }; + util::assert_win_len(data.len(), size_of::<_EcI2cPassthruResponse>()); // No extra data other than the header + debug_assert_eq!(res.messages as usize, messages.len()); + Ok(EcI2cPassthruResponse { + i2c_status: res.i2c_status, + data: vec![], // Writing doesn't return any data + }) +} diff --git a/framework_lib/src/chromium_ec/mod.rs b/framework_lib/src/chromium_ec/mod.rs index f86fa839..ed1210e6 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; @@ -20,12 +21,17 @@ use num_derive::FromPrimitive; pub mod command; pub mod commands; -#[cfg(feature = "cros_ec_driver")] +#[cfg(target_os = "linux")] mod cros_ec; +pub mod i2c_passthrough; pub mod input_deck; +#[cfg(not(windows))] mod portio; +#[cfg(not(windows))] mod portio_mec; -#[cfg(feature = "win_driver")] +#[allow(dead_code)] +mod protocol; +#[cfg(windows)] mod windows; use alloc::format; @@ -33,6 +39,7 @@ use alloc::string::String; use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; + #[cfg(feature = "uefi")] use core::prelude::rust_2021::derive; use num_traits::FromPrimitive; @@ -119,18 +126,86 @@ pub enum EcResponseStatus { Busy = 16, } -pub fn has_mec() -> bool { - let platform = smbios::get_platform().unwrap(); - if let Platform::GenericFramework(_, _, has_mec) = platform { - return has_mec; - } +#[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, +} - !matches!( - smbios::get_platform().unwrap(), - Platform::Framework13Amd | Platform::Framework16 | Platform::IntelCoreUltra1 - ) +/// 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 trait CrosEcDriver { fn read_memory(&self, offset: u16, length: u16) -> Option>; fn send_command(&self, command: u16, command_version: u8, data: &[u8]) -> EcResult>; @@ -151,14 +226,20 @@ impl Default for CrosEc { /// /// Depending on the availability we choose the first one as default fn available_drivers() -> Vec { - vec![ - #[cfg(feature = "win_driver")] - CrosEcDriverType::Windows, - #[cfg(feature = "cros_ec_driver")] - CrosEcDriverType::CrosEc, - #[cfg(not(feature = "windows"))] - CrosEcDriverType::Portio, - ] + let mut drivers = vec![]; + + #[cfg(windows)] + drivers.push(CrosEcDriverType::Windows); + + #[cfg(target_os = "linux")] + if std::path::Path::new(cros_ec::DEV_PATH).exists() { + drivers.push(CrosEcDriverType::CrosEc); + } + + #[cfg(not(windows))] + drivers.push(CrosEcDriverType::Portio); + + drivers } impl CrosEc { @@ -174,6 +255,7 @@ impl CrosEc { return None; } debug!("Chromium EC Driver: {:?}", driver); + Some(CrosEc { driver }) } @@ -194,25 +276,25 @@ impl CrosEc { } } - pub fn check_mem_magic(&self) -> Option<()> { + pub fn check_mem_magic(&self) -> EcResult<()> { match self.read_memory(EC_MEMMAP_ID, 2) { Some(ec_id) => { if ec_id.len() != 2 { - error!(" Unexpected length returned: {:?}", ec_id.len()); - return None; - } - if ec_id[0] != b'E' || ec_id[1] != b'C' { - error!(" This machine doesn't look like it has a Framework EC"); - None + Err(EcError::DeviceError(format!( + " Unexpected length returned: {:?}", + ec_id.len() + ))) + } else if ec_id[0] != b'E' || ec_id[1] != b'C' { + Err(EcError::DeviceError( + "This machine doesn't look like it has a Framework EC".to_string(), + )) } else { - println!(" Verified that Framework EC is present!"); - Some(()) + Ok(()) } } - None => { - error!(" Failed to read EC ID from memory map"); - None - } + None => Err(EcError::DeviceError( + "Failed to read EC ID from memory map".to_string(), + )), } } @@ -271,6 +353,57 @@ impl CrosEc { )) } + pub fn motionsense_sensor_count(&self) -> EcResult { + EcRequestMotionSenseDump { + cmd: MotionSenseCmd::Dump as u8, + max_sensor_count: 0, + } + .send_command(self) + .map(|res| res.sensor_count) + } + + pub fn motionsense_sensor_info(&self) -> EcResult> { + let count = self.motionsense_sensor_count()?; + + let mut sensors = vec![]; + for sensor_num in 0..count { + let info = EcRequestMotionSenseInfo { + cmd: MotionSenseCmd::Info as u8, + sensor_num, + } + .send_command(self)?; + sensors.push(MotionSenseInfo { + sensor_type: FromPrimitive::from_u8(info.sensor_type).unwrap(), + location: FromPrimitive::from_u8(info.location).unwrap(), + chip: FromPrimitive::from_u8(info.chip).unwrap(), + }); + } + Ok(sensors) + } + + pub fn motionsense_sensor_list(&self) -> EcResult { + EcRequestMotionSenseDump { + cmd: MotionSenseCmd::Dump as u8, + max_sensor_count: 0, + } + .send_command(self) + .map(|res| res.sensor_count) + } + + pub fn remap_caps_to_ctrl(&self) -> EcResult<()> { + self.remap_key(6, 15, 0x0014) + } + + pub fn remap_key(&self, row: u8, col: u8, scanset: u16) -> EcResult<()> { + let _current_matrix = EcRequestUpdateKeyboardMatrix { + num_items: 1, + write: 1, + scan_update: [KeyboardMatrixMap { row, col, scanset }], + } + .send_command(self)?; + Ok(()) + } + /// Get current status of Framework Laptop's microphone and camera privacy switches /// [true = device enabled/connected, false = device disabled] pub fn get_privacy_info(&self) -> EcResult<(bool, bool)> { @@ -307,6 +440,53 @@ 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 + let limits = &[percentage, 0x00]; + let data = self.send_command(EcCommands::FpLedLevelControl as u16, 1, limits)?; + + util::assert_win_len(data.len(), 0); + + Ok(()) + } + pub fn set_fp_led_level(&self, level: FpLedBrightnessLevel) -> EcResult<()> { // Sending bytes manually because the Set command, as opposed to the Get command, // does not return any data @@ -319,16 +499,33 @@ impl CrosEc { } /// Get fingerprint led brightness level - pub fn get_fp_led_level(&self) -> EcResult { - let res = EcRequestFpLedLevelControl { - set_level: 0xFF, + pub fn get_fp_led_level(&self) -> EcResult<(u8, Option)> { + let res = EcRequestFpLedLevelControlV1 { + set_percentage: 0xFF, get_level: 0xFF, } - .send_command(self)?; + .send_command(self); + + // If V1 does not exist, fall back + if let Err(EcError::Response(EcResponseStatus::InvalidVersion)) = res { + let res = EcRequestFpLedLevelControlV0 { + set_level: 0xFF, + get_level: 0xFF, + } + .send_command(self)?; + debug!("Current Brightness: {}%", res.percentage); + return Ok((res.percentage, None)); + } + + let res = res?; - debug!("Level Raw: {}", res.level); + debug!("Current Brightness: {}%", res.percentage); + debug!("Level Raw: {}", res.level); - Ok(res.level) + // TODO: can turn this into None and log + let level = FromPrimitive::from_u8(res.level) + .ok_or(EcError::DeviceError(format!("Invalid level {}", res.level)))?; + Ok((res.percentage, Some(level))) } /// Get the intrusion switch status (whether the chassis is open or not) @@ -359,6 +556,67 @@ 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()?; + let sleep_l = self.get_gpio("sleep_l")?; + println!("Chassis Closed: {}", !intrusion.currently_open); + println!("Input Deck State: {:?}", status.state); + println!("Touchpad present: {}", status.touchpad_present); + println!("SLEEP# GPIO high: {}", sleep_l); + 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)?; @@ -381,7 +639,6 @@ impl CrosEc { } /// Check the current brightness of the keyboard backlight - /// pub fn get_keyboard_backlight(&self) -> EcResult { let kblight = EcRequestPwmGetDuty { pwm_type: PwmType::KbLight as u8, @@ -392,6 +649,47 @@ impl CrosEc { Ok((kblight.duty / (PWM_MAX_DUTY / 100)) as u8) } + pub fn ps2_emulation_enable(&self, enable: bool) -> EcResult<()> { + EcRequestDisablePs2Emulation { + disable: !enable as u8, + } + .send_command(self) + } + + 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; + let res = EcRequestSetTabletMode { mode }.send_command(self); + print_err(res); + } + /// Overwrite RO and RW regions of EC flash /// MEC/Legacy EC /// | Start | End | Size | Region | @@ -407,7 +705,8 @@ impl CrosEc { /// | 3C000 | 3FFFF | 04000 | Preserved | /// | 40000 | 3C000 | 39000 | RO Region | /// | 79000 | 79FFF | 01000 | Flash Flags | - pub fn reflash(&self, data: &[u8], ft: EcFlashType) -> EcResult<()> { + pub fn reflash(&self, data: &[u8], ft: EcFlashType, dry_run: bool) -> EcResult<()> { + let mut res = Ok(()); if ft == EcFlashType::Full || ft == EcFlashType::Ro { if let Some(version) = ec_binary::read_ec_version(data, true) { println!("EC RO Version in File: {:?}", version.version); @@ -427,9 +726,21 @@ impl CrosEc { } } - if ft == EcFlashType::Full || ft == EcFlashType::Ro { - println!("For safety reasons flashing RO firmware is disabled."); - return Ok(()); + // Determine recommended flash parameters + let info = EcRequestFlashInfo {}.send_command(self)?; + + // Check that our hardcoded offsets are valid for the available flash + if FLASH_RO_SIZE + FLASH_RW_SIZE > info.flash_size { + return Err(EcError::DeviceError(format!( + "RO+RW larger than flash 0x{:X}", + { info.flash_size } + ))); + } + if FLASH_RW_BASE + FLASH_RW_SIZE > info.flash_size { + return Err(EcError::DeviceError(format!( + "RW overruns end of flash 0x{:X}", + { info.flash_size } + ))); } println!("Unlocking flash"); @@ -444,18 +755,32 @@ impl CrosEc { if ft == EcFlashType::Full || ft == EcFlashType::Rw { let rw_data = &data[FLASH_RW_BASE as usize..(FLASH_RW_BASE + FLASH_RW_SIZE) as usize]; - println!("Erasing RW region"); - self.erase_ec_flash(FLASH_BASE + FLASH_RW_BASE, FLASH_RW_SIZE)?; + println!( + "Erasing RW region{}", + if dry_run { " (DRY RUN)" } else { "" } + ); + self.erase_ec_flash( + FLASH_BASE + FLASH_RW_BASE, + FLASH_RW_SIZE, + dry_run, + info.erase_block_size, + )?; + println!(" Done"); - println!("Writing RW region"); - self.write_ec_flash(FLASH_BASE + FLASH_RW_BASE, rw_data)?; + println!( + "Writing RW region{}", + if dry_run { " (DRY RUN)" } else { "" } + ); + self.write_ec_flash(FLASH_BASE + FLASH_RW_BASE, rw_data, dry_run)?; + println!(" Done"); println!("Verifying RW region"); let flash_rw_data = self.read_ec_flash(FLASH_BASE + FLASH_RW_BASE, FLASH_RW_SIZE)?; if rw_data == flash_rw_data { - println!("RW verify success"); + println!(" RW verify success"); } else { - println!("RW verify fail"); + error!("RW verify fail!"); + res = Err(EcError::DeviceError("RW verify fail!".to_string())); } } @@ -463,17 +788,25 @@ impl CrosEc { let ro_data = &data[FLASH_RO_BASE as usize..(FLASH_RO_BASE + FLASH_RO_SIZE) as usize]; println!("Erasing RO region"); - self.erase_ec_flash(FLASH_BASE + FLASH_RO_BASE, FLASH_RO_SIZE)?; + self.erase_ec_flash( + FLASH_BASE + FLASH_RO_BASE, + FLASH_RO_SIZE, + dry_run, + info.erase_block_size, + )?; + println!(" Done"); println!("Writing RO region"); - self.write_ec_flash(FLASH_BASE + FLASH_RO_BASE, ro_data)?; + self.write_ec_flash(FLASH_BASE + FLASH_RO_BASE, ro_data, dry_run)?; + println!(" Done"); println!("Verifying RO region"); let flash_ro_data = self.read_ec_flash(FLASH_BASE + FLASH_RO_BASE, FLASH_RO_SIZE)?; if ro_data == flash_ro_data { - println!("RO verify success"); + println!(" RO verify success"); } else { - println!("RO verify fail"); + error!("RO verify fail!"); + res = Err(EcError::DeviceError("RW verify fail!".to_string())); } } @@ -481,43 +814,50 @@ impl CrosEc { self.flash_notify(MecFlashNotify::AccessSpiDone)?; self.flash_notify(MecFlashNotify::FirmwareDone)?; - println!("Flashing EC done. You can reboot the EC now"); - // TODO: Should we force a reboot if currently running one was reflashed? + if res.is_ok() { + println!("Flashing EC done. You can reboot the EC now"); + } - Ok(()) + res } /// Write a big section of EC flash. Must be unlocked already - fn write_ec_flash(&self, addr: u32, data: &[u8]) -> EcResult<()> { - let info = EcRequestFlashInfo {}.send_command(self)?; - println!("Flash info: {:?}", info); + fn write_ec_flash(&self, addr: u32, data: &[u8], dry_run: bool) -> EcResult<()> { + // TODO: Use flash info to help guide ideal chunk size + // let info = EcRequestFlashInfo {}.send_command(self)?; //let chunk_size = ((0x80 / info.write_ideal_size) * info.write_ideal_size) as usize; + let chunk_size = 0x80; let chunks = data.len() / chunk_size; + println!( + " Will write flash from 0x{:X} to 0x{:X} in {}*{}B chunks", + addr, + data.len(), + chunks, + chunk_size + ); for chunk_no in 0..chunks { let offset = chunk_no * chunk_size; // Current chunk might be smaller if it's the last let cur_chunk_size = std::cmp::min(chunk_size, data.len() - chunk_no * chunk_size); if chunk_no % 100 == 0 { - println!(); - print!( - "Writing chunk {:>4}/{:>4} ({:>6}/{:>6}): X", - chunk_no, - chunks, - offset, - cur_chunk_size * chunks - ); + if chunk_no != 0 { + println!(); + } + print!(" Chunk {:>4}: X", chunk_no); } else { print!("X"); } let chunk = &data[offset..offset + cur_chunk_size]; - let res = self.write_ec_flash_chunk(addr + offset as u32, chunk); - if let Err(err) = res { - println!(" Failed to write chunk: {:?}", err); - return Err(err); + if !dry_run { + let res = self.write_ec_flash_chunk(addr + offset as u32, chunk); + if let Err(err) = res { + println!(" Failed to write chunk: {:?}", err); + return Err(err); + } } } println!(); @@ -535,8 +875,38 @@ impl CrosEc { .send_command_extra(self, data) } - fn erase_ec_flash(&self, offset: u32, size: u32) -> EcResult<()> { - EcRequestFlashErase { offset, size }.send_command(self) + fn erase_ec_flash( + &self, + offset: u32, + size: u32, + dry_run: bool, + chunk_size: u32, + ) -> EcResult<()> { + // Erasing a big section takes too long sometimes and the linux kernel driver times out, so + // split it up into chunks. + let mut cur_offset = offset; + + while cur_offset < offset + size { + let rem_size = offset + size - cur_offset; + let cur_size = if rem_size < chunk_size { + rem_size + } else { + chunk_size + }; + debug!( + "EcRequestFlashErase (0x{:05X}, 0x{:05X})", + cur_offset, cur_size + ); + if !dry_run { + EcRequestFlashErase { + offset: cur_offset, + size: cur_size, + } + .send_command(self)?; + } + cur_offset += chunk_size; + } + Ok(()) } pub fn flash_notify(&self, flag: MecFlashNotify) -> EcResult<()> { @@ -702,30 +1072,25 @@ impl CrosEc { // Everything before is probably a header. // TODO: I don't think there are magic bytes on zephyr firmware // - if has_mec() { - println!(" Check MCHP magic byte at start of firmware code."); - // Make sure we can read at an offset and with arbitrary length - let data = self.read_ec_flash(FLASH_PROGRAM_OFFSET, 16).unwrap(); - debug!("Expecting beginning with 50 48 43 4D ('PHCM' in ASCII)"); - debug!("{:02X?}", data); - println!( - " {:02X?} ASCII:{:?}", - &data[..4], - core::str::from_utf8(&data[..4]) - ); + debug!(" Check MCHP magic bytes at start of firmware code."); + // Make sure we can read at an offset and with arbitrary length + let data = self.read_ec_flash(FLASH_PROGRAM_OFFSET, 16).unwrap(); + debug!("Expecting beginning with 50 48 43 4D ('PHCM' in ASCII)"); + debug!("{:02X?}", data); + debug!( + " {:02X?} ASCII:{:?}", + &data[..4], + core::str::from_utf8(&data[..4]) + ); - if data[0..4] != [0x50, 0x48, 0x43, 0x4D] { - println!(" INVALID: {:02X?}", &data[0..3]); - res = Err(EcError::DeviceError(format!( - "INVALID: {:02X?}", - &data[0..3] - ))); - } + let has_mec = data[0..4] == [0x50, 0x48, 0x43, 0x4D]; + if has_mec { + debug!(" Found MCHP magic bytes at start of firmware code."); } // ===== Test 4 ===== println!(" Read flash flags"); - let data = if has_mec() { + let data = if has_mec { self.read_ec_flash(MEC_FLASH_FLAGS, 0x80).unwrap() } else { self.read_ec_flash(NPC_FLASH_FLAGS, 0x80).unwrap() @@ -749,66 +1114,288 @@ 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"); + } + + let res = EcRequestGetGpuPcie {}.send_command(self)?; + let config: Option = FromPrimitive::from_u8(res.gpu_pcie_config); + let vendor: Option = FromPrimitive::from_u8(res.gpu_vendor); + if let Some(config) = config { + println!(" Config: {:?}", config); + } else { + println!(" Config: Unknown ({})", res.gpu_pcie_config); + } + if let Some(vendor) = vendor { + println!(" Vendor: {:?}", vendor); + } else { + println!(" Vendor: Unknown ({})", res.gpu_vendor); + } + + Ok(()) + } + + /// Get the GPU Serial + /// + pub fn get_gpu_serial(&self) -> EcResult { + let gpuserial: EcResponseGetGpuSerial = + EcRequestGetGpuSerial { idx: 0 }.send_command(self)?; + let serial: String = String::from_utf8(gpuserial.serial.to_vec()).unwrap(); + + if gpuserial.valid == 0 { + return Err(EcError::DeviceError("No valid GPU serial".to_string())); + } + + Ok(serial) + } + + /// Set the GPU Serial + /// + /// # Arguments + /// `newserial` - a string that is 18 characters long + pub fn set_gpu_serial(&self, magic: u8, newserial: String) -> EcResult { + let mut array_tmp: [u8; 20] = [0; 20]; + array_tmp[..18].copy_from_slice(newserial.as_bytes()); + let result = EcRequestSetGpuSerial { + magic, + idx: 0, + serial: array_tmp, + } + .send_command(self)?; + Ok(result.valid) + } + + pub fn read_ec_gpu_chunk(&self, addr: u16, len: u16) -> EcResult> { + let eeprom_port = 0x05; + let eeprom_addr = 0x50; + let mut data: Vec = Vec::with_capacity(len.into()); + + while data.len() < len.into() { + let remaining = len - data.len() as u16; + let chunk_len = std::cmp::min(i2c_passthrough::MAX_I2C_CHUNK, remaining.into()); + let offset = addr + data.len() as u16; + let i2c_response = i2c_passthrough::i2c_read( + self, + eeprom_port, + eeprom_addr, + offset, + chunk_len as u16, + )?; + if let Err(EcError::DeviceError(err)) = i2c_response.is_successful() { + return Err(EcError::DeviceError(format!( + "I2C read was not successful: {:?}", + err + ))); + } + data.extend(i2c_response.data); + } + + Ok(data) + } + + pub fn write_ec_gpu_chunk(&self, offset: u16, data: &[u8]) -> EcResult<()> { + let result = i2c_passthrough::i2c_write(self, 5, 0x50, offset, data)?; + result.is_successful() + } + + /// Writes EC GPU descriptor to the GPU EEPROM. + pub fn set_gpu_descriptor(&self, data: &[u8], dry_run: bool) -> EcResult<()> { + println!( + "Writing GPU EEPROM {}", + if dry_run { " (DRY RUN)" } else { "" } + ); + // Need to program the EEPROM 32 bytes at a time. + let chunk_size = 32; + + let chunks = data.len() / chunk_size; + for chunk_no in 0..chunks { + let offset = chunk_no * chunk_size; + // Current chunk might be smaller if it's the last + let cur_chunk_size = std::cmp::min(chunk_size, data.len() - chunk_no * chunk_size); + + if chunk_no % 100 == 0 { + println!(); + print!( + "Writing chunk {:>4}/{:>4} ({:>6}/{:>6}): X", + chunk_no, + chunks, + offset, + cur_chunk_size * chunks + ); + } else { + print!("X"); + } + if dry_run { + continue; + } + + let chunk = &data[offset..offset + cur_chunk_size]; + let res = self.write_ec_gpu_chunk((offset as u16).to_be(), chunk); + // Don't read too fast, wait 100ms before writing more to allow for page erase/write cycle. + os_specific::sleep(100_000); + if let Err(err) = res { + println!(" Failed to write chunk: {:?}", err); + return Err(err); + } + } + println!(); + Ok(()) + } + + pub fn read_gpu_descriptor(&self) -> EcResult> { + let header = self.read_gpu_desc_header()?; + if header.magic != [0x32, 0xAC, 0x00, 0x00] { + return Err(EcError::DeviceError( + "Invalid descriptor hdr magic".to_string(), + )); + } + self.read_ec_gpu_chunk(0x00, header.descriptor_length as u16) + } + + pub fn read_gpu_desc_header(&self) -> EcResult { + let bytes = + self.read_ec_gpu_chunk(0x00, core::mem::size_of::() as u16)?; + let header: *const GpuCfgDescriptor = unsafe { std::mem::transmute(bytes.as_ptr()) }; + let header = unsafe { *header }; + + Ok(header) + } + /// Requests recent 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 with a leading 0x01 instead of + // an empty response. + if data.is_empty() || data.iter().all(|x| *x == 0 || *x == 1) { + debug!("Empty EC response. Stopping console read"); + // Don't read too fast, wait 100ms before reading more + os_specific::sleep(100_000); + EcRequestConsoleSnapshot {}.send_command(self)?; + cmd.subcmd = ConsoleReadSubCommand::ConsoleReadRecent as u8; + continue; } let utf8 = std::str::from_utf8(&data).unwrap(); - let ascii = utf8 - .replace(|c: char| !c.is_ascii(), "") - .replace(['\0'], ""); + let full_ascii = utf8.replace(|c: char| !c.is_ascii(), ""); + let ascii = full_ascii.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); + } + }; } - .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) + } + + pub fn get_charge_state(&self, power_info: &power::PowerInfo) -> EcResult<()> { + let res = EcRequestChargeStateGetV0 { + cmd: ChargeStateCmd::GetState as u8, + param: 0, + } + .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; @@ -816,7 +1403,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); } } @@ -876,11 +1464,107 @@ impl CrosEc { let mut request = EcRequestGpioGetV0 { name: [0; MAX_LEN] }; let end = MAX_LEN.min(name.len()); - request.name[..end].copy_from_slice(name[..end].as_bytes()); + request.name[..end].copy_from_slice(&name.as_bytes()[..end]); let res = request.send_command(self)?; Ok(res.val == 1) } + + pub fn get_all_gpios(&self) -> EcResult { + let res = EcRequestGpioGetV1Count { + subcmd: GpioGetSubCommand::Count as u8, + } + .send_command(self)?; + let gpio_count = res.val; + + debug!("Found {} GPIOs", gpio_count); + + for index in 0..res.val { + let res = EcRequestGpioGetV1Info { + subcmd: GpioGetSubCommand::Info as u8, + index, + } + .send_command(self)?; + + let name = std::str::from_utf8(&res.name) + .map_err(|utf8_err| { + EcError::DeviceError(format!("Failed to decode GPIO name: {:?}", utf8_err)) + })? + .trim_end_matches(char::from(0)) + .to_string(); + + if log_enabled!(Level::Info) { + // Same output as ectool + println!("{:>32}: {:>2} 0x{:04X}", res.val, name, { res.flags }); + } else { + // Simple output, just name and level high/low + println!("{:<32} {}", name, res.val); + } + } + + Ok(gpio_count) + } + + 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 { + start_key: start_key + ((chunk * EC_RGBKBD_MAX_KEY_COUNT) as u8), + length: colors.len() as u8, + color: [(); EC_RGBKBD_MAX_KEY_COUNT].map(|()| Default::default()), + }; + + for (i, color) in colors.iter().enumerate() { + request.color[i] = *color; + } + + let _res = request.send_command(self)?; + } + Ok(()) + } } #[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))] @@ -917,10 +1601,11 @@ impl CrosEcDriver for CrosEc { // TODO: Change this function to return EcResult instead and print the error only in UI code print_err(match self.driver { + #[cfg(not(windows))] CrosEcDriverType::Portio => portio::read_memory(offset, length), - #[cfg(feature = "win_driver")] + #[cfg(windows)] CrosEcDriverType::Windows => windows::read_memory(offset, length), - #[cfg(feature = "cros_ec_driver")] + #[cfg(target_os = "linux")] CrosEcDriverType::CrosEc => cros_ec::read_memory(offset, length), _ => Err(EcError::DeviceError("No EC driver available".to_string())), }) @@ -938,10 +1623,11 @@ impl CrosEcDriver for CrosEc { } match self.driver { + #[cfg(not(windows))] CrosEcDriverType::Portio => portio::send_command(command, command_version, data), - #[cfg(feature = "win_driver")] + #[cfg(windows)] CrosEcDriverType::Windows => windows::send_command(command, command_version, data), - #[cfg(feature = "cros_ec_driver")] + #[cfg(target_os = "linux")] CrosEcDriverType::CrosEc => cros_ec::send_command(command, command_version, data), _ => Err(EcError::DeviceError("No EC driver available".to_string())), } @@ -974,7 +1660,7 @@ pub fn print_err(something: EcResult) -> Option { } /// Which of the two EC images is currently in-use -#[derive(PartialEq)] +#[derive(PartialEq, Debug)] pub enum EcCurrentImage { Unknown = 0, RO = 1, @@ -996,3 +1682,28 @@ pub struct IntrusionStatus { /// That means we only know if it was opened at least once, while off, not how many times. pub vtr_open_count: u8, } + +#[derive(Clone, Debug, Copy, PartialEq)] +#[repr(C, packed)] +pub struct GpuCfgDescriptor { + /// Expansion bay card magic value that is unique + pub magic: [u8; 4], + /// Length of header following this field + pub length: u32, + /// descriptor version, if EC max version is lower than this, ec cannot parse + pub desc_ver_major: u16, + pub desc_ver_minor: u16, + /// Hardware major version + pub hardware_version: u16, + /// Hardware minor revision + pub hardware_revision: u16, + /// 18 digit Framework Serial that starts with FRA + /// the first 10 digits must be allocated by framework + pub serial: [u8; 20], + /// Length of descriptor following heade + pub descriptor_length: u32, + /// CRC of descriptor + pub descriptor_crc32: u32, + /// CRC of header before this value + pub crc32: u32, +} diff --git a/framework_lib/src/chromium_ec/portio.rs b/framework_lib/src/chromium_ec/portio.rs index b453c41c..d76f8a6b 100644 --- a/framework_lib/src/chromium_ec/portio.rs +++ b/framework_lib/src/chromium_ec/portio.rs @@ -4,131 +4,21 @@ use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; use core::convert::TryInto; -#[cfg(any(feature = "linux_pio", feature = "freebsd_pio", feature = "raw_pio"))] +#[cfg(not(windows))] use hwio::{Io, Pio}; -#[cfg(all(feature = "linux_pio", target_os = "linux"))] +#[cfg(target_os = "linux")] use libc::ioperm; use log::Level; -#[cfg(feature = "linux_pio")] +#[cfg(target_os = "linux")] use nix::unistd::Uid; use num::FromPrimitive; -#[cfg(feature = "linux_pio")] -use std::sync::{Arc, Mutex}; +use spin::Mutex; -use crate::chromium_ec::{has_mec, portio_mec}; +use crate::chromium_ec::protocol::*; +use crate::chromium_ec::{portio_mec, EC_MEMMAP_ID}; use crate::os_specific; use crate::util; -/* - * Value written to legacy command port / prefix byte to indicate protocol - * 3+ structs are being used. Usage is bus-dependent. - */ -const EC_COMMAND_PROTOCOL_3: u8 = 0xda; - -// LPC command status byte masks -/// EC has written data but host hasn't consumed it yet -const _EC_LPC_STATUS_TO_HOST: u8 = 0x01; -/// Host has written data/command but EC hasn't consumed it yet -const EC_LPC_STATUS_FROM_HOST: u8 = 0x02; -/// EC is still processing a command -const EC_LPC_STATUS_PROCESSING: u8 = 0x04; -/// Previous command wasn't data but command -const _EC_LPC_STATUS_LAST_CMD: u8 = 0x08; -/// EC is in burst mode -const _EC_LPC_STATUS_BURST_MODE: u8 = 0x10; -/// SCI event is pending (requesting SCI query) -const _EC_LPC_STATUS_SCI_PENDING: u8 = 0x20; -/// SMI event is pending (requesting SMI query) -const _EC_LPC_STATUS_SMI_PENDING: u8 = 0x40; -/// Reserved -const _EC_LPC_STATUS_RESERVED: u8 = 0x80; - -/// EC is busy -const EC_LPC_STATUS_BUSY_MASK: u8 = EC_LPC_STATUS_FROM_HOST | EC_LPC_STATUS_PROCESSING; - -// I/O addresses for ACPI commands -const _EC_LPC_ADDR_ACPI_DATA: u16 = 0x62; -const _EC_LPC_ADDR_ACPI_CMD: u16 = 0x66; - -// I/O addresses for host command -const EC_LPC_ADDR_HOST_DATA: u16 = 0x200; -const EC_LPC_ADDR_HOST_CMD: u16 = 0x204; - -// I/O addresses for host command args and params -// Protocol version 2 -const EC_LPC_ADDR_HOST_ARGS: u16 = 0x800; /* And 0x801, 0x802, 0x803 */ -const _EC_LPC_ADDR_HOST_PARAM: u16 = 0x804; /* For version 2 params; size is - * EC_PROTO2_MAX_PARAM_SIZE */ -// Protocol version 3 -const _EC_LPC_ADDR_HOST_PACKET: u16 = 0x800; /* Offset of version 3 packet */ -const EC_LPC_HOST_PACKET_SIZE: u16 = 0x100; /* Max size of version 3 packet */ - -const MEC_MEMMAP_OFFSET: u16 = 0x100; -const NPC_MEMMAP_OFFSET: u16 = 0xE00; - -// The actual block is 0x800-0x8ff, but some BIOSes think it's 0x880-0x8ff -// and they tell the kernel that so we have to think of it as two parts. -const _EC_HOST_CMD_REGION0: u16 = 0x800; -const _EC_HOST_CMD_REGION1: u16 = 0x8800; -const _EC_HOST_CMD_REGION_SIZE: u16 = 0x80; - -// EC command register bit functions -const _EC_LPC_CMDR_DATA: u16 = 1 << 0; // Data ready for host to read -const _EC_LPC_CMDR_PENDING: u16 = 1 << 1; // Write pending to EC -const _EC_LPC_CMDR_BUSY: u16 = 1 << 2; // EC is busy processing a command -const _EC_LPC_CMDR_CMD: u16 = 1 << 3; // Last host write was a command -const _EC_LPC_CMDR_ACPI_BRST: u16 = 1 << 4; // Burst mode (not used) -const _EC_LPC_CMDR_SCI: u16 = 1 << 5; // SCI event is pending -const _EC_LPC_CMDR_SMI: u16 = 1 << 6; // SMI event is pending - -const EC_HOST_REQUEST_VERSION: u8 = 3; - -/// Request header of version 3 -#[repr(C, packed)] -struct EcHostRequest { - /// Version of this request structure (must be 3) - pub struct_version: u8, - - /// Checksum of entire request (header and data) - /// Everything added together adds up to 0 (wrapping around u8 limit) - pub checksum: u8, - - /// Command number - pub command: u16, - - /// Command version, usually 0 - pub command_version: u8, - - /// Reserved byte in protocol v3. Must be 0 - pub reserved: u8, - - /// Data length. Data is immediately after the header - pub data_len: u16, -} - -const EC_HOST_RESPONSE_VERSION: u8 = 3; - -/// Response header of version 3 -#[repr(C, packed)] -struct EcHostResponse { - /// Version of this request structure (must be 3) - pub struct_version: u8, - - /// Checksum of entire request (header and data) - pub checksum: u8, - - /// Status code of response. See enum _EcStatus - pub result: u16, - - /// Data length. Data is immediately after the header - pub data_len: u16, - - /// Reserved byte in protocol v3. Must be 0 - pub reserved: u16, -} -#[allow(dead_code)] -pub const HEADER_LEN: usize = std::mem::size_of::(); - fn transfer_write(buffer: &[u8]) { if has_mec() { return portio_mec::transfer_write(buffer); @@ -172,65 +62,73 @@ fn transfer_read(port: u16, address: u16, size: u16) -> Vec { buffer } -#[cfg(feature = "linux_pio")] +#[derive(PartialEq)] +#[allow(dead_code)] enum Initialized { NotYet, + SucceededMec, Succeeded, Failed, } -#[cfg(feature = "linux_pio")] lazy_static! { - static ref INITIALIZED: Arc> = Arc::new(Mutex::new(Initialized::NotYet)); + static ref INITIALIZED: Mutex = Mutex::new(Initialized::NotYet); } -#[cfg(not(feature = "linux_pio"))] -fn init() -> bool { - // Nothing to do for bare-metal (UEFI) port I/O - true +fn has_mec() -> bool { + let init = INITIALIZED.lock(); + *init != Initialized::Succeeded } -// In Linux userspace has to first request access to ioports -// TODO: Close these again after we're done -#[cfg(feature = "linux_pio")] fn init() -> bool { - let mut init = INITIALIZED.lock().unwrap(); + let mut init = INITIALIZED.lock(); match *init { // Can directly give up, trying again won't help Initialized::Failed => return false, // Already initialized, no need to do anything. - Initialized::Succeeded => return true, + Initialized::Succeeded | Initialized::SucceededMec => return true, Initialized::NotYet => {} } + // In Linux userspace has to first request access to ioports + // TODO: Close these again after we're done + #[cfg(target_os = "linux")] if !Uid::effective().is_root() { error!("Must be root to use port based I/O for EC communication."); *init = Initialized::Failed; return false; } + // First try on MEC + if !portio_mec::init() { + *init = Initialized::Failed; + return false; + } + let ec_id = portio_mec::transfer_read(MEC_MEMMAP_OFFSET + EC_MEMMAP_ID, 2); + if ec_id[0] == b'E' && ec_id[1] == b'C' { + *init = Initialized::SucceededMec; + return true; + } + + #[cfg(target_os = "linux")] unsafe { - if has_mec() { - portio_mec::mec_init(); - } else { - // 8 for request/response header, 0xFF for response - let res = ioperm(EC_LPC_ADDR_HOST_ARGS as u64, 8 + 0xFF, 1); - if res != 0 { - error!( - "ioperm failed. portio driver is likely block by Linux kernel lockdown mode" - ); - return false; - } - - let res = ioperm(EC_LPC_ADDR_HOST_CMD as u64, 1, 1); - assert_eq!(res, 0); - let res = ioperm(EC_LPC_ADDR_HOST_DATA as u64, 1, 1); - assert_eq!(res, 0); - - let res = ioperm(NPC_MEMMAP_OFFSET as u64, super::EC_MEMMAP_SIZE as u64, 1); - assert_eq!(res, 0); + // 8 for request/response header, 0xFF for response + let res = ioperm(EC_LPC_ADDR_HOST_ARGS as u64, 8 + 0xFF, 1); + if res != 0 { + error!("ioperm failed. portio driver is likely block by Linux kernel lockdown mode"); + *init = Initialized::Failed; + return false; } + + let res = ioperm(EC_LPC_ADDR_HOST_CMD as u64, 1, 1); + assert_eq!(res, 0); + let res = ioperm(EC_LPC_ADDR_HOST_DATA as u64, 1, 1); + assert_eq!(res, 0); + + let res = ioperm(NPC_MEMMAP_OFFSET as u64, super::EC_MEMMAP_SIZE as u64, 1); + assert_eq!(res, 0); } + *init = Initialized::Succeeded; true } diff --git a/framework_lib/src/chromium_ec/portio_mec.rs b/framework_lib/src/chromium_ec/portio_mec.rs index 974b8686..1f855d0f 100644 --- a/framework_lib/src/chromium_ec/portio_mec.rs +++ b/framework_lib/src/chromium_ec/portio_mec.rs @@ -5,11 +5,11 @@ use alloc::vec::Vec; use log::Level; use hwio::{Io, Pio}; -#[cfg(feature = "linux_pio")] +#[cfg(target_os = "linux")] use libc::ioperm; // I/O addresses for host command -#[cfg(feature = "linux_pio")] +#[cfg(target_os = "linux")] const EC_LPC_ADDR_HOST_DATA: u16 = 0x200; const MEC_EC_BYTE_ACCESS: u16 = 0x00; @@ -22,10 +22,19 @@ const _MEC_LPC_DATA_REGISTER1: u16 = 0x0805; const MEC_LPC_DATA_REGISTER2: u16 = 0x0806; const _MEC_LPC_DATA_REGISTER3: u16 = 0x0807; -#[cfg(feature = "linux_pio")] -pub unsafe fn mec_init() { - ioperm(EC_LPC_ADDR_HOST_DATA as u64, 8, 1); - ioperm(MEC_LPC_ADDRESS_REGISTER0 as u64, 10, 1); +pub fn init() -> bool { + #[cfg(target_os = "linux")] + unsafe { + let res = ioperm(EC_LPC_ADDR_HOST_DATA as u64, 8, 1); + if res != 0 { + error!("ioperm failed. portio driver is likely block by Linux kernel lockdown mode"); + return false; + } + let res = ioperm(MEC_LPC_ADDRESS_REGISTER0 as u64, 10, 1); + assert_eq!(res, 0); + } + + true } // TODO: Create a wrapper diff --git a/framework_lib/src/chromium_ec/protocol.rs b/framework_lib/src/chromium_ec/protocol.rs new file mode 100644 index 00000000..80152842 --- /dev/null +++ b/framework_lib/src/chromium_ec/protocol.rs @@ -0,0 +1,108 @@ +/* + * Value written to legacy command port / prefix byte to indicate protocol + * 3+ structs are being used. Usage is bus-dependent. + */ +pub const EC_COMMAND_PROTOCOL_3: u8 = 0xda; + +// LPC command status byte masks +/// EC has written data but host hasn't consumed it yet +const _EC_LPC_STATUS_TO_HOST: u8 = 0x01; +/// Host has written data/command but EC hasn't consumed it yet +pub const EC_LPC_STATUS_FROM_HOST: u8 = 0x02; +/// EC is still processing a command +pub const EC_LPC_STATUS_PROCESSING: u8 = 0x04; +/// Previous command wasn't data but command +const _EC_LPC_STATUS_LAST_CMD: u8 = 0x08; +/// EC is in burst mode +const _EC_LPC_STATUS_BURST_MODE: u8 = 0x10; +/// SCI event is pending (requesting SCI query) +const _EC_LPC_STATUS_SCI_PENDING: u8 = 0x20; +/// SMI event is pending (requesting SMI query) +const _EC_LPC_STATUS_SMI_PENDING: u8 = 0x40; +/// Reserved +const _EC_LPC_STATUS_RESERVED: u8 = 0x80; + +/// EC is busy +pub const EC_LPC_STATUS_BUSY_MASK: u8 = EC_LPC_STATUS_FROM_HOST | EC_LPC_STATUS_PROCESSING; + +// I/O addresses for ACPI commands +const _EC_LPC_ADDR_ACPI_DATA: u16 = 0x62; +const _EC_LPC_ADDR_ACPI_CMD: u16 = 0x66; + +// I/O addresses for host command +pub const EC_LPC_ADDR_HOST_DATA: u16 = 0x200; +pub const EC_LPC_ADDR_HOST_CMD: u16 = 0x204; + +// I/O addresses for host command args and params +// Protocol version 2 +pub const EC_LPC_ADDR_HOST_ARGS: u16 = 0x800; /* And 0x801, 0x802, 0x803 */ +const _EC_LPC_ADDR_HOST_PARAM: u16 = 0x804; /* For version 2 params; size is + * EC_PROTO2_MAX_PARAM_SIZE */ +// Protocol version 3 +const _EC_LPC_ADDR_HOST_PACKET: u16 = 0x800; /* Offset of version 3 packet */ +pub const EC_LPC_HOST_PACKET_SIZE: u16 = 0x100; /* Max size of version 3 packet */ + +pub const MEC_MEMMAP_OFFSET: u16 = 0x100; +pub const NPC_MEMMAP_OFFSET: u16 = 0xE00; + +// The actual block is 0x800-0x8ff, but some BIOSes think it's 0x880-0x8ff +// and they tell the kernel that so we have to think of it as two parts. +const _EC_HOST_CMD_REGION0: u16 = 0x800; +const _EC_HOST_CMD_REGION1: u16 = 0x8800; +const _EC_HOST_CMD_REGION_SIZE: u16 = 0x80; + +// EC command register bit functions +const _EC_LPC_CMDR_DATA: u16 = 1 << 0; // Data ready for host to read +const _EC_LPC_CMDR_PENDING: u16 = 1 << 1; // Write pending to EC +const _EC_LPC_CMDR_BUSY: u16 = 1 << 2; // EC is busy processing a command +const _EC_LPC_CMDR_CMD: u16 = 1 << 3; // Last host write was a command +const _EC_LPC_CMDR_ACPI_BRST: u16 = 1 << 4; // Burst mode (not used) +const _EC_LPC_CMDR_SCI: u16 = 1 << 5; // SCI event is pending +const _EC_LPC_CMDR_SMI: u16 = 1 << 6; // SMI event is pending + +pub const EC_HOST_REQUEST_VERSION: u8 = 3; + +/// Request header of version 3 +#[repr(C, packed)] +pub struct EcHostRequest { + /// Version of this request structure (must be 3) + pub struct_version: u8, + + /// Checksum of entire request (header and data) + /// Everything added together adds up to 0 (wrapping around u8 limit) + pub checksum: u8, + + /// Command number + pub command: u16, + + /// Command version, usually 0 + pub command_version: u8, + + /// Reserved byte in protocol v3. Must be 0 + pub reserved: u8, + + /// Data length. Data is immediately after the header + pub data_len: u16, +} + +pub const EC_HOST_RESPONSE_VERSION: u8 = 3; + +/// Response header of version 3 +#[repr(C, packed)] +pub struct EcHostResponse { + /// Version of this request structure (must be 3) + pub struct_version: u8, + + /// Checksum of entire request (header and data) + pub checksum: u8, + + /// Status code of response. See enum _EcStatus + pub result: u16, + + /// Data length. Data is immediately after the header + pub data_len: u16, + + /// Reserved byte in protocol v3. Must be 0 + pub reserved: u16, +} +pub const HEADER_LEN: usize = std::mem::size_of::(); diff --git a/framework_lib/src/chromium_ec/windows.rs b/framework_lib/src/chromium_ec/windows.rs index 4e026663..557ab430 100644 --- a/framework_lib/src/chromium_ec/windows.rs +++ b/framework_lib/src/chromium_ec/windows.rs @@ -11,9 +11,11 @@ use windows::{ }, }; -use crate::chromium_ec::portio::HEADER_LEN; +use crate::chromium_ec::protocol::HEADER_LEN; use crate::chromium_ec::EC_MEMMAP_SIZE; use crate::chromium_ec::{EcError, EcResponseStatus, EcResult}; +use crate::smbios; +use crate::util::Platform; // Create a wrapper around HANDLE to mark it as Send. // I'm not sure, but I think it's safe to do that for this type of HANDL. @@ -25,31 +27,54 @@ lazy_static! { static ref DEVICE: Arc>> = Arc::new(Mutex::new(None)); } -fn init() { +fn init() -> bool { let mut device = DEVICE.lock().unwrap(); if (*device).is_some() { - return; + return true; } let path = w!(r"\\.\GLOBALROOT\Device\CrosEC"); - unsafe { - *device = Some(DevHandle( - CreateFileW( - path, - FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0, - FILE_SHARE_READ | FILE_SHARE_WRITE, - None, - OPEN_EXISTING, - FILE_FLAGS_AND_ATTRIBUTES(0), - None, - ) - .unwrap(), - )); - } + let res = unsafe { + CreateFileW( + path, + FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + ) + }; + let handle = match res { + Ok(h) => h, + Err(err) => { + let platform = smbios::get_platform(); + match platform { + Some(platform @ Platform::IntelGen11) + | Some(platform @ Platform::IntelGen12) + | Some(platform @ Platform::IntelGen13) + | Some(platform @ Platform::Framework13Amd7080) + | Some(platform @ Platform::Framework16Amd7080) => { + println!("The windows driver is not enabled on {:?}.", platform); + println!("Please stay tuned for future BIOS and driver updates."); + println!(); + } + _ => (), + } + + error!("Failed to find Windows driver. {:?}", err); + return false; + } + }; + + *device = Some(DevHandle(handle)); + true } pub fn read_memory(offset: u16, length: u16) -> EcResult> { - init(); + if !init() { + return Err(EcError::DeviceError("Failed to initialize".to_string())); + } let mut rm = CrosEcReadMem { offset: offset as u32, bytes: length as u32, diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 30e54d2e..a26aa4ba 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -1,11 +1,16 @@ //! Module to factor out commandline interaction //! This way we can use it in the regular OS commandline tool on Linux and Windows, //! as well as on the UEFI shell tool. +use clap::error::ErrorKind; use clap::Parser; +use clap::{arg, command, Arg, Args, FromArgMatches}; +use clap_num::maybe_hex; +use crate::chromium_ec::commands::SetGpuSerialMagic; use crate::chromium_ec::CrosEcDriverType; use crate::commandline::{ - Cli, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg, RebootEcArg, + Cli, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg, LogLevel, RebootEcArg, + TabletModeArg, }; /// Swiss army knife for Framework laptops @@ -52,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, @@ -64,6 +83,18 @@ struct ClapCli { #[arg(long)] pd_info: bool, + /// Reset a specific PD controller (for debugging only) + #[arg(long)] + pd_reset: Option, + + /// Disable all ports on a specific PD controller (for debugging only) + #[arg(long)] + pd_disable: Option, + + /// Enable all ports on a specific PD controller (for debugging only) + #[arg(long)] + pd_enable: Option, + /// Show details about connected DP or HDMI Expansion Cards #[arg(long)] dp_hdmi_info: bool, @@ -98,17 +129,17 @@ struct ClapCli { /// Parse UEFI Capsule information from binary file #[arg(long)] - ho2_capsule: Option, + h2o_capsule: Option, /// Dump EC flash contents #[arg(long)] dump_ec_flash: Option, - /// Flash EC with new firmware from file + /// Flash EC (RO+RW) with new firmware from file - may render your hardware unbootable! #[arg(long)] flash_ec: Option, - /// Flash EC with new RO firmware from file + /// Flash EC with new RO firmware from file - may render your hardware unbootable! #[arg(long)] flash_ro_ec: Option, @@ -122,28 +153,78 @@ 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 GPIO value by name + /// Set max charge current limit #[arg(long)] - get_gpio: Option, + #[clap(num_args = 1..=2)] + charge_current_limit: Vec, - /// Get or set fingerprint LED brightness + /// Set max charge current limit #[arg(long)] - fp_brightness: Option>, + #[clap(num_args = 1..=2)] + charge_rate_limit: Vec, + + /// Get GPIO value by name or all, if no name provided + #[arg(long)] + get_gpio: Option>, + + /// Get or set fingerprint LED brightness level + #[arg(long)] + fp_led_level: Option>, + + /// Get or set fingerprint LED brightness percentage + #[arg(long)] + fp_brightness: Option>, /// Set keyboard backlight percentage or get, if no value provided #[arg(long)] kblight: Option>, + /// Remap a key by changing the scancode + #[arg(long, value_parser=maybe_hex::)] + #[clap(num_args = 3)] + remap_key: Vec, + + /// Set the color of to . Multiple colors for adjacent keys can be set at once. + /// [ ...] + /// Example: 0 0xFF000 0x00FF00 0x0000FF + #[clap(num_args = 2..)] + #[arg(long, value_parser=maybe_hex::)] + rgbkbd: Vec, + + /// Control PS2 touchpad emulation (DEBUG COMMAND, if touchpad not working, reboot system) + #[clap(value_enum, hide(true))] + #[arg(long)] + ps2_enable: Option, + + /// Set tablet mode override + #[clap(value_enum)] + #[arg(long)] + tablet_mode: Option, + + /// Enable/disable touchscreen + #[clap(value_enum)] + #[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)] @@ -154,6 +235,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, @@ -164,56 +250,138 @@ struct ClapCli { driver: Option, /// Specify I2C addresses of the PD chips (Advanced) - #[clap(number_of_values = 2, requires("pd_ports"), requires("has_mec"))] + #[clap(number_of_values = 3, requires("pd_ports"))] #[arg(long)] pd_addrs: Vec, /// Specify I2C ports of the PD chips (Advanced) - #[clap(number_of_values = 2, requires("pd_addrs"), requires("has_mec"))] + #[clap(number_of_values = 3, requires("pd_addrs"))] #[arg(long)] pd_ports: Vec, - /// Specify the type of EC chip (MEC/MCHP or other) - #[clap(requires("pd_addrs"), requires("pd_ports"))] - #[arg(long)] - has_mec: Option, - /// Run self-test to check if interaction with EC is possible #[arg(long, short)] test: bool, + + /// Force execution of an unsafe command - may render your hardware unbootable! + #[arg(long, short)] + force: bool, + + /// Simulate execution of a command (e.g. --flash-ec) + #[arg(long)] + dry_run: bool, + + /// File to write to the gpu EEPROM + #[arg(long)] + flash_gpu_descriptor_file: Option, + + /// File to dump the gpu EEPROM to + #[arg(long)] + dump_gpu_descriptor_file: Option, } /// Parse a list of commandline arguments and return the struct pub fn parse(args: &[String]) -> Cli { - let args = ClapCli::parse_from(args); + // Step 1 - Define args that can't be derived + let cli = command!() + .arg(Arg::new("fgd").long("flash-gpu-descriptor").num_args(2)) + .disable_version_flag(true); + // Step 2 - Define args from derived struct + let mut cli = ClapCli::augment_args(cli); + + // Step 3 - Parse args that can't be derived + let matches = cli.clone().get_matches_from(args); + let fgd = matches + .get_many::("fgd") + .unwrap_or_default() + .map(|v| v.as_str()) + .collect::>(); + let flash_gpu_descriptor = if !fgd.is_empty() { + let hex_magic = if let Some(hex_magic) = fgd[0].strip_prefix("0x") { + u8::from_str_radix(hex_magic, 16) + } else { + // Force parse error + u8::from_str_radix("", 16) + }; + + let magic = if let Ok(magic) = fgd[0].parse::() { + magic + } else if let Ok(hex_magic) = hex_magic { + hex_magic + } else if fgd[0].to_uppercase() == "GPU" { + SetGpuSerialMagic::WriteGPUConfig as u8 + } else if fgd[0].to_uppercase() == "SSD" { + SetGpuSerialMagic::WriteSSDConfig as u8 + } else { + cli.error( + ErrorKind::InvalidValue, + "First argument of --flash-gpu-descriptor must be an integer or one of: 'GPU', 'SSD'", + ) + .exit(); + }; + if fgd[1].len() != 18 { + cli.error( + ErrorKind::InvalidValue, + "Second argument of --flash-gpu-descriptor must be an 18 digit serial number", + ) + .exit(); + } + Some((magic, fgd[1].to_string())) + } else { + None + }; + + // Step 4 - Parse from derived struct + let args = ClapCli::from_arg_matches(&matches) + .map_err(|err| err.exit()) + .unwrap(); let pd_addrs = match args.pd_addrs.len() { - 2 => Some((args.pd_addrs[0], args.pd_addrs[1])), + 3 => Some((args.pd_addrs[0], args.pd_addrs[1], args.pd_addrs[2])), 0 => None, - _ => { - // Actually unreachable, checked by clap - println!( - "Must provide exactly to PD Addresses. Provided: {:?}", - args.pd_addrs - ); - std::process::exit(1); - } + // Checked by clap + _ => unreachable!(), }; let pd_ports = match args.pd_ports.len() { - 2 => Some((args.pd_ports[0], args.pd_ports[1])), + 3 => Some((args.pd_ports[0], args.pd_ports[1], args.pd_ports[2])), 0 => None, - _ => { - // Actually unreachable, checked by clap - println!( - "Must provide exactly to PD Ports. Provided: {:?}", - args.pd_ports - ); - std::process::exit(1); - } + // 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, + }; + let remap_key = match args.remap_key.len() { + 3 => Some(( + args.remap_key[0] as u8, + args.remap_key[1] as u8, + args.remap_key[2], + )), + _ => None, }; Cli { - verbosity: args.verbosity.log_level_filter(), + verbosity: LogLevel(args.verbosity.log_level_filter()), versions: args.versions, version: args.version, features: args.features, @@ -223,8 +391,14 @@ 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, + pd_reset: args.pd_reset, + pd_disable: args.pd_disable, + pd_enable: args.pd_enable, dp_hdmi_info: args.dp_hdmi_info, dp_hdmi_update: args .dp_hdmi_update @@ -241,8 +415,8 @@ pub fn parse(args: &[String]) -> Cli { .capsule .map(|x| x.into_os_string().into_string().unwrap()), dump: args.dump.map(|x| x.into_os_string().into_string().unwrap()), - ho2_capsule: args - .ho2_capsule + h2o_capsule: args + .h2o_capsule .map(|x| x.into_os_string().into_string().unwrap()), dump_ec_flash: args .dump_ec_flash @@ -257,20 +431,32 @@ 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, kblight: args.kblight, + remap_key, + rgbkbd: args.rgbkbd, + ps2_enable: args.ps2_enable, + 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, pd_ports, - has_mec: args.has_mec, test: args.test, + dry_run: args.dry_run, + force: args.force, // TODO: Set help. Not very important because Clap handles this by itself help: false, // UEFI only for now. Don't need to handle @@ -278,6 +464,13 @@ pub fn parse(args: &[String]) -> Cli { // UEFI only - every command needs to implement a parameter to enable the pager paginate: false, info: args.info, + flash_gpu_descriptor, + flash_gpu_descriptor_file: args + .flash_gpu_descriptor_file + .map(|x| x.into_os_string().into_string().unwrap()), + dump_gpu_descriptor_file: args + .dump_gpu_descriptor_file + .map(|x| x.into_os_string().into_string().unwrap()), raw_command: vec![], } } diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 9a28c66b..e56d348d 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -7,6 +7,7 @@ use alloc::format; use alloc::string::String; use alloc::string::ToString; use alloc::vec::Vec; +use guid_create::{CGuid, GUID}; use log::Level; use num_traits::FromPrimitive; @@ -17,12 +18,14 @@ pub mod uefi; #[cfg(not(feature = "uefi"))] use std::fs; -#[cfg(all(not(feature = "uefi"), feature = "std"))] +#[cfg(not(feature = "uefi"))] use std::io::prelude::*; #[cfg(feature = "rusb")] use crate::audio_card::check_synaptics_fw_version; use crate::built_info; +#[cfg(feature = "rusb")] +use crate::camera::check_camera_version; use crate::capsule; use crate::capsule_content::{ find_bios_version, find_ec_in_bios_cap, find_pd_in_bios_cap, find_retimer_version, @@ -30,26 +33,36 @@ use crate::capsule_content::{ use crate::ccgx::device::{FwMode, PdController, PdPort}; #[cfg(feature = "hidapi")] use crate::ccgx::hid::{check_ccg_fw_version, find_devices, DP_CARD_PID, HDMI_CARD_PID}; -use crate::ccgx::{self, SiliconId::*}; +use crate::ccgx::{self, MainPdVersions, PdVersions, SiliconId::*}; use crate::chromium_ec; use crate::chromium_ec::commands::DeckStateMode; use crate::chromium_ec::commands::FpLedBrightnessLevel; use crate::chromium_ec::commands::RebootEcCmd; +use crate::chromium_ec::commands::RgbS; +use crate::chromium_ec::commands::TabletModeOverride; use crate::chromium_ec::EcResponseStatus; use crate::chromium_ec::{print_err, EcFlashType}; use crate::chromium_ec::{EcError, EcResult}; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use crate::csme; use crate::ec_binary; use crate::esrt; +#[cfg(feature = "rusb")] +use crate::inputmodule::check_inputmodule_version; +use crate::os_specific; use crate::power; use crate::smbios; use crate::smbios::ConfigDigit0; use crate::smbios::{dmidecode_string_val, get_smbios, is_framework}; +#[cfg(feature = "hidapi")] +use crate::touchpad::print_touchpad_fw_ver; +#[cfg(feature = "hidapi")] +use crate::touchscreen; #[cfg(feature = "uefi")] use crate::uefi::enable_page_break; -use crate::util; -use crate::util::{Config, Platform}; +#[cfg(feature = "rusb")] +use crate::usbhub::check_usbhub_version; +use crate::util::{self, Config, Platform, PlatformFamily}; #[cfg(feature = "hidapi")] use hidapi::HidApi; use sha2::{Digest, Sha256, Sha384, Sha512}; @@ -61,6 +74,14 @@ use crate::chromium_ec::{CrosEc, CrosEcDriverType, HardwareDeviceType}; #[cfg(feature = "uefi")] use core::prelude::rust_2021::derive; +#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))] +#[derive(Clone, Debug, PartialEq)] +pub enum TabletModeArg { + Auto, + Tablet, + Laptop, +} + #[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))] #[derive(Clone, Debug, PartialEq)] pub enum ConsoleArg { @@ -84,6 +105,8 @@ pub enum FpBrightnessArg { High, Medium, Low, + UltraLow, + Auto, } impl From for FpLedBrightnessLevel { fn from(w: FpBrightnessArg) -> FpLedBrightnessLevel { @@ -91,6 +114,8 @@ impl From for FpLedBrightnessLevel { FpBrightnessArg::High => FpLedBrightnessLevel::High, FpBrightnessArg::Medium => FpLedBrightnessLevel::Medium, FpBrightnessArg::Low => FpLedBrightnessLevel::Low, + FpBrightnessArg::UltraLow => FpLedBrightnessLevel::UltraLow, + FpBrightnessArg::Auto => FpLedBrightnessLevel::Auto, } } } @@ -112,13 +137,22 @@ impl From for DeckStateMode { } } +#[derive(Debug)] +pub struct LogLevel(log::LevelFilter); + +impl Default for LogLevel { + fn default() -> Self { + LogLevel(log::LevelFilter::Error) + } +} + /// Shadows `clap_std::ClapCli` with extras for UEFI /// /// The UEFI commandline currently doesn't use clap, so we need to shadow the struct. /// Also it has extra options. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Cli { - pub verbosity: log::LevelFilter, + pub verbosity: LogLevel, pub versions: bool, pub version: bool, pub features: bool, @@ -128,9 +162,15 @@ 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, + pub pd_reset: Option, + pub pd_disable: Option, + pub pd_enable: Option, pub dp_hdmi_info: bool, pub dp_hdmi_update: Option, pub audio_card_info: bool, @@ -138,28 +178,43 @@ pub struct Cli { pub ec_bin: Option, pub capsule: Option, pub dump: Option, - pub ho2_capsule: Option, + pub h2o_capsule: Option, pub dump_ec_flash: Option, pub flash_ec: Option, pub flash_ro_ec: Option, pub flash_rw_ec: Option, pub driver: Option, pub test: bool, + pub dry_run: bool, + pub force: 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 get_gpio: Option, - pub fp_brightness: 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>, pub kblight: Option>, + pub remap_key: Option<(u8, u8, u16)>, + pub rgbkbd: Vec, + pub ps2_enable: Option, + 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)>, - pub has_mec: Option, + pub pd_addrs: Option<(u16, u16, u16)>, + pub pd_ports: Option<(u8, u8, u8)>, pub help: bool, pub info: bool, + pub flash_gpu_descriptor: Option<(u8, String)>, + pub flash_gpu_descriptor_file: Option, + pub dump_gpu_descriptor_file: Option, // UEFI only pub allupdate: bool, pub paginate: bool, @@ -169,9 +224,87 @@ pub struct Cli { pub fn parse(args: &[String]) -> Cli { #[cfg(feature = "uefi")] - return uefi::parse(args); + let cli = uefi::parse(args); #[cfg(not(feature = "uefi"))] - return clap_std::parse(args); + let cli = clap_std::parse(args); + + if cfg!(feature = "readonly") { + // Initialize a new Cli with no arguments + // Set all arguments that are readonly/safe + // We explicitly only cope the safe ones so that if we add new arguments in the future, + // which might be unsafe, we can't forget to exclude them from the safe set. + // TODO: Instead of silently ignoring blocked command, we should remind the user + Cli { + verbosity: cli.verbosity, + versions: cli.versions, + version: cli.version, + features: cli.features, + esrt: cli.esrt, + device: cli.device, + compare_version: cli.compare_version, + power: cli.power, + thermal: cli.thermal, + sensors: cli.sensors, + // fansetduty + // fansetrpm + // autofanctrl + pdports: cli.pdports, + privacy: cli.privacy, + pd_info: cli.version, + // pd_reset + // pd_disable + // pd_enable + dp_hdmi_info: cli.dp_hdmi_info, + // dp_hdmi_update + audio_card_info: cli.audio_card_info, + pd_bin: cli.pd_bin, + ec_bin: cli.ec_bin, + capsule: cli.capsule, + dump: cli.dump, + h2o_capsule: cli.h2o_capsule, + // dump_ec_flash + // flash_ec + // flash_ro_ec + // flash_rw_ec + driver: cli.driver, + test: cli.test, + dry_run: cli.dry_run, + // force + intrusion: cli.intrusion, + inputdeck: cli.inputdeck, + inputdeck_mode: cli.inputdeck_mode, + expansion_bay: cli.expansion_bay, + // charge_limit + // charge_current_limit + // charge_rate_limit + get_gpio: cli.get_gpio, + fp_led_level: cli.fp_led_level, + fp_brightness: cli.fp_brightness, + kblight: cli.kblight, + remap_key: cli.remap_key, + rgbkbd: cli.rgbkbd, + ps2_enable: cli.ps2_enable, + // tablet_mode + // touchscreen_enable + stylus_battery: cli.stylus_battery, + console: cli.console, + reboot_ec: cli.reboot_ec, + // ec_hib_delay + hash: cli.hash, + pd_addrs: cli.pd_addrs, + pd_ports: cli.pd_ports, + help: cli.help, + info: cli.info, + // flash_gpu_descriptor + // flash_gpu_descriptor_file + // allupdate + paginate: cli.paginate, + // raw_command + ..Default::default() + } + } else { + cli + } } fn print_single_pd_details(pd: &PdController) { @@ -179,6 +312,7 @@ fn print_single_pd_details(pd: &PdController) { println!(" Silicon ID: 0x{:X}", si); } else { println!(" Failed to read Silicon ID/Family"); + return; } if let Ok((mode, frs)) = pd.get_device_info() { println!(" Mode: {:?}", mode); @@ -186,6 +320,17 @@ fn print_single_pd_details(pd: &PdController) { } else { println!(" Failed to device info"); } + if let Ok(port_mask) = pd.get_port_status() { + let ports = match port_mask { + 1 => "0", + 2 => "1", + 3 => "0, 1", + _ => "None", + }; + println!(" Ports Enabled: {}", ports); + } else { + println!(" Ports Enabled: Unknown"); + } pd.print_fw_info(); } @@ -194,13 +339,16 @@ fn print_pd_details(ec: &CrosEc) { println!("Only supported on Framework systems"); return; } - let pd_01 = PdController::new(PdPort::Left01, ec.clone()); - let pd_23 = PdController::new(PdPort::Right23, ec.clone()); + let pd_01 = PdController::new(PdPort::Right01, ec.clone()); + let pd_23 = PdController::new(PdPort::Left23, ec.clone()); + let pd_back = PdController::new(PdPort::Back, ec.clone()); - println!("Left / Ports 01"); + println!("Right / Ports 01"); print_single_pd_details(&pd_01); - println!("Right / Ports 23"); + println!("Left / Ports 23"); print_single_pd_details(&pd_23); + println!("Back"); + print_single_pd_details(&pd_back); } #[cfg(feature = "hidapi")] @@ -212,7 +360,7 @@ fn print_audio_card_details() { } #[cfg(feature = "hidapi")] -fn print_dp_hdmi_details() { +fn print_dp_hdmi_details(verbose: bool) { match HidApi::new() { Ok(api) => { for dev_info in find_devices(&api, &[HDMI_CARD_PID, DP_CARD_PID], None) { @@ -230,11 +378,11 @@ fn print_dp_hdmi_details() { dev_info.product_string().unwrap_or(NOT_SET) ); - println!( + debug!( " Serial Number: {}", dev_info.serial_number().unwrap_or(NOT_SET) ); - check_ccg_fw_version(&device); + check_ccg_fw_version(&device, verbose); } } Err(e) => { @@ -303,22 +451,51 @@ 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!("Tool Version: {}", built_info::PKG_VERSION); + println!("OS Version: {}", os_specific::get_os_version()); + println!("Mainboard Hardware"); + if let Some(ver) = smbios::get_product_name() { + println!(" Type: {}", ver); + } else { + println!(" Type: Unknown"); + } + if let Some(ver) = smbios::get_baseboard_version() { + println!(" Revision: {:?}", ver); + } else { + println!(" Revision: Unknown"); + } println!("UEFI BIOS"); if let Some(smbios) = get_smbios() { let bios_entries = smbios.collect::(); let bios = bios_entries.first().unwrap(); println!(" Version: {}", bios.version()); println!(" Release Date: {}", bios.release_date()); + } else { + println!(" Version: Unknown"); } println!("EC Firmware"); let ver = print_err(ec.version_info()).unwrap_or_else(|| "UNKNOWN".to_string()); - println!(" Build version: {:?}", ver); + println!(" Build version: {}", ver); if let Some((ro, rw, curr)) = ec.flash_version() { - println!(" RO Version: {:?}", ro); - println!(" RW Version: {:?}", rw); + if ro != rw || log_enabled!(Level::Info) { + println!(" RO Version: {}", ro); + println!(" RW Version: {}", rw); + } print!(" Current image: "); if curr == chromium_ec::EcCurrentImage::RO { println!("RO"); @@ -334,125 +511,217 @@ fn print_versions(ec: &CrosEc) { } println!("PD Controllers"); - - if let Ok(pd_versions) = ccgx::get_pd_controller_versions(ec) { - let right = &pd_versions.controller01; - let left = &pd_versions.controller23; - println!(" Right (01)"); - // let active_mode = + let ccgx_pd_vers = ccgx::get_pd_controller_versions(ec); + if let Ok(PdVersions::RightLeft((right, left))) = ccgx_pd_vers { if let Some(Platform::IntelGen11) = smbios::get_platform() { + if right.main_fw.base != right.backup_fw.base { + println!(" Right (01)"); + println!( + " Main: {}{}", + right.main_fw.base, + active_mode(&right.active_fw, FwMode::MainFw) + ); + println!( + " Backup: {}{}", + right.backup_fw.base, + active_mode(&right.active_fw, FwMode::BackupFw) + ); + } else { + println!( + " Right (01): {} ({:?})", + right.main_fw.base, right.active_fw + ); + } + } else if right.main_fw.app != right.backup_fw.app { println!( - " Main: {}{}", - right.main_fw.base, + " Main: {}{}", + right.main_fw.app, active_mode(&right.active_fw, FwMode::MainFw) ); println!( - " Backup: {}{}", - right.backup_fw.base, + " Backup: {}{}", + right.backup_fw.app, active_mode(&right.active_fw, FwMode::BackupFw) ); } else { println!( - " Main: {}{}", - right.main_fw.app, - active_mode(&right.active_fw, FwMode::MainFw) - ); - println!( - " Backup: {}{}", - right.backup_fw.app, - active_mode(&right.active_fw, FwMode::BackupFw) + " Right (01): {} ({:?})", + right.main_fw.app, right.active_fw ); } - println!(" Left (23)"); if let Some(Platform::IntelGen11) = smbios::get_platform() { + if left.main_fw.base != left.backup_fw.base { + println!(" Left (23)"); + println!( + " Main: {}{}", + left.main_fw.base, + active_mode(&left.active_fw, FwMode::MainFw) + ); + println!( + " Backup: {}{}", + left.backup_fw.base, + active_mode(&left.active_fw, FwMode::BackupFw) + ); + } else { + println!( + " Left (23): {} ({:?})", + left.main_fw.base, left.active_fw + ); + } + } else if left.main_fw.app != left.backup_fw.app { + println!(" Left (23)"); println!( - " Main: {}{}", - left.main_fw.base, + " Main: {}{}", + left.main_fw.app, active_mode(&left.active_fw, FwMode::MainFw) ); println!( - " Backup: {}{}", - left.backup_fw.base, + " Backup: {}{}", + left.backup_fw.app, active_mode(&left.active_fw, FwMode::BackupFw) ); } else { println!( - " Main: {}{}", - left.main_fw.app, - active_mode(&left.active_fw, FwMode::MainFw) + " Left (23): {} ({:?})", + left.main_fw.app, left.active_fw + ); + } + } else if let Ok(PdVersions::Many(versions)) = ccgx_pd_vers { + for (i, version) in versions.into_iter().enumerate() { + if version.main_fw.app != version.backup_fw.app { + println!(" PD {}", 1); + println!( + " Main: {}{}", + version.main_fw.app, + active_mode(&version.active_fw, FwMode::MainFw) + ); + println!( + " Backup: {}{}", + version.backup_fw.app, + active_mode(&version.active_fw, FwMode::BackupFw) + ); + } else { + println!( + " PD {}: {} ({:?})", + i, version.main_fw.app, version.active_fw + ); + } + } + } else if let Ok(PdVersions::Single(pd)) = ccgx_pd_vers { + if pd.main_fw.app != pd.backup_fw.app { + println!( + " Main: {}{}", + pd.main_fw.app, + active_mode(&pd.active_fw, FwMode::MainFw) ); println!( - " Backup: {}{}", - left.backup_fw.app, - active_mode(&left.active_fw, FwMode::BackupFw) + " Backup: {}{}", + pd.backup_fw.app, + active_mode(&pd.active_fw, FwMode::BackupFw) ); + } else { + println!(" Version: {} ({:?})", pd.main_fw.app, pd.active_fw); } } else if let Ok(pd_versions) = power::read_pd_version(ec) { // As fallback try to get it from the EC. But not all EC versions have this command - println!(" Right (01): {}", pd_versions.controller01.app); - println!(" Left (23): {}", pd_versions.controller23.app); + debug!(" Fallback to PD Host command"); + match pd_versions { + MainPdVersions::RightLeft((controller01, controller23)) => { + if let Some(Platform::IntelGen11) = smbios::get_platform() { + println!(" Right (01): {}", controller01.base); + println!(" Left (23): {}", controller23.base); + } else { + println!(" Right (01): {}", controller01.app); + println!(" Left (23): {}", controller23.app); + } + } + MainPdVersions::Single(version) => { + println!(" Version: {}", version.app); + } + MainPdVersions::Many(versions) => { + for (i, version) in versions.into_iter().enumerate() { + println!(" PD {}: {}", i, version.app); + } + } + } } else { 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 { + match GUID::from(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 - ); + right_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 - ); + left_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")] - { + #[cfg(target_os = "linux")] + if smbios::get_platform().and_then(Platform::which_cpu_vendor) != Some(util::CpuVendor::Amd) { println!("CSME"); if let Ok(csme) = csme::csme_from_sysfs() { - println!(" Enabled: {}", csme.enabled); - println!(" Version: {}", csme.main_ver); - println!(" Recovery Ver: {}", csme.recovery_ver); - println!(" Original Ver: {}", csme.fitc_ver); + info!(" Enabled: {}", csme.enabled); + println!(" Firmware Version: {}", csme.main_ver); + if csme.main_ver != csme.recovery_ver || csme.main_ver != csme.fitc_ver { + println!(" Recovery Ver: {}", csme.recovery_ver); + println!(" Original Ver: {}", csme.fitc_ver); + } } else { println!(" Unknown"); } } + #[cfg(feature = "rusb")] + let _ignore_err = check_camera_version(); + + #[cfg(feature = "rusb")] + let _ignore_err = check_usbhub_version(); + + #[cfg(feature = "rusb")] + let _ignore_err = check_inputmodule_version(); + + #[cfg(feature = "hidapi")] + let _ignore_err = print_touchpad_fw_ver(); + + #[cfg(feature = "hidapi")] + if let Some(Platform::Framework12IntelGen13) = smbios::get_platform() { + let _ignore_err = touchscreen::print_fw_ver(); + } + #[cfg(feature = "hidapi")] + print_dp_hdmi_details(false); } fn print_esrt() { @@ -463,31 +732,26 @@ fn print_esrt() { } } -fn flash_ec(ec: &CrosEc, ec_bin_path: &str, flash_type: EcFlashType) { +fn flash_ec(ec: &CrosEc, ec_bin_path: &str, flash_type: EcFlashType, dry_run: bool) { #[cfg(feature = "uefi")] let data = crate::uefi::fs::shell_read_file(ec_bin_path); #[cfg(not(feature = "uefi"))] let data: Option> = { - let _data = match fs::read(ec_bin_path) { + match fs::read(ec_bin_path) { Ok(data) => Some(data), // TODO: Perhaps a more user-friendly error Err(e) => { println!("Error {:?}", e); None } - }; - - // EC communication from OS is not stable enough yet, - // it can't be trusted to reliably flash the EC without risk of damage. - println!("Sorry, flashing EC from the OS is not supported yet."); - None + } }; if let Some(data) = data { println!("File"); println!(" Size: {:>20} B", data.len()); println!(" Size: {:>20} KB", data.len() / 1024); - if let Err(err) = ec.reflash(&data, flash_type) { + if let Err(err) = ec.reflash(&data, flash_type, dry_run) { println!("Error: {:?}", err); } else { println!("Success!"); @@ -498,7 +762,7 @@ fn flash_ec(ec: &CrosEc, ec_bin_path: &str, flash_type: EcFlashType) { fn dump_ec_flash(ec: &CrosEc, dump_path: &str) { let flash_bin = ec.get_entire_ec_flash().unwrap(); - #[cfg(all(not(feature = "uefi"), feature = "std"))] + #[cfg(not(feature = "uefi"))] { let mut file = fs::File::create(dump_path).unwrap(); file.write_all(&flash_bin).unwrap(); @@ -512,6 +776,24 @@ fn dump_ec_flash(ec: &CrosEc, dump_path: &str) { } } +fn dump_dgpu_eeprom(ec: &CrosEc, dump_path: &str) { + let flash_bin = ec.read_gpu_descriptor().unwrap(); + + #[cfg(not(feature = "uefi"))] + { + let mut file = fs::File::create(dump_path).unwrap(); + file.write_all(&flash_bin).unwrap(); + } + #[cfg(feature = "uefi")] + { + let ret = crate::uefi::fs::shell_write_file(dump_path, &flash_bin); + if ret.is_err() { + println!("Failed to dump EC FW image."); + } + } + println!("Wrote {} bytes to {}", flash_bin.len(), dump_path); +} + fn compare_version(device: Option, version: String, ec: &CrosEc) -> i32 { println!("Target Version {:?}", version); @@ -541,8 +823,8 @@ fn compare_version(device: Option, version: String, ec: &Cro } } Some(HardwareDeviceType::PD0) => { - if let Ok(pd_versions) = ccgx::get_pd_controller_versions(ec) { - let ver = pd_versions.controller01.active_fw_ver(); + if let Ok(PdVersions::RightLeft((pd01, _pd23))) = ccgx::get_pd_controller_versions(ec) { + let ver = pd01.active_fw_ver(); println!("Comparing PD0 version {:?}", ver); if ver.contains(&version) { @@ -553,8 +835,8 @@ fn compare_version(device: Option, version: String, ec: &Cro } } Some(HardwareDeviceType::PD1) => { - if let Ok(pd_versions) = ccgx::get_pd_controller_versions(ec) { - let ver = pd_versions.controller23.active_fw_ver(); + if let Ok(PdVersions::RightLeft((_pd01, pd23))) = ccgx::get_pd_controller_versions(ec) { + let ver = pd23.active_fw_ver(); println!("Comparing PD1 version {:?}", ver); if ver.contains(&version) { @@ -599,7 +881,7 @@ fn compare_version(device: Option, version: String, ec: &Cro if let Some(esrt) = esrt::get_esrt() { for entry in &esrt.entries { - match entry.fw_class { + match GUID::from(entry.fw_class) { esrt::TGL_RETIMER01_GUID | esrt::ADL_RETIMER01_GUID | esrt::RPL_RETIMER01_GUID => { if device == Some(HardwareDeviceType::RTM01) { println!("Comparing RTM01 version {:?}", entry.fw_version.to_string()); @@ -628,7 +910,7 @@ fn compare_version(device: Option, version: String, ec: &Cro pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { #[cfg(feature = "uefi")] { - log::set_max_level(args.verbosity); + log::set_max_level(args.verbosity.0); } #[cfg(not(feature = "uefi"))] { @@ -637,7 +919,7 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { // .filter("FRAMEWORK_COMPUTER_LOG") // .write_style("FRAMEWORK_COMPUTER_LOG_STYLE"); - let level = args.verbosity.as_str(); + let level = args.verbosity.0.as_str(); env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(level)) .format_target(false) .format_timestamp(None) @@ -645,12 +927,8 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } // Must be run before any application code to set the config - if args.pd_addrs.is_some() && args.pd_ports.is_some() && args.has_mec.is_some() { - let platform = Platform::GenericFramework( - args.pd_addrs.unwrap(), - args.pd_ports.unwrap(), - args.has_mec.unwrap(), - ); + if args.pd_addrs.is_some() && args.pd_ports.is_some() { + let platform = Platform::GenericFramework(args.pd_addrs.unwrap(), args.pd_ports.unwrap()); Config::set(platform); } @@ -681,7 +959,7 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } else if args.version { print_tool_version(); } else if args.features { - ec.get_features().unwrap(); + print_err(ec.get_features()); } else if args.esrt { print_esrt(); } else if let Some(compare_version_ver) = &args.compare_version { @@ -705,32 +983,69 @@ 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(), + // If we don't know which platform it is, we can use some heuristics + _ => { + // Only Framework 16 has this GPIO + if ec.get_gpio("sleep_l").is_ok() { + ec.print_fw16_inputdeck_status() + } else { + println!(" Unable to tell"); + 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); + } + if let Ok(header) = ec.read_gpu_desc_header() { + println!(" Expansion Bay EEPROM"); + println!( + " Valid: {:?}", + header.magic == [0x32, 0xAC, 0x00, 0x00] + ); + println!(" HW Version: {}.{}", { header.hardware_version }, { + header.hardware_revision + }); + if log_enabled!(Level::Info) { + println!(" Hdr Length {} B", { header.length }); + println!(" Desc Ver: {}.{}", { header.desc_ver_major }, { + header.desc_ver_minor + }); + println!(" Serialnumber:{:X?}", { header.serial }); + println!(" Desc Length: {} B", { header.descriptor_length }); + println!(" Desc CRC: {:X}", { header.descriptor_crc32 }); + println!(" Hdr CRC: {:X}", { header.crc32 }); + } + } } 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) { - println!("{:?}", value); + if let Some(gpio_name) = gpio_name { + print!("GPIO {}: ", gpio_name); + if let Ok(value) = ec.get_gpio(gpio_name) { + println!("{:?}", value); + } else { + println!("Not found"); + } } else { - println!("Not found"); + print_err(ec.get_all_gpios()); } + } else if let Some(maybe_led_level) = &args.fp_led_level { + print_err(handle_fp_led_level(&ec, *maybe_led_level)); } else if let Some(maybe_brightness) = &args.fp_brightness { print_err(handle_fp_brightness(&ec, *maybe_brightness)); } else if let Some(Some(kblight)) = args.kblight { @@ -743,6 +1058,42 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } else { println!("Unable to tell"); } + } else if let Some((row, col, scanset)) = args.remap_key { + print_err(ec.remap_key(row, col, scanset)); + } else if !args.rgbkbd.is_empty() { + if args.rgbkbd.len() < 2 { + println!( + "Must provide at least 2 arguments. Provided only: {}", + args.rgbkbd.len() + ); + } else { + let start_key = args.rgbkbd[0] as u8; + let colors = args.rgbkbd[1..].iter().map(|color| RgbS { + r: ((color & 0x00FF0000) >> 16) as u8, + g: ((color & 0x0000FF00) >> 8) as u8, + b: (color & 0x000000FF) as u8, + }); + ec.rgbkbd_set_color(start_key, colors.collect()).unwrap(); + } + } else if let Some(enable) = args.ps2_enable { + print_err(ec.ps2_emulation_enable(enable)); + } else if let Some(tablet_arg) = &args.tablet_mode { + let mode = match tablet_arg { + TabletModeArg::Auto => TabletModeOverride::Default, + TabletModeArg::Tablet => TabletModeOverride::ForceTablet, + TabletModeArg::Laptop => TabletModeOverride::ForceClamshell, + }; + ec.set_tablet_mode(mode); + } else if let Some(_enable) = &args.touchscreen_enable { + #[cfg(feature = "hidapi")] + 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 => { @@ -777,6 +1128,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); @@ -790,15 +1146,54 @@ 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 { smbios_info(); } else if args.pd_info { print_pd_details(&ec); + } else if let Some(pd) = args.pd_reset { + println!("Resetting PD {}...", pd); + print_err(match pd { + 0 => PdController::new(PdPort::Right01, ec.clone()).reset_device(), + 1 => PdController::new(PdPort::Left23, ec.clone()).reset_device(), + 2 => PdController::new(PdPort::Back, ec.clone()).reset_device(), + _ => { + error!("PD {} does not exist", pd); + Ok(()) + } + }); + } else if let Some(pd) = args.pd_disable { + println!("Disabling PD {}...", pd); + print_err(match pd { + 0 => PdController::new(PdPort::Right01, ec.clone()).enable_ports(false), + 1 => PdController::new(PdPort::Left23, ec.clone()).enable_ports(false), + 2 => PdController::new(PdPort::Back, ec.clone()).enable_ports(false), + _ => { + error!("PD {} does not exist", pd); + Ok(()) + } + }); + } else if let Some(pd) = args.pd_enable { + println!("Enabling PD {}...", pd); + print_err(match pd { + 0 => PdController::new(PdPort::Right01, ec.clone()).enable_ports(true), + 1 => PdController::new(PdPort::Left23, ec.clone()).enable_ports(true), + 2 => PdController::new(PdPort::Back, ec.clone()).enable_ports(true), + _ => { + error!("PD {} does not exist", pd); + Ok(()) + } + }); } else if args.dp_hdmi_info { #[cfg(feature = "hidapi")] - print_dp_hdmi_details(); + print_dp_hdmi_details(true); } else if let Some(pd_bin_path) = &args.dp_hdmi_update { #[cfg(feature = "hidapi")] flash_dp_hdmi_card(pd_bin_path); @@ -880,7 +1275,7 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { println!(" Size: {:>20} B", data.len()); println!(" Size: {:>20} KB", data.len() / 1024); if let Some(header) = analyze_capsule(&data) { - if header.capsule_guid == esrt::WINUX_GUID { + if header.capsule_guid == CGuid::from(esrt::WINUX_GUID) { let ux_header = capsule::parse_ux_header(&data); if let Some(dump_path) = &args.dump { // TODO: Better error handling, rather than just panicking @@ -891,7 +1286,7 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { println!("Capsule is invalid."); } } - } else if let Some(capsule_path) = &args.ho2_capsule { + } else if let Some(capsule_path) = &args.h2o_capsule { #[cfg(feature = "uefi")] let data = crate::uefi::fs::shell_read_file(capsule_path); #[cfg(not(feature = "uefi"))] @@ -913,10 +1308,16 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { println!(" BIOS Version: {:>18}", cap.version); } if let Some(ec_bin) = find_ec_in_bios_cap(&data) { + debug!("Found EC binary in BIOS capsule"); analyze_ec_fw(ec_bin); + } else { + debug!("Didn't find EC binary in BIOS capsule"); } if let Some(pd_bin) = find_pd_in_bios_cap(&data) { + debug!("Found PD binary in BIOS capsule"); analyze_ccgx_pd_fw(pd_bin); + } else { + debug!("Didn't find PD binary in BIOS capsule"); } } } else if let Some(dump_path) = &args.dump_ec_flash { @@ -924,11 +1325,19 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { // TODO: Should have progress indicator dump_ec_flash(&ec, dump_path); } else if let Some(ec_bin_path) = &args.flash_ec { - flash_ec(&ec, ec_bin_path, EcFlashType::Full); + if args.force { + flash_ec(&ec, ec_bin_path, EcFlashType::Full, args.dry_run); + } else { + error!("Flashing EC RO region is unsafe. Use --flash-ec-rw instead"); + } } else if let Some(ec_bin_path) = &args.flash_ro_ec { - flash_ec(&ec, ec_bin_path, EcFlashType::Ro); + if args.force { + flash_ec(&ec, ec_bin_path, EcFlashType::Ro, args.dry_run); + } else { + error!("Flashing EC RO region is unsafe. Use --flash-ec-rw instead"); + } } else if let Some(ec_bin_path) = &args.flash_rw_ec { - flash_ec(&ec, ec_bin_path, EcFlashType::Rw); + flash_ec(&ec, ec_bin_path, EcFlashType::Rw, args.dry_run); } else if let Some(hash_file) = &args.hash { println!("Hashing file: {}", hash_file); #[cfg(feature = "uefi")] @@ -948,6 +1357,45 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { println!(" Size: {:>20} KB", data.len() / 1024); hash(&data); } + } else if let Some(gpu_descriptor) = &args.flash_gpu_descriptor { + let res = ec.set_gpu_serial(gpu_descriptor.0, gpu_descriptor.1.to_ascii_uppercase()); + match res { + Ok(1) => println!("GPU Descriptor successfully written"), + Ok(x) => println!("GPU Descriptor write failed with status code: {}", x), + Err(err) => println!("GPU Descriptor write failed with error: {:?}", err), + } + } else if let Some(gpu_descriptor_file) = &args.flash_gpu_descriptor_file { + if matches!( + smbios::get_family(), + Some(PlatformFamily::Framework16) | None + ) { + #[cfg(feature = "uefi")] + let data: Option> = crate::uefi::fs::shell_read_file(gpu_descriptor_file); + #[cfg(not(feature = "uefi"))] + let data = match fs::read(gpu_descriptor_file) { + Ok(data) => Some(data), + // TODO: Perhaps a more user-friendly error + Err(e) => { + println!("Error {:?}", e); + None + } + }; + if let Some(data) = data { + println!("File"); + println!(" Size: {:>20} B", data.len()); + println!(" Size: {:>20} KB", data.len() / 1024); + let res = ec.set_gpu_descriptor(&data, args.dry_run); + match res { + Ok(()) => println!("GPU Descriptor successfully written"), + Err(err) => println!("GPU Descriptor write failed with error: {:?}", err), + } + } + } else { + println!("Unsupported on this platform"); + } + } else if let Some(dump_path) = &args.dump_gpu_descriptor_file { + println!("Dumping to {}", dump_path); + dump_dgpu_eeprom(&ec, dump_path); } 0 @@ -973,6 +1421,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 @@ -981,21 +1432,28 @@ Options: --ec-bin Parse versions from EC firmware binary file --capsule Parse UEFI Capsule information from binary file --dump Dump extracted UX capsule bitmap image to a file - --ho2-capsule Parse UEFI Capsule information from binary file + --h2o-capsule Parse UEFI Capsule information from binary file --dump-ec-flash Dump EC flash contents --flash-ec Flash EC with new firmware from file --flash-ro-ec Flash EC with new firmware from file --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') - --get-gpio Get GPIO value by name - --fp-brightness []Get or set fingerprint LED brightness level [possible values: high, medium, low] + --charge-current-limit [] Get or set battery current charge limit (Percentage number as arg, e.g. '100') + --get-gpio Get GPIO value by name or all, if no name provided + --fp-led-level [] Get or set fingerprint LED brightness level [possible values: high, medium, low] + --fp-brightness []Get or set fingerprint LED brightness percentage --kblight [] Set keyboard backlight percentage or get, if no value provided --console Get EC console, choose whether recent or to follow the output [possible values: recent, follow] --hash Hash a file of arbitrary data + --flash-gpu-descriptor <18 DIGIT SN> Overwrite the GPU bay descriptor SN and type. + --flash-gpu-descriptor-file Write the GPU bay descriptor with a descriptor file. + -f, --force Force execution of an unsafe command - may render your hardware unbootable! + --dry-run Simulate execution of a command (e.g. --flash-ec) -t, --test Run self-test to check if interaction with EC is possible -h, --help Print help information -b Print output one screen at a time @@ -1033,11 +1491,11 @@ fn hash(data: &[u8]) { println!("Hashes"); print!(" SHA256: "); - util::print_buffer_short(sha256); + util::print_buffer(sha256); print!(" SHA384: "); - util::print_buffer_short(sha384); + util::print_buffer(sha384); print!(" SHA512: "); - util::print_buffer_short(sha512); + util::print_buffer(sha512); } fn selftest(ec: &CrosEc) -> Option<()> { @@ -1046,7 +1504,7 @@ fn selftest(ec: &CrosEc) -> Option<()> { } else { println!(" SMBIOS Platform: Unknown"); println!(); - println!("Specify custom platform parameters with --pd-ports --pd-addrs --has-mec"); + println!("Specify custom platform parameters with --pd-ports --pd-addrs"); return None; }; @@ -1054,11 +1512,13 @@ fn selftest(ec: &CrosEc) -> Option<()> { if let Some(mem) = ec.dump_mem_region() { util::print_multiline_buffer(&mem, 0); } else { - println!(" Failed to read EC memory region") + println!(" Failed to read EC memory region"); + return None; } println!(" Checking EC memory mapped magic bytes"); - ec.check_mem_magic()?; + print_err(ec.check_mem_magic())?; + println!(" Verified that Framework EC is present!"); println!(" Reading EC Build Version"); print_err(ec.version_info())?; @@ -1094,8 +1554,8 @@ fn selftest(ec: &CrosEc) -> Option<()> { println!(" - OK"); } - let pd_01 = PdController::new(PdPort::Left01, ec.clone()); - let pd_23 = PdController::new(PdPort::Right23, ec.clone()); + let pd_01 = PdController::new(PdPort::Right01, ec.clone()); + let pd_23 = PdController::new(PdPort::Left23, ec.clone()); print!(" Getting PD01 info through I2C tunnel"); print_err(pd_01.get_silicon_id())?; print_err(pd_01.get_device_info())?; @@ -1228,7 +1688,6 @@ fn analyze_ccgx_pd_fw(data: &[u8]) { println!("FW 2"); ccgx::binary::print_fw(&versions.main_fw); - return; } else if let Some(versions) = ccgx::binary::read_versions(data, Ccg6) { println!("Detected CCG6 firmware"); println!("FW 1 (Backup)"); @@ -1236,9 +1695,8 @@ fn analyze_ccgx_pd_fw(data: &[u8]) { println!("FW 2 (Main)"); ccgx::binary::print_fw(&versions.main_fw); - return; } else { - println!("Failed to read versions") + println!("Failed to read PD versions") } } @@ -1247,13 +1705,13 @@ pub fn analyze_ec_fw(data: &[u8]) { if let Some(ver) = ec_binary::read_ec_version(data, true) { ec_binary::print_ec_version(&ver, true); } else { - println!("Failed to read version") + println!("Failed to read EC version") } // Readwrite firmware if let Some(ver) = ec_binary::read_ec_version(data, false) { ec_binary::print_ec_version(&ver, false); } else { - println!("Failed to read version") + println!("Failed to read EC version") } } @@ -1261,7 +1719,7 @@ pub fn analyze_capsule(data: &[u8]) -> Option { let header = capsule::parse_capsule_header(data)?; capsule::print_capsule_header(&header); - match header.capsule_guid { + match GUID::from(header.capsule_guid) { esrt::TGL_BIOS_GUID => { println!(" Type: Framework TGL Insyde BIOS"); } @@ -1272,22 +1730,22 @@ pub fn analyze_capsule(data: &[u8]) -> Option { println!(" Type: Framework RPL Insyde BIOS"); } esrt::TGL_RETIMER01_GUID => { - println!(" Type: Framework TGL Retimer01 (Left)"); + println!(" Type: Framework TGL Retimer01 (Right)"); } esrt::TGL_RETIMER23_GUID => { - println!(" Type: Framework TGL Retimer23 (Right)"); + println!(" Type: Framework TGL Retimer23 (Left)"); } esrt::ADL_RETIMER01_GUID => { - println!(" Type: Framework ADL Retimer01 (Left)"); + println!(" Type: Framework ADL Retimer01 (Right)"); } esrt::ADL_RETIMER23_GUID => { - println!(" Type: Framework ADL Retimer23 (Right)"); + println!(" Type: Framework ADL Retimer23 (Left)"); } esrt::RPL_RETIMER01_GUID => { - println!(" Type: Framework RPL Retimer01 (Left)"); + println!(" Type: Framework RPL Retimer01 (Right)"); } esrt::RPL_RETIMER23_GUID => { - println!(" Type: Framework RPL Retimer23 (Right)"); + println!(" Type: Framework RPL Retimer23 (Left)"); } esrt::WINUX_GUID => { println!(" Type: Windows UX capsule"); @@ -1338,13 +1796,34 @@ fn handle_charge_limit(ec: &CrosEc, maybe_limit: Option) -> EcResult<()> { Ok(()) } -fn handle_fp_brightness(ec: &CrosEc, maybe_brightness: Option) -> EcResult<()> { +fn handle_fp_led_level(ec: &CrosEc, maybe_led_level: Option) -> EcResult<()> { + if let Some(led_level) = maybe_led_level { + ec.set_fp_led_level(led_level.into())?; + } + + let (brightness, level) = ec.get_fp_led_level()?; + // TODO: Rename to power button + println!("Fingerprint LED Brightness"); + if let Some(level) = level { + println!(" Requested: {:?}", level); + } + println!(" Brightness: {}%", brightness); + + Ok(()) +} + +fn handle_fp_brightness(ec: &CrosEc, maybe_brightness: Option) -> EcResult<()> { if let Some(brightness) = maybe_brightness { - ec.set_fp_led_level(brightness.into())?; + ec.set_fp_led_percentage(brightness)?; } - let level = ec.get_fp_led_level()?; - println!("Fingerprint LED Brightness: {:?}%", level); + let (brightness, level) = ec.get_fp_led_level()?; + // TODO: Rename to power button + println!("Fingerprint LED Brightness"); + if let Some(level) = level { + println!(" Requested: {:?}", level); + } + println!(" Brightness: {}%", brightness); Ok(()) } diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 121fc052..2a18cabd 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -1,4 +1,4 @@ -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; @@ -6,54 +6,27 @@ use alloc::vec::Vec; use log::{debug, error, info, trace}; use uefi::prelude::BootServices; use uefi::proto::shell_params::*; -use uefi::table::boot::{OpenProtocolAttributes, OpenProtocolParams, SearchType}; -use uefi::Identify; +use uefi::Handle; +use crate::chromium_ec::commands::SetGpuSerialMagic; use crate::chromium_ec::{CrosEcDriverType, HardwareDeviceType}; -use crate::commandline::Cli; +use crate::commandline::{Cli, LogLevel}; -use super::{ConsoleArg, FpBrightnessArg, InputDeckModeArg, RebootEcArg}; +use super::{ConsoleArg, FpBrightnessArg, InputDeckModeArg, RebootEcArg, TabletModeArg}; /// Get commandline arguments from UEFI environment -pub fn get_args(boot_services: &BootServices) -> Vec { - // TODO: I think i should open this from the ImageHandle? - let shell_params_h = - boot_services.locate_handle_buffer(SearchType::ByProtocol(&ShellParameters::GUID)); - let shell_params_h = if let Ok(shell_params_h) = shell_params_h { - shell_params_h +pub fn get_args(bs: &BootServices, image_handle: Handle) -> Vec { + if let Ok(shell_params) = bs.open_protocol_exclusive::(image_handle) { + shell_params.get_args() } else { - error!("ShellParameters protocol not found"); - return vec![]; - }; - - for handle in &*shell_params_h { - let params_handle = unsafe { - boot_services - .open_protocol::( - OpenProtocolParams { - handle: *handle, - agent: boot_services.image_handle(), - controller: None, - }, - OpenProtocolAttributes::GetProtocol, - ) - .expect("Failed to open ShellParameters handle") - }; - - // Ehm why are there two and one has no args? - // Maybe one is the shell itself? - if params_handle.argc == 0 { - continue; - } - - return params_handle.get_args(); + // No protocol found if the application wasn't executed by the shell + vec![] } - vec![] } pub fn parse(args: &[String]) -> Cli { let mut cli = Cli { - verbosity: log::LevelFilter::Error, + verbosity: LogLevel(log::LevelFilter::Error), paginate: false, versions: false, version: false, @@ -64,8 +37,14 @@ pub fn parse(args: &[String]) -> Cli { power: false, thermal: false, sensors: false, + fansetduty: None, + fansetrpm: None, + autofanctrl: false, pdports: false, pd_info: false, + pd_reset: None, + pd_disable: None, + pd_enable: None, dp_hdmi_info: false, dp_hdmi_update: None, audio_card_info: false, @@ -78,24 +57,39 @@ pub fn parse(args: &[String]) -> Cli { flash_rw_ec: None, capsule: None, dump: None, - ho2_capsule: 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, kblight: None, + remap_key: None, + rgbkbd: vec![], + ps2_enable: None, + 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), pd_addrs: None, pd_ports: None, - has_mec: None, test: false, + dry_run: false, + force: false, help: false, + flash_gpu_descriptor: None, + flash_gpu_descriptor_file: None, + dump_gpu_descriptor_file: None, allupdate: false, info: false, raw_command: vec![], @@ -109,15 +103,15 @@ pub fn parse(args: &[String]) -> Cli { for (i, arg) in args.iter().enumerate() { if arg == "-q" { - cli.verbosity = log::LevelFilter::Off; + cli.verbosity = LogLevel(log::LevelFilter::Off); } else if arg == "-v" { - cli.verbosity = log::LevelFilter::Warn; + cli.verbosity = LogLevel(log::LevelFilter::Warn); } else if arg == "-vv" { - cli.verbosity = log::LevelFilter::Info; + cli.verbosity = LogLevel(log::LevelFilter::Info); } else if arg == "-vvv" { - cli.verbosity = log::LevelFilter::Debug; + cli.verbosity = LogLevel(log::LevelFilter::Debug); } else if arg == "-vvvv" { - cli.verbosity = log::LevelFilter::Trace; + cli.verbosity = LogLevel(log::LevelFilter::Trace); } else if arg == "--versions" { cli.versions = true; found_an_option = true; @@ -142,6 +136,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; @@ -154,29 +209,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::() { @@ -192,11 +250,69 @@ 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()) + Some(Some(args[i + 1].clone())) } else { - None + Some(None) }; found_an_option = true; } else if arg == "--kblight" { @@ -214,17 +330,95 @@ pub fn parse(args: &[String]) -> Cli { Some(None) }; found_an_option = true; - } else if arg == "--fp-brightness" { - cli.fp_brightness = if args.len() > i + 1 { - let fp_brightness_arg = &args[i + 1]; - if fp_brightness_arg == "high" { + } else if arg == "--rgbkbd" { + cli.rgbkbd = if args.len() > i + 2 { + let mut colors = Vec::::new(); + for color_i in i + 1..args.len() { + // TODO: Fail parsing instead of unwrap() + colors.push(args[color_i].parse::().unwrap()); + } + colors + } else { + println!("--rgbkbd requires at least 2 arguments, the start key and an RGB value"); + vec![] + } + } else if arg == "--ps2-enable" { + cli.ps2_enable = if args.len() > i + 1 { + let enable_arg = &args[i + 1]; + if enable_arg == "true" { + Some(true) + } else if enable_arg == "false" { + Some(false) + } else { + println!( + "Need to provide a value for --ps2-enable: '{}'. {}", + args[i + 1], + "Must be `true` or `false`", + ); + None + } + } else { + println!("Need to provide a value for --tablet-mode. One of: `auto`, `tablet` or `laptop`"); + None + }; + found_an_option = true; + } else if arg == "--tablet-mode" { + cli.tablet_mode = if args.len() > i + 1 { + let tablet_mode_arg = &args[i + 1]; + if tablet_mode_arg == "auto" { + Some(TabletModeArg::Auto) + } else if tablet_mode_arg == "tablet" { + Some(TabletModeArg::Tablet) + } else if tablet_mode_arg == "laptop" { + Some(TabletModeArg::Laptop) + } else { + println!( + "Need to provide a value for --tablet-mode: '{}'. {}", + args[i + 1], + "Must be one of: `auto`, `tablet` or `laptop`", + ); + None + } + } else { + println!("Need to provide a value for --tablet-mode. One of: `auto`, `tablet` or `laptop`"); + None + }; + found_an_option = true; + } else if arg == "--fp-led-level" { + cli.fp_led_level = if args.len() > i + 1 { + let fp_led_level_arg = &args[i + 1]; + if fp_led_level_arg == "high" { Some(Some(FpBrightnessArg::High)) - } else if fp_brightness_arg == "medium" { + } else if fp_led_level_arg == "medium" { Some(Some(FpBrightnessArg::Medium)) - } else if fp_brightness_arg == "low" { + } else if fp_led_level_arg == "low" { Some(Some(FpBrightnessArg::Low)) + } else if fp_led_level_arg == "ultra-low" { + Some(Some(FpBrightnessArg::UltraLow)) + } else if fp_led_level_arg == "auto" { + Some(Some(FpBrightnessArg::Auto)) + } else { + println!("Invalid value for --fp-led-level: {}", fp_led_level_arg); + None + } + } else { + Some(None) + }; + found_an_option = true; + } else if arg == "--fp-brightness" { + cli.fp_brightness = if args.len() > i + 1 { + if let Ok(fp_brightness_arg) = args[i + 1].parse::() { + if fp_brightness_arg == 0 || fp_brightness_arg > 100 { + println!( + "Invalid value for --fp-brightness: {}. Must be in the range of 1-100", + fp_brightness_arg + ); + None + } else { + Some(Some(fp_brightness_arg)) + } } else { - println!("Invalid value for --fp-brightness: {}", fp_brightness_arg); + println!("Invalid value for --fp-brightness. Must be in the range of 1-100"); None } } else { @@ -269,15 +463,86 @@ 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; + } else if arg == "-f" || arg == "--force" { + cli.force = true; + found_an_option = true; + } else if arg == "--dry-run" { + cli.dry_run = true; + found_an_option = true; } else if arg == "-h" || arg == "--help" { cli.help = true; found_an_option = true; } else if arg == "--pd-info" { cli.pd_info = true; found_an_option = true; + } else if arg == "--pd-reset" { + cli.pd_reset = if args.len() > i + 1 { + if let Ok(pd) = args[i + 1].parse::() { + Some(pd) + } else { + println!( + "Invalid value for --pd-reset: '{}'. Must be 0 or 1.", + args[i + 1], + ); + None + } + } else { + println!("--pd-reset requires specifying the PD controller"); + None + }; + found_an_option = true; + } else if arg == "--pd-disable" { + cli.pd_reset = if args.len() > i + 1 { + if let Ok(pd) = args[i + 1].parse::() { + Some(pd) + } else { + println!( + "Invalid value for --pd-disable: '{}'. Must be 0 or 1.", + args[i + 1], + ); + None + } + } else { + println!("--pd-disable requires specifying the PD controller"); + None + }; + found_an_option = true; + } else if arg == "--pd-enable" { + cli.pd_enable = if args.len() > i + 1 { + if let Ok(pd) = args[i + 1].parse::() { + Some(pd) + } else { + println!( + "Invalid value for --pd-enable: '{}'. Must be 0 or 1.", + args[i + 1], + ); + None + } + } else { + println!("--pd-enable requires specifying the PD controller"); + None + }; + found_an_option = true; } else if arg == "--privacy" { cli.privacy = true; found_an_option = true; @@ -313,11 +578,11 @@ pub fn parse(args: &[String]) -> Cli { None }; found_an_option = true; - } else if arg == "--ho2-capsule" { - cli.ho2_capsule = if args.len() > i + 1 { + } else if arg == "--h2o-capsule" { + cli.h2o_capsule = if args.len() > i + 1 { Some(args[i + 1].clone()) } else { - println!("--ho2-capsule requires extra argument to denote input file"); + println!("--h2o-capsule requires extra argument to denote input file"); None }; found_an_option = true; @@ -362,35 +627,39 @@ pub fn parse(args: &[String]) -> Cli { }; found_an_option = true; } else if arg == "--pd-addrs" { - cli.pd_addrs = if args.len() > i + 2 { + cli.pd_addrs = if args.len() > i + 3 { let left = args[i + 1].parse::(); let right = args[i + 2].parse::(); - if left.is_ok() && right.is_ok() { - Some((left.unwrap(), right.unwrap())) + let back = args[i + 3].parse::(); + if left.is_ok() && right.is_ok() && back.is_ok() { + Some((left.unwrap(), right.unwrap(), back.unwrap())) } else { println!( - "Invalid values for --pd-addrs: '{} {}'. Must be u16 integers.", + "Invalid values for --pd-addrs: '{} {} {}'. Must be u16 integers.", args[i + 1], - args[i + 2] + args[i + 2], + args[i + 3] ); None } } else { - println!("--pd-addrs requires two arguments, one for each address"); + println!("--pd-addrs requires three arguments, one for each address"); None }; found_an_option = true; } else if arg == "--pd-ports" { - cli.pd_ports = if args.len() > i + 2 { + cli.pd_ports = if args.len() > i + 3 { let left = args[i + 1].parse::(); let right = args[i + 2].parse::(); - if left.is_ok() && right.is_ok() { - Some((left.unwrap(), right.unwrap())) + let back = args[i + 3].parse::(); + if left.is_ok() && right.is_ok() && back.is_ok() { + Some((left.unwrap(), right.unwrap(), back.unwrap())) } else { println!( - "Invalid values for --pd-ports: '{} {}'. Must be u16 integers.", + "Invalid values for --pd-ports: '{} {} {}'. Must be u16 integers.", args[i + 1], - args[i + 2] + args[i + 2], + args[i + 3] ); None } @@ -399,22 +668,6 @@ pub fn parse(args: &[String]) -> Cli { None }; found_an_option = true; - } else if arg == "--has-mec" { - cli.has_mec = if args.len() > i + 1 { - if let Ok(b) = args[i + 1].parse::() { - Some(b) - } else { - println!( - "Invalid value for --has-mec: '{}'. Must be 'true' or 'false'.", - args[i + 1] - ); - None - } - } else { - println!("--has-mec requires extra boolean argument."); - None - }; - found_an_option = true; } else if arg == "--raw-command" { cli.raw_command = args[1..].to_vec(); } else if arg == "--compare-version" { @@ -452,14 +705,62 @@ pub fn parse(args: &[String]) -> Cli { println!("Need to provide a value for --console. Possible values: bios, ec, pd0, pd1, rtm01, rtm23, ac-left, ac-right"); None }; + } else if arg == "--flash-gpu-descriptor" { + cli.flash_gpu_descriptor = if args.len() > i + 2 { + let sn = args[i + 2].to_string(); + let magic = &args[i + 1]; + + let hex_magic = if let Some(hex_magic) = magic.strip_prefix("0x") { + u8::from_str_radix(hex_magic, 16) + } else { + // Force parse error + u8::from_str_radix("", 16) + }; + + if let Ok(magic) = magic.parse::() { + Some((magic, sn)) + } else if let Ok(hex_magic) = hex_magic { + Some((hex_magic, sn)) + } else if magic.to_uppercase() == "GPU" { + Some((SetGpuSerialMagic::WriteGPUConfig as u8, sn)) + } else if magic.to_uppercase() == "SSD" { + Some((SetGpuSerialMagic::WriteSSDConfig as u8, sn)) + } else { + println!( + "Invalid values for --flash_gpu_descriptor: '{} {}'. Must be u8, 18 character string.", + args[i + 1], + args[i + 2] + ); + None + } + } else { + println!("Need to provide a value for --flash_gpu_descriptor. TYPE_MAGIC SERIAL"); + None + }; + found_an_option = true; + } else if arg == "--flash-gpu-descriptor-file" { + cli.flash_gpu_descriptor_file = if args.len() > i + 1 { + Some(args[i + 1].clone()) + } else { + println!("Need to provide a value for --flash_gpu_descriptor_file. PATH"); + None + }; + found_an_option = true; + } else if arg == "--dump-gpu-descriptor-file" { + cli.dump_gpu_descriptor_file = if args.len() > i + 1 { + Some(args[i + 1].clone()) + } else { + println!("Need to provide a value for --dump_gpu_descriptor_file. PATH"); + None + }; + found_an_option = true; } } - let custom_platform = cli.pd_addrs.is_some() && cli.pd_ports.is_some() && cli.has_mec.is_some(); - let no_customization = - cli.pd_addrs.is_none() && cli.pd_ports.is_none() && cli.has_mec.is_none(); + let custom_platform = cli.pd_addrs.is_some() && cli.pd_ports.is_some(); + let no_customization = cli.pd_addrs.is_none() && cli.pd_ports.is_none(); if !(custom_platform || no_customization) { - println!("To customize the platform you need to provide all of --pd-addrs, --pd-ports and --has-mec"); + println!("To customize the platform you need to provide all of --pd-addrs, and --pd-ports"); } if args.len() == 1 && cli.paginate { diff --git a/framework_lib/src/csme.rs b/framework_lib/src/csme.rs index 610560a2..5bd6c9eb 100644 --- a/framework_lib/src/csme.rs +++ b/framework_lib/src/csme.rs @@ -3,11 +3,11 @@ //! Currently only works on Linux (from sysfs). use core::fmt; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::fs; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::io; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::path::Path; pub struct CsmeInfo { @@ -85,7 +85,7 @@ impl fmt::Display for CsmeVersion { } } -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] pub fn csme_from_sysfs() -> io::Result { let dir = Path::new("/sys/class/mei"); let mut csme_info: Option = None; diff --git a/framework_lib/src/ec_binary.rs b/framework_lib/src/ec_binary.rs index 092367de..15193105 100644 --- a/framework_lib/src/ec_binary.rs +++ b/framework_lib/src/ec_binary.rs @@ -145,38 +145,47 @@ pub fn parse_ec_version_str(version: &str) -> Option { /// Parse version information from EC FW image buffer pub fn read_ec_version(data: &[u8], ro: bool) -> Option { + // First try to find the legacy EC version let offset = if ro { EC_RO_VER_OFFSET } else { EC_RW_VER_OFFSET }; - let offset_zephyr = if ro { - EC_RO_VER_OFFSET_ZEPHYR - } else { - EC_RW_VER_OFFSET_ZEPHYR - }; - if data.len() < offset + core::mem::size_of::<_ImageVersionData>() { return None; } let v: _ImageVersionData = unsafe { std::ptr::read(data[offset..].as_ptr() as *const _) }; if v.cookie1 != CROS_EC_IMAGE_DATA_COOKIE1 { - debug!("Failed to find Cookie 1. Found: {:X?}", { v.cookie1 }); + debug!("Failed to find legacy Cookie 1. Found: {:X?}", { + v.cookie1 + }); } else if v.cookie2 != CROS_EC_IMAGE_DATA_COOKIE2 { - debug!("Failed to find Cookie 2. Found: {:X?}", { v.cookie2 }); + debug!("Failed to find legacy Cookie 2. Found: {:X?}", { + v.cookie2 + }); } else { return parse_ec_version(&v); } + // If not present, find Zephyr EC version + let offset_zephyr = if ro { + EC_RO_VER_OFFSET_ZEPHYR + } else { + EC_RW_VER_OFFSET_ZEPHYR + }; if data.len() < offset_zephyr + core::mem::size_of::<_ImageVersionData>() { return None; } let v: _ImageVersionData = unsafe { std::ptr::read(data[offset_zephyr..].as_ptr() as *const _) }; if v.cookie1 != CROS_EC_IMAGE_DATA_COOKIE1 { - debug!("Failed to find Cookie 1. Found: {:X?}", { v.cookie1 }); + debug!("Failed to find Zephyr Cookie 1. Found: {:X?}", { + v.cookie1 + }); } else if v.cookie2 != CROS_EC_IMAGE_DATA_COOKIE2 { - debug!("Failed to find Cookie 2. Found: {:X?}", { v.cookie2 }); + debug!("Failed to find Zephyr Cookie 2. Found: {:X?}", { + v.cookie2 + }); } else { return parse_ec_version(&v); } diff --git a/framework_lib/src/esrt/mod.rs b/framework_lib/src/esrt/mod.rs index 521c6352..5d43ecf0 100644 --- a/framework_lib/src/esrt/mod.rs +++ b/framework_lib/src/esrt/mod.rs @@ -14,19 +14,14 @@ use log::{debug, error, info, trace}; use std::prelude::v1::*; -#[cfg(not(feature = "uefi"))] -use crate::guid::Guid; use core::prelude::v1::derive; -#[cfg(not(feature = "uefi"))] -use guid_macros::guid; -#[cfg(feature = "uefi")] -use uefi::{guid, Guid}; +use guid_create::{CGuid, GUID}; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::fs; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::io; -#[cfg(feature = "linux")] +#[cfg(target_os = "linux")] use std::path::Path; #[cfg(target_os = "freebsd")] @@ -38,66 +33,145 @@ use std::os::fd::AsRawFd; #[cfg(target_os = "freebsd")] use std::os::unix::fs::OpenOptionsExt; -/// Decode from GUID string version -/// -/// # Examples -/// ``` -/// use framework_lib::esrt::*; -/// use framework_lib::guid::*; -/// -/// let valid_guid = Guid::from_values(0xA9C91B0C, 0xC0B8, 0x463D, 0xA7DA, 0xA5D6EC646333); -/// // Works with lower-case -/// let guid = guid_from_str("a9c91b0c-c0b8-463d-a7da-a5d6ec646333"); -/// assert_eq!(guid, Some(valid_guid)); -/// // And upper-case -/// let guid = guid_from_str("A9C91B0C-C0B8-463D-A7DA-A5D6EC646333"); -/// assert_eq!(guid, Some(valid_guid)); -/// -/// let guid = guid_from_str("invalid-guid"); -/// assert_eq!(guid, None); -/// ``` -pub fn guid_from_str(string: &str) -> Option { - let string = string.strip_suffix('\n').unwrap_or(string); - let sections: Vec<&str> = string.split('-').collect(); - let time_low = u32::from_str_radix(sections[0], 16).ok()?; - let time_mid = u16::from_str_radix(sections[1], 16).ok()?; - let time_high_and_version = u16::from_str_radix(sections[2], 16).ok()?; - let clock_seq_and_variant = u16::from_str_radix(sections[3], 16).ok()?; - let node = u64::from_str_radix(sections[4], 16).ok()?; - - Some(Guid::from_values( - time_low, - time_mid, - time_high_and_version, - clock_seq_and_variant, - node, - )) -} - -pub const TGL_BIOS_GUID: Guid = guid!("b3bdb2e4-c5cb-5c1b-bdc3-e6fc132462ff"); -pub const ADL_BIOS_GUID: Guid = guid!("a30a8cf3-847f-5e59-bd59-f9ec145c1a8c"); -pub const RPL_BIOS_GUID: Guid = guid!("13fd4ed2-cba9-50ba-bb91-aece0acb4cc3"); -pub const MTL_BIOS_GUID: Guid = guid!("72cecb9b-2b37-5ec2-a9ff-c739aabaadf3"); - -pub const TGL_RETIMER01_GUID: Guid = guid!("832af090-2ef9-7c47-8f6d-b405c8c7f156"); -pub const TGL_RETIMER23_GUID: Guid = guid!("20ef4108-6c64-d049-b6de-11ee35980b8f"); -pub const ADL_RETIMER01_GUID: Guid = guid!("a9c91b0c-c0b8-463d-a7da-a5d6ec646333"); -pub const ADL_RETIMER23_GUID: Guid = guid!("ba2e4e6e-3b0c-4f25-8a59-4c553fc86ea2"); -pub const RPL_RETIMER01_GUID: Guid = guid!("0c42b824-818f-428f-8687-5efcaf059bea"); -pub const RPL_RETIMER23_GUID: Guid = guid!("268ccbde-e087-420b-bf82-2212bd3f9bfc"); -pub const MTL_RETIMER01_GUID: Guid = guid!("c57fd615-2ac9-4154-bf34-4dc715344408"); -pub const MTL_RETIMER23_GUID: Guid = guid!("bdffce36-809c-4fa6-aecc-54536922f0e0"); - -pub const FL16_BIOS_GUID: Guid = guid!("6ae76af1-c002-5d64-8e18-658d205acf34"); -pub const AMD13_BIOS_GUID: Guid = guid!("b5f7dcc1-568c-50f8-a4dd-e39d1f93fda1"); -pub const RPL_CSME_GUID: Guid = guid!("865d322c-6ac7-4734-b43e-55db5a557d63"); -pub const MTL_CSME_GUID: Guid = guid!("32d8d677-eebc-4947-8f8a-0693a45240e5"); +pub const TGL_BIOS_GUID: GUID = GUID::build_from_components( + 0xb3bdb2e4, + 0xc5cb, + 0x5c1b, + &[0xbd, 0xc3, 0xe6, 0xfc, 0x13, 0x24, 0x62, 0xff], +); +pub const ADL_BIOS_GUID: GUID = GUID::build_from_components( + 0xa30a8cf3, + 0x847f, + 0x5e59, + &[0xbd, 0x59, 0xf9, 0xec, 0x14, 0x5c, 0x1a, 0x8c], +); +pub const RPL_BIOS_GUID: GUID = GUID::build_from_components( + 0x13fd4ed2, + 0xcba9, + 0x50ba, + &[0xbb, 0x91, 0xae, 0xce, 0x0a, 0xcb, 0x4c, 0xc3], +); +pub const MTL_BIOS_GUID: GUID = GUID::build_from_components( + 0x72cecb9b, + 0x2b37, + 0x5ec2, + &[0xa9, 0xff, 0xc7, 0x39, 0xaa, 0xba, 0xad, 0xf3], +); +pub const FW12_RPL_BIOS_GUID: GUID = GUID::build_from_components( + 0x6bc0986c, + 0xd281, + 0x5ba3, + &[0x96, 0x5c, 0x2f, 0x8d, 0x13, 0xe1, 0xee, 0xe8], +); + +pub const TGL_RETIMER01_GUID: GUID = GUID::build_from_components( + 0x832af090, + 0x2ef9, + 0x7c47, + &[0x8f, 0x6d, 0xb4, 0x05, 0xc8, 0xc7, 0xf1, 0x56], +); +pub const TGL_RETIMER23_GUID: GUID = GUID::build_from_components( + 0x20ef4108, + 0x6c64, + 0xd049, + &[0xb6, 0xde, 0x11, 0xee, 0x35, 0x98, 0x0b, 0x8f], +); +pub const ADL_RETIMER01_GUID: GUID = GUID::build_from_components( + 0xa9c91b0c, + 0xc0b8, + 0x463d, + &[0xa7, 0xda, 0xa5, 0xd6, 0xec, 0x64, 0x63, 0x33], +); +pub const ADL_RETIMER23_GUID: GUID = GUID::build_from_components( + 0xba2e4e6e, + 0x3b0c, + 0x4f25, + &[0x8a, 0x59, 0x4c, 0x55, 0x3f, 0xc8, 0x6e, 0xa2], +); +pub const RPL_RETIMER01_GUID: GUID = GUID::build_from_components( + 0x0c42b824, + 0x818f, + 0x428f, + &[0x86, 0x87, 0x5e, 0xfc, 0xaf, 0x05, 0x9b, 0xea], +); +pub const RPL_RETIMER23_GUID: GUID = GUID::build_from_components( + 0x268ccbde, + 0xe087, + 0x420b, + &[0xbf, 0x82, 0x22, 0x12, 0xbd, 0x3f, 0x9b, 0xfc], +); +pub const MTL_RETIMER01_GUID: GUID = GUID::build_from_components( + 0xc57fd615, + 0x2ac9, + 0x4154, + &[0xbf, 0x34, 0x4d, 0xc7, 0x15, 0x34, 0x44, 0x08], +); +pub const MTL_RETIMER23_GUID: GUID = GUID::build_from_components( + 0xbdffce36, + 0x809c, + 0x4fa6, + &[0xae, 0xcc, 0x54, 0x53, 0x69, 0x22, 0xf0, 0xe0], +); + +pub const FL16_BIOS_GUID: GUID = GUID::build_from_components( + 0x6ae76af1, + 0xc002, + 0x5d64, + &[0x8e, 0x18, 0x65, 0x8d, 0x20, 0x5a, 0xcf, 0x34], +); +pub const AMD16_AI300_BIOS_GUID: GUID = GUID::build_from_components( + 0x820436ee, + 0x8208, + 0x463b, + &[0x92, 0xb8, 0x82, 0x77, 0xd6, 0x38, 0x4d, 0x93], +); +pub const AMD13_RYZEN7040_BIOS_GUID: GUID = GUID::build_from_components( + 0xb5f7dcc1, + 0x568c, + 0x50f8, + &[0xa4, 0xdd, 0xe3, 0x9d, 0x1f, 0x93, 0xfd, 0xa1], +); +pub const AMD13_AI300_BIOS_GUID: GUID = GUID::build_from_components( + 0x9c13b7f1, + 0xd618, + 0x5d68, + &[0xbe, 0x61, 0x6b, 0x17, 0x88, 0x10, 0x14, 0xa7], +); +pub const DESKTOP_AMD_AI300_BIOS_GUID: GUID = GUID::build_from_components( + 0xeb68dbae, + 0x3aef, + 0x5077, + &[0x92, 0xae, 0x90, 0x16, 0xd1, 0xf0, 0xc8, 0x56], +); +pub const RPL_CSME_GUID: GUID = GUID::build_from_components( + 0x865d322c, + 0x6ac7, + 0x4734, + &[0xb4, 0x3e, 0x55, 0xdb, 0x5a, 0x55, 0x7d, 0x63], +); +pub const RPL_U_CSME_GUID: GUID = GUID::build_from_components( + 0x0f74c56d, + 0xd5ba, + 0x4942, + &[0x96, 0xfa, 0xd3, 0x75, 0x60, 0xf4, 0x05, 0x54], +); +pub const MTL_CSME_GUID: GUID = GUID::build_from_components( + 0x32d8d677, + 0xeebc, + 0x4947, + &[0x8f, 0x8a, 0x06, 0x93, 0xa4, 0x52, 0x40, 0xe5], +); // In EDK2 // Handled by MdeModulePkg/Library/DxeCapsuleLibFmp/DxeCapsuleLib.c // Defined by MdePkg/Include/IndustryStandard/WindowsUxCapsule.h /// gWindowsUxCapsuleGuid from MdePkg/MdePkg.dec -pub const WINUX_GUID: Guid = guid!("3b8c8162-188c-46a4-aec9-be43f1d65697"); +pub const WINUX_GUID: GUID = GUID::build_from_components( + 0x3b8c8162, + 0x188c, + 0x46a4, + &[0xae, 0xc9, 0xbe, 0x43, 0xf1, 0xd6, 0x56, 0x97], +); #[derive(Debug)] pub enum FrameworkGuidKind { @@ -105,6 +179,7 @@ pub enum FrameworkGuidKind { AdlBios, RplBios, MtlBios, + Fw12RplBios, TglRetimer01, TglRetimer23, AdlRetimer01, @@ -114,21 +189,29 @@ pub enum FrameworkGuidKind { MtlRetimer01, MtlRetimer23, RplCsme, + RplUCsme, MtlCsme, Fl16Bios, - Amd13Bios, + Amd16Ai300Bios, + Amd13Ryzen7040Bios, + Amd13Ai300Bios, + DesktopAmdAi300Bios, WinUx, Unknown, } -pub fn match_guid_kind(guid: &Guid) -> FrameworkGuidKind { - match *guid { +pub fn match_guid_kind(guid: &CGuid) -> FrameworkGuidKind { + match GUID::from(*guid) { TGL_BIOS_GUID => FrameworkGuidKind::TglBios, ADL_BIOS_GUID => FrameworkGuidKind::AdlBios, RPL_BIOS_GUID => FrameworkGuidKind::RplBios, MTL_BIOS_GUID => FrameworkGuidKind::MtlBios, + FW12_RPL_BIOS_GUID => FrameworkGuidKind::Fw12RplBios, FL16_BIOS_GUID => FrameworkGuidKind::Fl16Bios, - AMD13_BIOS_GUID => FrameworkGuidKind::Amd13Bios, + AMD16_AI300_BIOS_GUID => FrameworkGuidKind::Amd16Ai300Bios, + AMD13_RYZEN7040_BIOS_GUID => FrameworkGuidKind::Amd13Ryzen7040Bios, + AMD13_AI300_BIOS_GUID => FrameworkGuidKind::Amd13Ai300Bios, + DESKTOP_AMD_AI300_BIOS_GUID => FrameworkGuidKind::DesktopAmdAi300Bios, TGL_RETIMER01_GUID => FrameworkGuidKind::TglRetimer01, TGL_RETIMER23_GUID => FrameworkGuidKind::TglRetimer23, ADL_RETIMER01_GUID => FrameworkGuidKind::AdlRetimer01, @@ -138,13 +221,14 @@ pub fn match_guid_kind(guid: &Guid) -> FrameworkGuidKind { MTL_RETIMER01_GUID => FrameworkGuidKind::MtlRetimer01, MTL_RETIMER23_GUID => FrameworkGuidKind::MtlRetimer23, RPL_CSME_GUID => FrameworkGuidKind::RplCsme, + RPL_U_CSME_GUID => FrameworkGuidKind::RplUCsme, MTL_CSME_GUID => FrameworkGuidKind::MtlCsme, WINUX_GUID => FrameworkGuidKind::WinUx, _ => FrameworkGuidKind::Unknown, } } -#[repr(packed)] +#[repr(C, packed)] struct _Esrt { resource_count: u32, resource_count_max: u32, @@ -216,7 +300,7 @@ impl UpdateStatus { // TODO: Decode into proper Rust types #[derive(Clone)] pub struct EsrtResourceEntry { - pub fw_class: Guid, + pub fw_class: CGuid, pub fw_type: u32, // ResourceType pub fw_version: u32, pub lowest_supported_fw_version: u32, @@ -262,7 +346,7 @@ pub fn print_esrt(esrt: &Esrt) { } } -#[cfg(all(not(feature = "uefi"), feature = "std", feature = "linux"))] +#[cfg(target_os = "linux")] /// On Linux read the ESRT table from the sysfs /// resource_version and resource_count_max are reported by sysfs, so they're defaulted to reaesonable values /// capsule_flags in sysfs seems to be 0 always. Not sure why. @@ -288,8 +372,9 @@ fn esrt_from_sysfs(dir: &Path) -> io::Result { let last_attempt_version = fs::read_to_string(path.join("last_attempt_version"))?; let last_attempt_status = fs::read_to_string(path.join("last_attempt_status"))?; let esrt = EsrtResourceEntry { - // TODO: Parse GUID - fw_class: guid_from_str(&fw_class).expect("Kernel provided wrong value"), + fw_class: CGuid::from( + GUID::parse(fw_class.trim()).expect("Kernel provided wrong value"), + ), fw_type: fw_type .trim() .parse::() @@ -323,7 +408,7 @@ fn esrt_from_sysfs(dir: &Path) -> io::Result { Ok(esrt_table) } -#[cfg(all(not(feature = "uefi"), feature = "linux", target_os = "linux"))] +#[cfg(target_os = "linux")] pub fn get_esrt() -> Option { let res = esrt_from_sysfs(Path::new("/sys/firmware/efi/esrt/entries")).ok(); if res.is_none() { @@ -332,7 +417,7 @@ pub fn get_esrt() -> Option { res } -#[cfg(all(not(feature = "uefi"), feature = "windows"))] +#[cfg(all(not(feature = "uefi"), windows))] pub fn get_esrt() -> Option { let mut esrt_table = Esrt { resource_count: 0, @@ -358,8 +443,8 @@ pub fn get_esrt() -> Option { let guid_str = caps.get(1).unwrap().as_str().to_string(); let ver_str = caps.get(2).unwrap().as_str().to_string(); - let guid = guid_from_str(&guid_str).unwrap(); - let guid_kind = match_guid_kind(&guid); + let guid = GUID::parse(guid_str.trim()).expect("Kernel provided wrong value"); + let guid_kind = match_guid_kind(&CGuid::from(guid)); let ver = u32::from_str_radix(&ver_str, 16).unwrap(); debug!("ESRT Entry {}", i); debug!(" Name: {:?}", guid_kind); @@ -379,7 +464,7 @@ pub fn get_esrt() -> Option { // TODO: The missing fields are present in Device Manager // So there must be a way to get at them let esrt = EsrtResourceEntry { - fw_class: guid, + fw_class: CGuid::from(guid), fw_type, fw_version: ver, // TODO: Not exposed by windows @@ -428,9 +513,9 @@ pub fn get_esrt() -> Option { let mut buf: Vec = Vec::new(); let mut table = EfiGetTableIoc { buf: std::ptr::null_mut(), - uuid: SYSTEM_RESOURCE_TABLE_GUID.to_bytes(), - buf_len: 0, + uuid: SYSTEM_RESOURCE_TABLE_GUID_BYTES, table_len: 0, + buf_len: 0, }; unsafe { let fd = file.as_raw_fd(); @@ -448,7 +533,15 @@ pub fn get_esrt() -> Option { } /// gEfiSystemResourceTableGuid from MdePkg/MdePkg.dec -pub const SYSTEM_RESOURCE_TABLE_GUID: Guid = guid!("b122a263-3661-4f68-9929-78f8b0d62180"); +pub const SYSTEM_RESOURCE_TABLE_GUID: GUID = GUID::build_from_components( + 0xb122a263, + 0x3661, + 0x4f68, + &[0x99, 0x29, 0x78, 0xf8, 0xb0, 0xd6, 0x21, 0x80], +); +pub const SYSTEM_RESOURCE_TABLE_GUID_BYTES: [u8; 16] = [ + 0x63, 0xa2, 0x22, 0xb1, 0x61, 0x36, 0x68, 0x4f, 0x99, 0x29, 0x78, 0xf8, 0xb0, 0xd6, 0x21, 0x80, +]; #[cfg(feature = "uefi")] pub fn get_esrt() -> Option { @@ -458,7 +551,8 @@ pub fn get_esrt() -> Option { for table in config_tables { // TODO: Why aren't they the same type? //debug!("Table: {:?}", table); - let table_guid: Guid = unsafe { std::mem::transmute(table.guid) }; + let table_guid: CGuid = unsafe { std::mem::transmute(table.guid) }; + let table_guid = GUID::from(table_guid); match table_guid { SYSTEM_RESOURCE_TABLE_GUID => unsafe { return esrt_from_buf(table.address as *const u8); diff --git a/framework_lib/src/guid.rs b/framework_lib/src/guid.rs deleted file mode 100644 index 2de63088..00000000 --- a/framework_lib/src/guid.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Taken from https://github.com/rust-osdev/uefi-rs/blob/main/uefi/src/data_types/guid.rs -use core::fmt; - -/// A globally unique identifier -/// -/// GUIDs are used by UEFI to identify protocols and other objects. They are -/// mostly like variant 2 UUIDs as specified by RFC 4122, but differ from them -/// in that the first 3 fields are little endian instead of big endian. -/// -/// The `Display` formatter prints GUIDs in the canonical format defined by -/// RFC 4122, which is also used by UEFI. -#[derive(Debug, Default, Copy, Clone, Eq, Ord, PartialEq, PartialOrd)] -#[repr(C)] -pub struct Guid { - /// The low field of the timestamp. - a: u32, - /// The middle field of the timestamp. - b: u16, - /// The high field of the timestamp multiplexed with the version number. - c: u16, - /// Contains, in this order: - /// - The high field of the clock sequence multiplexed with the variant. - /// - The low field of the clock sequence. - /// - The spatially unique node identifier. - d: [u8; 8], -} - -impl Guid { - /// Creates a new GUID from its canonical representation - #[must_use] - pub const fn from_values( - time_low: u32, - time_mid: u16, - time_high_and_version: u16, - clock_seq_and_variant: u16, - node: u64, - ) -> Self { - assert!(node.leading_zeros() >= 16, "node must be a 48-bit integer"); - // intentional shadowing - let node = node.to_be_bytes(); - - Guid { - a: time_low, - b: time_mid, - c: time_high_and_version, - d: [ - (clock_seq_and_variant / 0x100) as u8, - (clock_seq_and_variant % 0x100) as u8, - // first two elements of node are ignored, we only want the low 48 bits - node[2], - node[3], - node[4], - node[5], - node[6], - node[7], - ], - } - } - - /// Create a GUID from a 16-byte array. No changes to byte order are made. - #[must_use] - pub const fn from_bytes(bytes: [u8; 16]) -> Self { - let a = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - let b = u16::from_le_bytes([bytes[4], bytes[5]]); - let c = u16::from_le_bytes([bytes[6], bytes[7]]); - let d = [ - bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], - ]; - - Self { a, b, c, d } - } - - /// Convert to a 16-byte array. - #[must_use] - #[rustfmt::skip] - pub const fn to_bytes(self) -> [u8; 16] { - let a = self.a.to_le_bytes(); - let b = self.b.to_le_bytes(); - let c = self.c.to_le_bytes(); - let d = self.d; - - [ - a[0], a[1], a[2], a[3], - b[0], b[1], c[0], c[1], - d[0], d[1], d[2], d[3], - d[4], d[5], d[6], d[7], - ] - } -} - -impl fmt::Display for Guid { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let a = self.a; - let b = self.b; - let c = self.c; - - let d = { - let mut buf = [0u8; 2]; - buf[..].copy_from_slice(&self.d[0..2]); - u16::from_be_bytes(buf) - }; - - let e = { - let mut buf = [0u8; 8]; - // first two elements of node are ignored, we only want the low 48 bits - buf[2..].copy_from_slice(&self.d[2..8]); - u64::from_be_bytes(buf) - }; - - write!(fmt, "{a:08x}-{b:04x}-{c:04x}-{d:04x}-{e:012x}",) - } -} - -/// Several entities in the UEFI specification can be referred to by their GUID, -/// this trait is a building block to interface them in uefi-rs. -/// -/// You should never need to use the `Identify` trait directly, but instead go -/// for more specific traits such as `Protocol` or `FileProtocolInfo`, which -/// indicate in which circumstances an `Identify`-tagged type should be used. -/// -/// For the common case of implementing this trait for a protocol, use -/// the `unsafe_protocol` macro. -/// -/// # Safety -/// -/// Implementing `Identify` is unsafe because attaching an incorrect GUID to a -/// type can lead to type unsafety on both the Rust and UEFI side. -pub unsafe trait Identify { - /// Unique protocol identifier. - const GUID: Guid; -} - -#[cfg(test)] -mod tests { - use super::*; - use guid_macros::guid; - - #[test] - fn test_guid_display() { - assert_eq!( - alloc::format!( - "{}", - Guid::from_values(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef0) - ), - "12345678-9abc-def0-1234-56789abcdef0" - ); - } - - #[test] - fn test_guid_macro() { - assert_eq!( - guid!("12345678-9abc-def0-1234-56789abcdef0"), - Guid::from_values(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef0) - ); - } - - #[test] - fn test_to_from_bytes() { - #[rustfmt::skip] - let bytes = [ - 0x78, 0x56, 0x34, 0x12, - 0xbc, 0x9a, - 0xf0, 0xde, - 0x12, 0x34, - 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, - ]; - assert_eq!( - Guid::from_bytes(bytes), - Guid::from_values(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef0) - ); - assert_eq!(Guid::from_bytes(bytes).to_bytes(), bytes); - } -} diff --git a/framework_lib/src/inputmodule.rs b/framework_lib/src/inputmodule.rs new file mode 100644 index 00000000..8cd68a16 --- /dev/null +++ b/framework_lib/src/inputmodule.rs @@ -0,0 +1,63 @@ +pub const FRAMEWORK_VID: u16 = 0x32AC; +pub const LEDMATRIX_PID: u16 = 0x0020; +pub const FRAMEWORK16_INPUTMODULE_PIDS: [u16; 7] = [ + 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 + 0x0030, // Keyboard White Backlight ANSI Copilot + 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 40be376d..f205403e 100644 --- a/framework_lib/src/lib.rs +++ b/framework_lib/src/lib.rs @@ -1,6 +1,7 @@ //! A library to interact with [Framework Computer](https://frame.work) hardware and building tools to do so. #![cfg_attr(feature = "uefi", no_std)] +#![allow(clippy::uninlined_format_args)] extern crate alloc; #[cfg(feature = "uefi")] @@ -14,6 +15,18 @@ extern crate log; #[cfg(feature = "rusb")] pub mod audio_card; +#[cfg(feature = "rusb")] +pub mod camera; +#[cfg(feature = "rusb")] +pub mod inputmodule; +#[cfg(feature = "hidapi")] +pub mod touchpad; +#[cfg(feature = "hidapi")] +pub mod touchscreen; +#[cfg(all(feature = "hidapi", windows))] +pub mod touchscreen_win; +#[cfg(feature = "rusb")] +pub mod usbhub; #[cfg(feature = "uefi")] #[macro_use] @@ -27,8 +40,6 @@ pub mod commandline; pub mod csme; pub mod ec_binary; pub mod esrt; -#[cfg(not(feature = "uefi"))] -pub mod guid; mod os_specific; pub mod power; pub mod smbios; diff --git a/framework_lib/src/os_specific.rs b/framework_lib/src/os_specific.rs index 83ae2f37..b77deb87 100644 --- a/framework_lib/src/os_specific.rs +++ b/framework_lib/src/os_specific.rs @@ -3,6 +3,39 @@ #[cfg(not(feature = "uefi"))] use std::{thread, time}; +#[cfg(feature = "uefi")] +use alloc::string::{String, ToString}; + +// Could report the implemented UEFI spec version +// But that's not very useful. Just look at the BIOS version +// But at least it's useful to see that the tool was run on UEFI +#[cfg(feature = "uefi")] +pub fn get_os_version() -> String { + "UEFI".to_string() +} + +#[cfg(target_family = "windows")] +pub fn get_os_version() -> String { + let ver = windows_version::OsVersion::current(); + format!("{}.{}.{}.{}", ver.major, ver.minor, ver.pack, ver.build) +} + +#[cfg(target_family = "unix")] +pub fn get_os_version() -> String { + if let Ok(uts) = nix::sys::utsname::uname() { + // uname -a without hostname + format!( + "{} {} {} {}", + uts.sysname().to_string_lossy(), + uts.release().to_string_lossy(), + uts.version().to_string_lossy(), + uts.machine().to_string_lossy(), + ) + } else { + "Unknown".to_string() + } +} + /// Sleep a number of microseconds pub fn sleep(micros: u64) { #[cfg(not(feature = "uefi"))] diff --git a/framework_lib/src/power.rs b/framework_lib/src/power.rs index 1f8835a7..3ae05cb5 100644 --- a/framework_lib/src/power.rs +++ b/framework_lib/src/power.rs @@ -1,6 +1,7 @@ //! Get information about system power (battery, AC, PD ports) -use alloc::string::String; +use alloc::format; +use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use core::convert::TryInto; @@ -10,11 +11,10 @@ use log::Level; use crate::ccgx::{AppVersion, Application, BaseVersion, ControllerVersion, MainPdVersions}; use crate::chromium_ec::command::EcRequestRaw; -use crate::chromium_ec::commands::{EcRequestReadPdVersion, EcRequestUsbPdPowerInfo}; -use crate::chromium_ec::{print_err_ref, CrosEc, CrosEcDriver, EcResult}; +use crate::chromium_ec::commands::*; +use crate::chromium_ec::*; use crate::smbios; -use crate::smbios::get_platform; -use crate::util::Platform; +use crate::util::{Platform, PlatformFamily}; /// Maximum length of strings in memmap const EC_MEMMAP_TEXT_MAX: u16 = 8; @@ -54,12 +54,13 @@ const EC_MEMMAP_BATT_SERIAL: u16 = 0x70; // Battery Serial Number String const EC_MEMMAP_BATT_TYPE: u16 = 0x78; // Battery Type String const EC_MEMMAP_ALS: u16 = 0x80; // ALS readings in lux (2 X 16 bits) // Unused 0x84 - 0x8f -const _EC_MEMMAP_ACC_STATUS: u16 = 0x90; // Accelerometer status (8 bits ) - // Unused 0x91 -const _EC_MEMMAP_ACC_DATA: u16 = 0x92; // Accelerometers data 0x92 - 0x9f - // 0x92: u16Lid Angle if available, LID_ANGLE_UNRELIABLE otherwise - // 0x94 - 0x99: u161st Accelerometer - // 0x9a - 0x9f: u162nd Accelerometer +const EC_MEMMAP_ACC_STATUS: u16 = 0x90; // Accelerometer status (8 bits ) + // Unused 0x91 +const EC_MEMMAP_ACC_DATA: u16 = 0x92; // Accelerometers data 0x92 - 0x9f + // 0x92: u16Lid Angle if available, LID_ANGLE_UNRELIABLE otherwise + // 0x94 - 0x99: u161st Accelerometer + // 0x9a - 0x9f: u162nd Accelerometer +const LID_ANGLE_UNRELIABLE: u16 = 500; const _EC_MEMMAP_GYRO_DATA: u16 = 0xa0; // Gyroscope data 0xa0 - 0xa5 // Unused 0xa6 - 0xdf @@ -70,7 +71,12 @@ const EC_BATT_FLAG_DISCHARGING: u8 = 0x04; const EC_BATT_FLAG_CHARGING: u8 = 0x08; const EC_BATT_FLAG_LEVEL_CRITICAL: u8 = 0x10; -#[derive(Debug)] +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, PartialEq)] enum TempSensor { Ok(u8), NotPresent, @@ -163,6 +169,53 @@ impl From for ReducedPowerInfo { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccelData { + pub x: i16, + pub y: i16, + pub z: i16, +} +impl From> for AccelData { + fn from(t: Vec) -> Self { + Self { + x: i16::from_le_bytes([t[0], t[1]]), + y: i16::from_le_bytes([t[2], t[3]]), + z: i16::from_le_bytes([t[4], t[5]]), + } + } +} +impl fmt::Display for AccelData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let quarter: f32 = 0xFFFF as f32 / 4.0; + let x = (self.x as f32) / quarter; + let y = (self.y as f32) / quarter; + let z = (self.z as f32) / quarter; + write!(f, "X={:+.2}G Y={:+.2}G, Z={:+.2}G", x, y, z) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LidAngle { + Angle(u16), + Unreliable, +} +impl From for LidAngle { + fn from(a: u16) -> Self { + match a { + LID_ANGLE_UNRELIABLE => Self::Unreliable, + _ => Self::Angle(a), + } + } +} +impl fmt::Display for LidAngle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Angle(deg) => write!(f, "{}", deg), + Self::Unreliable => write!(f, "Unreliable"), + } + } +} + fn read_string(ec: &CrosEc, address: u16) -> String { let bytes = ec.read_memory(address, EC_MEMMAP_TEXT_MAX).unwrap(); String::from_utf8_lossy(bytes.as_slice()).replace(['\0'], "") @@ -192,14 +245,111 @@ pub fn print_memmap_version_info(ec: &CrosEc) { } /// Not supported on TGL EC -pub fn get_als_reading(ec: &CrosEc) -> Option { +pub fn get_als_reading(ec: &CrosEc, index: usize) -> Option { let als = ec.read_memory(EC_MEMMAP_ALS, 0x04)?; - Some(u32::from_le_bytes([als[0], als[1], als[2], als[3]])) + let offset = index + 4 * index; + Some(u32::from_le_bytes([ + als[offset], + als[1 + offset], + als[2 + offset], + als[3 + offset], + ])) +} + +pub fn get_accel_data(ec: &CrosEc) -> (AccelData, AccelData, LidAngle) { + // bit 4 = busy + // bit 7 = present + // #define EC_MEMMAP_ACC_STATUS_SAMPLE_ID_MASK 0x0f + let _acc_status = ec.read_memory(EC_MEMMAP_ACC_STATUS, 0x01).unwrap()[0]; + // While busy, keep reading + + let lid_angle = ec.read_memory(EC_MEMMAP_ACC_DATA, 0x02).unwrap(); + let lid_angle = u16::from_le_bytes([lid_angle[0], lid_angle[1]]); + let accel_1 = ec.read_memory(EC_MEMMAP_ACC_DATA + 2, 0x06).unwrap(); + let accel_2 = ec.read_memory(EC_MEMMAP_ACC_DATA + 8, 0x06).unwrap(); + + // TODO: Make sure we got a new sample + // println!(" Status Bit: {} 0x{:X}", acc_status, acc_status); + // println!(" Present: {}", (acc_status & 0x80) > 0); + // println!(" Busy: {}", (acc_status & 0x8) > 0); + ( + AccelData::from(accel_1), + AccelData::from(accel_2), + LidAngle::from(lid_angle), + ) } pub fn print_sensors(ec: &CrosEc) { - let als_int = get_als_reading(ec).unwrap(); - println!("ALS: {:>4} Lux", als_int); + let mut has_als = false; + let mut accel_locations = vec![]; + + match ec.motionsense_sensor_info() { + Ok(sensors) => { + info!("Sensors: {}", sensors.len()); + for sensor in sensors { + info!(" Type: {:?}", sensor.sensor_type); + info!(" Location: {:?}", sensor.location); + info!(" Chip: {:?}", sensor.chip); + if sensor.sensor_type == MotionSenseType::Light { + has_als = true; + } + if sensor.sensor_type == MotionSenseType::Accel { + accel_locations.push(sensor.location); + } + } + } + Err(EcError::Response(EcResponseStatus::InvalidCommand)) => { + debug!("Motionsense commands not supported") + } + err => _ = print_err(err), + } + + // If we can't detect it based on motionsense, check the system family + // If family is unknown, assume it has + let als_family = matches!( + smbios::get_family(), + Some(PlatformFamily::Framework13) | Some(PlatformFamily::Framework16) | None + ); + + if has_als || als_family { + let als_int = get_als_reading(ec, 0).unwrap(); + println!("ALS: {:>4} Lux", als_int); + } + + // bit 4 = busy + // bit 7 = present + // #define EC_MEMMAP_ACC_STATUS_SAMPLE_ID_MASK 0x0f + let acc_status = ec.read_memory(EC_MEMMAP_ACC_STATUS, 0x01).unwrap()[0]; + // While busy, keep reading + + let lid_angle = ec.read_memory(EC_MEMMAP_ACC_DATA, 0x02).unwrap(); + let lid_angle = u16::from_le_bytes([lid_angle[0], lid_angle[1]]); + let accel_1 = ec.read_memory(EC_MEMMAP_ACC_DATA + 2, 0x06).unwrap(); + let accel_2 = ec.read_memory(EC_MEMMAP_ACC_DATA + 8, 0x06).unwrap(); + + let present = (acc_status & 0x80) > 0; + if present { + println!("Accelerometers:"); + debug!(" Status Bit: {} 0x{:X}", acc_status, acc_status); + debug!(" Present: {}", present); + debug!(" Busy: {}", (acc_status & 0x8) > 0); + print!(" Lid Angle: "); + if lid_angle == LID_ANGLE_UNRELIABLE { + println!("Unreliable"); + } else { + println!("{} Deg", lid_angle); + } + println!( + " {:<12} {}", + format!("{:?} Sensor:", accel_locations[0]), + AccelData::from(accel_1) + ); + println!( + " {:<12} {}", + format!("{:?} Sensor:", accel_locations[1]), + AccelData::from(accel_2) + ); + } } pub fn print_thermal(ec: &CrosEc) { @@ -207,27 +357,71 @@ pub fn print_thermal(ec: &CrosEc) { let fans = ec.read_memory(EC_MEMMAP_FAN, 0x08).unwrap(); let platform = smbios::get_platform(); - match platform { + let family = smbios::get_family(); + let remaining_sensors = match platform { Some(Platform::IntelGen11) | Some(Platform::IntelGen12) | Some(Platform::IntelGen13) => { println!(" F75303_Local: {:>4}", TempSensor::from(temps[0])); println!(" F75303_CPU: {:>4}", TempSensor::from(temps[1])); println!(" F75303_DDR: {:>4}", TempSensor::from(temps[2])); println!(" Battery: {:>4}", TempSensor::from(temps[3])); println!(" PECI: {:>4}", TempSensor::from(temps[4])); - println!(" F57397_VCCGT: {:>4}", TempSensor::from(temps[5])); + if matches!( + platform, + Some(Platform::IntelGen12) | Some(Platform::IntelGen13) + ) { + println!(" F57397_VCCGT: {:>4}", TempSensor::from(temps[5])); + } + 2 + } + + Some(Platform::IntelCoreUltra1) => { + println!(" F75303_Local: {:>4}", TempSensor::from(temps[0])); + println!(" F75303_CPU: {:>4}", TempSensor::from(temps[1])); + println!(" Battery: {:>4}", TempSensor::from(temps[2])); + println!(" F75303_DDR: {:>4}", TempSensor::from(temps[3])); + println!(" PECI: {:>4}", TempSensor::from(temps[4])); + 3 + } + + Some(Platform::Framework12IntelGen13) => { + println!(" F75303_CPU: {:>4}", TempSensor::from(temps[0])); + println!(" F75303_Skin: {:>4}", TempSensor::from(temps[1])); + println!(" F75303_Local: {:>4}", TempSensor::from(temps[2])); + println!(" Battery: {:>4}", TempSensor::from(temps[3])); + println!(" PECI: {:>4}", TempSensor::from(temps[4])); + println!(" Charger IC {:>4}", TempSensor::from(temps[5])); + 2 } - Some(Platform::Framework13Amd | Platform::Framework16) => { + + Some( + Platform::Framework13Amd7080 + | Platform::Framework13AmdAi300 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300, + ) => { println!(" F75303_Local: {:>4}", TempSensor::from(temps[0])); println!(" F75303_CPU: {:>4}", TempSensor::from(temps[1])); println!(" F75303_DDR: {:>4}", TempSensor::from(temps[2])); println!(" APU: {:>4}", TempSensor::from(temps[3])); - if matches!(platform, Some(Platform::Framework16)) { + if family == Some(PlatformFamily::Framework16) { println!(" dGPU VR: {:>4}", TempSensor::from(temps[4])); println!(" dGPU VRAM: {:>4}", TempSensor::from(temps[5])); println!(" dGPU AMB: {:>4}", TempSensor::from(temps[6])); println!(" dGPU temp: {:>4}", TempSensor::from(temps[7])); + 0 + } else { + 4 } } + + Some(Platform::FrameworkDesktopAmdAiMax300) => { + println!(" F75303_APU: {:>4}", TempSensor::from(temps[0])); + println!(" F75303_DDR: {:>4}", TempSensor::from(temps[1])); + println!(" F75303_AMB: {:>4}", TempSensor::from(temps[2])); + println!(" APU: {:>4}", TempSensor::from(temps[3])); + 4 + } + _ => { println!(" Temp 0: {:>4}", TempSensor::from(temps[0])); println!(" Temp 1: {:>4}", TempSensor::from(temps[1])); @@ -237,11 +431,42 @@ pub fn print_thermal(ec: &CrosEc) { println!(" Temp 5: {:>4}", TempSensor::from(temps[5])); println!(" Temp 6: {:>4}", TempSensor::from(temps[6])); println!(" Temp 7: {:>4}", TempSensor::from(temps[7])); + 0 + } + }; + + // Just in case EC has more sensors than we know about, print them + for (i, temp) in temps.iter().enumerate().take(8).skip(8 - remaining_sensors) { + let temp = TempSensor::from(*temp); + if temp != TempSensor::NotPresent { + println!(" Temp {}: {:>4}", i, temp); } } - 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 @@ -312,6 +537,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; @@ -321,6 +547,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"); @@ -495,7 +722,7 @@ pub fn get_pd_info(ec: &CrosEc, ports: u8) -> Vec> { } pub fn get_and_print_pd_info(ec: &CrosEc) { - let fl16 = Some(crate::util::Platform::Framework16) == get_platform(); + let fl16 = Some(PlatformFamily::Framework16) == smbios::get_family(); let ports = 4; // All our platforms have 4 PD ports so far let infos = get_pd_info(ec, ports); for (port, info) in infos.iter().enumerate().take(ports.into()) { @@ -573,6 +800,11 @@ pub fn is_charging(ec: &CrosEc) -> EcResult<(bool, bool)> { Ok((port0 || port1, port2 || port3)) } +fn parse_pd_ver_slice(data: &[u8]) -> ControllerVersion { + parse_pd_ver(&[ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]) +} fn parse_pd_ver(data: &[u8; 8]) -> ControllerVersion { ControllerVersion { base: BaseVersion { @@ -590,15 +822,37 @@ fn parse_pd_ver(data: &[u8; 8]) -> ControllerVersion { } } -// NOTE: Only works on ADL at the moment! -// TODO: Not on TGL, need to check if RPL and later have it. +// NOTE: TGL (hx20) does not have this host command pub fn read_pd_version(ec: &CrosEc) -> EcResult { - let info = EcRequestReadPdVersion {}.send_command(ec)?; + let info = EcRequestReadPdVersionV1 {}.send_command_vec(ec); - Ok(MainPdVersions { - controller01: parse_pd_ver(&info.controller01), - controller23: parse_pd_ver(&info.controller23), - }) + // If v1 not available, fall back + if let Err(EcError::Response(EcResponseStatus::InvalidVersion)) = info { + let info = EcRequestReadPdVersionV0 {}.send_command(ec)?; + + return Ok(if info.controller23 == [0, 0, 0, 0, 0, 0, 0, 0] { + MainPdVersions::Single(parse_pd_ver(&info.controller01)) + } else { + MainPdVersions::RightLeft(( + parse_pd_ver(&info.controller01), + parse_pd_ver(&info.controller23), + )) + }); + } + // If any other error, exit + let info = info?; + + let mut versions = vec![]; + let pd_count = info[0] as usize; + for i in 0..pd_count { + // TODO: Is there a safer way to check the range? + if info.len() < 1 + 8 * (i + 1) { + return Err(EcError::DeviceError("Not enough data returned".to_string())); + } + versions.push(parse_pd_ver_slice(&info[1 + 8 * i..1 + 8 * (i + 1)])); + } + + Ok(MainPdVersions::Many(versions)) } pub fn standalone_mode(ec: &CrosEc) -> bool { diff --git a/framework_lib/src/smbios.rs b/framework_lib/src/smbios.rs index 9c0ab1c7..a55b8eff 100644 --- a/framework_lib/src/smbios.rs +++ b/framework_lib/src/smbios.rs @@ -5,8 +5,10 @@ use std::prelude::v1::*; #[cfg(all(not(feature = "uefi"), not(target_os = "freebsd")))] use std::io::ErrorKind; -use crate::util::{Config, Platform}; +use crate::util::Config; +pub use crate::util::{Platform, PlatformFamily}; use num_derive::FromPrimitive; +use num_traits::FromPrimitive; use smbioslib::*; #[cfg(feature = "uefi")] use spin::Mutex; @@ -45,7 +47,7 @@ pub enum ConfigDigit0 { pub fn is_framework() -> bool { if matches!( get_platform(), - Some(Platform::GenericFramework((_, _), (_, _), _)) + Some(Platform::GenericFramework((_, _, _), (_, _, _))) | Some(Platform::UnknownSystem) ) { return true; } @@ -106,7 +108,7 @@ pub struct Smbios3 { } #[cfg(target_os = "freebsd")] -#[repr(packed)] +#[repr(C, packed)] pub struct Smbios { pub anchor: [u8; 4], pub checksum: u8, @@ -214,7 +216,7 @@ pub fn get_smbios() -> Option { } } -fn get_product_name() -> Option { +pub fn get_product_name() -> Option { // On FreeBSD we can short-circuit and avoid parsing SMBIOS #[cfg(target_os = "freebsd")] if let Ok(product) = kenv_get("smbios.system.product") { @@ -224,6 +226,7 @@ fn get_product_name() -> Option { let smbios = get_smbios(); if smbios.is_none() { println!("Failed to find SMBIOS"); + return None; } let mut smbios = smbios.into_iter().flatten(); smbios.find_map(|undefined_struct| { @@ -236,6 +239,42 @@ fn get_product_name() -> Option { }) } +pub fn get_baseboard_version() -> Option { + // TODO: On FreeBSD we can short-circuit and avoid parsing SMBIOS + // #[cfg(target_os = "freebsd")] + // if let Ok(product) = kenv_get("smbios.system.product") { + // return Some(product); + // } + + let smbios = get_smbios(); + if smbios.is_none() { + error!("Failed to find SMBIOS"); + return None; + } + let mut smbios = smbios.into_iter().flatten(); + smbios.find_map(|undefined_struct| { + if let DefinedStruct::BaseBoardInformation(data) = undefined_struct.defined_struct() { + if let Some(version) = dmidecode_string_val(&data.version()) { + // Assumes it's ASCII, which is guaranteed by SMBIOS + let config_digit0 = &version[0..1]; + let config_digit0 = u8::from_str_radix(config_digit0, 16); + if let Ok(version_config) = + config_digit0.map(::from_u8) + { + return version_config; + } else { + error!(" Invalid BaseBoard Version: {}'", version); + } + } + } + None + }) +} + +pub fn get_family() -> Option { + get_platform().and_then(Platform::which_family) +} + pub fn get_platform() -> Option { #[cfg(feature = "uefi")] let mut cached_platform = CACHED_PLATFORM.lock(); @@ -251,7 +290,10 @@ pub fn get_platform() -> Option { // Except if it's a GenericFramework platform let config = Config::get(); let platform = &(*config).as_ref().unwrap().platform; - if matches!(platform, Platform::GenericFramework((_, _), (_, _), _)) { + if matches!( + platform, + Platform::GenericFramework((_, _, _), (_, _, _)) | Platform::UnknownSystem + ) { return Some(*platform); } } @@ -262,11 +304,15 @@ pub fn get_platform() -> Option { "Laptop" => Some(Platform::IntelGen11), "Laptop (12th Gen Intel Core)" => Some(Platform::IntelGen12), "Laptop (13th Gen Intel Core)" => Some(Platform::IntelGen13), - "Laptop 13 (AMD Ryzen 7040Series)" => Some(Platform::Framework13Amd), - "Laptop 13 (AMD Ryzen 7040 Series)" => Some(Platform::Framework13Amd), + "Laptop 13 (AMD Ryzen 7040Series)" => Some(Platform::Framework13Amd7080), + "Laptop 13 (AMD Ryzen 7040 Series)" => Some(Platform::Framework13Amd7080), + "Laptop 13 (AMD Ryzen AI 300 Series)" => Some(Platform::Framework13AmdAi300), + "Laptop 12 (13th Gen Intel Core)" => Some(Platform::Framework12IntelGen13), "Laptop 13 (Intel Core Ultra Series 1)" => Some(Platform::IntelCoreUltra1), - "Laptop 16 (AMD Ryzen 7040 Series)" => Some(Platform::Framework16), - _ => None, + "Laptop 16 (AMD Ryzen 7040 Series)" => Some(Platform::Framework16Amd7080), + "Laptop 16 (AMD Ryzen AI 300 Series)" => Some(Platform::Framework16AmdAi300), + "Desktop (AMD Ryzen AI Max 300 Series)" => Some(Platform::FrameworkDesktopAmdAiMax300), + _ => Some(Platform::UnknownSystem), }; if let Some(platform) = platform { diff --git a/framework_lib/src/touchpad.rs b/framework_lib/src/touchpad.rs new file mode 100644 index 00000000..35c4fc55 --- /dev/null +++ b/framework_lib/src/touchpad.rs @@ -0,0 +1,119 @@ +use hidapi::{HidApi, HidDevice, HidError}; +use log::Level; + +pub const PIX_VID: u16 = 0x093A; +pub const P274_REPORT_ID: u8 = 0x43; +pub const P239_REPORT_ID: u8 = 0x42; + +fn read_byte(device: &HidDevice, report_id: u8, addr: u8) -> Result { + device.send_feature_report(&[report_id, addr, 0x10, 0])?; + + let mut buf = [0u8; 4]; + buf[0] = report_id; + + device.get_feature_report(&mut buf)?; + Ok(buf[3]) +} + +fn read_239_ver(device: &HidDevice) -> Result { + Ok(u16::from_le_bytes([ + read_byte(device, P239_REPORT_ID, 0x16)?, + read_byte(device, P239_REPORT_ID, 0x18)?, + ])) +} + +fn read_274_ver(device: &HidDevice) -> Result { + Ok(u16::from_le_bytes([ + read_byte(device, P274_REPORT_ID, 0xb2)?, + read_byte(device, P274_REPORT_ID, 0xb3)?, + ])) +} + +pub fn print_touchpad_fw_ver() -> Result<(), HidError> { + debug!("Looking for touchpad HID device"); + 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(); + let hid_ver = dev_info.release_number(); + + debug!( + " Found {:04X}:{:04X} (Usage Page {:04X})", + vid, pid, usage_page + ); + if vid != PIX_VID || (pid != 0x0274 && pid != 0x0239) { + debug!( + " Skipping VID:PID. Expected {:04X}:{:04X}/{:04X}", + PIX_VID, 0x0274, 0x0239 + ); + continue; + } + if usage_page != 0xFF00 { + debug!(" Skipping usage page. Expected {:04X}", 0xFF00); + continue; + } + + debug!(" Found matching touchpad HID device"); + let device = dev_info.open_device(&api).unwrap(); + + println!("Touchpad"); + info!(" IC Type: {:04X}", pid); + + let ver = match pid { + 0x0239 => format!("{:04X}", read_239_ver(&device)?), + 0x0274 => format!("{:04X}", read_274_ver(&device)?), + _ => "Unsupported".to_string(), + }; + println!(" Firmware Version: v{}", ver); + + if log_enabled!(Level::Debug) { + println!(" Config space 1"); + print!(" "); + for x in 0..16 { + print!("0{:X} ", x); + } + println!(); + for y in 0..16 { + print!("{:X}0 ", y); + for x in 0..16 { + print!("{:02X} ", read_byte(&device, 0x42, x + 16 * y)?); + } + println!(); + } + println!(" Config space 2"); + print!(" "); + for x in 0..16 { + print!("0{:X} ", x); + } + println!(); + for y in 0..16 { + print!("{:X}0 ", y); + for x in 0..16 { + print!("{:02X} ", read_byte(&device, 0x43, x + 16 * y)?); + } + println!(); + } + } + + // Linux does not expose a useful version number for I2C HID devices + #[cfg(target_os = "linux")] + debug!(" HID Version {:04X}", hid_ver); + #[cfg(not(target_os = "linux"))] + if ver != format!("{:04X}", hid_ver) || log_enabled!(Level::Debug) { + println!(" HID Version v{:04X}", hid_ver); + } + + // If we found one, there's no need to look for more + return Ok(()); + } + } + Err(e) => { + eprintln!("Failed to open hidapi. Error: {e}"); + return Err(e); + } + }; + + Ok(()) +} diff --git a/framework_lib/src/touchscreen.rs b/framework_lib/src/touchscreen.rs new file mode 100644 index 00000000..de74c9f9 --- /dev/null +++ b/framework_lib/src/touchscreen.rs @@ -0,0 +1,266 @@ +use hidapi::{HidApi, HidDevice}; + +#[cfg(windows)] +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(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; + } + + // Unwrapping because if we can enumerate it, we should be able to open it + let device = dev_info.open_device(&api).unwrap(); + debug!(" Opened device."); + + return Some(HidapiTouchScreen { device }); + } + } + Err(e) => { + error!("Failed to open hidapi. Error: {e}"); + } + }; + + None + } + + fn send_message(&self, message_id: u8, read_len: usize, data: Vec) -> Option> { + let report_id = 0x03; + let data_len = data.len(); + let mut msg = [0u8; 0x40]; + msg[0] = report_id; + msg[1] = 0xA3; + msg[2] = data_len as u8; + msg[3] = read_len as u8; + msg[4] = message_id; + for (i, b) in data.into_iter().enumerate() { + msg[5 + i] = b; + } + + // Not sure why, but on Windows we just have to write an output report + // HidApiError { message: "HidD_SetFeature: (0x00000057) The parameter is incorrect." } + // Still doesn't work on Windows. Need to write a byte more than the buffer is long + #[cfg(target_os = "windows")] + let send_feature_report = false; + #[cfg(not(target_os = "windows"))] + let send_feature_report = true; + + if send_feature_report { + debug!(" send_feature_report {:X?}", msg); + self.device.send_feature_report(&msg).ok()?; + } else { + debug!(" Writing {:X?}", msg); + self.device.write(&msg).ok()?; + }; + + if read_len == 0 { + return Some(vec![]); + } + + let msg_len = 3 + data_len; + let mut buf: [u8; 0x40] = [0; 0x40]; + debug!(" Reading"); + let res = self.device.read(&mut buf); + debug!(" res: {:?}", res); + 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(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() + .skip(1) + .fold(format!("{:02X}", res[0]), |acc, &x| { + acc + "." + &format!("{:02X}", x) + }); + // Expecting 06.00.0A + debug!(" Protocol Version: v{}", ver); + + let res = self.send_message(0x40, 8, vec![0])?; + let ver = res + .iter() + .skip(1) + .fold(res[0].to_string(), |acc, &x| acc + "." + &x.to_string()); + println!(" Firmware Version: v{}", ver); + + let res = self.send_message(0x20, 16, vec![0])?; + let mut protocols = vec![]; + if (res[15] & USI_BITMAP) > 0 { + protocols.push("USI"); + } + if (res[15] & MPP_BITMAP) > 0 { + protocols.push("MPP"); + } + println!(" Protocols: {}", protocols.join(", ")); + + Some(()) + } + + fn enable_touch(&self, enable: bool) -> Option<()> { + 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(VENDOR_USAGE_PAGE, 0)?; + #[cfg(not(target_os = "windows"))] + 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(VENDOR_USAGE_PAGE, 0)?; + #[cfg(not(target_os = "windows"))] + 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 new file mode 100644 index 00000000..f5fd42a0 --- /dev/null +++ b/framework_lib/src/touchscreen_win.rs @@ -0,0 +1,173 @@ +use hidapi::HidApi; +use std::path::Path; + +use crate::touchscreen::{TouchScreen, ILI_PID, ILI_VID}; +#[allow(unused_imports)] +use windows::{ + core::*, + Win32::{ + Devices::HumanInterfaceDevice::*, + Devices::Properties::*, + Foundation::*, + Storage::FileSystem::*, + System::Threading::ResetEvent, + System::IO::{CancelIoEx, DeviceIoControl}, + System::{Ioctl::*, IO::*}, + }, +}; + +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(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}"); + } + }; + + None + } + + fn send_message(&self, message_id: u8, read_len: usize, data: Vec) -> Option> { + let report_id = 0x03; + let data_len = data.len(); + let mut msg = [0u8; 0x40]; + let msg_len = 3 + data_len; + msg[0] = report_id; + msg[1] = 0xA3; + msg[2] = data_len as u8; + msg[3] = read_len as u8; + msg[4] = message_id; + for (i, b) in data.into_iter().enumerate() { + msg[5 + i] = b; + } + + let mut buf = [0u8; 0x40]; + buf[0] = report_id; + + unsafe { + debug!(" HidD_SetOutputReport {:X?}", msg); + let success = HidD_SetOutputReport( + self.handle, + // Microsoft docs says that the first byte of the message has to be the report ID. + // This is normal with HID implementations. + // But it seems on Windows (at least for this device's firmware) we have to set the + // length as one more than the buffer is long. + // Otherwise no data is returned in the read call later. + msg.as_mut_ptr() as _, + msg.len() as u32 + 1, + ); + debug!(" Success: {}", success); + + if read_len == 0 { + return Some(vec![]); + } + + let mut bytes_read = 0; + debug!(" ReadFile"); + // HidD_GetFeature doesn't work, have to use ReadFile + // Microsoft does recommend that + // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/obtaining-hid-reports + let res = ReadFile(self.handle, Some(&mut buf), Some(&mut bytes_read), None); + debug!(" Success: {:?}, Bytes: {}", res, bytes_read); + debug!(" Read buf: {:X?}", buf); + debug!(" Read msg: {:X?}", msg); + } + + 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/uefi/mod.rs b/framework_lib/src/uefi/mod.rs index 2d6ec747..71d85b44 100644 --- a/framework_lib/src/uefi/mod.rs +++ b/framework_lib/src/uefi/mod.rs @@ -99,7 +99,7 @@ pub fn enable_page_break() { } } -#[repr(packed)] +#[repr(C, packed)] pub struct Smbios { pub anchor: [u8; 4], pub checksum: u8, diff --git a/framework_lib/src/usbhub.rs b/framework_lib/src/usbhub.rs new file mode 100644 index 00000000..53f25576 --- /dev/null +++ b/framework_lib/src/usbhub.rs @@ -0,0 +1,26 @@ +pub const REALTEK_VID: u16 = 0x0BDA; +pub const RTL5432_PID: u16 = 0x5432; +pub const RTL5424_PID: u16 = 0x5424; + +/// Get and print the firmware version of the usbhub +pub fn check_usbhub_version() -> Result<(), rusb::Error> { + for dev in rusb::devices().unwrap().iter() { + let dev_descriptor = dev.device_descriptor().unwrap(); + if dev_descriptor.vendor_id() != REALTEK_VID + || (dev_descriptor.product_id() != RTL5432_PID + && dev_descriptor.product_id() != RTL5424_PID) + { + debug!( + "Skipping {:04X}:{:04X}", + dev_descriptor.vendor_id(), + dev_descriptor.product_id() + ); + continue; + } + + let dev_descriptor = dev.device_descriptor()?; + println!("USB Hub RTL{:04X}", dev_descriptor.product_id()); + println!(" Firmware Version: {}", dev_descriptor.device_version()); + } + Ok(()) +} diff --git a/framework_lib/src/util.rs b/framework_lib/src/util.rs index 18a5a577..ae966209 100644 --- a/framework_lib/src/util.rs +++ b/framework_lib/src/util.rs @@ -6,17 +6,19 @@ use std::prelude::v1::*; #[cfg(feature = "uefi")] use core::prelude::rust_2021::derive; -#[cfg(not(feature = "std"))] +#[cfg(feature = "uefi")] use alloc::sync::Arc; -#[cfg(not(feature = "std"))] +#[cfg(feature = "uefi")] use spin::{Mutex, MutexGuard}; -#[cfg(feature = "std")] +#[cfg(not(feature = "uefi"))] use std::sync::{Arc, Mutex, MutexGuard}; use crate::smbios; #[derive(Debug, PartialEq, Clone, Copy)] pub enum Platform { + /// Framework 12 + Framework12IntelGen13, /// Framework 13 - Intel 11th Gen, Codenamed TigerLake IntelGen11, /// Framework 13 - Intel 11th Gen, Codenamed AlderLake @@ -25,13 +27,69 @@ pub enum Platform { IntelGen13, /// Framework 13 - Intel Core Ultra Series 1, Codenamed MeteorLake IntelCoreUltra1, - /// Framework 13 - AMD Ryzen - Framework13Amd, - /// Framework 16 - Framework16, + /// Framework 13 - AMD Ryzen 7080 Series + Framework13Amd7080, + /// Framework 13 - AMD Ryzen AI 300 Series + Framework13AmdAi300, + /// Framework 16 - AMD Ryzen 7080 Series + Framework16Amd7080, + /// Framework 16 - AMD Ryzen AI 300 Series + Framework16AmdAi300, + /// Framework Desktop - AMD Ryzen AI Max 300 + FrameworkDesktopAmdAiMax300, /// Generic Framework device - /// pd_addrs, pd_ports, has_mec - GenericFramework((u16, u16), (u8, u8), bool), + /// pd_addrs, pd_ports + GenericFramework((u16, u16, u16), (u8, u8, u8)), + UnknownSystem, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum PlatformFamily { + Framework12, + Framework13, + Framework16, + FrameworkDesktop, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum CpuVendor { + Intel, + Amd, +} + +impl Platform { + pub fn which_cpu_vendor(self) -> Option { + match self { + Platform::Framework12IntelGen13 + | Platform::IntelGen11 + | Platform::IntelGen12 + | Platform::IntelGen13 + | Platform::IntelCoreUltra1 => Some(CpuVendor::Intel), + Platform::Framework13Amd7080 + | Platform::Framework13AmdAi300 + | Platform::Framework16Amd7080 + | Platform::Framework16AmdAi300 + | Platform::FrameworkDesktopAmdAiMax300 => Some(CpuVendor::Amd), + Platform::GenericFramework(..) => None, + Platform::UnknownSystem => None, + } + } + 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::Framework16AmdAi300 => Some(PlatformFamily::Framework16), + Platform::FrameworkDesktopAmdAiMax300 => Some(PlatformFamily::FrameworkDesktop), + Platform::GenericFramework(..) => None, + Platform::UnknownSystem => None, + } + } } #[derive(Debug)] @@ -43,9 +101,9 @@ pub struct Config { impl Config { pub fn set(platform: Platform) { - #[cfg(feature = "std")] + #[cfg(not(feature = "uefi"))] let mut config = CONFIG.lock().unwrap(); - #[cfg(not(feature = "std"))] + #[cfg(feature = "uefi")] let mut config = CONFIG.lock(); if (*config).is_none() { @@ -56,9 +114,9 @@ impl Config { } } pub fn is_set() -> bool { - #[cfg(feature = "std")] + #[cfg(not(feature = "uefi"))] let config = CONFIG.lock().unwrap(); - #[cfg(not(feature = "std"))] + #[cfg(feature = "uefi")] let config = CONFIG.lock(); (*config).is_some() @@ -67,9 +125,9 @@ impl Config { pub fn get() -> MutexGuard<'static, Option> { trace!("Config::get() entry"); let unset = { - #[cfg(feature = "std")] + #[cfg(not(feature = "uefi"))] let config = CONFIG.lock().unwrap(); - #[cfg(not(feature = "std"))] + #[cfg(feature = "uefi")] let config = CONFIG.lock(); (*config).is_none() }; @@ -84,9 +142,9 @@ impl Config { None }; - #[cfg(feature = "std")] + #[cfg(not(feature = "uefi"))] let mut config = CONFIG.lock().unwrap(); - #[cfg(not(feature = "std"))] + #[cfg(feature = "uefi")] let mut config = CONFIG.lock(); if new_config.is_some() { @@ -126,7 +184,7 @@ pub unsafe fn any_vec_as_u8_slice(p: &[T]) -> &[u8] { /// Print a byte buffer as a series of hex bytes pub fn print_buffer(buffer: &[u8]) { for byte in buffer { - print!("{:#X} ", byte); + print!("{:02x}", byte); } println!(); } @@ -201,15 +259,8 @@ pub fn find_sequence(haystack: &[u8], needle: &[u8]) -> Option { /// Assert length of an EC response from the windows driver /// It's always 20 more than expected. TODO: Figure out why pub fn assert_win_len(left: N, right: N) { - #[cfg(feature = "win_driver")] + #[cfg(windows)] assert_eq!(left, right + NumCast::from(20).unwrap()); - #[cfg(not(feature = "win_driver"))] + #[cfg(not(windows))] assert_eq!(left, right); } - -pub fn print_buffer_short(buffer: &[u8]) { - for byte in buffer { - print!("{:02x}", byte); - } - println!(); -} diff --git a/framework_tool/Cargo.toml b/framework_tool/Cargo.toml index f4d3bfa9..6dceeabb 100644 --- a/framework_tool/Cargo.toml +++ b/framework_tool/Cargo.toml @@ -1,17 +1,35 @@ [package] name = "framework_tool" -version = "0.2.1" +version = "0.4.5" +description = "Tool to control Framework Computer systems" +homepage = "https://github.com/FrameworkComputer/framework-system" +repository = "https://github.com/FrameworkComputer/framework-system" +readme = "README.md" +license = "BSD-3-Clause" edition = "2021" +[[bin]] +name = "framework_tool" +path = "src/main.rs" + [features] -default = ["linux"] -linux = ["framework_lib/linux"] -freebsd = ["framework_lib/freebsd"] -windows = ["framework_lib/windows"] +default = [ ] +readonly = [ "framework_lib/readonly" ] [dependencies.framework_lib] path = "../framework_lib" -default-features = false [build-dependencies] +# Note: Only takes effect in release builds static_vcruntime = "2.0" +embed-resource = "3.0" +winresource = "0.1.17" + +[target.'cfg(windows)'.dependencies.winapi] +version = "0.3.9" +features = [ + "wincon" +] + +[package.metadata.winresource] +LegalCopyright = "Framework Computer Inc © 2022" diff --git a/framework_tool/build.rs b/framework_tool/build.rs index 20e1c8e9..87d2140e 100644 --- a/framework_tool/build.rs +++ b/framework_tool/build.rs @@ -1,3 +1,19 @@ fn main() { - static_vcruntime::metabuild(); + // Add app icon + if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { + winresource::WindowsResource::new() + .set_icon("..\\res\\framework_startmenuicon.ico") + .compile() + .unwrap(); + } + + if !cfg!(debug_assertions) { + // Statically link vcruntime to allow running on clean install + static_vcruntime::metabuild(); + + // Embed resources file to force running as admin + embed_resource::compile("framework_tool-manifest.rc", embed_resource::NONE) + .manifest_optional() + .unwrap(); + } } diff --git a/framework_tool/framework_tool-manifest.rc b/framework_tool/framework_tool-manifest.rc new file mode 100644 index 00000000..68429585 --- /dev/null +++ b/framework_tool/framework_tool-manifest.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "framework_tool.exe.manifest" diff --git a/framework_tool/framework_tool.exe.manifest b/framework_tool/framework_tool.exe.manifest new file mode 100644 index 00000000..287bba26 --- /dev/null +++ b/framework_tool/framework_tool.exe.manifest @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework_tool/src/main.rs b/framework_tool/src/main.rs index 3a4d54a4..d755aa77 100644 --- a/framework_tool/src/main.rs +++ b/framework_tool/src/main.rs @@ -6,9 +6,49 @@ fn get_args() -> Vec { } fn main() -> Result<(), &'static str> { - let args = commandline::parse(&get_args()); + let args = get_args(); + + // If the user double-clicks (opens from explorer/desktop), + // then we want to have the default behavior of showing a report of + // all firmware versions. + #[cfg(windows)] + let (args, double_clicked) = { + let double_clicked = unsafe { + // See https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922 + let mut plist: winapi::shared::minwindef::DWORD = 0; + let processes = winapi::um::wincon::GetConsoleProcessList(&mut plist, 1); + + // If we're the only process that means we're in a fresh terminal + // without CMD or powershell. This happens in some cases, for example + // if the user double-clicks the app from Explorer. + processes == 1 + }; + // But it also happens if launched from the commandline and a UAC prompt is necessary, + // for example with sudo set to open "In a new windows", therefore we also have to + // check that no commandline arguments were provided. + if double_clicked && args.len() == 1 { + ( + vec![args[0].clone(), "--versions".to_string()], + double_clicked, + ) + } else { + (args, double_clicked) + } + }; + + let args = commandline::parse(&args); if (commandline::run_with_args(&args, false)) != 0 { return Err("Fail"); } + + // Prevent command prompt from auto closing + #[cfg(windows)] + if double_clicked { + println!(); + println!("Press ENTER to exit..."); + let mut line = String::new(); + let _ = std::io::stdin().read_line(&mut line).unwrap(); + } + Ok(()) } diff --git a/framework_uefi/Cargo.toml b/framework_uefi/Cargo.toml index a8e579b0..1995042a 100644 --- a/framework_uefi/Cargo.toml +++ b/framework_uefi/Cargo.toml @@ -1,6 +1,11 @@ [package] name = "framework_uefi" -version = "0.2.1" +version = "0.4.5" +description = "UEFI Tool to control Framework Computer systems" +homepage = "https://github.com/FrameworkComputer/framework-system" +repository = "https://github.com/FrameworkComputer/framework-system" +readme = "README.md" +license = "BSD-3-Clause" edition = "2021" # Minimum Supported Rust Version rust-version = "1.74" @@ -9,6 +14,10 @@ rust-version = "1.74" name = "uefitool" path = "src/main.rs" +[features] +default = [ ] +readonly = [ "framework_lib/readonly" ] + [dependencies] uefi = { version = "0.20", features = ["alloc"] } uefi-services = "0.17" diff --git a/framework_uefi/Makefile b/framework_uefi/Makefile index 6963d8f2..0814ff42 100644 --- a/framework_uefi/Makefile +++ b/framework_uefi/Makefile @@ -1,5 +1,6 @@ TARGET?=x86_64-unknown-uefi BUILD=build/$(TARGET) +FEATURES?='' SRC_DIR=. @@ -15,8 +16,10 @@ QEMU_FLAGS=\ all: $(BUILD)/boot.img +iso: $(BUILD)/UEFI-Shell-fwk.iso + clean: - rm -r $(BUILD) + rm -rf $(BUILD) qemu: $(BUILD)/boot.img $(QEMU) $(QEMU_FLAGS) $< @@ -36,13 +39,30 @@ $(BUILD)/efi.img: $(BUILD)/boot.efi mkfs.vfat $@.tmp mmd -i $@.tmp efi mmd -i $@.tmp efi/boot + echo 'efi\boot\bootx64.efi --version' > startup.nsh + mcopy -i $@.tmp startup.nsh ::efi/boot/startup.nsh + rm -f startup.nsh mcopy -i $@.tmp $< ::efi/boot/bootx64.efi mv $@.tmp $@ +$(BUILD)/shellx64.efi: + wget https://github.com/pbatard/UEFI-Shell/releases/download/24H2/shellx64.efi -O $@ + +$(BUILD)/UEFI-Shell-fwk.iso: $(BUILD)/boot.efi $(BUILD)/shellx64.efi + mkdir -p $(BUILD)/$@.tmp/efi/boot + cp $(BUILD)/boot.efi $(BUILD)/$@.tmp/efi/boot/fwk.efi + cp $(BUILD)/shellx64.efi $(BUILD)/$@.tmp/efi/boot/bootx64.efi + genisoimage -v \ + -V "UEFI SHELL with fwk.efi" \ + -JR \ + -o "$(BUILD)/UEFI-Shell-fwk.iso" \ + $(BUILD)/$@.tmp + $(BUILD)/boot.efi: ../Cargo.lock $(SRC_DIR)/Cargo.toml $(SRC_DIR)/src/* mkdir -p $(BUILD) cargo rustc \ --target $(TARGET) \ + --features $(FEATURES) \ --release \ -- \ --emit link=framework_uefi/$@ 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/framework_uefi/src/main.rs b/framework_uefi/src/main.rs index b81cbe63..caeeee2e 100644 --- a/framework_uefi/src/main.rs +++ b/framework_uefi/src/main.rs @@ -12,11 +12,11 @@ extern crate alloc; use framework_lib::commandline; #[entry] -fn main(_handle: Handle, mut system_table: SystemTable) -> Status { +fn main(image_handle: Handle, mut system_table: SystemTable) -> Status { uefi_services::init(&mut system_table).unwrap(); let bs = system_table.boot_services(); - let args = commandline::uefi::get_args(bs); + let args = commandline::uefi::get_args(bs, image_handle); let args = commandline::parse(&args); if commandline::run_with_args(&args, false) == 0 { return Status::SUCCESS; diff --git a/guid_macros/Cargo.toml b/guid_macros/Cargo.toml deleted file mode 100644 index d1680814..00000000 --- a/guid_macros/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "guid_macros" -version = "0.11.0" -authors = ["The Rust OSDev team"] -readme = "README.md" -edition = "2021" -description = "Procedural macros for the `uefi` crate." -repository = "https://github.com/rust-osdev/uefi-rs" -keywords = ["uefi", "efi"] -categories = ["embedded", "no-std", "api-bindings"] -license = "MPL-2.0" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0.28" -quote = "1.0.9" -syn = { version = "2.0.4", features = ["full"] } - -#[dev-dependencies] -#trybuild = "1.0.61" -#uefi = { version = "0.20.0", default-features = false } diff --git a/guid_macros/src/lib.rs b/guid_macros/src/lib.rs deleted file mode 100644 index 7556a5d5..00000000 --- a/guid_macros/src/lib.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Taken from - -use proc_macro::TokenStream; - -use proc_macro2::{TokenStream as TokenStream2, TokenTree}; -use quote::{quote, ToTokens}; -use syn::spanned::Spanned; -use syn::{parse_macro_input, Error, LitStr}; - -macro_rules! err { - ($span:expr, $message:expr $(,)?) => { - Error::new($span.span(), $message).to_compile_error() - }; - ($span:expr, $message:expr, $($args:expr),*) => { - Error::new($span.span(), format!($message, $($args),*)).to_compile_error() - }; -} - -/// Create a `Guid` at compile time. -/// -/// # Example -/// -/// ``` -/// use uefi::{guid, Guid}; -/// const EXAMPLE_GUID: Guid = guid!("12345678-9abc-def0-1234-56789abcdef0"); -/// ``` -#[proc_macro] -pub fn guid(args: TokenStream) -> TokenStream { - let (time_low, time_mid, time_high_and_version, clock_seq_and_variant, node) = - match parse_guid(parse_macro_input!(args as LitStr)) { - Ok(data) => data, - Err(tokens) => return tokens.into(), - }; - - quote!({ - const g: crate::guid::Guid = crate::guid::Guid::from_values( - #time_low, - #time_mid, - #time_high_and_version, - #clock_seq_and_variant, - #node, - ); - g - }) - .into() -} - -fn parse_guid(guid_lit: LitStr) -> Result<(u32, u16, u16, u16, u64), TokenStream2> { - let guid_str = guid_lit.value(); - - // We expect a canonical GUID string, such as "12345678-9abc-def0-fedc-ba9876543210" - if guid_str.len() != 36 { - return Err(err!( - guid_lit, - "\"{}\" is not a canonical GUID string (expected 36 bytes, found {})", - guid_str, - guid_str.len() - )); - } - let mut offset = 1; // 1 is for the starting quote - let mut guid_hex_iter = guid_str.split('-'); - let mut next_guid_int = |len: usize| -> Result { - let guid_hex_component = guid_hex_iter.next().unwrap(); - - // convert syn::LitStr to proc_macro2::Literal.. - let lit = match guid_lit.to_token_stream().into_iter().next().unwrap() { - TokenTree::Literal(lit) => lit, - _ => unreachable!(), - }; - // ..so that we can call subspan and nightly users (us) will get the fancy span - let span = lit - .subspan(offset..offset + guid_hex_component.len()) - .unwrap_or_else(|| lit.span()); - - if guid_hex_component.len() != len * 2 { - return Err(err!( - span, - "GUID component \"{}\" is not a {}-bit hexadecimal string", - guid_hex_component, - len * 8 - )); - } - offset += guid_hex_component.len() + 1; // + 1 for the dash - u64::from_str_radix(guid_hex_component, 16).map_err(|_| { - err!( - span, - "GUID component \"{}\" is not a hexadecimal number", - guid_hex_component - ) - }) - }; - - // The GUID string is composed of a 32-bit integer, three 16-bit ones, and a 48-bit one - Ok(( - next_guid_int(4)? as u32, - next_guid_int(2)? as u16, - next_guid_int(2)? as u16, - next_guid_int(2)? as u16, - next_guid_int(6)?, - )) -} diff --git a/nix.md b/nix.md new file mode 100644 index 00000000..19b140ca --- /dev/null +++ b/nix.md @@ -0,0 +1,12 @@ +# Building with Nix + +## Building with devenv + + +Must have Nix and devenv installed + +``` +devenv shell +cargo build +sudo ./target/debug/framework_tool +``` diff --git a/res/framework_startmenuicon.ico b/res/framework_startmenuicon.ico new file mode 100644 index 00000000..8c6be29a Binary files /dev/null and b/res/framework_startmenuicon.ico differ diff --git a/rgbkbd.py b/rgbkbd.py new file mode 100755 index 00000000..c997d897 --- /dev/null +++ b/rgbkbd.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# Example invocation in fish shell +# cargo build && sudo ./target/debug/framework_tool \ +# --driver portio --pd-ports 1 1 --pd-addrs 64 64 \ +# (./rgbkbd.py | string split ' ') + +BRIGHTNESS = 1 +RED = int(0xFF * BRIGHTNESS) << 16 +GREEN = int(0xFF * BRIGHTNESS) << 8 +BLUE = int(0xFF * BRIGHTNESS) +CYAN = GREEN + BLUE +YELLOW = RED + GREEN +PURPLE = BLUE + RED +WHITE = RED + GREEN + BLUE + +grid_4x4 = [ + [ YELLOW, RED, RED, RED, YELLOW ], + [ RED, WHITE, GREEN, WHITE, RED ], + [ RED, GREEN, BLUE, GREEN, RED ], + [ RED, WHITE, GREEN, WHITE, RED ], + [ YELLOW, RED, RED, RED, YELLOW ], +] + +fan_8leds = [[ + # WHITE, CYAN, BLUE, GREEN, PURPLE, RED, YELLOW, WHITE + RED, RED, RED, RED, + GREEN, GREEN, GREEN, GREEN +]] + +# colors = grid_4x4 +colors = fan_8leds + +print('--rgbkbd 0', end='') +for row in colors: + for col in row: + print(' ', end='') + print(col, end='') 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 |