hwci: auto settings tuner — find the best AM32 settings for a motor on the rig#26
Merged
Conversation
AM32 reads its 192-byte EEprom_t page once at boot, so settings trials need no rebuild: seed a blob from the device's current page, mutate only the tuned bytes (identity bytes at offsets 1/3/4 preserved so the boot version-mismatch rewrite never fires), validate against firmware-accepted ranges (out-of-range values are REFUSED, not clamped - the firmware would silently fall back to defaults and the trial would lie), and program it with the existing one-shot OpenOCD flash+reset flow. The live eeprom_address global (bootloader devinfo can move the page) is resolved via ELF symbol + 4-byte live read and sanity-checked against the known per-target page addresses. elf.union_layout() walks DWARF unions recursively (EEprom_u wraps all named settings in an anonymous first struct member, which struct_layout's named-struct walk cannot see) so EEPROM_FIELDS offsets are cross-checked against the flashed ELF, mirroring PerfReader._check_layout policy. Offline test compiles the real Inc/eeprom.h with a stub main.h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
RigSimulator now optionally decodes the SAME 192-byte EEprom blob the hardware path flashes (SimSettings.from_blob -> hwci.settings decoder) and responds phenomenologically: efficiency has injectable interior optima in timing advance (gaussian) and log2 PWM frequency, auto_advance scores as a fixed equivalent advance, variable_pwm trades a small low-throttle efficiency bonus for extra zero-cross jitter, low startup_power fails start attempts, and low max_ramp raises the throttle-jump threshold of the existing desync machinery (deterministic slew-limit realization of 'reduces desync probability'). settings=None keeps behaviour bit-identical for every existing caller. build_sim_sources() gains optional sim/settings_blob params so one simulator (pack, temperature, RNG state) persists across tuner trials. Also fixes a pre-existing circular import: flightstand/simulator.py imported hwci.sim at module top while hwci.sim imports the flightstand package, so 'import hwci.sim' failed whenever it ran first (the full test suite only passed because alphabetically-earlier test modules imported flightstand first). The sim import is now lazy in __init__. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Coordinate-descent search over AM32 EEPROM settings, maximizing weighted g/W over steady probe points (>= min_power_w only - low-power points are bench noise) subject to hard constraints that DISQUALIFY, never score: demag/bemf timeouts, aborted runs, settings readback mismatch, temp bounds, failed starts, and zero-cross jitter regressed vs the session's default-settings anchor. Ties within the noise floor break toward lower jitter, then lower FET temp, then closest-to-default. Noise handling per hwci/baseline.py reality (9.8% same-firmware spread, in-session pack drift): incumbent anchor runs every anchors_every trials, scores reported raw AND normalized to the linear interpolation between surrounding anchors; finals run winner-vs-default in interleaved ABBA blocks on the full sweep and confirm only on a positive median paired delta with zero constraint failures (plus a startup-reliability check). Sessions checkpoint manifest.json atomically after every trial; --resume replays the deterministic plan, reuses completed trials (trial.json + metrics.json), quarantines partial dirs as *.incomplete, re-programs the incumbent page (a crash can leave arbitrary settings flashed), and restarts any ABBA block that straddled a pack swap. Low pack voltage prompts for a swap (recorded as a pack_event) or exits cleanly under --no-prompt for a later resume. Tune specs are strict YAML (unknown keys/params fail loudly, values validated against firmware-accepted ranges); see hwci/tunes/example.yaml. Deviations from the reviewed design, forced by the host demag detector's semantics (observed in the offline sim): - tune_probe gains a w10/w20 warmup staircase (mirroring efficiency_sweep) because ramping from a stop through the detector's 0.2 'running' threshold reads spool-up commutation intervals as spike events; - the max_ramp constraint-only stage defaults to an inline 'tune_step' profile that snaps from a SPINNING state (demag_step_stress snaps from ~stop and self-flags 3 spike events even with nothing desynced); - constraint-only sweeps try values in listed order and stop at the first pass (list values best-first) rather than scoring all; - sim zc-jitter deviations use stochastic rounding: plain int()+floor-at-1 quantized every configuration to the same 1-tick deviation, hiding variable_pwm's jitter penalty from the accumulated means. TODO: startup_stats/startup_profile delegate to PR #22's startup_reliability metric when it lands on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
hwci tune --spec X.yaml --config rig.yaml --out runs/tune-1
[--sim] [--battery-cells N] [--no-prompt]
hwci tune --resume runs/tune-1
hwci settings read|write|diff --config rig.yaml [--bin file] [--sim]
Mode selection follows the existing rule (--sim or no config = simulator);
per-trial hardware flow mirrors cmd_ci: one-shot settings flash first,
then build_live_sources. Resume reloads the spec/mode/config from the
session dir. A pack-too-low pause under --no-prompt exits with code 3.
'hwci settings' is the standalone primitive: read prints/saves the live
page, diff exits 1 on any byte difference, write flashes + verifies (on
hardware the live Tcl-RPC session is closed around the one-shot flash so
two openocd processes never race on one ST-Link; --sim uses a persistent
in-process mock page).
README gains a tune section: workflow, spec format, noise handling,
session dir/resume, pack-swap behaviour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
When a `hwci tune` session finishes, write a multi-page `tune_report.pdf` alongside the existing `report.md`: - cover page: run metadata, a CONFIRMED / DEFAULT-KEPT verdict banner, paired-delta summary, and the default->best settings diff table - progress page: per-trial raw + drift-normalized objective (with disqualified trials marked) and the finals ABBA paired-delta bars - stage-winner and full (paginated) trial tables Rendered via matplotlib's PdfPages, so it needs nothing beyond the existing optional `plot` extra. Best-effort: if matplotlib is missing the Markdown report is still written and the tune never fails. The CLI now surfaces the PDF path when present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsAdzs6F2KczTZz4uLtYdg
53a9460 to
93213da
Compare
First hardware tune session findings, each with tests: - probe_profile/tune_probe.yaml: ramp into any measured point >10% above the previous throttle. Unramped 0.5->0.7 snaps drew 43-75 A one-sample transients on the 6S bench (harness abort at 40 A, and the tripping sample is raised before it reaches samples.csv, so the recorded peaks looked innocently low). 10% steps stay unramped - efficiency_sweep's staircase is bench-proven. - step_profile: snap-transient current headroom (max(spec, 55 A), the demag_step_stress.yaml precedent). Its deliberate 0.3->0.95 snaps peak ~50 A and would abort at the probe's 40 A limit before demag was even assessed. - Tuner.run: flash best_settings back to the device at session end. The last finals trial leaves the unconfirmed winner active while the report says "default kept". - SimTuneBackend: pack gate only fires with an injected voltage_fn - a hardware spec's battery_cells must not block a --sim dry-run (sim nominal battery is not a real cell count; mirrors run/ci). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep stages can hill-climb the sorted value list from the incumbent instead of testing the whole grid: walk in the first improving direction, stop at the first non-improvement. Valid for unimodal responses (efficiency vs advance or pwm frequency) - a 7-value grid typically costs 3-4 trials and never visits the far side. Disqualified values read as worse than any qualified one, so the climb walks away from constraint violations instead of stopping on them. refine_step still applies around the resulting argmax. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The session baseline anchors everything downstream: the zc-jitter regression gate's reference, drift normalization, and the finals' default legs. On the bench, a session that soldiered past an aborted baseline burned all 19 trials and ended unconfirmable - and silently took its jitter reference from the disqualified run's partial data (an earlier abort would have left the gate OFF all session). Now: retry the baseline once; if it disqualifies again, quarantine the baseline ledger entries and pause (TunePaused) so a resume re-runs them fresh after the fix. The jitter reference is only taken from a qualified baseline, with an explicit warning when no zc data exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace sweep-the-list ramp tuning with a measurement: one trial runs a 200 Hz step profile (two 0.20->0.55 snaps from a spinning state) with max_ramp at the field max, so firmware duty slew is not the limiter and the eRPM rise time is the powertrain's own. From it: mech time constant tau (63.2% crossing), slew, and k = transient current per % of commanded duty step. During a firmware-limited ramp at rate r the duty leads the mechanical state by ~r*tau, and transient current ~ k*lead, so the fastest ramp whose worst-case transient stays inside the current budget is r = budget/(k*tau); a spec margin (default 0.8) keeps a fraction. The computed value is VERIFIED on the step-stress profile with 0.7x back-off on failure; measured constants land in the manifest/report. Bench-validated (ARK 4IN1 + JS 2306 + HQ5136, 6S): tau 26 ms, k 0.70 A/% -> max_ramp 15 (1.5%/ms), verify clean on first try - next to the hand-tuned 20 on the device page and ~10x below the firmware default of 160. The measurement profile tops out at 0.55: this bench desyncs arriving at fresh-pack t70 (~172-176k eRPM), independent of this stage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rendered from a real bench session, the trials table clipped off BOTH page edges (auto_set_column_width lets a wide table overflow: the index/stage/kind columns vanished left, abort reasons ran off right). - Trials: landscape page, explicit column-width fractions, wrapped overrides/disqualification text, per-row heights from wrapped line counts, pagination by a line budget instead of a fixed row count. - Progress chart: labeled alternating stage bands, anchors as open circles (pack drift readable at a glance), disqualified markers in their own band below the data, integer trial/ABBA ticks. - Cover: stage-winners table folded in (its old page was ~empty), winner overrides in compact k=v form, proportional table widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tunes/quick.yaml (new): ~10 min coarse pass, bench-validated at 10.4 min wall / 19 trials (runs/tune-quick-3, confirmed variable_pwm 0->1, median ABBA delta +0.042 g/W). Coarse 3-point advance climb, 2-point pwm, one variable_pwm A/B, mech-ramp measure stage, short finals on the probe profile. Single-shot scoring - indicative, not confirmatory. tunes/example.yaml: battery_cells 4->6 (this is a 6S bench - the 4S resting gate was uselessly low), t60 instead of t70 probe points (fresh-pack t70 arrives at ~172-176k eRPM where default advance desynced 8/8; t60 holds and t90 arrivals are clean), 4 s dwells (truncation analysis: 3 s tail moves gated points < +/-2%), climb on both sweeps, anchors every 6, one finals ABBA block, and the ramp stage switched to measure: ramp_rate. ~20 min instead of ~45-60. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hwci tune: put a motor/prop on the rig, point it at a tune spec, and it finds the best AM32 eeprom settings for that motor —advance_level,pwm_frequency,variable_pwm,auto_advance,max_ramp, extensible by explicit byte offset (e.g. the demag level once #24 lands). Objective: weighted g/W across the throttle range, subject to hard constraints (zero demag/bemf-timeouts, jitter not regressed vs the session's default anchor, FET/motor temps bounded, startup reliability preserved).Settings write path — no rebuild per trial
AM32 reads its 192-byte settings page once at boot, so a trial is: write the eeprom flash page over SWD → reset → run. This reuses the existing one-shot OpenOCD
program verify resetflow (~4 s/trial). Details that matter:eeprom_addressglobal (ELF symbol + SWD read, sanity-checked against the known AM32 addresses) — the bootloader devinfo can relocate it, so it's never hard-coded.EEprom_uin the ELF at tune start (same pattern as the perf-struct layout check), and every write is read back and verified before a trial scores.pwm_frequency), which would make a "trial" a lie.hwci settings read|write|diffexposes the primitive standalone (e.g. apply a winner to the other 3 MCUs of the 4-in-1).Search strategy — designed for a noisy bench
The rig's own history says same-firmware efficiency spread reaches ~9.8% at ≥20 W and pack state drifts within a session, so:
efficiency_sweeponly for finals.--no-promptfor unattended exit/resume); ABBA blocks never straddle a swap. Optional FET-temp cool-down gate between trials.--resumereplays the ledger exactly (complete trials reused, partial ones quarantined, incumbent settings re-programmed to the device).Budget: ~30–40 trials ≈ 35–45 min motor time and 1–2 pack swaps for the example spec.
Verified
hwci tune --sim --spec tunes/example.yaml): 40 trials, staged sweep converges on the injected optimum (advance 26 / 24 kHz), and the ABBA finals correctly refuse to confirm a within-noise delta — "default kept". The honest-negative path works.hwci.sim↔flightstand/simulator.py).Not verified (needs the rig)
hwci settings read/write/diffround-trips).startup_statsonce that merges.🤖 Generated with Claude Code
https://claude.ai/code/session_011pvp2XLcgsviGSVn9PaaP6
Generated by Claude Code