Skip to content

Update framework dotnet + add better adaptive control - #82

Merged
TekuSP merged 6 commits into
masterfrom
update_framework_dotnet
Aug 30, 2026
Merged

Update framework dotnet + add better adaptive control#82
TekuSP merged 6 commits into
masterfrom
update_framework_dotnet

Conversation

@TekuSP

@TekuSP TekuSP commented Aug 30, 2026

Copy link
Copy Markdown
Owner

No description provided.

TekuSP added 6 commits August 30, 2026 14:33
Where calibration judged the EC could hold a commanded speed, the duty
demand was converted to an RPM setpoint and handed to the firmware's own
loop. That loop clamps the target to its configured maximum for the
channel — around the 4900 RPM this repo already records as
StandardFirmwareMaximumRpm on Framework 16, where the same fan reaches
past 6200 on a direct duty write.

So the top quarter of the range could not be asked for. Calibration
measured MaximumRpm at 100% duty (correct), then verified speed tracking
at only 60% of it — comfortably under the ceiling, so it passed and
declared cascade good for the whole range. At runtime full demand mapped
to a speed the EC quietly refused: the fan sat ~1600 RPM short and,
because the shortfall happened inside the firmware rather than at the
controller's own clamp, the anti-windup never fired and the trim wound to
+100% against a ceiling it had no way to see.

The learner was the worse casualty. It was told CommandedDutyPercent =
<the demand> while the EC had substituted its own duty to chase an RPM,
so the model was being fitted against duty values that were never
applied.

Cascade's one advantage — the firmware holding a speed steady as a fan
ages — is what the adaptive learner already does, so removing it costs
nothing.

Speeds are still shown. FanCalibrationSnapshot.EstimateRpm interpolates
between the two speeds calibration actually measures, the minimum-spin
point and full duty, instead of scaling the maximum by the duty fraction:
a fan does not start turning at zero, so the old line read far too low
across the whole lower half of the range. On a 20%/1200 -> 100%/6000 fan,
60% duty is 3600 RPM, not 3000. It is named ExpectedRpm because it is an
estimate of an outcome, never a demand.

The adaptive card's "off setpoint by N RPM" is gone with the setpoint it
referred to. It now reports only a fan running SHORT of expectation —
which can mean an obstruction — with a tolerance proportional to the
speed being predicted, since the estimate's error scales with it.

Removed end to end: FanSpeedTrackingMode, TrackingMode on the calibration
snapshot and its persisted key, the worker's speed path, and the
explainer's cascade/fallback verdict. Proto field 12 is reserved and
setpoint_rpm becomes expected_rpm. Calibration no longer probes speed
tracking, so a run is one settle shorter.

Re-run calibration on each fan to capture the full-speed reading the new
display is anchored to. The proto changed, so the service and the app
must be rebuilt together.

Tests: worker asserts Adaptive writes a duty and never a speed;
calibration asserts it issues no speed command even when the simulated
fan would honour one; EstimateRpm covered at both anchors, mid-range
(explicitly not the duty-fraction answer), below stall, uncalibrated, and
for monotonicity. 717 pass.
1 — EC diagnostics	Warnings & Issues: new "Embedded controller" card. Throttle itself is behaviour, surfacing through the existing "Throttle escalation" legend row
2 — Sensors	Sensor chips + Thermal page take firmware names (via the existing channel name, no new markup); warning band on the thermal chart; Adaptive target slider bounded with a caption naming the bound
3 — USB-C	Negotiated contract row on each port card, with a shortfall line when the slot could take more
4 — Battery	"Pack health" expander on Power Telemetry: per-cell bars, imbalance verdict, age beside cycles, what the pack is asking for, its own state of health
5 — Firmware	"Component firmware" card in Device Capabilities

Modules slot cards now carry a Firmware tile — matched by vendor + product id, not slot index. That was my original objection: peripheral descriptors number slots by USB enumeration order, which has no relation to the physical positions that page draws, so an index join would have put a confident wrong version under a picture of a slot. Vendor+product identifies the module type, which is sound.
Drive detail falls back to the drive's own NVMe firmware when the OS reports none — an exact join on device path. WMI's value still wins when present, since that's what every other tool shows.
…s promptly

