Skip to content

0.9.7: probe, fan rpm to Manual, Custom floor, perf read-back - #12

Merged
sqmagellan merged 17 commits into
mainfrom
release/0.9.7
Oct 21, 2026
Merged

sqmagellan merged 17 commits into
mainfrom
release/0.9.7

Conversation

@sqmagellan

@sqmagellan sqmagellan commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Everything for the next release except the Blade 17 (2022) profile (#11), which waits on #10.

What's in it

  • razer-cli probe: a read-only capture of every setting on both fan zones, with the EC's raw answers, as one JSON file for model reports. probe --writes (tray closed) also changes perf mode, boosts, Max Fan and the fan, reads each back, then restores and verifies. It keeps a recovery file; Ctrl-C restores, and probe --restore FILE replays one.
  • auto fan rpm switches the fan to Manual first ([model] Razer Blade 17 2022, PID 0x028b #10), on a laptop that can read its fan mode, and puts it back if the set point fails.
  • Blade 16 (2023): in Custom a manual fan runs at max(set point, 3800). auto fan rpm, auto info, auto json (fan_floor_rpm) and the tray's fan presets say so.
  • A perf mode write the EC acknowledges and ignores is caught: the tray reads the mode back and retries up to 3 times, and the CLI's mode commands fail instead of reporting success.
  • Tests run against simulated ECs built from four laptops' reports and hardware runs (librazer::sim, librazer/tests/ec/). Unmodeled commands answer "unmodeled", never an echo. CI also runs razer-cli against them.

Tested

  • macOS: librazer 236, razer-cli on the simulated ECs 24, clippy and fmt clean. Linux: fmt, clippy and tests clean.
  • Blade 16 (2023), 0x029F, Windows 11: razer-tray 19 tests; the full scenario run on afeb7d3 (33 steps, every write read back, restore verified, tray back); the Custom floor check (fans held 3800 at a 3000 set point, restore verified); probe --writes on the harness branch, 10-20 (every write read back, restore verified).
  • Auto fan under a CPU and GPU load on the same laptop: 4100 RPM in Balanced, 4600 in Performance and Custom.

CHANGELOG has an Unreleased section; versions are bumped at release.

sqmagellan and others added 17 commits October 19, 2026 23:36
0x029F answered 0x0d02 with success and its args echoed while staying
in Balanced, hours into Modern Standby (2026-10-19): `auto perf mode
custom` exited 0. On 2026-09-24 the tray's Battery profile, applied as
the laptop woke, left the EC in Silent with no retry.

confirm_perf_mode reads 0x0d82 back (one re-read after 200 ms) and
returns PerfNotApplied, which is retryable and exits 5. The CLI's
`auto perf mode` and `auto fan auto|manual` use it, and apply's switch
to Custom uses it before writing boosts. An ignored mode no longer stops
apply's lighting, brightness and charge-limit writes.

The tray reads the mode back after each apply, also when another write
failed, and resyncs up to three times per intended state (about 2, 4
and 8 s apart) without reopening the device. After giving up, the sync's
reconcile only reads. get_perf_mode now keeps a failed zone read's typed
cause, so a busy or unsupported read retries and exits as before the
formatted error dropped it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command only wrote the set point, so on a fan in Auto it changed
nothing anyone could see, and a user's sweep could measure the firmware
curve instead (#10). It now switches to Manual first, the order the
tray uses, through the confirmed fan-mode write, and says so on stderr.
If the set point write then fails, the fan goes back to its old mode.
A non-multiple of 100 is rejected before anything is written, and a
laptop without the mode read keeps the old set-point-only behavior.
Outside Balanced it adds a note: on 0x029F Hyperboost runs its own
curve when that is faster than the set point.

The issue template's fan-range step now says the old behavior ends at
0.9.6. The macro only `rpm` used is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue #10's Blade 17 (2022) answers 0x05 to Max Fan Enable. If Disable is
rejected too, a catalogued 0x028b fails every Custom apply and reconcile,
because only the generic profile learns. The generic profile doesn't learn
Max Fan yet either; feat/max-fan-lacking changes that.
`razer-cli probe` reads every getter the tools use, once each through
the normal retry loop, and writes what the EC answered as JSON: each
attempt's status byte, reply args and timing, recorded below Device's
retries. It only reads; a test checks every command is a getter.

librazer::sim (tests, or --features sim) answers from an EC model:
per-command rules tagged with their evidence, per-zone state, actual
fan speed separate from the set point, its own clock and injectable
faults. A command nobody observed fails as unmodeled instead of
echoing, so it shows up as a coverage gap. The sim and Device share one
retry policy (librazer::retry), and HidTransport gains pause(), so
timed logic runs on simulated time in tests.
librazer/tests/ec/ holds a model for each laptop we have evidence for:
the Blade 16 (2023) from hardware runs, the Blade 18 (2023) from #1,
the Blade 15 (2018) Base from #2 and the Blade 17 (2022) from #10.
Every rule names its source; a test checks it.

The conformance suite runs each model through the same scenarios
(startup read, apply each profile, Max Fan, manual fan, enforce after
drift, fan reads, the probe) and compares the results with
tests/ec/expected.txt. Commands nobody has observed show up as gaps,
listed per laptop with the open question, so the file is also the list
of what to ask each reporter.

Three bugs from this project's history are replayed on the Blade 16
model: a lone-zone zero fan read, a set point below the fan floor, and
an acknowledged perf write the EC ignored. The last is ignored until
the read-back fix lands.
razer-cli probe --writes reads the settings it will change, saves them
to a recovery file, then sets each perf mode, the boosts and Max Fan
in Custom, and a manual fan sweep in Balanced over the chassis's range,
reading each write back and watching the fans settle. It then restores
only what moved, in dependency order (a perf write resets the fan to
Auto), and verifies. It refuses to run while the tray is open, and
writes nothing if the snapshot can't be read cleanly.
razer-cli probe --restore FILE puts back an interrupted run.

The restore is tested on the simulated EC: a clean run, a write that
fails midway, a Max Fan the EC rejects, and zones that disagree.
A build with `--features sim` takes a global `--sim MODEL.json`.
Every subcommand then talks to librazer::sim::SimTransport built from
that EC model instead of the hardware: auto info and json, the perf,
fan, boost, lighting and battery setters, manual --pid, probe and
enumerate. The descriptor comes from the model's PID: a catalogued PID
gets its SUPPORTED entry, any other the generic profile, which learns
what the EC lacks. A model names no SKU, so an uncatalogued SKU with a
catalogued PID is not modeled. A notice on stderr says the run is
simulated, a simulated probe names the model in its tool field, and
requests the model has no observation for are listed on stderr at the
end, because json and info tolerate some failed reads. A --sim without
a usable value is an error before anything is opened, never a fallback
to the hardware. taskkill refuses to run simulated.

Handlers took librazer::device::Device. They now take a Laptop, which
holds the descriptor and either backend and implements HidTransport
and Recording by delegation; command::* take `&impl HidTransport`, so
a trait object would not fit. The hardware backend is gated to Windows
and Linux, so a sim build compiles and runs on macOS.

Without the feature the binary has no --sim option and behaves as
before. The tests reach librazer's sim feature through a
dev-dependency, so a plain `cargo test -p razer-cli` (the Windows CI
job) runs the real handlers against an inline model: json fields, a
perf mode set read back, Max Fan answering 0x05 exiting 4 on a
catalogued and a generic model, and an unmodeled command failing with
an error that names the gap. CONTRIBUTING.md says how to run it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
razer-cli built with --features sim takes --sim MODEL.json and runs
every subcommand against librazer::sim, including probe --writes; a
simulated capture says so in its tool field. A model may name its BIOS
model number, and the profile is then picked the way Device::detect
picks it; the Blade 18, Blade 15 and Blade 17 models now do. CI runs
the CLI tests on the simulator.
From review: a write step now fails unless its read-back matches what
was asked, and a step whose prerequisite didn't take is skipped (the
fan sweep needs Balanced and Manual confirmed). The snapshot covers
every register the write set changes, both zones' set points in any
mode and both boosts included, and nothing is written if it can't be
read. The restore keeps going past a failed write, verifies every
field, is recorded as a step, and lists what is still different.
Ctrl-C stops the run and goes straight to the restore.

The recovery file names the laptop it came from; --restore refuses
another laptop's, and a new run refuses to overwrite an unfinished
run's file. The tray check also covers --restore. A generic profile
sweeps only the middle of its guessed range unless --fan-range is
given. The default settle time is 150 s, enough for a full-range move
at the measured slew.

Models: rules from a capture are per request; a writes capture is
refused. Manual fan behavior applies only in the perf modes it was seen
in, Max Fan outside Custom is a gap, and the fixtures no longer claim
boost values or perf modes nobody reported. Conformance checks the
state each profile leaves and lists every 0x05 answer.
From the second review: the restore reads and compares each field on
its own, so one failed read only makes that field's write
unconditional, and a boost snapshot with one known value restores
that value. Verification checks every snapshotted field and lists
each one that is unreadable or still different. razer-cli probe
--writes saves the capture either way, keeps the recovery file, and
exits with an error when the restore didn't verify.
On the maintainer's Blade 16 (2023), RZ09-0483U, probe --writes read
back every write, held 5000, 3500 and 2000 RPM in Balanced/Manual (up
at about 33 RPM/s, down at about 30), and verified its restore. The
model now names that SKU and cites the run.
Measured on the maintainer's Blade 16 (2023): in Custom with the fan
in Manual, the fans run at the larger of the set point and 3800 RPM,
whatever the boosts. A 3000 set point reads back as 3000 while the
fans hold 3800; 4500 is followed. A 90 s CPU load raised neither
Custom nor Balanced above its set point. The simulator takes a manual
floor per perf mode, and the Blade 16 model uses it.
On the Blade 16 (2023) a manual fan in Custom runs at 3800 RPM or
more, while a lower set point still reads back as written. The CLI
and tray showed that set point as if the fans followed it.

A per-PID table beside KNOWN_LACKING now records the floor (0x029F,
Custom, 3800, from the 2026-10-20 hardware runs). A conformance test
keeps it equal to the EC models and checks that the speed it predicts
is the one the simulated fans settle at.

razer-cli: fan rpm below the floor still writes the value and prints
one line saying what the fans run at; auto info prints the same line.
auto json adds fan_floor_rpm in Manual where a floor is known, and
leaves it out otherwise, so other output is unchanged.

razer-tray: in Custom, presets below the floor are labeled "(runs at
3800)", and the tooltip shows "Fan 3800 floor" instead of the set
point. Nothing new is enforced or counted as drift.
From review: the floor describes a manual fan, and Max Fan drives the
fans past any set point, so "runs at 3800" was wrong in those states.
The CLI note now needs Manual, and Max Fan off in Custom; the tray
label and tooltip skip the floor while Max Fan is on.

The fan handler prints through a sink, so the tests check the lines
fan rpm and info print through the real handler. The README and the
fan_floor_rpm comment say the speeds are settled ones with Max Fan
off, and that the Black edition shares the PID untested.
Brings fix/perf-write-confirm and feat/cli-fan-rpm-manual onto the
harness branch.

Conflicts: razer-cli's fan handler moved into fan_handle on the
harness side, so the rpm range and multiple-of-100 checks, the switch
to Manual and the confirmed auto/manual writes now live there. `fan
rpm` reads the modes once, to switch to Manual, and the Custom floor
note reuses that read. The generic "Balanced holds a set point" note
is left out for a mode with a listed floor, whose own note covers it.

The replayed 10-19 ignored-write test runs again. It now checks
confirm_modes, which the tray runs after apply: apply alone can't
see an acknowledged write the EC ignored.
`auto fan rpm` puts the fan back to its old mode, confirmed, when the
switch to Manual fails its read-back as well as when the set point
fails: a switch whose read-back failed may still have taken.

A laptop that can't read its perf mode can't confirm a write, so
confirm_perf_mode lets the write stand there instead of failing a
command that worked. A Busy read still fails. The re-read waits
through HidTransport::pause, so the sim and the mocks don't sleep.
@sqmagellan
sqmagellan merged commit afeb7d3 into main Oct 21, 2026
5 checks passed
@sqmagellan
sqmagellan deleted the release/0.9.7 branch October 25, 2026 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant