Repository navigation
feat: improve drive measurement fidelity for lifted-wheel tests - #172
Merged
Merged
Conversation
The 実機の状態 wheel charts now draw the raw feedback (rpm_raw) as the main solid line, the node's low-pass filtered value as a thin faint helper and the sent command (DriveStatus target_rpm) dashed. The wheel card's big number is the raw value with the filtered one beside it (older bridges unchanged). The strip's sparkline is the chassis speed computed from the raw wheels and wheel_radius, falling back to the node speed. Missing samples are no longer joined: wheel series keep non-finite values and get a break after a pause longer than WHEEL_GAP_SECONDS (0.5 s), and html-chart splits a line at non-finite points (one polyline per run, the label on the last finite point). Gap-free data draws exactly as before. The panel notes that it shows /drive_status thinned to 20 Hz and that the rosbag is for analysis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New questix_msgs/DriveControlSample + DriveControlWheelSample, published by drive_component once per control tick on /drive_control_sample (rate = control_rate, reliable + volatile + keep_last(100), ACTIVE only, on by default; publish_control_sample / control_sample_topic, not runtime changeable). Each sample carries a tick seq (loss detection), the tick's action, shaped chassis command, per-wheel pre-correction target, sent command (and the current raw in current mode), whether this tick received a new feedback frame plus a cumulative frame count (duplicate detection), the round trip, timeout flag, receipt stamp and the raw wire fields. DdtMotorLib keeps a per-motor transaction record (frames parsed, exchanges, timeouts, last round trip, last current raw sent) behind state_mutex_. The tick body moved into runControlTick() so that every tick, early returns included, publishes exactly one sample. /drive_status, MotorFeedback and DriveStatus are unchanged (type hash, recorded bags). Docs: the topic contract in questix_msgs/README.md (diagnostic only), the /drive_status default rate corrected to 50 Hz, the 4096-step encoder behind position_raw, CHANGELOG, the parameter tables, AGENTS.md; record.sh records the topic when present. Tests: the pure message conversion and the library counters over a pseudo terminal that answers with real frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feedback wait after each command was a fixed 10 ms in sendFrameWithFeedback. DdtMotorLib now keeps it as a setting (default 10, setResponseTimeoutMs clamps to [2, 50] ms and logs the applied value) and drive_component exposes serial_response_timeout_ms (default 10, clamped with a WARN when out of range, not runtime changeable). The default keeps the behaviour unchanged; single_ddt_motor and joy_axis_drive keep the library default. The YAML comment gives the lower bound (response frame 1.74 ms at 57600 baud + measured firmware time + margin) and where to read the measured round trip (the latency INFO on deactivate). Tests: the clamp, the library default, the parameter default and its rejection at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ControlCore decides RUN from the larger |wheel target| and used to apply the velocity RUN LQR+FF to both wheels. The sign guard in runWheel() only covered ref > 0 / ref < 0, so a wheel whose target was 0 (turning about it) could be sent up to +/-max_correction_rpm, and a slow inner wheel below the RUN range was corrected with a model that does not hold there. Now each wheel is corrected only when its own target is nonzero and |target| >= the normalized run_exit threshold (wheelInRunRange); any other wheel gets the feedforward (its target as is) and its observer/LQR state is dropped. A wheel whose target changes sign drops its state too, so a forward observer and disturbance estimate never carry into reverse. Output gains left_lqr_active / right_lqr_active; lqr_active is their OR. The feature is off by default (velocity_run_lqr_enabled: false), so the default driving is unchanged. Tests: a zero-target wheel, a wheel below run_exit, two wheels at different speeds, a reversal, lost feedback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… assumptions No default behaviour changes. drive_component now warns at configure when the velocity RUN LQR is effective with control_rate != 50 Hz (its tick-based parameters assume 50 Hz), and in current mode about the pure-P default gains, the unchecked sign of current_invert_measured and the undocumented driver behaviour when commands stop. The LQR warning is also raised when the LQR or the RUN thresholds change at runtime. YAML comments no longer assume a 50 Hz tick, state that the LQR is not an oscillation fix (first-order model), and describe how the last part of a stop is left to the firmware. The motor_control_app README records the review (fixed / documented / why not), the timing of a tick (fixed vs measured dt, overrun, round trips, idle refresh rate, E-stop latency, /drive_status vs the tick), the current-mode evaluation premises with a sign-check procedure and safety notes, and the unconfirmed differences from the M6 datasheet (115200 baud, +/-330 rpm), which the code keeps as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/identify/ripple_analysis.py separates the wheel-speed ripple into a rotation-synchronous part and a fixed-frequency part. It reads a rosbag2 (/drive_control_sample first: seq for losses, feedback_new / feedback_count for duplicates; /drive_status of older bags deduplicated by each wheel's receipt stamp) or a CSV (its own --export-csv, or a QUESTiX LAB -messages.csv). It unwraps position_raw against the speed, extracts the constant-command segments, fits a synchronous average in rotation angle (orders), takes the time spectrum of the raw speed and of the residual, tabulates rpm vs peak (Campbell), analyses the left/right sum and difference, and classifies peaks as proportional to rotation or constant; components below the encoder error (1 % x order by default) are reported as not resolvable. rosbag2_py and matplotlib are imported only when used. Tests use synthetic data (order-1 ripple + 1.8 Hz sine, integer rpm, duplicate and lost samples) for both CSV paths. test_control_core gains a second-order firmware loop model (assumed 1.8 Hz, damping 0.15, dead time, integer rpm) with a once-per-revolution disturbance: the swing peaks at 95-110 rpm, the state feedback of the first-order LQR does not damp it, and its disturbance compensation excites it up to the correction limit (about 10 -> 84 rpm in this model). The YAML and README now warn against enabling the disturbance compensation before the resonance is identified. The identify README and handout describe the lifted-wheel test (record.sh --levels 20,...,150 --hold 30), counting detents by hand, comparing position_raw across a power cycle, the command-rate test and floor runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whitespace and line wrapping only, from .clang-format (checked with clang-format 14, which reports no difference on every other C++ file of the repository). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After #170 a practice launch with ENABLE_GPIO_REF=false still moves: operation_manager runs with operation_manager.no_gpio.yaml and publishes /emergency_stop released, but GPIO5 is not read, so a press of the physical E-stop never reaches ROS (no software stop edge, no diagnostics; only RLY1's power cut remains). The production practice launcher followed launch.env, and Robot Manager's 管理設定 could persist ENABLE_GPIO_REF=false (issue #168). Production policy only; #170's architecture (questix_safety, EmergencyStopMonitor, always-on operation_manager, the no_gpio profile, fail-close, teacher permission) is unchanged. - questix_robot_launcher.sh (systemd and Ansible copy): practice passes enable_gpio_ref:=true / enable_autoreferee:=false like competition passes true / true; ENABLE_GPIO_REF is never read, so a legacy false is ignored. - PUT /api/launch-config refuses ENABLE_GPIO_REF other than "true" (422), migrates a legacy false to true on save, and logs each changed key as "launch-config updated: KEY old -> new". - 管理設定 no longer has the switch; it shows GPIO5 as always on. - enable_gpio_ref:=false stays a questix_core capability for explicit manual diagnostic launches; docs, YAML/launch comments and test comments say so. - Tests: practice launcher contract inverted (false / true / empty / an unexpected value all launch with enable_gpio_ref:=true), launcher static test pins the fixed practice args, new launch-config API/UI tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjS1yNf6dAUWTwi1RTpWeP
…ent default questix_core took enable_gpio_ref's default from $(env ENABLE_GPIO_REF true), so an exported ENABLE_GPIO_REF=false, or a shell that sourced a legacy launch.env, put a plain `ros2 launch questix_core.launch.xml` into the no-GPIO mode without anyone asking for it on the command line (issue #168). - questix_core.launch.xml: enable_gpio_ref defaults to a literal true. Only an explicit enable_gpio_ref:=false on that launch selects no-GPIO; the override lives in that process only and nothing persists it. - Tests: static test pins the literal default and that it never reads ENABLE_GPIO_REF / $(env ...); runtime test exports ENABLE_GPIO_REF=false, leaves the argument out and expects gpio_reader, gpio_safety_enabled=true and the GPIO E-stop (not "released (no GPIO safety path)"). The explicit false runtime cases from #170 stay as they are. - Docs, launch.env comments and AGENTS.md: ENABLE_GPIO_REF is a legacy field nothing reads; no-GPIO needs the launch argument and does not survive process exit or a power cycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjS1yNf6dAUWTwi1RTpWeP
Stack this branch on #169 (which is based on #170 in main). Conflicts with #170's E-stop / teacher-permission refactor are resolved as: - drive_component.cpp: kRequiresReconfigure takes #170's teacher_permission_* names and keeps serial_response_timeout_ms, publish_control_sample and control_sample_topic. - drive_component.hpp: drop emergency_stop_topic_ (now owned by EmergencyStopMonitor), keep the control-sample members. - motor_control_app/README.md: keep both the review section and the build-time section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
The per-wheel range check used run_exit only, so a wheel target hovering around run_exit switched the correction on and off every tick, dropping the wheel's observer/LQR state each time and letting the command jump by up to max_correction_rpm. A wheel now enters at run_enter and stays while |target| >= run_exit (the same hysteresis as the chassis CREEP<->RUN); a sign change re-enters. LQR stays off by default, so the default drive is unchanged. Test: a wheel crossing run_exit back and forth (29/31 rpm) stays out until it reaches run_enter again; fails on the previous rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
The bridge's latest-wins limiter sends a message only when at least 1/drive_max_hz has passed, so drive_component's 50 Hz /drive_status at drive_max_hz 20 reaches the pages every third message (0.06 s, about 17 Hz), not 20 Hz. The same limiter thins the bridge's records. Correct the panel's note, the WHEEL_GAP_SECONDS and recording-core comments, the lab_bridge.yaml comment and the bridge README. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
…istics A 10-byte frame that arrived but was not valid feedback (CRC mismatch, another ID) counted as "no valid response" in the per-motor record behind /drive_control_sample, but as a measured round trip (and not a timeout) in the latency statistics logged at deactivate. Both now count it as no valid response: no round trip recorded, one timeout. Test with a fake motor that answers with a corrupted CRC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
The E-stop callback's immediate stop and configure/activate exchange frames outside the tick, so feedback_count can step by 2 or more between two samples with a contiguous seq. Say so in the message, the topic contract and ripple_analysis (whose "frames_missing" is reported as frames not carried by any sample, separate from lost samples). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
pytest scripts/identify (fit_models, step_sequence, ripple_analysis) ran nowhere in CI: robot_manager's job only collects scripts/robot_manager and the identification tests need NumPy. Install NumPy pinned to the robot's apt version (1.26.4) and run them in the existing python-tests job. They use synthetic data only; rosbag2_py and matplotlib are imported only when used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Yuichiroh-Kobayashi
changed the base branch from
main
to
claude/eloquent-carson-jlyi52
October 4, 2026 09:47
Yuichiroh-Kobayashi
added this pull request to stack #174
October 4, 2026 09:50
The 管理設定 card showed 「安全入力(GPIO5、非常停止ボタン)」 as a read-only item. It is not a setting, has no state of its own and looked like a disabled switch; the header's 非常停止ボタン already shows the live E-stop state. The test now asserts neither a switch nor the item exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
…ent-fidelity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
5 tasks done
#169 was squash-merged as 2deeb87; its tree equals the stacked base, so this merge only moves the merge base for the PR diff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
This was referenced Oct 5, 2026
…_raw docs Hardware results from 2026-10-05 (ID13, lifted, 50 Hz) and an independent review showed several gaps in the measurement tooling and its contract: - ripple_analysis.py: track fixed orders (--track-orders, default 20) by fitting the rotation phase directly instead of picking the largest peak; mark components above the Nyquist frequency as aliased (with the observed frequency) and tag dominant/residual peaks that match a tracked order; say whether "insufficient" means too few segments or too few resolvable peaks; call the residual "orders 1..N removed" instead of non-synchronous; state that the 1 % encoder error is an uncalibrated assumption. - position_raw: the wire value moves in 1 LSB steps (all residues mod 8 occur), not 8. Record the power-cycle result and the unexplained hand-turn behaviour; do not treat it as a calibrated absolute angle. - command_sent: true also when DdtMotorLib skips re-sending a stop frame within stop_resend_interval_ms; it is not proof that a frame went out. - The latency INFO appears only on lifecycle deactivate; point to the bag's roundtrip_ms / response_timeout instead. - identify README: the E-stop does not cut DDT power on ID13; read position only after release from new frames (~0.32 s while stopped). Message comments only; field types and type hashes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
…arams in record.sh For the next lifted/low-speed tests on the Pi: - step_sequence.py: --schedule rpm:sec,... (per-level hold), --lead-in-rpm / --lead-in-sec (reach a slow level from a faster one without passing zero), --pattern straight|spin|pivot-left|pivot-right (pivot = one wheel held at 0, the other at the level), and --min-command-rpm to refuse a schedule the drive_component stop gate would not follow (below min_command_rpm, or a start from rest below min_command_rpm + 2). - record.sh: pass those through with --sign; read wheel_radius / wheel_separation from drive_component; check the stop gate before the prompts; --set-param NAME=VALUE (min_command_rpm, stop_resend_interval_ms only) changes a parameter for the recording and restores and verifies it on exit, including Ctrl-C and failures, recording both in meta.yaml. - record.sh preflight: give the /target_twist echo its type and stop on a CLI failure instead of treating it as "no traffic". - README: stop-resend comparison, low-speed and pivot procedures. Tests: test_step_sequence.py 11 pass; test_evidence.sh 74 pass (adds the stop-gate refusal, --set-param allow list, restore after success and after an interrupted run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
4 tasks
… parameter restore From an independent review of 51dbf6d: - step_sequence.py now subscribes to /emergency_stop (questix_msgs/EmergencyStop, reliable + transient_local) and aborts with exit code 4, publishing zeros, when a press arrives or the topic goes silent for --estop-timeout (1 s). A later release does not resume the remaining steps: drive_component blocks commands while pressed, but a sequence that kept publishing would move the wheels again the moment the E-stop was released. It does not start without a release; --no-estop-watch is only for diagnostics without the topic. - record.sh reads each --set-param value back right after setting it and refuses to record on a mismatch; a failed restore now ends the run with exit code 5 (also from the EXIT trap) and is recorded in meta.yaml, instead of printing an error and exiting 0. Exit code 4 is recorded as step_sequence: "aborted_emergency_stop". Tests: test_step_sequence.py 13 pass (EstopGuard); test_evidence.sh 82 pass (read-back mismatch, restore failure -> 5, E-stop abort -> 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Yuichiroh-Kobayashi
pushed a commit
that referenced
this pull request
Oct 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
…he DDT power notes step_sequence.py refused to start three times on 2026-10-06 although /emergency_stop was flowing: it gave up when nothing arrived within the 2 s --listen-before window, and recorded the refusal as an E-stop abort. - Wait up to --estop-wait (default 10 s) for the first /emergency_stop message, separately from --listen-before; foreign-publisher detection keeps running while waiting. - Log the time to the first message, the message count, the publisher count and RMW_IMPLEMENTATION before starting. - Exit 4 only when a press was received; exit 6 when no message (or a stale one) was received before the start. record.sh records exit 6 as step_sequence: "refused_estop_not_received". - README / msg / questix_msgs README: whether the E-stop cuts the DDT power is not verified (the 2026-10-05 note said it does not; the 2026-10-06 observations disagree); unplugging the RS485 side does not cut the power; position_raw across a power loss is not an angle or a turn count. Add the 2026-10-06 hand-turn results. Comments only in the msg file (type hash unchanged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Collaborator
Author
|
このコミットの変更ではないと判断した理由:
いま用意できる修正はありません(テストの起動待ちのしかたは、この PR の範囲外です)。失敗したジョブを 1 回だけ再実行します。再実行でも同じ失敗が出たら、原因を調べ直します。 Generated by Claude Code Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Yuichiroh-Kobayashi
pushed a commit
that referenced
this pull request
Oct 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
5 tasks
The operator confirmed that the DDT drive power drops to zero volts while the E-stop is pressed (RS485 lines stay powered). Replace the 'not verified' notes, explain the 1.3-1.6 s of no response after each release as the DDT booting, and mark the 2026-10-05 'not cut' note as wrong. Comments only in the msg file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
8 tasks
A level above drive_component's max_motor_rpm (or the 475 rpm spec clamp) is silently clipped, so the identification input differs from the request. record.sh now reads max_motor_rpm and step_sequence.py refuses such a schedule before recording (--max-command-rpm). The default levels stop at 300 rpm, inside the M6 datasheet's +/-330 rpm, until the motor variant is confirmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Yuichiroh-Kobayashi
pushed a commit
that referenced
this pull request
Oct 6, 2026
…ntify Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
This was referenced Oct 7, 2026
Closed
Yuichiroh-Kobayashi
pushed a commit
that referenced
this pull request
Oct 7, 2026
Yuichiroh-Kobayashi
marked this pull request as ready for review
October 7, 2026 09:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Yuichiroh-Kobayashi
pushed a commit
that referenced
this pull request
Oct 7, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp
Collaborator
Author
|
Generated by Claude Code Generated by Claude Code |
asa-naki
approved these changes
Oct 10, 2026
asa-naki
added a commit
that referenced
this pull request
Oct 10, 2026
Resolve the drive_component.cpp and shot_component.cpp conflicts by carrying main's changes (#172, #176, #180) into the split translation units. warnIfLqrTicksAssumeAnotherRate and warnCurrentModeAssumptions go to drive_component_params.cpp with kLqrTickRateHz, and the unused <thread> include is dropped from the shot split files as main dropped it from shot_component.cpp. Function bodies match main's single files. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
車輪を浮かせた動作テストの前準備です。足回りの測定忠実度を上げ(LAB の生値表示、制御 tick ごとの診断トピック、応答タイムアウトのパラメータ化)、velocity / current モードの制御実装を見直し、振動の切り分け用の解析スクリプトと共振モデルのテストを追加します。既定の走行挙動は変えていません(根拠は下記)。
実機の結果(2026-10-05)と、それを受けた修正(
e67d59f)verify/main-172-176@371c110(main2deeb87+ この PR8cf0bf0+ fix: start shot servos without blocking emergency stop handling #176)、機体 ID13、車輪を浮かせた試験。GPIO の非常停止経路を有効にして実施。独立レビュー(セカンドオピニオン)で、bag の統計・CSV・繰り返し性の表が再計算で一致することを確認済み。/drive_control_sampleは 50.00 Hz、seqの欠落 0、response_timeout0、fault_code0。feedback_new=false(8.8 %)はすべて指令 0 の周期(停止フレームの再送の間引き 300 ms。停止中の更新は中央値 約 320 ms)。回転中の欠落ではない。position_rawは 1 LSB 刻み。モータの電源を入れ直しても値は同じ(±1 LSB)。モータで回すと 1 回転で 1 周し、目視の回転数と合う。ただし手で約 90° 回したときの変化は約 9〜17° で、理由は未解明。velocity_rpm_rawの平均は、position から求めた回転より約 0.45 rpm 小さく(モータの符号で)、さらに約 0.7〜0.9 % の比例のずれがある(HYP:切り捨ての量子化と速さの目盛りの差)。オドメトリへの影響はこの PR の範囲外として別途確認する。e67d59fの修正(ロボットの動作は変わらない。msg はコメントだけで型と型ハッシュは同じ):ripple_analysis.py:--track-orders、既定 20)。pytest scripts/identify19 passed。position_rawの説明(msg・questix_msgs/README.md・scripts/identify/README.md):「8 LSB 刻み」を削除し、上の観察結果と「校正済みの絶対角としては扱わない」を記載。識別の README の手順に「非常停止では DDT の電源が切れない(ID13)」「値は解除後に新しいフレームだけを読む」を追加。電源の記述は誤りで、732a846で訂正した(下記 2026-10-06 の結果)。command_sent:停止フレームの再送を省いた周期でも true になる(実際に送った証拠ではない。送受信はfeedback_newで見る)ことを msg と README に明記。roundtrip_ms/response_timeoutを使うよう変更。追加の実機試験に向けた記録スクリプトの拡張(
51dbf6d、a7b54e7)2026-10-06 の追加試験(エンコーダの手回し、停止フレームの再送の間引きの比較、微速・信地旋回)に使うため、
scripts/identify/record.shとstep_sequence.pyを拡張しました。ノードのコードと設定値は変えていません。51dbf6d(機能の追加):--schedule 3:60,5:60,...:レベルごとに保持時間を指定できる。--lead-in-rpm/--lead-in-sec:止まった状態から動き出せないレベルの前に、助走を入れる。助走の後は 0 を通らずにそのレベルへ下げる。--pattern straight|spin|pivot-left|pivot-right:信地旋回では、片輪を止めて反対の輪だけを回す。--set-param NAME=VALUE:記録の間だけパラメータを変え、終了時に元へ戻す。変えられるのはmin_command_rpmとstop_resend_interval_msだけで、値は整数。max(1, min_command_rpm) + 2rpm 以上が要るので、回らないレベルがあれば質問の前に開始を断る。--set-paramの値も検査に含める。wheel_radiusとwheel_separationはノードから読む。meta.yaml にパターン・スケジュール・助走・車輪の寸法・変えたパラメータと戻した値を残す。a7b54e7(独立レビューの指摘を受けた修正):step_sequence.pyが/emergency_stopを購読する(reliable + transient_local、depth 1)。active=false)を受信していなければ開始しない。--estop-timeout)、0 を送って中断する。終了コードは 4、meta.yaml はstep_sequence: "aborted_emergency_stop"。record.shの--set-param:param_restore: failedを残す。修正前は警告だけで、成功扱いで終わっていた。--no-estop-watchを追加した(既定は監視あり)。record.sh/step_sequence.pyは、非常停止を解除してから実行する必要がある(README に記載)。監視にはquestix_msgsが要るため、questix のワークスペースを source しておく(無ければ開始時にその旨を表示して終わる)。実機の結果(2026-10-06)と、それを受けた修正(
732a846、dc9c423、5cb6fe1)verify/main-172-176-20261006@e2e9680(main2deeb87+ この PRa7b54e7+ fix: start shot servos without blocking emergency stop handling #176)、機体 ID13。a7b54e7):実行中の押下から +3 ms で 0 の指令を送り、終了コード 4、meta.yaml はaborted_emergency_stop。解除しても回り出さないことは作業者の観察のみ(bag は解除の前に終わっていた)。--set-param:10 回すべてで、設定・読み返し・復元が一致した。終了コード 5 は 0 回。--schedule/--lead-in-rpm、浮かせた状態):1〜10 rpm の正転・逆転で、位置から求めた平均速度は指令の 0.975〜1.081 倍。0.2 s の窓での止まりかけ・逆回りは 0。step_sequence.pyが 2 s の待ちで/emergency_stopを 1 件も受け取れず、開始しなかった。ros2 bag record)でも、/emergency_stopの最初の受信が最大 3.0 s 遅れた。position_rawの手回し:非常停止の押下中に手で 30° ずつ回した(押下で DDT の駆動電源が無電圧になることは、作業者が確認)。Control tick overrunの WARN は出ていない(待機の経路では判定していない)。wheel_radiusが実寸の 2 倍だったこと → fix: wheel_radius が実寸の 2 倍(0.1 m、実寸 0.05 m)。物理単位を正し、走り方は変えない #179。test_control_coreの共振モデル(1.8 Hz、ζ 0.15)は、仮定値のまま。732a846の修正(記録スクリプトと文書。ロボットの動作は変わらない。msg はコメントだけ):step_sequence.py:/emergency_stopを待つ時間を--listen-beforeとは別に設けた(--estop-wait、既定 10 s)。待つ間も、他の送り手の検出は続ける。RMW_IMPLEMENTATIONを表示する。record.shは meta.yaml にrefused_estop_not_receivedと残す。scripts/identify/README.md、questix_msgs/README.md、DriveControlWheelSample.msgのコメント):5cb6fe1で「ID13 では非常停止の押下で DDT の駆動電源が切れる」に更新した(解除のたびに DDT が起動し直し、最初の応答まで 1.3〜1.6 s)。position_rawは、角度としても回転数としても使わない。5cb6fe1:上の電源の記述を、作業者の確認(押下で駆動電源が無電圧。RS485 の信号線は生きている)に合わせて更新した(文書と msg のコメントだけ)。dc9c423:motor_control_app/README.mdとdrive_component.yamlのコメントに、周期超過の WARN は駆動指令を送る tick だけで判定し、待機中の超過は bag のtick_duration_secで見ることを書いた(コードの変更なし)。外部レビューを受けた修正(
cbc20a3)drive_componentのmax_motor_rpm(と仕様上限 475 rpm、kSpecVelocityMaxRpm)を超えると、指令が切り詰められ、同定の入力が指定と変わる。これまでは黙って切り詰められていた。record.shがmax_motor_rpmを読み、step_sequence.py --max-command-rpmで、超えるステップ(助走を含む)があれば記録の前に拒否する。単体のstep_sequence.pyも--max-command-rpmを渡せば同じ判定をする。record.sh20〜300、step_sequence.py50〜300)。M6 規格書 V1.0 の速度 ±330 rpm(下の「データシートとの食い違い」)に収めるため。搭載モータの仕様が確定するまでの暫定。test_step_sequence.pyに 1 件、test_evidence.shに 3 件(max_motor_rpm=330で 400 rpm を拒否し、出力ディレクトリを作らない)。base と履歴(#170 → #169 → この PR)
main。fix: keep practice runs moving without gpio ref or teacher permission #170(14aceb8)と fix: require GPIO5 E-stop ROS monitoring in all production launches #169(squash merge2deeb87)はどちらも main にマージ済みです。2b4fe5f、6809491。履歴は書き換えていません)。2b4fe5fの衝突は 3 ファイルで、fix: keep practice runs moving without gpio ref or teacher permission #170 の E-stop / 教員許可の作り直しを優先して解消しました。drive_component.cpp: 実行時変更不可のリストは fix: keep practice runs moving without gpio ref or teacher permission #170 のteacher_permission_*を使い、この PR のserial_response_timeout_ms/publish_control_sample/control_sample_topicを足した。drive_component.hpp:emergency_stop_topic_は fix: keep practice runs moving without gpio ref or teacher permission #170 でEmergencyStopMonitorに移ったので削除し、診断サンプルのメンバは残した。motor_control_app/README.md: この PR の「レビュー」節と build: reuse precompiled rclcpp headers and use ccache in ci and on kits #167 の「ビルド時間」節の両方を残した。6809491は fix: require GPIO5 E-stop ROS monitoring in all production launches #169 の58285f4(管理設定 card の read-only 項目「安全入力(GPIO5)」の削除)の追従で、衝突なし。mainを8cf0bf0でこのブランチへマージしました。2deeb87の tree は stack 元の fix: require GPIO5 E-stop ROS monitoring in all production launches #169 head と同一なので、このマージで tree は変わりません。runControlTick())とsafetyStopの診断サンプルの書き込みは、fix: keep practice runs moving without gpio ref or teacher permission #170 の判定(applyGate()、onEmergencyStop)の上でそのまま動きます。tick の外(非常停止コールバック)からのsafetyStopはサンプルに書き込みません。レビュー後の修正(
db70920〜c43b69a)/drive_statusを 3 回に 1 回(0.06 s、約 17 Hz)にする。ブリッジの記録も同じWHEEL_GAP_SECONDS/ recording-core のコメント、lab_bridge.yamlのコメント、ブリッジ README を実際の値に修正(6a3d3c7、挙動の変更なし)max_correction_rpm跳ねるdb70920、LQR は既定 OFF)scripts/identifyのテスト(fit_models / step_sequence / ripple_analysis)が CI のどこでも走っていなかったpytest scripts/identifyを追加(c43b69a)b216708)feedback_countは tick の外の送受信(非常停止コールバックの即時停止、configure/activate)でも増えるquestix_msgs/README.md)・ripple_analysisに明記。ripple_analysisの表示は「載らなかったフレーム」(サンプルの欠落とは別)(220dbe8)変更点
1. LAB に平滑化前の生値を描く(
d49cd24、注記は S1 で修正)/drive_statusのtarget_rpm)。wheel_radiusから求めた車体速度。生値が無ければ従来のspeedに戻す。WHEEL_GAP_SECONDS)を超える間隔の前に切れ目を入れる。html-chart.jsは非有限点で線を分け、ラベルは最後の有限点に置く。欠測の無い既存の図(motor / slam)の見た目は変わらない(テストで確認)。2. 診断トピック
/drive_control_sample(3380781)questix_msgs/DriveControlSampleとDriveControlWheelSample。MotorFeedback/DriveStatusは変更していない(型ハッシュと既存 bag の互換のため)。drive_componentが制御 tick ごとに 1 回 publish(control_rate、reliable + volatile + keep_last(100)、ACTIVE 時のみ、既定で有効。publish_control_sample/control_sample_topic、実行時変更不可)。tick 本体をrunControlTick()に移し、途中で return する tick も含めて毎 tick ちょうど 1 回出す。tick_action(IDLE / DRIVE / TIMEOUT_STOP / FAULT_STOP)付き。DdtMotorLibがモータごとの送受信の記録(受信フレーム数・送受信回数・有効な応答なしの数・直近の往復時間・最後に送った電流 raw)をstate_mutex_の下で持つ。questix_msgs/README.md(診断専用、時刻の意味、seqによる欠落の検出、feedback_new/feedback_countによる重複の検出、既存トピックを変えない理由)。既存の誤記も修正:/drive_statusの既定は 10 Hz → 50 Hz。position_rawの説明はe67d59fで実測に合わせて修正。CHANGELOG、record.shの OPTIONAL_TOPICS、AGENTS.md。3. 応答タイムアウトのパラメータ化(
ad43268)readFeedbackFrame(..., 10)の固定値をDdtMotorLibの設定値にした(既定 10、[2, 50] ms にクランプ)。drive_componentにserial_response_timeout_ms(既定 10。範囲外はクランプ + WARN、実行時変更不可)。single_ddt_motor/joy_axis_driveはライブラリ既定のまま。4. velocity / current 実装のレビュー(
6559eaf修正、43b4ae1警告と文書、db70920S2)max_correction_rpmが出得るcontrol_rate≠ 50 のとき起動時 WARN。YAML を周期によらない表現にcurrent_invert_measured: trueが未確認/drive_statusと tick、アイドル時の再取得、非常停止の待ち)max_motor_rpmを超えるステップは拒否(cbc20a3)5. 解析スクリプトと共振モデル(
e868a76、e67d59f)scripts/identify/ripple_analysis.py: rosbag2(/drive_control_sample優先、旧 bag は/drive_status)または CSV。重複・欠落の処理、position_rawのつなぎ直し、定速区間の抽出、回転角での同期平均、次数スペクトル、時間スペクトル、キャンベル線図相当、左右の和・差、比例か一定かの判定、エンコーダ誤差未満を「判別不能」、追跡する次数(既定 20)の直接計算と折り返しの表示。test_control_coreに 2 次系のファームループモデル(仮定値: 1.8 Hz、ζ 0.15)。6. 記録スクリプト(
51dbf6d、a7b54e7、732a846、cbc20a3)既定の走行挙動を変えていない根拠
velocity_run_lqr_enabled: false(既定)の機能の中だけ。既定設定で出力が従来と一致することは既存のDefaultConfigIsIdenticalWithAndWithoutFeedback/DefaultConfigMatchesPreFsmPipelineが引き続き確認する。serial_response_timeout_msの既定 10 は従来の固定値。e67d59fはコメント・文書・オフラインの解析スクリプトだけ。51dbf6d/a7b54e7は、作業者が手で実行する記録スクリプトだけ。--set-paramは実行時変更可能な 2 つのパラメータを記録の間だけ変え、終了時に戻す(戻せなければ終了コード 5)。ノードのコード・YAML・launch は変えていない。732a846/dc9c423/5cb6fe1/cbc20a3は、記録スクリプト・文書・コメントだけ。検証
最初の版(
16f6cb1、作成者の PC: WSL2 Ubuntu 24.04 / ROS 2 Jazzy)colcon build/colcon test→ 1081 tests, 0 failures。pytest scripts/identify→ 15 passed。LAB 583 tests / 0 fail、Prettier OK。旧 base での CI は全 16 チェック success。スタックとレビュー修正(
c43b69a)git diff --checkクリーン。clang-format 18.1.3 --dry-run --Werrorを全 C++ ファイルに → 差分なし。test_control_core.cppを最小の gtest 互換シムでコンパイル・実行 → 42 tests, 0 failed。LABnode --test→ 578 pass / 0 fail、Prettier OK。flake8 --select=F(scripts/identify)0 件。workflow_dispatchの run 37193308833)。#169 追従(
6809491)、main 追従(8cf0bf0)6809491ローカル:PYTHONPATH=scripts pytest -q scripts/robot_manager→ 470 passed, 7 skipped。CI: 全 19 check run が success。実機の結果を受けた修正(
e67d59f)py_compile、flake8 --select=F(scripts/identify)0 件、git diff --checkクリーン(手元に NumPy が無いため pytest は CI で実行)。pytest -q scripts/identify→ 19 passed。記録スクリプトの拡張(
51dbf6d)、非常停止での中断とパラメータ復元の失敗(a7b54e7)bash scripts/identify/test_evidence.sh(ros2をスタブに差し替え)→ 82 passed, 0 failed。開始を断る停止判定の検査、--set-paramの許可リスト、復元(成功・中断)、読み返しの不一致、復元の失敗による終了コード 5、非常停止による終了コード 4 を含む。このテストは CI では実行していない。test_step_sequence.py(スケジュール、助走、開始の検査、信地旋回・超信地旋回、EstopGuard)→ 13 passed。flake8 --select=F(scripts/identify)0 件。git diff --checkはクリーン。a7b54e7。run 37386310682 / 37386310485 ほか):全 17 チェック success。pytest -q scripts/identify→ 27 passed。最初の E-stop の待ちと終了コード 6(
732a846)、周期超過の文書(dc9c423)test_evidence.sh→ 84 passed, 0 failed(終了コード 6 →refused_estop_not_receivedを追加)。test_step_sequence.py→ 14 passed(押下と受信なしの区別を追加)。CI と同じ設定の flake8 / pydocstyle、git diff --checkはクリーン。dc9c423):全 18 チェック success。732a846では、build-and-test (jazzy, amd64)で launcher のtest_gpio_safety_runtimeが 1 件失敗した(起動後 15 s 以内にノードが見つからない)。上限の検査(
cbc20a3)pytest scripts/identify29 passed、test_evidence.sh87 passed、CI と同じ設定の flake8(--select=F)・pydocstyle 0 件、bash -nOK、git diff --checkクリーン。cbc20a3):全チェック success。実機(Raspberry Pi 5)で確認すること
6aafa7dで確認、fix: require GPIO5 E-stop ROS monitoring in all production launches #169 は main にマージ済み)ros2 topic hz /drive_control_sampleがcontrol_rate(50 Hz)で出る(2026-10-05、50.00 Hz。INACTIVE で止まることは未確認)seqの飛び、feedback_newの割合、roundtrip_msの分布(2026-10-05、浮かせた試験。床上の走行中の bag は 2026-10-07 に ID15・ID14 で取得し、seqの欠落 0)drive_componentとros2 bag recordが各約 4.6 %。新トピック追加前との比較は未実施)serial_response_timeout_msが既定 10 で従来と同じ挙動(response_timeout0 件、周期の超過は 26,717 周期中 1 回。停止時のレイテンシ統計は deactivate のときだけ出ると判明し、文書を修正)publish_control_sample: trueとserial_response_timeout_ms: 10。velocity モードの既定では新しい WARN が出ないscripts/identify/README.md「振動の切り分け」)を通しで実行できる(GPIO の非常停止経路を有効にして実施)position_rawの電源の入れ直し(同じ値に戻る)を確かめ、questix_msgs/README.mdに反映(e67d59f)position_rawの手回しの追加検証(2026-10-06、型紙で 30° ずつ 17 点):手で回した角度には追従せず、同じ角度では同じ値に戻る。電源断をまたいだ値は角度として使えない、と文書に反映(732a846)。モータで回したときの四分点の通過は未実施e67d59fの解析スクリプトで、2026-10-05 の bag を解析し直す(2026-10-06:20次 … rpm64 件、折り返しの表示 32 件、「判別できるピークが N 個」の理由。run1・run2 とも)step_sequence.pyが実行中の非常停止の押下で中断する(2026-10-06:終了コード 4、aborted_emergency_stop、押下の +3 ms で 0 の指令)。解除しても回り出さないことは作業者の観察のみ(解除をまたいで記録した bag は無い)--set-paramの設定・読み返し・復元の表示と、記録の前後のros2 param getの値が一致する(2026-10-06:10 回、終了コード 5 なし。2026-10-07:30 回、すべて開始時の値に戻った)--schedule/--lead-in-rpmで、1・2 rpm のレベルを助走のあと 0 を通らずに保持できる(2026-10-06:2 回、平均速度は指令の 0.975〜1.081 倍)732a846:/emergency_stopの最初の受信までの時間が表示され、2 s を超えても拒否されずに始まる(拒否されたときは終了コード 6 とrefused_estop_not_received)。2026-10-07 の試験はe2e9680(この修正の前)で行ったため未確認(起動拒否は ID14 で 2 回)5cb6fe1)。2026-10-07 に ID15・ID14 でも無電圧を確認cbc20a3:record.shがmax_motor_rpmを表示し、上限以下のレベルでは従来どおり記録できる関連
wheel_radiusが実寸の 2 倍(2026-10-06 の信地旋回で判明)。物理単位を正し、走り方は変えない方針で fix: correct wheel_radius to the real 0.05 m and keep wheel speeds unchanged #180 にした(この PR の上に fix: start shot servos without blocking emergency stop handling #176 をマージして積んでいる)。test_gpio_safety_runtimeの起動待ち 15 s。🤖 Generated with Claude Code
https://claude.ai/code/session_01UtCWWiR4ExGRx34k3Y78Xp