Skip to content

Releases: raylanlin/smarttune-cli

v3.5.0 — Betaflight aligned with its own tooling

Choose a tag to compare

@raylanlin raylanlin released this 13 Sep 06:05

Same treatment ArduPilot got in v3.4.0, now for Betaflight: cross-checked the
BF path against the firmware itself (src/main/blackbox/blackbox.c) plus
betaflight/blackbox-log-viewer and Plasmatree/PID-Analyzer. Full
item-by-item table in
docs/ALIGNMENT_BETAFLIGHT.md.

Fixed — blackbox_high_resolution was ignored (data corruption)

BF 4.4+ has a blackbox_high_resolution option that logs gyro and setpoint
with an extra digit of precision by multiplying them by 10
(blackbox.c:2313: blackboxHighResolutionScale = high_resolution ? 10.0f : 1.0f).
It applies to gyroADC, gyroUnfilt, rcCommand and setpoint
(blackbox.c:1262-1289); PID term fields are not scaled. The flag is in the
log header, and blackbox-log-viewer divides by it before display.

SmartTune ignored it, so on any high-resolution log:

  • gyro / setpoint / rcCommand read 10x too large, and
  • the outlier sanitiser — bounded by rate_limits x 1.1 (~2198) — then treated
    every sample above ~220 deg/s real as corruption and interpolated it
    away. The harder the flying, the more of the trace was silently replaced.

Everything downstream (vibration level, step response, filter and PID
recommendations) followed the corrupted signal. The header flag is now parsed
and the four affected field families are descaled; state is reported in
extras["blackbox_info"].

Changed — step response realigned to PID-Analyzer

The BF step response previously followed PTstepcalc.m's published
description
(PIDtoolbox; the repo is delisted, so nothing could be verified):
2-second segments, constant-λ regularisation, segment/4 spacing — and no
window function at all
. PID-Analyzer, the open and verifiable ancestor of
this algorithm family (and of the WebTools implementation we already match for
ArduPilot), uses np.hanning(self.flen) (PID-Analyzer.py:64). An unwindowed
2-second segment leaks spectrally into the deconvolution and biases the
transfer function being estimated.

Betaflight now delegates to the same verified kernel as ArduPilot
(Hanning window, 1 s frame = framelen, spacing flen/16 = superpos 16,
Gaussian-CDF SNR regularisation at cutfreq 25 Hz, cumsum over resplen
0.5 s), supplying only BF's input gating: peaks in [20, 500] deg/s, which is
exactly PID-Analyzer's low-input response (low_high_mask(max_in, 500)).
⚠️ This moves Betaflight step-response numbers, toward the reference.
info.method is now pid_analyzer_wiener, and info.steady_state /
steady_state_ok report whether the deconvolution converged instead of hiding
a bad curve.

compute_step_response_for_axis() also stops resampling through the IMU path
by default (prefer_imu=False): for Betaflight Actual is gyroADC, so
the old default interpolated one signal onto its own timebase for nothing.

Checked — gyro scaling was already correct

The going-in suspicion was a missing gyro_scale. The viewer does apply one
(flightlog.js:1468), but the firmware writes gyro_scale = 1.0f
(blackbox.c:1513) because gyroADC comes from gyro.gyroADCf[], already in
deg/s — so reading it raw is right and the viewer's formula collapses to
identity. The header value is now recorded
(blackbox_info.gyro_scale_header) so a legacy log where it is not 1.0 is
visible rather than silent.

