Imported from scramble-robot/questix (
AGENTS.md). Install upstream withnpx skills add scramble-robot/questix. Copyright stays with the author.
QUESTiX Agent Guide
Project overview
- QUESTiX is a repository for robot control and ISO image builds targeting ROS 2 Jazzy on Ubuntu 24.04.
- The repository contains a mix of C++ ROS 2 packages, launch/config files, Ansible assets, systemd units, and the FastAPI-based
robot_manager.
Project naming and documentation style
- Use
QUESTiXas the project name in prose, headings, summaries, PR bodies, and generated documentation. - Do not mechanically rename repository names, package names, file paths, URLs, commands, service names, launch arguments, environment variables, or code identifiers.
- Preserve existing lowercase identifiers such as
questix_launcher,questix_robot, and repository/path names unless the user explicitly requests a code-level rename. - When generating documentation, distinguish between the product/project name
QUESTiXand implementation identifiers such as package names or file paths.
Environment baseline
- AMD64 development environment: Ubuntu 24.04 + ROS 2 Jazzy.
- ARM64 Raspberry Pi 5 robotics kit: Ubuntu 24.04 + ROS 2 Jazzy.
- WSL and desktop environments are useful for reading, editing, static checks, and non-hardware tests.
- Raspberry Pi 5 hardware validation is authoritative for GPIO, UART, I2C, SPI, systemd, robot startup, and controller integration.
Main directories
motor_control_lib/: Shared library for motor control.motor_control_app/: ROS 2 nodes and components for drive, shot, single DDT, and related motor-control applications.esc_motor_control_cpp/: C++ package for ESC/DDT motor control.joy_controller/,uart_joy_driver/,joy_gate/,gpio_reader/: Input, gating, and GPIO-related packages.operation_manager/: Operational state management.questix_safety/: the shared/emergency_stopcheck (header-onlyestop_check.hpp+EmergencyStopMonitor). drive_component, shot_component and esc_motor_control use it for the subscription, the parameters and the fail-closed rule; do not reimplement E-stop reception in a node.launcher/: Integrated entry point for ROS launch files. The ROS package name isquestix_launcher.description_launch/: URDF, RViz, and xacro assets.ansible/,scripts/,systemd/: OS setup, ISO build tooling, and resident services.scripts/robot_manager/: FastAPI web management UI. Its 教材 tab (lab.py) starts the QUESTiX LAB bridge with the recorder's rosbagOUTPUT_DIRasrosbag_dir(andRECORDS_DIRfromlab.envwhen set) and shows the records the bridge keeps (count, size, quota, folder) from itsGET /api/state.scripts/robot_manager/recorder.py+trial.py: the singleros2 bag recordauthority (genericPOST /api/rosbag/start) and its passive experiment evidence mode (POST /api/rosbag/start-trial: PII-free metadata, source identity from the service'sQUESTIX_SOURCE_DIR→ROBOT_WS→ROBOT_WS/src/*(git top level + markers + 40-char commit, else the evidence trial is refused), the robot's ownlaunch.envdomain, preflight of/target_twist/drive_status/emergency_stop, before/after parameters, the teacher's runtime authority, sidecars next to the bag, integrity clean only after SIGINT with the required topics present). Its UI is the 記録 tab's 「証拠付きで記録する」 switch (static/trial-view.js: the form's fields and limits repeattrial.py, kept identical bytests/trial_view.test.cjs). It changes no parameter, restores nothing and runs no A/B; MCAP + sidecars are the evidence, QUESTiX LAB JSON is learner-facing derived data.QUESTIX_SOURCE_DIRis set insystemd/questix_robot_manager.service,scripts/install-robot-manager.shand the Ansiblequestix_robot_manager.service.j2together.scripts/robot_manager/actuation.py+actuation_heartbeat.py: the teacher's runtime actuation authority for practice (操作 tab: ロボットの走行制御 / 発射機構の操作). Session-only switches, off at every manager start and boot and on every mode switch, robot service start/restart/stop, 「すべて止める」 and manager shutdown; refused in competition mode. While one is on, a heartbeat child publishesquestix_msgs/ActuationAuthorityon/actuation_authorityat 5 Hz (reliable + volatile + keep-last(1); never transient_local). The authority is a permission, not an emergency stop: by default it only gates the 教材 permissions, and the controller drives without it (as in 3.2.0). Enforcement in drive_component, shot_component and esc_motor_control is a practice opt-in (require_teacher_permission, questix_core arg defaultfalse, forwarded as$(and require_teacher_permission $(not enable_autoreferee)); 1.0 s lease on each node's monotonic receive time; controller and QUESTiX LAB alike). Switching one off also switches off the matching 教材 permission; switching one on never switches a 教材 permission on, and a 教材 permission cannot be switched on while its authority is off./emergency_stophas one publisher, operation_manager, which questix_core always starts: with the GPIO safety path (always in competition) it judges GPIO5 / GPIO27; without it (practiceENABLE_GPIO_REF=false,operation_manager.no_gpio.yaml,gpio_safety_enabled: false) it reads no GPIO and keeps publishingactive=false, reasonreleased (no GPIO safety path). So every actuating node and QUESTiX LAB keep one rule whatever the wiring: an unheard, silent (emergency_stop_timeout_sec) or active E-stop means stop (require_emergency_stop: truein the node YAMLs;falseonly for an explicit diagnostic run; no launch passes it). The authority switch comes from the launch, never from the node YAMLs. Contract:questix_msgs/README.md.scripts/robot_manager/wifi_ap.py+network_admin.py: 管理設定 card 「ネットワーク / QUESTiX Local」 (start/stop, SSID, password, band, channel, address of thewifi_access_pointaccess point). Robot Manager stays unprivileged on 127.0.0.1 (the whole app serves only Host 127.0.0.1 / localhost: TrustedHostMiddleware). Every network change must pass_browser_mutation_guard(Content-Type application/json, JSON body,Originequal to its own loopback origin when present, no cross-siteSec-Fetch-Site); it validates the request (unknown keys refused, the password never echoed), writes/etc/questix_robot/network_request.jsonand runssystemctl --no-ask-password start questix_network_admin.service(polkit: that unit, verbstart, the robot user only; no sudoers). The unit (systemd/questix_network_admin.service, never enabled) runs the root-owned copy/opt/questix_robot/questix_network_admin.py, which reads the request once (no symlink, owner check), trustswifi_ap.envonly when root-owned and not group/other writable, validates again, writes the role's keyfile /wifi_ap.env/ regdom files atomically and runs nmcli/iw as argument lists; its rendering is contract-tested against the role's templates. It never deletes the profile and knows nothing about QUESTiX LAB (Robot Manager starts the bridge after a successful start in practice mode with AUTOSTART on). Helper, unit and polkit rule are installed byrobot_autostart,scripts/install-robot-manager.shandscripts/update-robot-manager.shtogether.scripts/robot_manager/static/lab/: QUESTiX LAB web teaching material (static ES-module site served at/lab/; in-browser simulator lessons plus a live view of the real robot and guarded low-speed driving experiments). Ship only permissively licensed third-party files there (no GPL/AGPL); see itsassets/vendor/NOTICE.md. Onlyjs/live/drive-link.jsmay send drive frames and onlyjs/live/shoot-link.jsmay send launcher frames (roller / tilt / fire) to the robot; the only other frame a page sends isrecord_save(a finished recording for the bridge's record store, fromjs/live/robot-link.js), which moves nothing.questix_lab_bridge/: WebSocket bridge (ament_python) that mirrors/scan,/odom,/drive_status(each wheel's target / raw / filtered rpm in native sign with its feedback stamp, age and validity),/target_twist,/emergency_stop(streamestop, the authoritative E-stop for pages and recordings), and an optional camera topic to QUESTiX LAB. It also keeps QUESTiX LAB records on the robot as files inrecords_dir(records.py: pages'record_save, controller driving it auto-records from the payloads it already builds, and cached rosbag conversions; quota and free-space limits, auto records prune only themselves and caches, lab records are never deleted automatically), and lists and converts Robot Manager's rosbags inrosbag_dir(rosbags.py, rosbag2_py) over plain HTTP GET on the same port (records_api.py); none of this adds a ROS interface. It also mirrors the disc launcher's/roller/statusand/shot/status(std_msgs/String JSON) to every page. Observation only unlessallow_driveorallow_shootis set (bothfalseinlab_bridge.yaml; robot_manager 教材 tab: session-only permissions held in the robot_manager process, off at every manager start and boot, on only after the teacher switches them on, off again on 配信停止 / 「すべて止める」 / competition mode, never stored inlab.env; robot_manager always passes both explicitly). Withallow_driveit may publish/target_twist/lab(twist_arbiter's lab input) under the checks inquestix_lab_bridge/drive.py(no other publisher, arbiter present, E-stop known from/emergency_stopitself (estop_unknownuntil then; a derived/drive_statusor launcher-status "released" never counts) and released, one page, speed limits, dead-man timeout, controller takeover). Withallow_shootit may publish/roller/lab(Float32 0..1),/shot/lab/tilt(Float32 deg) and/shot/lab/fire(Empty) under the checks inquestix_lab_bridge/shoot.py(launcher nodes subscribed and reportinglab_accepted, no other publisher, E-stop known from/emergency_stopand released from any source, controller not in use (source/lab_locked, a controller shot), one page, roller dead-man 0.5 s, 30 s per session, power clampshoot_max_powerand tilt clampshoot_tilt_min/max, each narrowed (never widened) by the nodes' reportedlab_max_speed/tilt_min_deg/tilt_max_deg, roller 0 on every session end and new blocker, a coalesced tilt sent only for the session that asked for it (shoot.TiltCoalescer), fire only withconfirm, by the owner, after the roller ran ≥ 0.2 for 1.0 s, not while shooting, andshoot_fire_interval_sec/ the node'snext_fire_in_secafter any shot). It publishes nothing else. Do not add other publishers, services or actions, and do not weaken those checks.twist_arbiter/: C++ node that chooses between the controller (/target_twist/joy) and QUESTiX LAB (/target_twist/lab) for/target_twist; a moved stick always takes over. Started only by practice launches ofquestix_core.launch.xml; never withenable_autoreferee(competition keeps joy_controller →/target_twistdirect).src/: External packages imported viadependency.repos(ydlidar_ros2,ydlidar_sdk_vendor). Not part of the core QUESTiX codebase; do not edit unless explicitly requested.
Pre-work checks
- Always verify consistency across
package.xml,CMakeLists.txt,launch/, andconfig/before and after changes. - Do not assume directory names always match ROS package names. For example,
launcher/maps to the ROS package namequestix_launcher. - Hardware-dependent code is likely impossible to validate fully without the physical robot or target hardware.
Defect-prevention checklist
Recurring bug classes from this repository's fix history. Check each relevant item before submitting changes.
-
C++ member initialization: Initialize every new class data member in the constructor initializer list (or with an in-class default). Uninitialized primitives have caused undefined servo/motor behavior (cf. #73).
-
Parameter renames: When renaming a ROS parameter, grep and update
.cpp,.hpp,config/*.yaml, andlaunch/together in one change. A stale name in YAML fails silently: the node falls back to the code default with no error (cf. #71 pan→tilt). -
Single source of truth for defaults: Do not keep defaults for the same parameter in both YAML and launch arguments. Before removing a launch-file default, check whether it is the effective value that has been overriding YAML (cf. #47 fire_button).
-
Binary protocols: Document the byte order of multi-byte fields in code comments, and centralize packing/unpacking in helper functions instead of inline bit shifts. On an unknown or unsupported protocol response, log and skip instead of asserting (cf. byte-order and feedback-parsing fixes in the DDT motor protocol).
-
Control-loop state: Give PI integrators and similar control state an explicit reset function, and call it on mode transitions, stop, and feedback timeout. Any feedback retry loop must have a timeout and a clear exit condition (cf. #66).
-
Package renames/removals: When renaming or removing a package, update every
package.xml(build_depend/exec_depend/test_depend),CMakeLists.txt(find_package), and launch reference (pkg=,find-pkg-share). A stale dependency name breaksrosdep installon fresh setups (cf. #64). -
Mode-specific parameters: If a parameter only takes effect in one control mode, state the applicable mode in the YAML comment and the header doc comment, and log when the parameter is ignored in the current mode (cf.
brake_on_stop, velocity mode only). -
Configurable index bounds: When an array index (joy axis, button, pin) comes from a parameter, bounds-check it against the actual message/array size right before use. A fixed-size check such as
axes.size() < 4does not cover configurable indices. -
Single config copy: Do not keep divergent copies of the same node's parameter YAML in multiple packages (cf.
drive_component.yamlinlauncher/config/andmotor_control_app/config/). If duplication is unavoidable, add a cross-reference comment in both files and keep them identical. -
Launch dependencies in package.xml: Every package referenced by a launch file via
pkg=orfind-pkg-sharemust be listed asexec_dependin that package'spackage.xml, and must actually exist in the workspace or ROS repos. -
CI must fail on failure: Do not mask checks with
continue-on-error: trueor|| true. If a check cannot be enforced (e.g. Ansible check-mode limitations), leave a comment explaining why it is advisory. -
Installer/unit consistency: Static
systemd/unit files and their install-time substitutions (scripts/install-robot-manager.sh) and the Ansible templates inansible/roles/robot_autostart/templates/describe the same services; when changing a user, path, or environment variable in one, update all three. -
Persisted-value precedence for kitting config (
ROS_DOMAIN_ID):/etc/questix_robot/launch.envand the target user's~/.bashrcAnsible managed block are two independent persisted copies of the same value. Never let one silently win over the other, and never silently paper over a malformed persisted value — seescripts/resolve_ros_domain_id.pyand its decision table inansible/playbooks/vars/README.md. The resolver only reads and validates; only Ansible (robot_autostart/robotics_workspaceroles) writes. The allowed ranges (0-101, 215-232) are defined once inscripts/robot_manager/ros_domain.py(stdlib only; imported by the resolver from the source checkout and by the installed Robot Manager); the Jinja assert, the robot launcher and the settings form repeat them andscripts/robot_manager/test_ros_domain.pykeeps them identical. -
No root-equivalent defaults: Never ship a known password, a
NOPASSWDsudo rule or a broad polkit grant (.pkla,org.freedesktop.policykit.exec, a.rulesentry without a unit name). Custom images keep the robot user locked and SSH disabled until the console first-boot enrollment (scripts/iso/); polkit authority is onlysystemd/50-questix-robot.rules(questix_robot.servicestart/stop/restart,questix_network_admin.servicestart). Leftovers of older versions are removed byscripts/cleanup_legacy_privileges.py(exact matches only; exit 0 clean / 1 fixable / 2 an operator has to act, e.g. the old image's known passwordubuntu, and 2 is never reported as clean) frominstall-robot-manager.sh,update-robot-manager.shand thelegacy_privilege_cleanuprole;ansible/tests/scan_privileges.pykeeps them out.
Pull request titles
- Pull request titles must follow Conventional Commits because this repository runs a semantic pull request title check.
- Use the format:
<type>: <lowercase summary>. - Common types include
feat,fix,docs,style,refactor,perf,test,build,ci,chore, andrevert. - Prefer concise summaries after the colon.
- Examples:
fix: resolve recursive loop in setup_dev role varsci: align ISO workflow with Ubuntu 24.04 and ROS 2 Jazzydocs: add AI agent guidancechore: sync upstream main through PR #56
- Before opening or updating a PR, ensure the title passes the semantic pull request check.
C++ / ROS 2 implementation policy
- Use C++17 unless an existing package explicitly specifies a different standard.
- Follow
.clang-format. - Prefer
ament_cmake_autofor ROS 2 packages. - For component implementations, keep
rclcpp_components_register_nodesandRCLCPP_COMPONENTS_REGISTER_NODEentries consistent, including registration names and class names. - Update YAML parameters, launch arguments, and in-node
declare_parameter/get_parameterusage together.
C++ formatting
-
C++ formatting is checked by CI with
ament_clang_formatand the repository.clang-format. -
Before pushing C++ changes, run the CI-equivalent format check:
source /opt/ros/jazzy/setup.bash git ls-files -z -- '*.cpp' '*.hpp' | xargs -0 -r ament_clang_format --config .clang-format -
To reformat only the C++ files touched by a PR, run:
source /opt/ros/jazzy/setup.bash ament_clang_format --config .clang-format --reformat <changed-file-1> <changed-file-2> -
Do not reformat unrelated C++ files just to fix a PR unless the user explicitly asks for a repository-wide formatting change.
-
If
ament_clang_formatis missing on Ubuntu 24.04 / ROS 2 Jazzy, install it with:sudo apt update sudo apt install -y ros-jazzy-ament-clang-format
Command safety
- Do not run ISO builds, QEMU tests, package installation, permission changes, systemd commands, or hardware-facing commands unless explicitly requested.
- Do not run destructive commands such as
rm -rf, broadchmod, or broadchown. - Explain the expected impact before changing Ansible, shell scripts, systemd units, UART/GPIO behavior, or ISO build behavior.
- Treat
systemd/questix_robot*,scripts/build-iso.sh, andansible/playbooks/*.yamlwith extra care because they can have significant effects on real hardware or the OS.
Ansible and setup validation
- Treat Ansible, setup scripts, ISO provisioning, and robot startup as OS-impacting areas.
- Separate static validation from state-changing validation.
- Safe default validation candidates:
git diff --checkyamllint ansible/ansible-playbook ansible/playbooks/setup_kit.yaml --syntax-check -i localhost,ansible-playbook ansible/playbooks/setup_dev.yaml --syntax-check -i localhost,bash -n setup.sh setup_dev.sh scripts/install-robot-manager.sh scripts/apply-ansible-config.sh- sandbox-safe
make test-ansible
- Do not run
setup.sh,setup_dev.sh, package installation, systemd commands, GPIO/UART commands, ISO build, or QEMU unless explicitly requested. - If a real setup run is explicitly requested, record:
- branch and commit
- host OS and architecture
- execution user
- whether
/root/ros2_wsor/root/.bashrcwas accidentally touched - ROS 2, colcon, rosdep, and vcs post-checks
- skipped hardware/system checks
Ansible check mode limits
- Do not assume
ansible-playbook --check --difffully validates installer playbooks. - Check mode can skip modules that do not support it, and later tasks may fail if they depend on registered variables from skipped tasks.
check_mode: falseis acceptable only for read-only discovery tasks that must run to provide registered variables, such as:whoami- HTTP GET metadata lookups
- Do not add
check_mode: falseto package installation,get_urldownloads, file writes, systemd operations, GPIO/UART access, or other state-changing tasks just to make check mode pass. get_urlmay validate the URL in check mode without downloading the file body; do not treat a later missing downloaded file as proof that the real run will fail.- When check mode reaches this kind of installer limitation, report it clearly and ask before proceeding to a real setup run.
AMD64 setup_dev execution boundary
ansible/playbooks/setup_dev.yamlis intended for an AMD64 development machine where a regular user runs the playbook with sudo/become.- Keep safeguards that prevent accidental
/root/ros2_wsor/root/.bashrcconfiguration. - Do not weaken
/home/...assertions to support root-driven local or chroot execution unless explicitly requested. - Treat AMD64 root/chroot provisioning as a separate ISO workflow design issue.
- Keep repository-root
ansible.cfgand copied-subtreeansible/ansible.cfgroles distinct:- root
ansible.cfg: repository-root local setup and CI commands ansible/ansible.cfg: used from the image's own checkout (/home/ubuntu/questix/ansible,scripts/apply-ansible-config.sh) for chroot provisioning and should resolve siblingroles/
- root
Validation commands
The following commands are standard validation candidates. Whether they can run depends on the local environment, including ROS 2 availability, Ansible/lint tooling, hardware access, and OS permissions. Hardware-dependent behavior still requires real robot/Raspberry Pi validation.
git diff --checkcolcon build --symlink-install(run from the workspace root)colcon test --packages-skip ydlidar_ros2_driver ydlidar_sdk_vendor(run from the workspace root; the skip list matches CI)- For Ansible changes:
make test-ansible - For Ansible/YAML changes:
yamllint ansible/ - For GitHub Actions changes:
yamllint .github/workflows/andactionlintif available.
CI checks
What CI actually runs; reproduce the relevant parts locally before pushing.
ros2-build-test.yaml: always triggers on push/pull_request tomain(plusworkflow_dispatch). Its first job,changes, detects whether the changed paths are build-relevant; changes touching only**.md,ansible/**,scripts/**,docker/**, or.github/**skip thebuild-and-test,static-analysis, andformat-checkjobs, whilebuild-summarystill runs and reports as the single required check (no separate no-op companion workflow). When the build jobs run:colcon build(through ccache, cached per architecture) andcolcon teston amd64 and arm64 (tests skipydlidar_ros2_driverandydlidar_sdk_vendor), plus Python lint withflake8 --select=F --max-line-length=100andpydocstyle. On pull requests it also runs theament_clang_formatcheck described in "C++ formatting".ansible-check.yaml:ansible-lint, playbook syntax checks, and a check-mode dry run foransible/changes.robot-manager-lint.yaml("Robot Manager Checks"): runs forscripts/**changes, which the ROS workflow skips. flake8/pydocstyle overscripts/,pytest scripts/robot_manager(PYTHONPATH=scripts), and for the QUESTiX LAB sitenode --test test/*.test.mjsplusnpx prettier@3.9.8 --check .fromscripts/robot_manager/static/lab.semantic-pull_request.yaml: Conventional Commits PR title check (see "Pull request titles").workflow-lint.yaml:yamllint(repo.yamllint.yml) andactionlint(pinned, checksum-verified; shellcheck limited to warning+ severity) over.github/workflows/**. Runs when workflow files or.yamllint.ymlchange.
Cross-reference caution
- When changing package names, launch-file references to
find-pkg-share, or dependency package names, perform a cross-package search within this repository and update all related references.