Stopping the service took its entire 90-second shutdown budget whenever
the app was connected. That is what put "Installer is no longer
responding" in front of an uninstall and made it need retrying until the
budget ran out — six retries is roughly ninety seconds of MSI patience.

All fourteen Watch... calls stream forever by design, and every one waited
only on context.CancellationToken. ASP.NET Core fires that token when the
client disconnects, or when Kestrel ABORTS the request — and it only
aborts once graceful shutdown has already timed out. So on stop the host
sat waiting the full budget for streams that were never going to end on
their own, then killed them. The 90 seconds was chosen as headroom for
restoring fans to Auto on a contended EC; it was being spent every time
instead of never.

Each stream now runs on a token linked to IHostApplicationLifetime's
ApplicationStopping as well, via ServerCallContext.LinkToShutdown, and
unwinds the moment shutdown is signalled. The linked source is disposed at
every call site — otherwise each completed call would leave a registration
on the lifetime token for the life of the process.

The OperationCanceledException filters moved to the linked token too. A
shutdown-triggered cancellation does not set the request's own token, so
the previous `when (context.CancellationToken.IsCancellationRequested)`
would have let it through as an unhandled exception.

Covers WatchStatus, WatchServiceConfiguration, HoldFanPreview, the nine
telemetry streams and both hardware-info streams. Stopping, restarting and
upgrading the service benefit as much as uninstalling does.
Three places where raw library values reached the screen unprocessed.

Sensor names. Publishing FirmwareName verbatim put "apu_f75303@4d" and
"gpu_vram_f75303@4d" down the Dashboard's thermal snapshot. Firmware
reports wiring, not labels: that is the APU sensor, on an F75303 chip, at
I2C address 0x4d. The chip and the address are true and useless — they
name the part doing the measuring, not the thing measured — and they ate
the whole label column. FriendlyFirmwareName reduces a name to its
subject: drop everything from '@', drop any segment carrying a digit (no
sensor SUBJECT is spelled with one, so that separates part numbers from
words without a list of chips to maintain), drop a trailing "temp" since
every sensor here measures temperature, then render acronyms upper case.
"ambient_f75303@4d" becomes Ambient, "gpu_temp@40" becomes GPU. A name
that reduces to nothing falls through to the mapped name rather than
rendering blank.

Retimer version. I hand-joined the raw four-byte register payload and got
"17.0.1.0.17.128.4.0.1.0.0.0..." — every byte of a padded buffer in
decimal. The library ships VersionString for exactly this. Using a
formatter it already provides instead of writing my own.

Power-delivery controllers. MainFirmware.ToString() spells out the
bootloader base, the application, and the application's target: "Base:
3.7.0.407, App: 0.0.34 (Notebook)". Four facts in a column with room for
one. Now the application version alone, which is what Framework's own
firmware notes quote.

Their names were the EC's probe order — Right01 / Left23 / Back — which is
worse than terse: that port numbering appears nowhere a user can see it,
and it is off by one against the Power page's own USB-C 1..4 labels. Now
"Right side (USB-C 1 & 2)" and so on, with an unrecognised slot keeping
whatever the firmware called it; a wrong friendly name is worse than a
terse true one. The retimer joins that group rather than getting a heading
of its own, which had put "Retimer" over a single row reading "Retimer".

An unnamed component counts within its group ("USB hub") instead of
echoing "Slot 0" — a USB enumeration index that corresponds to nothing on
the machine.

Pack health reads itself when the section is expanded, and repeat reads
come from pulling it down. Opening the section IS the request, so a button
that repeats what the user just asked for was a second ask for the same
thing. RefreshContainer holds its deferral until the read completes, so
the visualizer spins for the real duration; the content is capped in a
ScrollViewer because RefreshContainer needs something to pull against.
@TekuSP
TekuSP merged commit 7123e86 into master Aug 30, 2026
10 checks passed
@TekuSP
TekuSP deleted the update_framework_dotnet branch August 30, 2026 18:59
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