aether-robotics 5.2.1__tar.gz → 5.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aether_robotics-5.2.1/aether_robotics.egg-info → aether_robotics-5.2.2}/PKG-INFO +19 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/README.md +18 -0
- aether_robotics-5.2.2/VERSION +1 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/__init__.py +1 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/app.py +20 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/doctor.py +46 -14
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits.py +4 -2
- aether_robotics-5.2.2/aether/core/boot_timer.py +98 -0
- aether_robotics-5.2.2/aether/core/discovery_cache.py +148 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/tool_discovery.py +57 -8
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/ui.py +75 -8
- {aether_robotics-5.2.1 → aether_robotics-5.2.2/aether_robotics.egg-info}/PKG-INFO +19 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether_robotics.egg-info/SOURCES.txt +2 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/pyproject.toml +1 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_parallel_executor.py +19 -4
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_plan_cache.py +120 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_v511_ui.py +97 -0
- aether_robotics-5.2.1/VERSION +0 -1
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/LICENSE +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/MANIFEST.in +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/__main__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/actions/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/_generator.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/gopigo3.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/sunfounder_picarx.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/sunfounder_pidog.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/adapters/yahboom_robot_hat.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/power_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/task_manager.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/analytics.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/autonomous.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/calibrate.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/fix.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits_install_recipes/freenove_4wd.yaml +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits_install_recipes/gopigo3.yaml +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits_install_recipes/sunfounder_picarx.yaml +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits_install_recipes/sunfounder_pidog.yaml +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/kits_install_recipes/yahboom_robot_hat.yaml +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/cli/memory.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/analytics.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/auto_installer.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/auto_updater.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/autonomous.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/banner.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/calibration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/decomposer.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/embeddings.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/executor.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/feedback.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/feedback_prompt.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/firstrun.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/genome.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/goal_parser.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/goals.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/intent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/json_repair.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/llm_planner.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/mapper.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/memory.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/message_bus.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/metrics.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/navigation_engine.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/net_guard.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/overrides.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/parallel_safety.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/patches.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/permissions.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/plan_cache.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/planner.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/_cache.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/aether_cloud.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/anthropic.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/base.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/gemini.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/groq.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/ollama.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/openai_compat.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/providers/openrouter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/quiet.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/security.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/telemetry_config.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/tool_builder.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/tool_registry.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/core/visualizer.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/faults/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/parallel/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/parallel/executor.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/simulation/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/simulation/environment.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/skills/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/skills/drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/skills/watch_distance.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/app.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/widgets/__init__.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/widgets/agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/widgets/common.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether/tui/widgets/doctor.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/drone_v1.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/configs/rover_v1.json +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/context/aether_definitions.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/install.sh +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/main.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/requirements.txt +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/setup.cfg +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_adapter_gpio_manifests.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_adapter_override_wiring.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_adapter_preflight.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_adapter_resolver_v397.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_analytics_cli.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_analytics_module.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_analytics_wiring.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_atexit_adapter_shutdown.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_atexit_reaches_stop_all.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_auto_updater.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_autonomous_cli.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_autonomous_loop.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_autonomous_loop_parallel.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_autonomous_manifest_parity.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_autonomous_no_between_step_emergency_stop.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_boot_resolver_flow.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_calibrate_wizard.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_calibration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_chaos.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_doctor.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_fix.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_fix_patches.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_fix_v3_10_6.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_fix_v3_10_8.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_fix_visibility.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_kits.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_cli_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_consent_flow.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_console_script_smoke.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_correction_agent.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_decomposer.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_decomposer_avoids_legacy.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_decomposer_reactive_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_decomposer_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_display_bug_fix.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_doctor_gpio_allocation.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_doctor_multi_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_embeddings.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_entrypoint.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_feedback_prompt.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_first_run_onboarding.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_fix_scope_validation.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_fix_sees_named_servos.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_genome.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_goal_persistence.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_gopigo3_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_gpio_conflict_detection.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_high_level_task_examples.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_install_environment.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_intent_category_fallback.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_json_repair_and_status.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_live_providers.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_map_room_high_level_task.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_mapper.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_memory_cli.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_memory_retrieval.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_migration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_multi_adapter_boot_flow.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_multi_adapter_registration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_multi_discovery.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_named_servo_registration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_oled_false_positive.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_overrides.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_overrides_multi_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_overrides_schema_v1_2.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_parallel_decomposer.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_parallel_safety_hardware.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_parallel_validator.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_pca9685_emergency_stop.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_pca9685_emergency_stop_hardware.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_pca9685_hard_off.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_pca9685_pwm_primitives.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_pca9685_wake_sequence.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_planner_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_planner_stop_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_planner_uses_retrieval.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_posthog_integration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_primary_adapter_designation.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_provider_model_resolution.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_provider_outage_visibility.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_providers.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_quiet.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_retrieval_planner_contract.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_robot_json_v1_1.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_security.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_security_v520.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_set_servo_angle_duration.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_stop_full_off_behavior.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_stop_servo_sleep_fallback.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_sunfounder_picarx_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_sunfounder_pidog_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_synthesizer_fidelity_check.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tier1_motion_cleanup.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tool_builder.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tool_namespacing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tui_app.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tui_backward_compat.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_tui_widgets.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_ui_autonomous.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_ui_dashboard.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_ui_module.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_ui_wizard.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_v511_hardware_bugs.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_v512_feedback_routing.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_validator_new_tools.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_validator_rejects_legacy.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_watch_distance.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_yahboom_robot_hat_adapter.py +0 -0
- {aether_robotics-5.2.1 → aether_robotics-5.2.2}/tests/test_yolo_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-robotics
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.2
|
|
4
4
|
Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
|
|
5
5
|
Author: Chahel Paatur
|
|
6
6
|
License: MIT
|
|
@@ -144,6 +144,24 @@ Full engineering detail, including the algorithms and where they came from: [doc
|
|
|
144
144
|
|
|
145
145
|
## What's New
|
|
146
146
|
|
|
147
|
+
### v5.2.2 — Boot is the latency now, so boot got measured
|
|
148
|
+
|
|
149
|
+
With planning cached at 0 ms and `get_distance` at 6 ms, the remaining ~5 s was boot — running before every objective, and entirely unmeasured. Instrumented first:
|
|
150
|
+
|
|
151
|
+
| phase | ms (macOS dev machine) |
|
|
152
|
+
|---|---|
|
|
153
|
+
| hardware_probe | 1485 |
|
|
154
|
+
| software_probe | 942 |
|
|
155
|
+
| camera_probe (nested) | 828 |
|
|
156
|
+
| i2c_scan | 0.1 |
|
|
157
|
+
|
|
158
|
+
`[BOOT] hardware_probe 1485ms · software_probe 942ms · … · total 2449ms` prints under `AETHER_VERBOSE=1`, and `boot_latency {phase, ms}` is emitted always — the point is the distribution across real installs, not just machines being debugged.
|
|
159
|
+
|
|
160
|
+
- **Cached hardware profile.** If the fingerprint is unchanged, the entire probe sweep is skipped: **3016 ms → 1 ms**. The fingerprint deliberately includes the I2C address scan (~340 ms on a Pi), not just device-node presence — swapping a PiCar-X HAT for a GoPiGo3 leaves `/dev/i2c-1` identical, and a fast boot onto the wrong adapter is not a win. Prints `[DISCOVERY] Using cached hardware profile (fingerprint match)`; invalidated by a hardware change or `aether doctor`.
|
|
161
|
+
- **One-shot mode:** `aether --mode agent --objective "what's the distance in front of you"` runs one objective and exits, skipping readline and the feedback prompt.
|
|
162
|
+
- **Input bar.** v5.2.1 printed a leading newline unconditionally, which put a blank row *inside* the panel after Enter — the reported symptom. It's now only printed when the cursor is genuinely mid-line (Ctrl-C, or teardown closing the box from a signal handler), plus a reserved row above and a blank line after the closing border so the panel renders fully on an 80×24.
|
|
163
|
+
- **Subcommands look like the rest of the program.** `doctor` renders section panels with a coloured `13 passing · 4 warnings · 0 failures` summary; bare `aether kits` lists the kits instead of dumping argparse usage (`--help` still does). Four "KEY not set" provider warnings collapse into one line — they were never four problems. Non-TTY output stays byte-identical.
|
|
164
|
+
|
|
147
165
|
### v5.2.1 — Plan cache tuning, measured against the pairs that must not match
|
|
148
166
|
|
|
149
167
|
A one-word difference (`"infront"` → `"in front"`) missed the cache and cost a fresh ~2.7 s planning call. Objectives are now **normalised before embedding** — lowercased, punctuation stripped, whitespace collapsed — so `"What's the distance?"` and `"whats the distance"` embed identically (measured 0.934 → 1.000).
|
|
@@ -96,6 +96,24 @@ Full engineering detail, including the algorithms and where they came from: [doc
|
|
|
96
96
|
|
|
97
97
|
## What's New
|
|
98
98
|
|
|
99
|
+
### v5.2.2 — Boot is the latency now, so boot got measured
|
|
100
|
+
|
|
101
|
+
With planning cached at 0 ms and `get_distance` at 6 ms, the remaining ~5 s was boot — running before every objective, and entirely unmeasured. Instrumented first:
|
|
102
|
+
|
|
103
|
+
| phase | ms (macOS dev machine) |
|
|
104
|
+
|---|---|
|
|
105
|
+
| hardware_probe | 1485 |
|
|
106
|
+
| software_probe | 942 |
|
|
107
|
+
| camera_probe (nested) | 828 |
|
|
108
|
+
| i2c_scan | 0.1 |
|
|
109
|
+
|
|
110
|
+
`[BOOT] hardware_probe 1485ms · software_probe 942ms · … · total 2449ms` prints under `AETHER_VERBOSE=1`, and `boot_latency {phase, ms}` is emitted always — the point is the distribution across real installs, not just machines being debugged.
|
|
111
|
+
|
|
112
|
+
- **Cached hardware profile.** If the fingerprint is unchanged, the entire probe sweep is skipped: **3016 ms → 1 ms**. The fingerprint deliberately includes the I2C address scan (~340 ms on a Pi), not just device-node presence — swapping a PiCar-X HAT for a GoPiGo3 leaves `/dev/i2c-1` identical, and a fast boot onto the wrong adapter is not a win. Prints `[DISCOVERY] Using cached hardware profile (fingerprint match)`; invalidated by a hardware change or `aether doctor`.
|
|
113
|
+
- **One-shot mode:** `aether --mode agent --objective "what's the distance in front of you"` runs one objective and exits, skipping readline and the feedback prompt.
|
|
114
|
+
- **Input bar.** v5.2.1 printed a leading newline unconditionally, which put a blank row *inside* the panel after Enter — the reported symptom. It's now only printed when the cursor is genuinely mid-line (Ctrl-C, or teardown closing the box from a signal handler), plus a reserved row above and a blank line after the closing border so the panel renders fully on an 80×24.
|
|
115
|
+
- **Subcommands look like the rest of the program.** `doctor` renders section panels with a coloured `13 passing · 4 warnings · 0 failures` summary; bare `aether kits` lists the kits instead of dumping argparse usage (`--help` still does). Four "KEY not set" provider warnings collapse into one line — they were never four problems. Non-TTY output stays byte-identical.
|
|
116
|
+
|
|
99
117
|
### v5.2.1 — Plan cache tuning, measured against the pairs that must not match
|
|
100
118
|
|
|
101
119
|
A one-word difference (`"infront"` → `"in front"`) missed the cache and cost a fresh ~2.7 s planning call. Objectives are now **normalised before embedding** — lowercased, punctuation stripped, whitespace collapsed — so `"What's the distance?"` and `"whats the distance"` embed identically (measured 0.934 → 1.000).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5.2.2
|
|
@@ -947,6 +947,7 @@ def run_agent(args) -> None:
|
|
|
947
947
|
updater.run(auto_update=auto_update, no_update=no_update)
|
|
948
948
|
|
|
949
949
|
# ── Step 4: Capability Discovery ──────────────────────────────────
|
|
950
|
+
from aether.core import boot_timer
|
|
950
951
|
discovery = ToolDiscovery()
|
|
951
952
|
discovery.discover()
|
|
952
953
|
# v5.1.1 UI 1: the ~70-line inventory is a debugging surface, not a boot
|
|
@@ -1309,9 +1310,23 @@ def run_agent(args) -> None:
|
|
|
1309
1310
|
faults_detected = 0
|
|
1310
1311
|
total_actions = 0
|
|
1311
1312
|
|
|
1313
|
+
from aether.core import boot_timer as _boot_timer
|
|
1312
1314
|
from aether.core import ui as _ui
|
|
1313
|
-
_ui.enable_readline() # v5.1.1 UI 2: history + line editing
|
|
1314
1315
|
_mark_boot_finished() # v5.1.2 FIX 5: [TOOLS]/[MEM] etc. are live now
|
|
1316
|
+
_boot_timer.report()
|
|
1317
|
+
|
|
1318
|
+
# v5.2.2 (FIX 1 step 3): one-shot mode for scripted use. Deliberately
|
|
1319
|
+
# BEFORE readline setup and before the feedback prompt — a scripted
|
|
1320
|
+
# invocation must not touch shell history or block waiting on input.
|
|
1321
|
+
_one_shot = (getattr(args, "objective", "") or "").strip()
|
|
1322
|
+
if _one_shot:
|
|
1323
|
+
_execute_objective(_one_shot, registry, llm_planner, corrector,
|
|
1324
|
+
parser, manifest, _load_agent_memory(5),
|
|
1325
|
+
available)
|
|
1326
|
+
_feedback.session_ended()
|
|
1327
|
+
return
|
|
1328
|
+
|
|
1329
|
+
_ui.enable_readline() # v5.1.1 UI 2: history + line editing
|
|
1315
1330
|
|
|
1316
1331
|
while True:
|
|
1317
1332
|
try:
|
|
@@ -2948,6 +2963,10 @@ def main():
|
|
|
2948
2963
|
"autonomous = high-level goal decomposed into "
|
|
2949
2964
|
"self-directed sub-tasks (v4.0.0)")
|
|
2950
2965
|
# ── v5.2.0 security: capabilities are OFF unless asked for ────────
|
|
2966
|
+
parser.add_argument("--objective", type=str, default="",
|
|
2967
|
+
help="Run a single objective and exit (scripted use). "
|
|
2968
|
+
"With a warm boot and a cache hit this is "
|
|
2969
|
+
"sub-second end to end.")
|
|
2951
2970
|
parser.add_argument("--allow-shell", action="store_true",
|
|
2952
2971
|
dest="allow_shell",
|
|
2953
2972
|
help="Allow the LLM to run shell commands "
|
|
@@ -288,6 +288,14 @@ def check_api_key() -> Group:
|
|
|
288
288
|
# persisted detection so this reflects reality, not a cached boot.
|
|
289
289
|
if not os.environ.get("AETHER_DOCTOR_NO_PROBE"):
|
|
290
290
|
forget_detection()
|
|
291
|
+
# v5.2.2: doctor is the explicit revalidation surface, so the cached
|
|
292
|
+
# hardware profile is dropped too — otherwise "run doctor" would
|
|
293
|
+
# report on a profile from a previous boot.
|
|
294
|
+
try:
|
|
295
|
+
from aether.core import discovery_cache
|
|
296
|
+
discovery_cache.invalidate()
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
291
299
|
probing = not os.environ.get("AETHER_DOCTOR_NO_PROBE")
|
|
292
300
|
results: List[Result] = []
|
|
293
301
|
verified_any = False
|
|
@@ -621,6 +629,28 @@ _CHECK_FNS = [
|
|
|
621
629
|
]
|
|
622
630
|
|
|
623
631
|
|
|
632
|
+
#: Providers whose absence is a configuration choice, not a fault.
|
|
633
|
+
_NOT_SET_MARKER = "not set ("
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _collapse_provider_warnings(title, results):
|
|
637
|
+
"""Fold N unconfigured-provider warnings into one actionable line."""
|
|
638
|
+
if title != "LLM Providers":
|
|
639
|
+
return results
|
|
640
|
+
kept, unset = [], []
|
|
641
|
+
for status, msg in results:
|
|
642
|
+
if status == WARN and _NOT_SET_MARKER in msg:
|
|
643
|
+
unset.append(msg.split()[0])
|
|
644
|
+
continue
|
|
645
|
+
kept.append((status, msg))
|
|
646
|
+
if len(unset) > 1:
|
|
647
|
+
kept.insert(0, (INFO, f"{len(unset)} providers not configured "
|
|
648
|
+
f"({', '.join(unset)}) — any one is enough"))
|
|
649
|
+
elif unset:
|
|
650
|
+
kept.insert(0, (WARN, f"{unset[0]} not configured"))
|
|
651
|
+
return kept
|
|
652
|
+
|
|
653
|
+
|
|
624
654
|
def run_doctor(argv=None) -> int:
|
|
625
655
|
"""Run every check group, print the report, and return an exit code
|
|
626
656
|
(0 if no failures, 1 if any check FAILed)."""
|
|
@@ -634,21 +664,23 @@ def run_doctor(argv=None) -> int:
|
|
|
634
664
|
except Exception as e: # noqa: BLE001 — a check must never crash doctor
|
|
635
665
|
groups.append((fn.__name__, [(FAIL, f"check crashed: {e}")]))
|
|
636
666
|
|
|
637
|
-
|
|
667
|
+
from aether.core import ui
|
|
668
|
+
|
|
669
|
+
# v5.2.2 (FIX 3): four "KEY not set" provider warnings are ONE decision,
|
|
670
|
+
# not four problems. Collapsing them stops the summary reading 4 warnings
|
|
671
|
+
# when the user simply has not configured three optional providers.
|
|
672
|
+
groups = [(title, _collapse_provider_warnings(title, results))
|
|
673
|
+
for title, results in groups]
|
|
674
|
+
|
|
638
675
|
n_pass = n_warn = n_fail = 0
|
|
639
|
-
for
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
n_pass += 1
|
|
645
|
-
elif status == WARN:
|
|
646
|
-
n_warn += 1
|
|
647
|
-
elif status == FAIL:
|
|
648
|
-
n_fail += 1
|
|
649
|
-
print()
|
|
650
|
-
|
|
651
|
-
print(f" Summary: {n_pass} passing, {n_warn} warning(s), {n_fail} failure(s)")
|
|
676
|
+
for _title, results in groups:
|
|
677
|
+
for status, _msg in results:
|
|
678
|
+
n_pass += status == PASS
|
|
679
|
+
n_warn += status == WARN
|
|
680
|
+
n_fail += status == FAIL
|
|
652
681
|
code = 1 if n_fail else 0
|
|
682
|
+
|
|
683
|
+
ui.print_check_report("AETHER doctor", groups, _SYMBOL,
|
|
684
|
+
{"pass": n_pass, "warn": n_warn, "fail": n_fail})
|
|
653
685
|
print(f" Exit code: {code}")
|
|
654
686
|
return code
|
|
@@ -320,5 +320,7 @@ def run_kits(argv=None) -> int:
|
|
|
320
320
|
return info_kit(args.name)
|
|
321
321
|
if args.action == "install":
|
|
322
322
|
return install_kit(args.name)
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
# v5.2.2 (FIX 3): a bare `aether kits` lists the kits, which is what
|
|
324
|
+
# someone typing it wants. Raw argparse usage read as a different, older
|
|
325
|
+
# program than the rest of the CLI. `--help` still prints the usage.
|
|
326
|
+
return list_kits()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Per-phase boot timing (v5.2.2 FIX 1, step 1).
|
|
2
|
+
|
|
3
|
+
The plan cache made planning free (0 ms) and get_distance runs in 6 ms, yet
|
|
4
|
+
perceived latency is ~5 s — because boot runs before every objective. Which
|
|
5
|
+
phase costs what was a guess, and optimising a guess is how you spend a day
|
|
6
|
+
speeding up something that was never the problem.
|
|
7
|
+
|
|
8
|
+
This measures. It is deliberately the first thing built for v5.2.2, and the
|
|
9
|
+
numbers it produced are what selected the optimisations that followed.
|
|
10
|
+
|
|
11
|
+
Zero cost when idle: a monotonic subtraction and a dict append per phase.
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from typing import Dict, List
|
|
17
|
+
|
|
18
|
+
_phases: List[Dict] = []
|
|
19
|
+
_enabled = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reset() -> None:
|
|
23
|
+
_phases.clear()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def disable() -> None:
|
|
27
|
+
"""Used by tests and by paths where timing output would be noise."""
|
|
28
|
+
global _enabled
|
|
29
|
+
_enabled = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def enable() -> None:
|
|
33
|
+
global _enabled
|
|
34
|
+
_enabled = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def phase(name: str):
|
|
39
|
+
"""Time a named boot phase. Never swallows the body's exception."""
|
|
40
|
+
if not _enabled:
|
|
41
|
+
yield
|
|
42
|
+
return
|
|
43
|
+
start = time.perf_counter()
|
|
44
|
+
try:
|
|
45
|
+
yield
|
|
46
|
+
finally:
|
|
47
|
+
elapsed = (time.perf_counter() - start) * 1000.0
|
|
48
|
+
_phases.append({"phase": name, "ms": round(elapsed, 1)})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def record(name: str, ms: float) -> None:
|
|
52
|
+
"""Record a phase measured by the caller (e.g. an async warm-up)."""
|
|
53
|
+
if _enabled:
|
|
54
|
+
_phases.append({"phase": name, "ms": round(float(ms), 1)})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def phases() -> List[Dict]:
|
|
58
|
+
return list(_phases)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def total_ms() -> float:
|
|
62
|
+
return round(sum(p["ms"] for p in _phases), 1)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def summary_line(limit: int = 6) -> str:
|
|
66
|
+
"""`i2c_scan 340ms · mavlink_probe 3100ms · ... · total 4810ms`.
|
|
67
|
+
|
|
68
|
+
Slowest first: the point of the line is to name the phase worth fixing,
|
|
69
|
+
and boot order is not interesting once you already have the total.
|
|
70
|
+
"""
|
|
71
|
+
if not _phases:
|
|
72
|
+
return ""
|
|
73
|
+
ranked = sorted(_phases, key=lambda p: -p["ms"])[:limit]
|
|
74
|
+
parts = [f"{p['phase']} {p['ms']:.0f}ms" for p in ranked]
|
|
75
|
+
return " · ".join(parts) + f" · total {total_ms():.0f}ms"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def report() -> None:
|
|
79
|
+
"""Print the summary under AETHER_VERBOSE=1 and always emit analytics.
|
|
80
|
+
|
|
81
|
+
The line is verbose-only because it is diagnostic detail, but the events
|
|
82
|
+
are unconditional — the whole point is to learn the real distribution
|
|
83
|
+
across installs rather than only on machines being debugged.
|
|
84
|
+
"""
|
|
85
|
+
if not _phases:
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
if os.environ.get("AETHER_VERBOSE"):
|
|
89
|
+
print(f" [BOOT] {summary_line()}")
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
try:
|
|
93
|
+
from aether.core.analytics import track
|
|
94
|
+
for entry in _phases:
|
|
95
|
+
track("boot_latency", {"phase": entry["phase"], "ms": entry["ms"]})
|
|
96
|
+
track("boot_latency", {"phase": "total", "ms": total_ms()})
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Cached hardware profile keyed on a physical fingerprint (v5.2.2 FIX 1).
|
|
2
|
+
|
|
3
|
+
Boot dominates perceived latency now that planning is cached. Measured phases:
|
|
4
|
+
camera probe ~830 ms, software probe ~940 ms, and on a Pi the MAVLink scan
|
|
5
|
+
costs ~3.1 s because `serial.Serial(port, 57600, timeout=2)` blocks for two
|
|
6
|
+
seconds per candidate port and has never once succeeded on that hardware.
|
|
7
|
+
|
|
8
|
+
None of it changes between boots unless the hardware changes. So: fingerprint
|
|
9
|
+
the hardware cheaply, and if the fingerprint matches, reuse the whole manifest.
|
|
10
|
+
|
|
11
|
+
WHY THE FINGERPRINT INCLUDES THE I2C ADDRESS SCAN
|
|
12
|
+
--------------------------------------------------
|
|
13
|
+
The tempting fingerprint is device-node presence alone — pure filesystem stat,
|
|
14
|
+
microseconds. It is also wrong in a way that matters: swapping a PiCar-X HAT
|
|
15
|
+
for a GoPiGo3 leaves /dev/i2c-1 exactly as it was, so a node-only fingerprint
|
|
16
|
+
would match and AETHER would load the previous robot's adapter and send its
|
|
17
|
+
motor commands to different hardware.
|
|
18
|
+
|
|
19
|
+
The I2C ACK scan is ~340 ms on a Pi. Paying that to be certain which robot is
|
|
20
|
+
attached, while skipping ~4.4 s of everything else, is the right trade — a fast
|
|
21
|
+
boot that drives the wrong motors is not a win.
|
|
22
|
+
"""
|
|
23
|
+
import glob
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
from typing import Dict, Optional
|
|
29
|
+
|
|
30
|
+
_PATH = os.path.join(os.path.expanduser("~"), ".aether", "hardware_profile.json")
|
|
31
|
+
|
|
32
|
+
#: A profile older than this is re-probed regardless of fingerprint. Guards
|
|
33
|
+
#: against drift we cannot see (firmware, permissions, a library upgrade).
|
|
34
|
+
MAX_AGE_S = 7 * 24 * 3600
|
|
35
|
+
|
|
36
|
+
#: Device-node globs that change when hardware is plugged or unplugged.
|
|
37
|
+
_NODE_GLOBS = ("/dev/i2c-*", "/dev/spidev*", "/dev/ttyUSB*", "/dev/ttyACM*",
|
|
38
|
+
"/dev/video*", "/dev/serial0", "/dev/gpiomem")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def disabled() -> bool:
|
|
42
|
+
return bool(os.environ.get("AETHER_NO_DISCOVERY_CACHE"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _device_nodes() -> list:
|
|
46
|
+
nodes = []
|
|
47
|
+
for pattern in _NODE_GLOBS:
|
|
48
|
+
try:
|
|
49
|
+
nodes.extend(glob.glob(pattern))
|
|
50
|
+
except Exception:
|
|
51
|
+
continue
|
|
52
|
+
return sorted(set(nodes))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _i2c_addresses() -> list:
|
|
56
|
+
"""ACK-scan bus 1. Identifies WHICH board is attached, not just that one is.
|
|
57
|
+
|
|
58
|
+
Returns [] when smbus2 is absent or the bus cannot be opened, which is the
|
|
59
|
+
same value a genuinely empty bus produces — acceptable, because in both
|
|
60
|
+
cases there is no I2C hardware to misidentify.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
import smbus2
|
|
64
|
+
except Exception:
|
|
65
|
+
return []
|
|
66
|
+
try:
|
|
67
|
+
bus = smbus2.SMBus(1)
|
|
68
|
+
except Exception:
|
|
69
|
+
return []
|
|
70
|
+
found = []
|
|
71
|
+
try:
|
|
72
|
+
for address in range(0x03, 0x78):
|
|
73
|
+
try:
|
|
74
|
+
bus.read_byte(address)
|
|
75
|
+
found.append(address)
|
|
76
|
+
except OSError:
|
|
77
|
+
pass
|
|
78
|
+
except Exception:
|
|
79
|
+
return []
|
|
80
|
+
finally:
|
|
81
|
+
try:
|
|
82
|
+
bus.close()
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def fingerprint() -> str:
|
|
89
|
+
"""Stable hash of what is physically attached."""
|
|
90
|
+
import platform
|
|
91
|
+
import sys
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
from aether import __version__ as version
|
|
95
|
+
except Exception:
|
|
96
|
+
version = "?"
|
|
97
|
+
parts = [
|
|
98
|
+
platform.system(), platform.machine(),
|
|
99
|
+
f"py{sys.version_info.major}.{sys.version_info.minor}",
|
|
100
|
+
f"aether{version}", # a new release may probe differently
|
|
101
|
+
",".join(_device_nodes()),
|
|
102
|
+
",".join(hex(a) for a in _i2c_addresses()),
|
|
103
|
+
]
|
|
104
|
+
return hashlib.sha256("\n".join(parts).encode()).hexdigest()[:32]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load(current: Optional[str] = None) -> Optional[Dict]:
|
|
108
|
+
"""The cached manifest when the fingerprint still matches, else None."""
|
|
109
|
+
if disabled():
|
|
110
|
+
return None
|
|
111
|
+
try:
|
|
112
|
+
with open(_PATH) as handle:
|
|
113
|
+
payload = json.load(handle)
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
if not isinstance(payload, dict):
|
|
117
|
+
return None
|
|
118
|
+
if time.time() - float(payload.get("ts", 0)) > MAX_AGE_S:
|
|
119
|
+
return None
|
|
120
|
+
expected = current if current is not None else fingerprint()
|
|
121
|
+
if payload.get("fingerprint") != expected:
|
|
122
|
+
return None
|
|
123
|
+
manifest = payload.get("manifest")
|
|
124
|
+
return manifest if isinstance(manifest, dict) else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def save(manifest: Dict, current: Optional[str] = None) -> None:
|
|
128
|
+
"""Persist a freshly-probed manifest. Never raises."""
|
|
129
|
+
if disabled() or not isinstance(manifest, dict):
|
|
130
|
+
return
|
|
131
|
+
try:
|
|
132
|
+
os.makedirs(os.path.dirname(_PATH), exist_ok=True)
|
|
133
|
+
tmp = _PATH + ".tmp"
|
|
134
|
+
with open(tmp, "w") as handle:
|
|
135
|
+
json.dump({"fingerprint": current or fingerprint(),
|
|
136
|
+
"ts": time.time(), "manifest": manifest}, handle)
|
|
137
|
+
os.replace(tmp, _PATH)
|
|
138
|
+
os.chmod(_PATH, 0o600)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def invalidate() -> None:
|
|
144
|
+
"""Force a full re-probe. Called by `aether doctor`."""
|
|
145
|
+
try:
|
|
146
|
+
os.remove(_PATH)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
@@ -37,6 +37,12 @@ def _probe_camera_cv2() -> Tuple[bool, Dict]:
|
|
|
37
37
|
busy, which is the normal case while probing. Opening the capture is what
|
|
38
38
|
talks, so the suppression wraps the probe rather than the import.
|
|
39
39
|
"""
|
|
40
|
+
from aether.core import boot_timer
|
|
41
|
+
with boot_timer.phase("camera_probe"):
|
|
42
|
+
return __probe_camera_cv2_impl()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __probe_camera_cv2_impl() -> Tuple[bool, Dict]:
|
|
40
46
|
from aether.core.quiet import quiet_vendor_import
|
|
41
47
|
|
|
42
48
|
try:
|
|
@@ -120,6 +126,12 @@ def _probe_i2c() -> Tuple[bool, Dict]:
|
|
|
120
126
|
failure. The "devices" field is ALWAYS a list, never None. The whole
|
|
121
127
|
body is wrapped so a bug here can never crash boot.
|
|
122
128
|
"""
|
|
129
|
+
from aether.core import boot_timer
|
|
130
|
+
with boot_timer.phase("i2c_scan"):
|
|
131
|
+
return __probe_i2c_impl()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def __probe_i2c_impl() -> Tuple[bool, Dict]:
|
|
123
135
|
try:
|
|
124
136
|
try:
|
|
125
137
|
import smbus2
|
|
@@ -152,6 +164,12 @@ def _probe_i2c() -> Tuple[bool, Dict]:
|
|
|
152
164
|
|
|
153
165
|
def _probe_mavlink_serial() -> Tuple[bool, Dict]:
|
|
154
166
|
"""Scan serial ports for flight controller at 57600 baud."""
|
|
167
|
+
from aether.core import boot_timer
|
|
168
|
+
with boot_timer.phase("mavlink_probe"):
|
|
169
|
+
return __probe_mavlink_serial_impl()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def __probe_mavlink_serial_impl() -> Tuple[bool, Dict]:
|
|
155
173
|
candidates = []
|
|
156
174
|
if sys.platform == "win32":
|
|
157
175
|
candidates = [f"COM{i}" for i in range(1, 20)]
|
|
@@ -214,6 +232,12 @@ def _probe_oled() -> Tuple[bool, Dict]:
|
|
|
214
232
|
- spi_ready=True but available=False: SPI ok, library missing
|
|
215
233
|
- spi_ready=False: SPI interface not enabled
|
|
216
234
|
"""
|
|
235
|
+
from aether.core import boot_timer
|
|
236
|
+
with boot_timer.phase("oled_probe"):
|
|
237
|
+
return __probe_oled_impl()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def __probe_oled_impl() -> Tuple[bool, Dict]:
|
|
217
241
|
base_info = {
|
|
218
242
|
"interface": "spi",
|
|
219
243
|
"type": "SSD1306",
|
|
@@ -1030,17 +1054,42 @@ class ToolDiscovery:
|
|
|
1030
1054
|
self._all_caps: Dict[str, Dict] = {} # key → {available, detail, category, weight}
|
|
1031
1055
|
|
|
1032
1056
|
def discover(self) -> Dict:
|
|
1033
|
-
"""Run all probes and build the full capability manifest.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1057
|
+
"""Run all probes and build the full capability manifest.
|
|
1058
|
+
|
|
1059
|
+
v5.2.2: each phase is timed. Boot dominates perceived latency now that
|
|
1060
|
+
planning is cached, and the breakdown was previously unmeasured.
|
|
1061
|
+
"""
|
|
1062
|
+
from aether.core import boot_timer, discovery_cache
|
|
1063
|
+
|
|
1064
|
+
# v5.2.2: if the hardware fingerprint is unchanged, the whole probe
|
|
1065
|
+
# sweep is skipped. The fingerprint includes the I2C ACK scan, so a
|
|
1066
|
+
# swapped HAT is a miss rather than a fast boot onto the wrong adapter.
|
|
1067
|
+
with boot_timer.phase("fingerprint"):
|
|
1068
|
+
current = discovery_cache.fingerprint()
|
|
1069
|
+
cached = discovery_cache.load(current)
|
|
1070
|
+
if cached is not None:
|
|
1071
|
+
print(" [DISCOVERY] Using cached hardware profile "
|
|
1072
|
+
"(fingerprint match)")
|
|
1073
|
+
self._manifest = cached
|
|
1074
|
+
return self._manifest
|
|
1075
|
+
|
|
1076
|
+
with boot_timer.phase("platform"):
|
|
1077
|
+
self._platform = _detect_platform()
|
|
1078
|
+
with boot_timer.phase("serial_enum"):
|
|
1079
|
+
self._serial_ports = _detect_serial_ports()
|
|
1080
|
+
|
|
1081
|
+
with boot_timer.phase("hardware_probe"):
|
|
1082
|
+
self._probe_hardware()
|
|
1083
|
+
with boot_timer.phase("software_probe"):
|
|
1084
|
+
self._probe_software()
|
|
1085
|
+
with boot_timer.phase("network_probe"):
|
|
1086
|
+
self._probe_network()
|
|
1087
|
+
with boot_timer.phase("environment_probe"):
|
|
1088
|
+
self._probe_environment()
|
|
1041
1089
|
|
|
1042
1090
|
self._compute_score()
|
|
1043
1091
|
self._manifest = self._build_manifest()
|
|
1092
|
+
discovery_cache.save(self._manifest, current)
|
|
1044
1093
|
return self._manifest
|
|
1045
1094
|
|
|
1046
1095
|
# ── Hardware ──────────────────────────────────────────────────
|