Skip to content

Add production battery charge limit Device control - #513

Merged
onehoon merged 2 commits into
mainfrom
refactor/battery-pr2-production-device-ui
Sep 12, 2026
Merged

onehoon merged 2 commits into
mainfrom
refactor/battery-pr2-production-device-ui

Conversation

@onehoon

@onehoon onehoon commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the production Device-page battery charge-limit Runtime using the existing MSI helper transport and block 215 hardware adapter.
  • Persist additive Device.Battery.ChargeLimit desired state in profiles.json with safe bootstrap, readback-backed reconcile, startup/restart, and delayed resume handling.
  • Add typed frontend capture/mutation RPCs and an inline Device SettingsCard with BatterySaver8 (E86B), ToggleSwitch, and 60-100% 5% slider commits.
  • Keep Developer battery validation, CTW, QAM, Overlay, and FrontendDeviceQuickSettingsSnapshot unchanged.

Validation

  • dotnet test tests/SteamInputAddonforClaw.Tests/SteamInputAddonforClaw.Tests.csproj: 2730 passed.
  • dotnet build tests/SteamInputAddonforClaw.Tests/SteamInputAddonforClaw.Tests.csproj -c Release: passed with 0 warnings and 0 errors.
  • git diff --check: passed.

Manual validation

  • Physical MSI Claw hardware validation is pending: verify block 215 GetData/SetData readback, startup/restart/resume reconciliation, UI Toggle behavior, and one commit per slider release on device.
  • No hardware or CI result is claimed by this PR.

@onehoon onehoon left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking issues remain before merge.

  1. Battery load/modify/save is not atomic under the shared ProfileMutationGate, so a normal concurrent Device/Profile edit can be lost.

SetEnabled() / SetPercent() load profiles.json before taking _mutationGate.Sync, then Commit() later locks only around Save(updated). The bootstrap path similarly loads, releases the gate, reads hardware, and then saves a document derived from the earlier load. This defeats the existing gate's purpose: another CPU/TDP/Power/Profile mutation can save between those two points, after which Battery writes the stale document and silently overwrites that newer change.

Please keep each persisted load-modify-save transaction on one fresh document under the existing shared gate. Do not add another lock/manager. Also avoid holding the gate across WMI I/O; persist under the gate, then apply hardware afterward.

For example:

private BatteryChargeLimitMutationResult SetPercentCore(int percent)
{
    DeviceBatteryChargeLimitSettings desired;

    lock (_mutationGate.Sync)
    {
        var loaded = _profileStore.Load();
        if (!loaded.CanSafelyReplace)
            return Result(BatteryChargeLimitMutationOutcome.PersistenceFailed,
                "Profile state is not safe to replace.", loaded);

        var saved = loaded.Document.Device.Battery?.ChargeLimit;
        var enabled = IsValidTarget(saved) ? saved!.Enabled : ReadEnabledForInitialization();
        if (enabled is null)
            return Result(BatteryChargeLimitMutationOutcome.Unavailable, "BatteryLimit read failed.", loaded);

        desired = new DeviceBatteryChargeLimitSettings
        {
            Enabled = enabled.Value,
            LimitPercent = percent
        };

        var updated = WithBatteryChargeLimit(loaded.Document, desired);
        try
        {
            _profileStore.Save(updated);
        }
        catch (Exception ex)
        {
            return Result(BatteryChargeLimitMutationOutcome.PersistenceFailed, ex.Message, loaded);
        }
    }

    // WMI/readback stays outside ProfileMutationGate.
    return Apply(desired);
}

For first-run bootstrap, after the hardware read succeeds, reacquire _mutationGate.Sync, reload the current profile, verify ChargeLimit is still null, and add Battery to that fresh document before saving. Please add a regression test where another Device/Profile field is changed between the battery's initial observation and battery persistence, and assert that field survives.

  1. The slider commit currently discards the user's draft before sending the RPC, so the requested percentage is not actually applied.

CommitBatteryChargeLimitPercentAsync() sets _batteryChargeLimitMutationBusy = true and immediately calls:

RenderBatteryChargeLimit(_batteryChargeLimitSnapshot, preserveDirtyDraft: false);

That render resets _batteryChargeLimitDraftPercent from the authoritative snapshot. Example: desired=80, user drags to 85, commit begins, render resets the draft back to 80, and the following RPC sends 80. The control also visibly snaps back before the request.

Capture the committed value first and do not re-render authoritative state until the mutation result arrives. A small local enable/disable helper is enough; no new state machine is needed. For example:

private async Task CommitBatteryChargeLimitPercentAsync()
{
    if (_suppressBatteryChargeLimitEvents || !_batteryChargeLimitDraftDirty ||
        _frontend is null || _batteryChargeLimitMutationBusy)
        return;

    var percent = _batteryChargeLimitDraftPercent;
    if (_batteryChargeLimitSnapshot.DesiredLimitPercent == percent)
    {
        _batteryChargeLimitDraftDirty = false;
        return;
    }

    _batteryChargeLimitDraftDirty = false;
    _batteryChargeLimitMutationBusy = true;
    UpdateBatteryChargeLimitControlEnabledState(); // must not rewrite Value/draft

    try
    {
        var result = await _frontend.SetDeviceBatteryChargeLimitPercentAsync(percent);
        RenderBatteryChargeLimit(result.Snapshot, preserveDirtyDraft: false);
        // existing failure presentation...
    }
    finally
    {
        _batteryChargeLimitMutationBusy = false;
        UpdateBatteryChargeLimitControlEnabledState();
    }
}

Please add a focused regression around the draft/commit policy (80 authoritative -> user draft 85 -> exactly one 85 commit after end-of-interaction). The current source-presence UI test does not exercise this behavior.

Everything else I checked is directionally aligned with the work order: existing block-215 hardware adapter/shared helper reuse, production-vs-developer RPC separation, protocol v30, Device-only surface, additive Device.Battery storage, enabled/disabled reconcile ordering, delayed resume integration, and no QAM/Overlay expansion in this PR.

@onehoon

onehoon commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Addressed both blocking review comments in commit 3bdf83653a79b5e1363d53eb7256c9a4aca11b42.

  • Battery profile persistence now performs a fresh load/modify/save under the shared ProfileMutationGate; hardware reads and writes remain outside the gate. Startup bootstrap reloads after observation and preserves a concurrent Device/Profile edit.
  • Slider commit captures the draft percentage before entering the busy state and only updates control enabled state before the RPC, so the authoritative render cannot overwrite the draft. The focused regression covers 80% authoritative -> 85% draft -> exactly one 85% commit.

Validation:

  • Focused battery/runtime and draft-policy tests: 8/8 passed.
  • Full test suite: 2732/2732 passed.
  • Release build: 0 warnings, 0 errors.
  • GitHub Actions build-and-test is running for the new head.
  • Physical MSI Claw hardware validation remains manual/pending.

@onehoon
onehoon marked this pull request as ready for review September 12, 2026 08:14
@onehoon
onehoon merged commit 091aa13 into main Sep 12, 2026
1 check passed
@onehoon

onehoon commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Follow-up: the new-head GitHub Actions �uild-and-test run completed successfully for 3bdf836 (5m47s). PR merge state is CLEAN.

@onehoon
onehoon deleted the refactor/battery-pr2-production-device-ui branch September 13, 2026 09:01
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