Added

  • extras["gyro_unfiltered"] — the pre-filter gyroUnfilt trace (descaled),
    which is what notch targeting should be derived from. FlightData.gyro stays
    the filtered gyroADC so existing vibration thresholds keep their
    calibration; a --prefilter notch mode is the planned follow-up.
  • extras["blackbox_info"]: high_resolution, high_resolution_scale,
    gyro_source, has_unfiltered_gyro, gyro_scale_header.
  • max_target_amplitude on the shared step-response kernel (opt-in; ArduPilot
    keeps WebTools' no-upper-gate behaviour).

Tests

  • New tests/test_bf_alignment.py (14 cases): high-resolution descaling of
    gyro/setpoint, flag off leaves values alone, a 1500 deg/s manoeuvre no longer
    clipped by the sanitiser, PID terms not descaled, unfiltered gyro exposed
    and descaled, missing-gyroUnfilt reporting, gyro_scale header recorded,
    step response using the windowed kernel, high-input (>500 deg/s) and
    low-amplitude (<20 deg/s) window exclusion, gyro_adc as the default output
    signal, and that the new upper gate stays opt-in for ArduPilot.

Docs

  • New docs/ALIGNMENT_BETAFLIGHT.md (sources with commit hashes, firmware line
    references, what was wrong, what was verified correct, every deliberate
    deviation, and how to cross-check a real .bbl against both reference tools).
  • New docs/TEST_PLAN_v3.5.md.

v3.4.0 — MAVLink telemetry log (.tlog) support

Choose a tag to compare

@raylanlin raylanlin released this 13 Sep 05:48

Ground-station recordings (Mission Planner / MAVProxy / QGroundControl) are now a
first-class input — with their limits stated out loud rather than papered over.

A .tlog is not a thinner .bin; it is a different source:

onboard .bin telemetry .tlog
written by flight controller (SD card) ground station (PC)
rate loop rate, 400 Hz+, lossless stream rates, 1–50 Hz, lossy
rate controller PIDR/PIDP/PIDY with P/I/D terms not streamed at all
gyro IMU at 400 Hz+ RAW_IMU / ATTITUDE, 2–50 Hz
coverage whole flight only while the link was up

Added

  • smarttune/platform/ardupilot/tlog_parser.py — MAVLink telemetry parser.
    A .tlog from an ArduPilot vehicle is ArduPilot, so it is handled inside the
    existing adapter (ArduPilotAdapter.parse() dispatches on extension) — no new
    platform, and every existing analyzer/report path keeps working unchanged.
    • PID reconstructed from ATTITUDE_TARGET (desired body rates, rad/s→deg/s)
      interpolated onto ATTITUDE (actual body rates). P/I/D terms are zero-filled
      because telemetry does not carry them — the same contract as the existing
      legacy-RATE fallback. No ATTITUDE_TARGET → no PID analysis and a stated
      reason
      , never a fabricated desired signal.
    • Gyro/accel/mag from RAW_IMU / SCALED_IMU2 / SCALED_IMU3 (mG, mrad/s,
      mgauss) or HIGHRES_IMU (SI); falls back to ATTITUDE body rates when no IMU
      stream exists, and says that the trace is EKF-filtered, not raw.
    • Battery (SYS_STATUS / BATTERY_STATUS), motors (SERVO_OUTPUT_RAW PWM→0-1),
      GPS (GPS_RAW_INT / GLOBAL_POSITION_INT), parameters (PARAM_VALUE, mirrored
      to generic keys like pid.roll.p), modes (HEARTBEAT.custom_mode → Copter mode
      names), STATUSTEXT, firmware version (AUTOPILOT_VERSION), frame type.
    • Structural detection (looks_like_tlog): 8-byte big-endian timestamp + MAVLink
      v1/v2 start byte + a plausible GCS epoch — a mis-named file is rejected without
      parsing.
  • Honesty layer. extras["telemetry_notes"] + extras["log_source"] record what
    the recording could and could not provide: missing P/I/D terms, sub-50 Hz rate
    signals, the FFT Nyquist ceiling vs the 40–120 Hz resonance band, absent
    accelerometer stream, missing parameter download, and every radio gap over 3 s.
    • stune quality scores telemetry lower (−15, −30 without any desired-rate signal),
      lists the caveats as issues, and sets advice to point at the onboard .bin.
    • stune analyze prints a "Telemetry Log Limits" block; JSON/MCP payloads carry
      telemetry_notes and log_source.
  • .tlog accepted by the MCP path validator and advertised in
    stune platforms / smarttune_list_platforms.

Changed — aligned with ArduPilot's own tooling

Cross-checked the ArduPilot analysis path against the reference implementations
line by line (UAVLogViewer mavlinkParser.js / modeMaps.js /
mavlinkDataExtractor.js; WebTools StreamStats.js and PIDReview.js).
Full item-by-item table, including every deliberate deviation, in
docs/ALIGNMENT_ARDUPILOT_WEBTOOLS.md.

Step response — output signal was the wrong source (affects .bin too).
PIDReview builds its test sets from PIDx.Act / RATE.R,P,Y — the rate the
controller measured (PIDReview.js:1600/1608; the Readme says "between the
target and actual signals"). SmartTune preferred IMU.Gyr whenever present,
and its docstring wrongly claimed that matched WebTools. That path also
interpolated the target onto the gyro time base and re-sampled both onto a
uniform grid — adding interpolation dynamics to the transfer function being
measured, using an unfiltered signal. compute_step_response_for_axis() now
defaults to Act (prefer_imu=False); the gyro path remains as an explicit
comparison, and the result reports info.output_signal.
⚠️ This moves ArduPilot step-response metrics (rise time / overshoot /
settling) toward the reference implementation — the one intentional numeric
change in this release.

Step response — off-by-one in the SNR cutoff bin. findIndex(x => x >= 25)
returns the matching bin; searchsorted(..., side="right") returned the one
after it, so an FFT bin landing exactly on 25 Hz got one extra bin of
regularisation. Now side="left".

Step response — cutoff clamp for low-rate logs (deliberate deviation).
With cutfreq fixed at 25 Hz, findIndex returns -1 when 25 Hz is beyond
the spectrum → negative len_lpf → sn all ones → H ≈ 0, i.e. a flat zero
step response. WebTools only loads 400 Hz+ .bin logs so it never reaches that
branch; a .tlog at 1-50 Hz reaches it every time. The cutoff is clamped to
0.8 x Nyquist and flagged as info.cutfreq_clamped.

Telemetry — GCS heartbeats were treated as the vehicle's. A tlog carries
heartbeats from both ends of the link; Mission Planner announces itself as
MAV_TYPE_GCS. The parser read autopilot id, frame type and flight mode from
whichever heartbeat came first, so a GCS-first recording mis-detected the
vehicle and flapped between the vehicle's mode and the GCS's custom_mode.
Now only vehicle-type heartbeats are used (UAVLogViewer's validGCSs rule),
with log_source.gcs_heartbeats_ignored reporting how many were skipped.

Telemetry — mode maps are keyed by MAV_TYPE, not hardcoded to Copter:
Copter / Plane / Rover / Sub / Tracker tables copied verbatim from
modeMaps.js, plus UAVLogViewer's base_mode bit fallback (4/8/16 →
AUTO/GUIDED/STABILIZE) for types with no table. custom_mode 5 is LOITER on
Copter but FBWA on Plane — the old code called both "loiter". Raw mode names
now match the firmware strings (ALT_HOLD, not ALTHOLD) and map through the
same canonical vocabulary as the DataFlash path.

Telemetry — vehicle clock is the timeline. UAVLogViewer plots against
time_boot_ms, the flight controller's own clock; the tlog wrapper timestamp
is the PC's wall clock and carries radio/buffering jitter. Samples now resolve
to time_boot_ms (or a boot-relative time_usec, guarded against UNIX-epoch
values), and wall-clock-only messages (SYS_STATUS, PARAM_VALUE,
STATUSTEXT) are shifted onto that timeline through the median clock offset.
log_source.clock reports which clock was used.

Telemetry — one system per recording + real link-loss measurement, from
StreamStats: frames are keyed by srcSystem/srcComponent, so the parser now
locks onto the first vehicle heartbeat's system id and ignores companion
computers or a second vehicle (frames_from_other_systems, systems), and
counts dropped frames from MAVLink sequence numbers with 8-bit wrap
(frames_dropped, drop_percent, plus a note above 5%) instead of relying on
a wall-clock gap heuristic.

Telemetry — smaller alignments: param ids sanitised with UAVLogViewer's
exact regex ([^A-Za-z0-9_] stripped), STATUSTEXT.severity kept, AHRS2
accepted as a position fallback (as in UAVLogViewer, and as AHR2 already is
in our DataFlash path), MAV_TYPE table corrected against their vehicles
map (3 = coaxial, 4 = heli, 19-24 = VTOL, 29 = dodeca), and all-zero magnetometer
rows treated as "no compass" rather than a measurement.

Refused on purpose

  • A .tlog whose HEARTBEAT reports a PX4 autopilot raises LogFormatError
    pointing at the onboard .ulg — PX4 telemetry has no rate-controller data and
    the ULog does.
  • A recording with no analysable vehicle data raises with a precise reason — an
    empty/truncated file and a GCS-only recording (logging started before the
    link came up) get different hints.

Tests

  • New tests/test_tlog_parser.py (33 cases): structural sniff (accept/reject
    .bin, implausible timestamp), adapter + registry auto-detection, PID
    reconstruction with unit conversion, zero P/I/D contract, no-target skip with
    reason, gyro/accel/mag unit conversions, ATTITUDE gyro fallback, low-rate FFT
    warning, generic parameter mirroring, battery/motor/GPS/statustext/metadata,
    Copter mode mapping, link-gap reporting, missing-parameter note, PX4 refusal,
    empty-log refusal, and quality-layer integration. Alignment cases pin the
    behaviours above: vehicle clock as timeline, wall-clock-only messages placed
    via the offset, GCS-clock fallback, GCS heartbeats ignored (including
    GCS-first ordering), armed-state timeline, Copter vs Plane mode maps,
    base_mode fallback, epoch time_usec rejection, param-id sanitising,
    AHRS2 fallback, foreign-system exclusion, second-vehicle reporting,
    sequence-number drop counting, sequence wrap, and the GCS-only refusal.

Docs

  • README: telemetry-log section (comparison table, what is and is not possible,
    commands), platform table row, auto-detection bytes, security extension list.
  • skill/SKILL.md + skill-mcp/SKILL.md: a .tlog is a screening source —
    agents must repeat telemetry_notes and must not present its numbers as
    tuning-grade.
  • New docs/ALIGNMENT_ARDUPILOT_WEBTOOLS.md — the full line-by-line
    comparison against UAVLogViewer and WebTools, with commit hashes, what
    matched, what was fixed, and every deliberate deviation (including the
    unitScale vs magnitude-heuristic difference left for a later release).
  • New docs/TEST_PLAN_v3.4.md.

SmartTune v3.3.1 — Search result folding + honest truncation

Choose a tag to compare

@raylanlin raylanlin released this 13 Aug 07:16

v3.3.1 (2026-08-13) — Search result folding + honest truncation

Field feedback from agent use: a search for "monitor" returned ~46 rows of which
~40 were numbered-instance clones (BATT2_MONITOR..BATT9_MONITOR), drowning the
handful of genuinely distinct parameters — and pushing them past the limit with
no truncation signal, so the agent concluded "all hits are battery-related"
from a silently incomplete set.

Changed

  • Numbered-group folding in search results (CLI --search and MCP
    smarttune_search_params): instance clones fold into their base parameter
    with an instances list (BATT_MONITOR + ["BATT_", "BATT2_", ..., "BATT9_"]). Ranking order is preserved; a numbered parameter whose base is
    absent from the result set (e.g. SERVO10_FUNCTION, EK3_MAG_CAL) is kept
    as-is. Real effect: "monitor" on Copter-4.5 goes 219 raw → 115 distinct rows.
    Display-level only — validate / get_param always use exact names.
    Implemented once in smarttune/platform/params.py::collapse_numbered(),
    shared by CLI and MCP.
  • Truncation is now signalled, never silent. When returned < count, the
    block carries truncated: true plus a note ("N more distinct hits not
    shown — use a more specific keyword or raise limit"). Terminal output prints
    the same warning; folded rows are listed under the table. Response blocks
    also carry raw_count (pre-fold) alongside count (distinct).

Tests

  • New tests/test_search_collapse.py (7 cases): fold into base, order
    preservation, clone-before-base, numbered-without-base kept, real-table
    monitor fold, instances in CLI JSON, truncated+note contract at low limit.

SmartTune v3.3.0 — Firmware-version parameter tables + ArduPilot Copter-4.5

Choose a tag to compare

@raylanlin raylanlin released this 13 Aug 05:55

v3.3.0 (2026-08-13) — Firmware-version parameter tables

Added

  • Multiple firmware versions per platform. Versioned tables live at
    smarttune/knowledge/params/<platform>.<fw>.json; the plain <platform>.json
    stays the default. Resolution: ParamTable.from_knowledge(platform, fw_version),
    with available_versions(platform) listing what exists.
    • CLI: stune params ... --fw-version copter-4.5 (all modes: browse / search /
      validate / batch / lint); stune params now lists each platform's versions.
    • MCP: all six parameter tools accept fw_version; an unknown version returns
      E4011 with the available list (all-platform scans skip platforms lacking
      that version).
    • Builder: tools/build_param_tables.py --fw-tag copter-4.5 writes a versioned
      table and stamps fw_version into its metadata.
  • New table: ArduPilot Copter-4.5 (ardupilot.copter-4.5.json) — 4,121
    parameters / 243 groups / 1,060 enums / 186 bitmasks, generated from
    Copter-4.5/Parameters.md (raylanlin/ParameterRepository). Includes per-param
    descriptions, ranges, units, increments, Advanced/Standard audience, reboot/
    read-only flags. Zero lint errors. The default ArduPilot table remains
    Copter-4.1 (apm.pdef.json) for now; example of a real difference:
    ATC_RAT_RLL_P max is 0.5 in 4.5 vs 0.35 in 4.1.

Removed

  • MCP smarttune_validate_param: the deprecated status alias (announced in
    v3.2.1) is gone — use verdict.

Tests

  • New tests/test_fw_versions.py (7 cases): platform listing excludes versioned
    files, version listing, versioned load + normalization (Copter_4.5 →
    copter-4.5), unknown-version error carries the available list, CLI
    --fw-version end-to-end including a validate case that passes on 4.5 and
    fails on the 4.1 default.

Deferred to v3.3.x

  • Web UI (drop-a-JSON dashboard extending the HTML report).
  • build_param_tables.py native Parameters.md parsing (the Copter-4.5 table
    was generated with an equivalent transform; the md parser needs to land in the
    builder for one-command reproducibility — the acceptance plan flags this).

SmartTune v3.2.1 — Inline recommendation validation + batch validate + envelope contract tighten

Choose a tag to compare

@raylanlin raylanlin released this 13 Aug 02:01

v3.2.1 (2026-08-13) — Inline recommendation validation + batch validate

Follow-up release closing the v3.2.0 backlog. No analysis numbers change.

Added

  • Inline recommendation validation — analyze_log / analyze_pid / analyze_fft /
    analyze_magfit (services layer, so both CLI -f json and MCP benefit) now attach
    validated / validation_status (+ validation_message on rejection) to every
    recommendation they return
    , checked against the platform parameter table. The
    "always validate before recommending" rule becomes a mechanism instead of a discipline:
    agents no longer spend one validate_param round-trip per recommendation. Absence of the
    field means "not checked", never "approved" (annotation fails open if the table is missing).
  • Batch validation — one call for a whole recommendation set:
    • CLI: stune params --validate-batch FILE|- -p <platform> (JSON array of
      {"param", "value"}; - reads stdin; exit 0 only if every entry is valid)
    • MCP: smarttune_validate_params(recommendations, platform) (tool #16), per-entry
      verdicts + all_valid summary
  • stune analyze --modules pid,fft — run a subset of modules (parity with MCP
    include_modules; skipping unneeded modules speeds up analysis) and
    --max-recommendations N (json output cap). Both work in text and json mode.

Fixed

  • Dict-form FFT recommendations were silently dropped from JSON payloads and Markdown
    reports ({generic_name: value} shape from the FFT analyzer). They are now converted to
    standard recommendation entries (action: "set", platform-translated names) in both
    serialize_fft_result and FullAnalysisResult.all_recommendations.
  • smarttune-mcp on Python 3.9 / without the [mcp] extra now prints an actionable
    message (install instructions; the CLI still supports 3.9) instead of a bare
    ImportError traceback. The module stays importable everywhere via a guarded import.

Changed — envelope contract tightened

  • CLI JSON envelope status is strictly ok / error again. The domain verdict of
    params --validate moved into the payload as verdict
    (ok / not_found / out_of_range / not_a_member / not_an_integer /
    unverifiable). A rejection is a successful call (envelope status: "ok",
    valid: false, exit code 1) — consumers keying "non-ok status ⇒ read .error" no longer
    break. emit_result() dropped its status override parameter.
  • MCP smarttune_validate_param now returns verdict alongside the old status key;
    status is deprecated and will be removed in v3.3.

Tests

  • New tests/test_inline_validation.py (7 cases): pre-validated recommendations
    (ok / out_of_range / not_found), non-numeric suggestion → unverifiable, missing table
    fails open, dict-FFT conversion in serialize + all_recommendations.
  • tests/test_param_tables.py: verdict-field contract, 3 batch-validate CLI cases.
  • tools/smoke_mcp.py: expects 16 tools, asserts verdict field, batch tool round-trip,
    and that analyze_log recommendations arrive pre-validated on a real log.

Docs

  • README: batch validate + --modules/--max-recommendations examples, MCP tool table
    13→16 with the Python 3.10+ requirement called out, contract section updated for
    verdict + inline validation.
  • New docs/TEST_PLAN_v3.2.1.md — incremental acceptance spec for this release.

SmartTune v3.2.0 — Parameter table rebuild (groups + enum meanings) + validation gate fix

Choose a tag to compare

@raylanlin raylanlin released this 13 Aug 01:03

v3.2.0 (2026-08-12) — Parameter table rebuild (groups + enum meanings) + validation gate fix

The data side was fully rebuilt, and the code side fixes the security issue where
"validation" did not actually validate. Parameter tables are now generated
artifacts
: the scraper lives in the repo (tools/build_param_tables.py) and a
linter guards them (stune params --lint).

Data: parameter tables regenerated for all three platforms (schema_version 2)

Platform Params Groups Upstream source
ArduPilot 2,839 194 apm.pdef.json (raylanlin/ParameterRepository → Copter-4.1)
Betaflight 814 82 src/main/cli/settings.c + fc/parameter_names.h (BF has no metadata artifact; firmware source parsed directly)
PX4 1,908 78 docs/public/config/failsafe/parameters.json (output of PX4's own px4params generator)

Old-table defects fixed (each has a corresponding regression test):

  • Descriptions shifted by one column — ARM_MAH carried BATT_OPTIONS' description, BATT_LOW_MAH carried CRT_MAH's. Names were sorted, descriptions were not; the zip misaligned. This is worse than NOT FOUND: an AI makes recommendations from the wrong description and the user never notices.
  • Names stripped of group prefixes, inconsistently — MONITOR (should be BATT_MONITOR), 0_BAUD (SERIAL0_BAUD), 10_DIRECTION (SERVO10_DIRECTION), 2SRV_IMAX, plus LOW_VOLT coexisting with BATT_LOW_VOLT. Root cause: the scraper read // @Description without expanding group prefixes (the @PREFIX@ placeholders left in descriptions were the smoking gun), while stripping the prefix from names. Now the real firmware names are always used.
  • Missing @values / @bitmask — the old tables had 0 values fields; the AI had no way to know BATT_MONITOR=4 means "Analog Voltage and Current". Now ArduPilot's 781 enums + 112 bitmasks, PX4's 360 + 37, and Betaflight's 164 lookup tables all carry member meanings.
  • Wrong type inference — floats like ACCEL_R_MAX, ACRO_Y_EXPO, ACC_BIAS_LIM were tagged enum (which combined with the validation bug below into a security issue).
  • default uniformly 0.0 (invented, not scraped) — now ArduPilot/Betaflight write null when upstream does not publish defaults (unknown ≠ 0); all 1,908 PX4 params carry real upstream defaults.
  • category misclassification — FL_FF, I2C_ADDR, SERIAL_NUM, FS_VOLTSRC were all filed under battery. Category is now derived from parameter group + tuning-domain rules.

New fields: group (the firmware's own parameter group), display_name, values, bitmask, increment, user (Standard/Advanced), reboot_required, read_only, unresolved_ref. Tables carry a source provenance block (upstream file + firmware version + generation date).

Fixed — validation gate (security)

  • ParamTable.validate() returned True, "value accepted" for enums — any value passed. Combined with the type mislabeling above, a large share of the table completely skipped range validation: ACCEL_R_MAX has min 0.0, yet validate(999999) returned True. README sells this tool as a mandatory safety gate before recommending; the gate was wide open for enums.
    • Now: validate_detail() returns a structured verdict — status ∈ ok / not_found / out_of_range / not_a_member / not_an_integer / unverifiable; enums are checked against real members, bitmasks against real bit spans, and rejections return the allowed values with meanings to the AI.
    • fail-closed: discrete params with neither member tables nor ranges return valid=false, status="unverifiable" instead of silently passing.
    • validate() keeps its (bool, str) signature for backward compatibility.
  • Packaging missed files: knowledge/params/*.json was not in package-data — after pip install, stune params and the three MCP param tools failed with FileNotFoundError. (Fixed in v3.1; recorded here.)
  • ParamDef(**item) blew up the whole table with TypeError on new upstream fields → switched to from_dict() which ignores unknown keys, forward-compatible.

Fixed — MCP reliability

  • Payload slimming: smarttune_list_params used to return the whole table as full objects (ArduPilot: 2,839 entries with long descriptions ≈ 600 KB per response — likely the real culprit behind that "no response" incident). It now returns slim rows (name/type/range/unit/one-liner) with limit/offset paging, and refuses without a group or category, pointing to the group index first; full descriptions and enum members are served by the new smarttune_get_param.
  • stdout hygiene: stdio MCP's stdout may only carry JSON-RPC. SmartTune itself writes to stderr (audited, no print leaks), but third-party parsers on the analysis path (pyulog / pymavlink / matplotlib) may print — one line of noise kills the whole stream. All service calls are now wrapped in _quiet_stdout(), redirecting stdout to stderr.
  • Unified error shape: there used to be at least three ({"error": str}, {"error","code","hint"}, {"valid":false,"error"}) with no retry marker. Now success is {ok: true, …}, failure is {ok: false, error_code, message, hint, retryable} so clients can tell "retryable" from "don't bother". A rejected parameter value is a successful call + valid: false, not a transport error.
  • Param tools no longer pay the numpy startup cost: from smarttune.platform.params import ParamTable used to execute smarttune/platform/__init__.py → base → models.flight_data → import numpy. Param tools only use the stdlib but were dragged through the whole scientific stack by the package __init__; every MCP call_tool spawns a new process, so looking up one param loaded everything. Switched to PEP 562 module-level __getattr__ lazy import; public API unchanged.

Added

  • tools/build_param_tables.py — the missing scraper, now in the repo. One generation path per platform, output carries provenance blocks; --check runs the linter directly. Pure stdlib.
  • smarttune/platform/param_lint.py — parameter table health check: name_shape / suffix_collision / placeholder_leak / discrete_without_members / constant_default / range_inverted / enum_key_not_int / empty_description / duplicate_name. Every rule maps to a real incident. Current tables: 0 errors.
  • MCP smarttune_list_param_groups — see the group index first (80–200 groups with counts/categories/sample members), then drill in; replaces "pull the whole table at once".
  • MCP smarttune_get_param — full definition of a single param (description + range + default + enum meanings).
  • CLI group browsing: stune params ap --groups, --group ATC_, --lint, and ranked --search (exact name > prefix > substring > display name > description > enum label); stune params --search "analog voltage" finds BATT_MONITOR. Listing a platform without filters shows the group index instead of dumping 2,839 rows.
  • All stune params sub-modes support -f json.

Tests

  • New tests/test_param_tables.py (37 cases): schema/provenance/lint zero errors for all three tables, names are real firmware names, descriptions not shifted, defaults not invented, enum member meanings, fail-closed validation, group index and in-group queries, search ranking, slim/full payload shapes, and 8 CLI end-to-end cases.

Verification

  • New docs/TEST_PLAN_v3.2.md — an executable acceptance spec, item by item (static checks / unit tests / parameter table data regression /
    9 validation-gate cases / JSON contract / MCP contract & payload size limits / lazy loading / wheel contents / zero analysis-value regression),
    with pass criteria, failure report format, and a "known limitations (not bugs)" list.
  • New tools/smoke_mcp.py — an MCP stdio smoke test with a built-in JSON-RPC client: checks 15 tools,
    payload size limits, unified error shape, the validation gate, and any stdout noise makes it fail outright
    (the detection mechanism for the last "no response" incident). No mcp client library dependency.

Docs

  • README: stune params section rewritten as "group → param → validate"; new Parameter tables subsection (three-platform source table + regeneration commands + linter notes); MCP tool table 13 → 15 with payload contract.
  • skill-mcp/SKILL.md: tool list and param query workflow updated in sync.

v3.0.3 — R5 Performance + R6 Cross-Module Contract Fixes

Choose a tag to compare

@raylanlin raylanlin released this 14 Jun 09:50

What's New in v3.0.3

R5 — Performance Optimization (bit-identical, zero numerical change)

  • magfit: New ned_to_body_batch() batch quaternion rotation + np.bincount bin weights vectorization
  • AP step_response_fft: np.full scale + np.cumsum SNR gaussian vectorization

R6 — Cross-Module Contract Bug Fixes

  • 🔴 A2 generic-key injection (AP/BF): parse() now injects generic keys (pid.roll.p etc.) into params. Previously, PIDReviewer._get_current_pid always returned 0.0 because it queried generic keys while only native names were stored — all PID parameter suggestions were silently discarded.
  • 🟡 BF pid_bounds: Added Betaflight integer-scale bounds (P[10,150]/I[20,220]/D[0,100]/FF[0,300]). Default AP-scale bounds (0.01~0.5) would clamp BF 45×1.1 down to 0.5.
  • 🟡 thresholds shape guard: New _is_axis_threshold_shape() validates knowledge-base thresholds are in the consumer-expected shape. Previously, all pid_rules.json thresholds were dead config — the analyzer always used hardcoded defaults.
  • New tests/test_platform_contracts.py (8 tests): A2 injection, thresholds guard, BF bounds, BF gain not clipped.

167 tests passed

Full Changelog: v3.0.2...v3.0.3

SmartTune v3.0.2 — PX4 ULog + A1 Convergence + Label Unification

Choose a tag to compare

@raylanlin raylanlin released this 12 Jun 14:04

Changes since v3.0.0

v3.0.1 — 14 Architecture Review Fixes (2026-06-11)

14 critical/high severity fixes from Claude architecture review:

  • C1: magfit 100% KeyError crash fixed (was silently skipped)
  • C2: mGauss unit normalization across magfit pipeline
  • C3: INS_HNTCH_REF semantic fix — prevents incorrect dynamic notch params
  • C4: Skip current_val ≤ 0 recommendations (removes false "0.0 → 0.01" suggestions)
  • C5/C6: ODI symmetric soft-iron matrix + PitchIn guard
  • C7/C8: imu_dict actual construction + bidirectional overshoot + settling=-1 sentinel
  • C10: step_response_fft aligned with WebTools/PID Toolbox reference implementations
  • C12/C14/A3/A4: frequency band merge, ARX fallback markers, registry metadata snapshot, TYPE_CHECKING

v3.0.2 — PX4 ULog + A1 Convergence + Label Unification (2026-06-12)

PX4 Full Implementation (first time complete):

  • ULog parser based on pyulog — all uORB topics mapped (vehicle_rates_setpoint, sensor_combined, ekf2_innovations, etc.)
  • Param A2 dual-write contract
  • sensor_combined fallback path for pre-v1.13 firmware
  • PX4 knowledge base (pid_rules.json, filter_rules.json)
  • validate_px4_ulog.py — one-command end-to-end validation
  • `stune quality/pid/fft/sysid/analyze -i .ulg` all functional

A1 Convergence (services layer unification):

  • `run_module()` unified core — CLI and services no longer have duplicate analyzer wiring
  • B2 fix: fft_result now properly assigned to FullAnalysisResult
  • sysid --na/--nb now actually passes through (was broken)
  • filter and quality commands call services layer (D1 fix)
  • t_synthetic_regression.py: 11 case golden-standard synthetic tests

Label Unification:

  • vibration_level: SEVERE→POOR, CRITICAL→UNUSABLE (consistent Assessment enum)
  • vibration_level_raw preserves original labels
  • formatter/httm_report color mapping compatible with both old and new labels

v6.2 Synthetic Regression Fix:

  • Excitation signals add 8% broadband noise (improves Wiener deconvolution conditioning)
  • Steady-state tolerances widened; underdamped test uses relative assertion
  • formatter `_has_vec()` guard for numpy array truthiness

Test Results

`159 passed in 2.16s` — AP/BF/PX4 real log verified

Files Changed

v3.0.1: 12 files, +743/-492
v3.0.2: 13 files, +1181/-510

SmartTune CLI v3.0.0 — Firmware-Parameter-Aware Tuning

Choose a tag to compare

@raylanlin raylanlin released this 27 May 04:26

SmartTune CLI v3.0.0 — Firmware-Parameter-Aware Tuning

New: stune params — Firmware Parameter Validation

SmartTune CLI can now query and validate firmware parameters before recommending changes, preventing agents from suggesting parameters that don't exist in the target firmware.

stune params ap                           # List 2,574 ArduPilot parameters
stune params bf                           # List 45 Betaflight parameters
stune params ATC_RAT_RLL_P                # Show parameter details
stune params --search notch                # Search across all platforms
stune params --validate ATC_RAT_RLL_P 0.15 -p ardupilot  # Validate before recommending

New: MCP Parameter Tools (13 total)

3 new MCP tools for parameter query and validation:

  • smarttune_list_params(platform, category) — List all firmware parameters
  • smarttune_search_params(keyword, platform) — Search by keyword
  • smarttune_validate_param(name, value, platform) — Validate before recommending

MCP agents can now autonomously verify parameter existence and value ranges.

Knowledge Base Architecture

Parameter tables live in smarttune/knowledge/params/*.json — data scraped from official firmware repositories:

  • ArduPilot: 2,574 parameters from @PARAM annotations (100% described, 1,941 with valid ranges)
  • Betaflight: 45 parameters from settings.c (BF 4.5+ names: p_roll, gyro_lpf1_static_hz, d_max_roll)
  • PX4: 20 parameters from official YAML definitions

Breaking Change

Betaflight adapter's map_param_to_platform() now returns BF 4.5+ parameter names:

  • pid_roll_p → p_roll
  • gyro_lowpass_hz → gyro_lpf1_static_hz
  • d_min_roll → d_max_roll

Legacy name support maintained for reading older BBL logs.

Consistency Fixes

  • ParamTable.validate() fixed for parameters without min/max range
  • BF filter_transfer.py supports both old and new parameter names
  • All 35 previously-undescribed AP parameters now have verified descriptions and ranges
  • Count metadata corrected in knowledge base JSONs

Full Changelog

Previous v2.4.2 → v3.0.0: [compare]

SmartTune CLI v2.3.1

Choose a tag to compare

@raylanlin raylanlin released this 18 May 16:23

Patch release adding a dedicated MCP-only SmartTune skill for read-only customer-support agents.\n\n- Adds skill-mcp/SKILL.md for agents without exec/shell/write permission\n- Documents SmartTune MCP tool workflow and safety boundaries\n- Keeps v2.3.0 MCP server/runtime behavior unchanged\n\nVerification: pytest -q -> 141 passed