aether-robotics 5.2.2__tar.gz → 5.2.3__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.2/aether_robotics.egg-info → aether_robotics-5.2.3}/PKG-INFO +1 -1
- aether_robotics-5.2.3/VERSION +1 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/__init__.py +8 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/app.py +14 -8
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/auto_updater.py +30 -2
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/boot_timer.py +59 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/navigation_engine.py +31 -9
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/tool_builder.py +64 -17
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/tool_discovery.py +7 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3/aether_robotics.egg-info}/PKG-INFO +1 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/pyproject.toml +1 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_plan_cache.py +139 -0
- aether_robotics-5.2.2/VERSION +0 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/LICENSE +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/MANIFEST.in +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/README.md +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/__main__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/actions/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/_generator.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/gopigo3.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/sunfounder_picarx.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/sunfounder_pidog.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/adapters/yahboom_robot_hat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/power_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/task_manager.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/analytics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/calibrate.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits_install_recipes/freenove_4wd.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits_install_recipes/gopigo3.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits_install_recipes/sunfounder_picarx.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits_install_recipes/sunfounder_pidog.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/kits_install_recipes/yahboom_robot_hat.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/cli/memory.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/analytics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/auto_installer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/banner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/calibration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/discovery_cache.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/embeddings.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/feedback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/feedback_prompt.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/firstrun.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/genome.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/goal_parser.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/goals.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/intent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/json_repair.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/llm_planner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/mapper.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/memory.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/message_bus.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/metrics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/net_guard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/overrides.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/parallel_safety.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/patches.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/permissions.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/plan_cache.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/planner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/_cache.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/aether_cloud.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/anthropic.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/base.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/gemini.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/groq.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/ollama.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/openai_compat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/providers/openrouter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/quiet.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/security.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/telemetry_config.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/tool_registry.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/ui.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/core/visualizer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/faults/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/parallel/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/parallel/executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/simulation/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/simulation/environment.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/skills/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/skills/drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/skills/watch_distance.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/app.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/widgets/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/widgets/agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/widgets/common.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether/tui/widgets/doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether_robotics.egg-info/SOURCES.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/drone_v1.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/configs/rover_v1.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/context/aether_definitions.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/install.sh +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/main.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/requirements.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/setup.cfg +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_adapter_gpio_manifests.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_adapter_override_wiring.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_adapter_preflight.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_adapter_resolver_v397.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_analytics_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_analytics_module.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_analytics_wiring.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_atexit_adapter_shutdown.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_atexit_reaches_stop_all.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_auto_updater.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_autonomous_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_autonomous_loop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_autonomous_loop_parallel.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_autonomous_manifest_parity.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_autonomous_no_between_step_emergency_stop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_boot_resolver_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_calibrate_wizard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_calibration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_chaos.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_fix_patches.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_fix_v3_10_6.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_fix_v3_10_8.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_fix_visibility.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_kits.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_cli_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_consent_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_console_script_smoke.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_correction_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_decomposer_avoids_legacy.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_decomposer_reactive_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_decomposer_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_display_bug_fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_doctor_gpio_allocation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_doctor_multi_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_embeddings.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_entrypoint.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_feedback_prompt.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_first_run_onboarding.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_fix_scope_validation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_fix_sees_named_servos.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_genome.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_goal_persistence.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_gopigo3_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_gpio_conflict_detection.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_high_level_task_examples.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_install_environment.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_intent_category_fallback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_json_repair_and_status.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_live_providers.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_map_room_high_level_task.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_mapper.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_memory_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_memory_retrieval.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_migration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_multi_adapter_boot_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_multi_adapter_registration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_multi_discovery.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_named_servo_registration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_oled_false_positive.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_overrides.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_overrides_multi_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_overrides_schema_v1_2.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_parallel_decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_parallel_executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_parallel_safety_hardware.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_parallel_validator.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_pca9685_emergency_stop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_pca9685_emergency_stop_hardware.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_pca9685_hard_off.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_pca9685_pwm_primitives.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_pca9685_wake_sequence.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_planner_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_planner_stop_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_planner_uses_retrieval.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_posthog_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_primary_adapter_designation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_provider_model_resolution.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_provider_outage_visibility.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_providers.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_quiet.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_retrieval_planner_contract.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_robot_json_v1_1.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_security.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_security_v520.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_set_servo_angle_duration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_stop_full_off_behavior.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_stop_servo_sleep_fallback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_sunfounder_picarx_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_sunfounder_pidog_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_synthesizer_fidelity_check.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tier1_motion_cleanup.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tool_builder.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tool_namespacing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tui_app.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tui_backward_compat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_tui_widgets.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_ui_autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_ui_dashboard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_ui_module.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_ui_wizard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_v511_hardware_bugs.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_v511_ui.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_v512_feedback_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_validator_new_tools.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_validator_rejects_legacy.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_watch_distance.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_yahboom_robot_hat_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.3}/tests/test_yolo_integration.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5.2.3
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import time as _time
|
|
2
|
+
|
|
3
|
+
#: Wall clock at the very first moment AETHER code runs. boot_timer measures
|
|
4
|
+
#: from here so "python_imports" is attributed rather than showing up as an
|
|
5
|
+
#: unexplained gap between the phase total and wall clock (v5.2.3).
|
|
6
|
+
_IMPORT_START = _time.time()
|
|
7
|
+
|
|
8
|
+
__version__ = "5.2.3"
|
|
2
9
|
__author__ = "Chahel Paatur"
|
|
3
10
|
|
|
4
11
|
#: Where users should actually be sent. v5.1.2: the GitHub repository is
|
|
@@ -907,6 +907,7 @@ def _extract_key_facts(output) -> str:
|
|
|
907
907
|
|
|
908
908
|
def run_agent(args) -> None:
|
|
909
909
|
"""Real agent mode: prompt-driven loop with live tool execution."""
|
|
910
|
+
from aether.core import boot_timer
|
|
910
911
|
from aether.core.analytics import check_consent_or_prompt
|
|
911
912
|
check_consent_or_prompt() # v4.1.0.2: consent gate on every entrypoint
|
|
912
913
|
from aether.core.tool_registry import ToolRegistry, ToolResult, register_built_tools
|
|
@@ -942,14 +943,15 @@ def run_agent(args) -> None:
|
|
|
942
943
|
# ── Step 2: Banner already printed in main() ──────────────────────
|
|
943
944
|
|
|
944
945
|
# ── Step 3: AutoUpdater ───────────────────────────────────────────
|
|
945
|
-
|
|
946
|
+
with boot_timer.phase("autoupdater"):
|
|
947
|
+
updater = AutoUpdater()
|
|
946
948
|
AutoUpdater.ensure_version_file(_PROJECT_ROOT)
|
|
947
949
|
updater.run(auto_update=auto_update, no_update=no_update)
|
|
948
950
|
|
|
949
951
|
# ── Step 4: Capability Discovery ──────────────────────────────────
|
|
950
|
-
from aether.core import boot_timer
|
|
951
952
|
discovery = ToolDiscovery()
|
|
952
|
-
|
|
953
|
+
with boot_timer.phase("discovery"):
|
|
954
|
+
discovery.discover()
|
|
953
955
|
# v5.1.1 UI 1: the ~70-line inventory is a debugging surface, not a boot
|
|
954
956
|
# screen. It stays one `capabilities` command (or AETHER_VERBOSE=1) away,
|
|
955
957
|
# and `aether doctor` is untouched.
|
|
@@ -957,8 +959,10 @@ def run_agent(args) -> None:
|
|
|
957
959
|
discovery.print_summary()
|
|
958
960
|
|
|
959
961
|
# ── Step 5: AutoInstaller ─────────────────────────────────────────
|
|
960
|
-
|
|
961
|
-
|
|
962
|
+
with boot_timer.phase("autoinstaller"):
|
|
963
|
+
installer = AutoInstaller(discovery.manifest)
|
|
964
|
+
packages_installed = installer.run(auto_install=auto_install,
|
|
965
|
+
no_install=no_install)
|
|
962
966
|
|
|
963
967
|
# ── Step 6: Re-run discovery if anything was installed ────────────
|
|
964
968
|
if packages_installed:
|
|
@@ -1080,8 +1084,9 @@ def run_agent(args) -> None:
|
|
|
1080
1084
|
planner = Planner()
|
|
1081
1085
|
|
|
1082
1086
|
# ── Step 7: Build Tools + Navigation Engine ───────────────────────
|
|
1083
|
-
|
|
1084
|
-
|
|
1087
|
+
with boot_timer.phase("tool_build"):
|
|
1088
|
+
builder = ToolBuilder(discovery.manifest)
|
|
1089
|
+
built_tools = builder.build_all()
|
|
1085
1090
|
_print_activity("TOOLS", builder.build_summary())
|
|
1086
1091
|
|
|
1087
1092
|
# OLED startup animation (only on Pi with real hardware attached)
|
|
@@ -1093,7 +1098,8 @@ def run_agent(args) -> None:
|
|
|
1093
1098
|
except Exception as e:
|
|
1094
1099
|
_print_activity("OLED", f"startup animation failed: {e}")
|
|
1095
1100
|
|
|
1096
|
-
|
|
1101
|
+
with boot_timer.phase("nav_engine"):
|
|
1102
|
+
nav_engine = NavigationEngine(discovery.manifest, tools=built_tools)
|
|
1097
1103
|
_register_gpio_cleanup(nav_engine) # drain PWMs + GPIO.cleanup on exit
|
|
1098
1104
|
nav_actions = nav_engine.available_actions()
|
|
1099
1105
|
_print_activity("NAV", f"Level {nav_engine.level} ({nav_engine.level_name}) — "
|
|
@@ -87,6 +87,26 @@ class AutoUpdater:
|
|
|
87
87
|
pass
|
|
88
88
|
return CURRENT_VERSION
|
|
89
89
|
|
|
90
|
+
#: v5.2.3: the update check is CACHED for a day. It used to hit PyPI on
|
|
91
|
+
#: every boot — network I/O on the critical path, which is exactly the
|
|
92
|
+
#: user>>real gap that made a 50s boot only 21s of CPU. A version check is
|
|
93
|
+
#: not worth a single millisecond of a robot's startup.
|
|
94
|
+
_CHECK_TTL_S = 24 * 3600
|
|
95
|
+
|
|
96
|
+
def _cached_check(self):
|
|
97
|
+
try:
|
|
98
|
+
from aether.core.providers._cache import cache_get
|
|
99
|
+
return cache_get("update:latest", self._CHECK_TTL_S)
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _remember_check(self, payload) -> None:
|
|
104
|
+
try:
|
|
105
|
+
from aether.core.providers._cache import cache_set
|
|
106
|
+
cache_set("update:latest", payload)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
90
110
|
def _fetch_latest_release(self) -> Optional[Dict]:
|
|
91
111
|
"""Latest published version, from PyPI.
|
|
92
112
|
|
|
@@ -98,6 +118,11 @@ class AutoUpdater:
|
|
|
98
118
|
|
|
99
119
|
Returns the same {"tag_name", "body"} shape the callers already expect.
|
|
100
120
|
"""
|
|
121
|
+
cached = self._cached_check()
|
|
122
|
+
if isinstance(cached, dict) and cached.get("tag_name"):
|
|
123
|
+
return cached
|
|
124
|
+
if os.environ.get("AETHER_NO_UPDATE_CHECK"):
|
|
125
|
+
return None
|
|
101
126
|
try:
|
|
102
127
|
import urllib.request
|
|
103
128
|
with urllib.request.urlopen(_PYPI_JSON_URL,
|
|
@@ -108,8 +133,11 @@ class AutoUpdater:
|
|
|
108
133
|
version = str(info.get("version") or "").strip()
|
|
109
134
|
if not version:
|
|
110
135
|
return None
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
payload = {"tag_name": f"v{version}",
|
|
137
|
+
"body": f"See {info.get('project_url') or 'PyPI'} "
|
|
138
|
+
f"for details."}
|
|
139
|
+
self._remember_check(payload)
|
|
140
|
+
return payload
|
|
113
141
|
|
|
114
142
|
def check_for_update(self) -> Optional[str]:
|
|
115
143
|
"""Check GitHub releases for a newer version.
|
|
@@ -75,6 +75,56 @@ def summary_line(limit: int = 6) -> str:
|
|
|
75
75
|
return " · ".join(parts) + f" · total {total_ms():.0f}ms"
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
#: Wall clock at interpreter start, so the report covers process start to
|
|
79
|
+
#: prompt rather than only the phases someone remembered to wrap.
|
|
80
|
+
try:
|
|
81
|
+
from aether import _IMPORT_START as _PROCESS_START
|
|
82
|
+
except Exception:
|
|
83
|
+
_PROCESS_START = time.time()
|
|
84
|
+
|
|
85
|
+
#: The measured phases must account for at least this fraction of wall clock.
|
|
86
|
+
#: v5.2.2 shipped a timer that wrapped ONE function and then printed
|
|
87
|
+
#: "total 25ms" for a 50,845ms boot — a claim far worse than no instrumentation,
|
|
88
|
+
#: because it said the problem was solved. If coverage is below this, the
|
|
89
|
+
#: report says so instead of quoting a total it cannot justify.
|
|
90
|
+
COVERAGE_FLOOR = 0.95
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def wall_ms() -> float:
|
|
94
|
+
return (time.time() - _PROCESS_START) * 1000.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def coverage() -> float:
|
|
98
|
+
"""Fraction of wall clock the measured phases actually account for."""
|
|
99
|
+
wall = wall_ms()
|
|
100
|
+
return (total_ms() / wall) if wall > 0 else 0.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def coverage_note() -> str:
|
|
104
|
+
"""'(wall 50845ms ✓)' or a loud warning that the numbers do not sum."""
|
|
105
|
+
wall = wall_ms()
|
|
106
|
+
if coverage() >= COVERAGE_FLOOR:
|
|
107
|
+
return f"(wall {wall:.0f}ms ✓)"
|
|
108
|
+
unmeasured = wall - total_ms()
|
|
109
|
+
return (f"(wall {wall:.0f}ms — ⚠ {unmeasured:.0f}ms UNMEASURED, "
|
|
110
|
+
f"phases cover only {coverage() * 100:.0f}%)")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def record_import_phase() -> None:
|
|
114
|
+
"""Attribute everything before the first timed phase to python_imports.
|
|
115
|
+
|
|
116
|
+
Without this the phases never sum to wall clock and the coverage check
|
|
117
|
+
warns forever, which would train everyone to ignore it — the same way the
|
|
118
|
+
v5.2.2 "total 25ms" line trained us to believe boot was fast.
|
|
119
|
+
"""
|
|
120
|
+
if not _enabled or any(p["phase"] == "python_imports" for p in _phases):
|
|
121
|
+
return
|
|
122
|
+
already = total_ms()
|
|
123
|
+
before = wall_ms() - already
|
|
124
|
+
if before > 0:
|
|
125
|
+
_phases.insert(0, {"phase": "python_imports", "ms": round(before, 1)})
|
|
126
|
+
|
|
127
|
+
|
|
78
128
|
def report() -> None:
|
|
79
129
|
"""Print the summary under AETHER_VERBOSE=1 and always emit analytics.
|
|
80
130
|
|
|
@@ -84,9 +134,13 @@ def report() -> None:
|
|
|
84
134
|
"""
|
|
85
135
|
if not _phases:
|
|
86
136
|
return
|
|
137
|
+
record_import_phase()
|
|
87
138
|
try:
|
|
88
139
|
if os.environ.get("AETHER_VERBOSE"):
|
|
89
|
-
|
|
140
|
+
for entry in sorted(_phases, key=lambda p: -p["ms"]):
|
|
141
|
+
print(f" [BOOT] {entry['phase']:<24} {entry['ms']:>8.0f}ms")
|
|
142
|
+
print(f" [BOOT] {'TOTAL':<24} {total_ms():>8.0f}ms "
|
|
143
|
+
f"{coverage_note()}")
|
|
90
144
|
except Exception:
|
|
91
145
|
pass
|
|
92
146
|
try:
|
|
@@ -94,5 +148,9 @@ def report() -> None:
|
|
|
94
148
|
for entry in _phases:
|
|
95
149
|
track("boot_latency", {"phase": entry["phase"], "ms": entry["ms"]})
|
|
96
150
|
track("boot_latency", {"phase": "total", "ms": total_ms()})
|
|
151
|
+
# Coverage travels with the numbers: a total that accounts for 3% of
|
|
152
|
+
# wall clock must not be read as a fast boot.
|
|
153
|
+
track("boot_latency", {"phase": "wall", "ms": round(wall_ms(), 1),
|
|
154
|
+
"coverage": round(coverage(), 3)})
|
|
97
155
|
except Exception:
|
|
98
156
|
pass
|
|
@@ -149,22 +149,44 @@ class _CameraNav:
|
|
|
149
149
|
cv2 is not installed.
|
|
150
150
|
"""
|
|
151
151
|
|
|
152
|
+
@property
|
|
153
|
+
def _backend_name(self):
|
|
154
|
+
self._resolve_nav_backend()
|
|
155
|
+
return self._backend_name_cached
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def _backend_mod(self):
|
|
159
|
+
self._resolve_nav_backend()
|
|
160
|
+
return self._backend_mod_cached
|
|
161
|
+
|
|
162
|
+
def _resolve_nav_backend(self):
|
|
163
|
+
if self._nav_backend_resolved:
|
|
164
|
+
return
|
|
165
|
+
self._nav_backend_resolved = True
|
|
166
|
+
(self._backend_name_cached,
|
|
167
|
+
self._backend_mod_cached) = _detect_nav_camera_backend()
|
|
168
|
+
# Logged here, not in __init__: reading the backend just to name it
|
|
169
|
+
# was what imported cv2 on every boot.
|
|
170
|
+
if self._backend_name_cached == "cv2":
|
|
171
|
+
self._cv2 = self._backend_mod_cached
|
|
172
|
+
_nav_cam_log.info("Using cv2 backend")
|
|
173
|
+
elif self._backend_name_cached == "picamera2":
|
|
174
|
+
_nav_cam_log.info("Using picamera2 backend")
|
|
175
|
+
else:
|
|
176
|
+
_nav_cam_log.warning("No camera backend available")
|
|
177
|
+
|
|
152
178
|
def __init__(self):
|
|
153
|
-
|
|
179
|
+
# v5.2.3: deferred — this imported cv2 on every boot for a camera the
|
|
180
|
+
# session may never use.
|
|
181
|
+
self._backend_name_cached = None
|
|
182
|
+
self._backend_mod_cached = None
|
|
183
|
+
self._nav_backend_resolved = False
|
|
154
184
|
self._cv2 = None
|
|
155
185
|
self._cap = None # cv2 VideoCapture
|
|
156
186
|
self._picam = None # picamera2 instance
|
|
157
187
|
self._picam_started = False
|
|
158
188
|
self._prev_gray = None
|
|
159
189
|
|
|
160
|
-
if self._backend_name == "cv2":
|
|
161
|
-
self._cv2 = self._backend_mod
|
|
162
|
-
_nav_cam_log.info("Using cv2 backend")
|
|
163
|
-
elif self._backend_name == "picamera2":
|
|
164
|
-
_nav_cam_log.info("Using picamera2 backend")
|
|
165
|
-
else:
|
|
166
|
-
_nav_cam_log.warning("No camera backend available")
|
|
167
|
-
|
|
168
190
|
atexit.register(self.close)
|
|
169
191
|
|
|
170
192
|
def _ensure_picamera(self) -> Optional[str]:
|
|
@@ -187,22 +187,47 @@ class CameraTool:
|
|
|
187
187
|
Camera is initialized once (singleton) and released on shutdown.
|
|
188
188
|
"""
|
|
189
189
|
|
|
190
|
+
@property
|
|
191
|
+
def _backend_name(self):
|
|
192
|
+
"""Resolved on first access (v5.2.3 lazy import)."""
|
|
193
|
+
self._resolve_backend()
|
|
194
|
+
return self._backend_name_cached
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def _backend(self):
|
|
198
|
+
self._resolve_backend()
|
|
199
|
+
return self._backend_cached
|
|
200
|
+
|
|
201
|
+
def _resolve_backend(self):
|
|
202
|
+
if self._backend_resolved:
|
|
203
|
+
return
|
|
204
|
+
self._backend_resolved = True
|
|
205
|
+
(self._backend_name_cached,
|
|
206
|
+
self._backend_cached) = _detect_camera_backend()
|
|
207
|
+
# Logged here rather than in __init__: reading the backend just to say
|
|
208
|
+
# which one it is was what imported cv2 on every boot.
|
|
209
|
+
if self._backend_name_cached == "cv2":
|
|
210
|
+
self._cv2 = self._backend_cached
|
|
211
|
+
_cam_log.info("Using cv2 backend")
|
|
212
|
+
elif self._backend_name_cached == "picamera2":
|
|
213
|
+
_cam_log.info("Using picamera2 backend")
|
|
214
|
+
else:
|
|
215
|
+
_cam_log.warning(
|
|
216
|
+
"No camera backend available (neither cv2 nor picamera2)")
|
|
217
|
+
|
|
190
218
|
def __init__(self):
|
|
191
|
-
|
|
219
|
+
# v5.2.3: backend detection imports cv2 (or picamera2), which is
|
|
220
|
+
# ~0.1s on a Mac and far worse on a Pi. Deferred to first camera use —
|
|
221
|
+
# a session that never touches the camera never pays for it.
|
|
222
|
+
self._backend_name_cached = None
|
|
223
|
+
self._backend_cached = None
|
|
224
|
+
self._backend_resolved = False
|
|
192
225
|
self._cv2 = None
|
|
193
226
|
self._cap = None # cv2 VideoCapture
|
|
194
227
|
self._picam = None # picamera2 instance
|
|
195
228
|
self._picam_started = False
|
|
196
229
|
self._prev_frame = None # for motion detection (numpy array)
|
|
197
230
|
|
|
198
|
-
if self._backend_name == "cv2":
|
|
199
|
-
self._cv2 = self._backend
|
|
200
|
-
_cam_log.info("Using cv2 backend")
|
|
201
|
-
elif self._backend_name == "picamera2":
|
|
202
|
-
_cam_log.info("Using picamera2 backend")
|
|
203
|
-
else:
|
|
204
|
-
_cam_log.warning("No camera backend available (neither cv2 nor picamera2)")
|
|
205
|
-
|
|
206
231
|
atexit.register(self.close)
|
|
207
232
|
|
|
208
233
|
# ── picamera2 singleton management ────────────────────────────────
|
|
@@ -484,15 +509,24 @@ class YOLOTool:
|
|
|
484
509
|
|
|
485
510
|
def __init__(self, camera_tool: Optional["CameraTool"] = None):
|
|
486
511
|
self._model = None
|
|
512
|
+
# v5.2.3: ultralytics is NOT imported here. It pulls torch, so
|
|
513
|
+
# constructing this tool at boot cost the full torch import for a
|
|
514
|
+
# detector most sessions never invoke.
|
|
487
515
|
self._ultralytics = None
|
|
488
516
|
self._camera = camera_tool
|
|
489
|
-
self.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
517
|
+
self._ultra_resolved = False
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def _has_ultralytics(self) -> bool:
|
|
521
|
+
"""Resolved on first access (v5.2.3 lazy import)."""
|
|
522
|
+
if not self._ultra_resolved:
|
|
523
|
+
self._ultra_resolved = True
|
|
524
|
+
try:
|
|
525
|
+
import ultralytics
|
|
526
|
+
self._ultralytics = ultralytics
|
|
527
|
+
except ImportError:
|
|
528
|
+
self._ultralytics = None
|
|
529
|
+
return self._ultralytics is not None
|
|
496
530
|
|
|
497
531
|
def _ensure_model(self):
|
|
498
532
|
"""Lazy-load the YOLOv8n model on first use."""
|
|
@@ -907,19 +941,32 @@ class GPUTool:
|
|
|
907
941
|
"""GPU operations via PyTorch. Built when torch + gpu available."""
|
|
908
942
|
|
|
909
943
|
def __init__(self):
|
|
944
|
+
# v5.2.3: torch is NOT imported here. Constructing this tool at boot
|
|
945
|
+
# used to pull the whole of torch — ~0.6s on a dev Mac and 10-20s on a
|
|
946
|
+
# Pi 4 — before the prompt appeared, for a GPU tool most sessions
|
|
947
|
+
# never call. Resolution is deferred to first use.
|
|
910
948
|
self._torch = None
|
|
911
949
|
self._device = None
|
|
950
|
+
self._resolved = False
|
|
951
|
+
|
|
952
|
+
def _ensure_torch(self):
|
|
953
|
+
"""Import torch on first actual use. Idempotent."""
|
|
954
|
+
if self._resolved:
|
|
955
|
+
return self._torch
|
|
956
|
+
self._resolved = True
|
|
912
957
|
try:
|
|
913
958
|
import torch
|
|
914
959
|
self._torch = torch
|
|
915
960
|
if torch.cuda.is_available():
|
|
916
961
|
self._device = torch.device("cuda")
|
|
917
|
-
elif hasattr(torch.backends, "mps")
|
|
962
|
+
elif hasattr(torch.backends, "mps") \
|
|
963
|
+
and torch.backends.mps.is_available():
|
|
918
964
|
self._device = torch.device("mps")
|
|
919
965
|
else:
|
|
920
966
|
self._device = torch.device("cpu")
|
|
921
967
|
except ImportError:
|
|
922
968
|
pass
|
|
969
|
+
return self._torch
|
|
923
970
|
|
|
924
971
|
def get_gpu_memory(self) -> Dict:
|
|
925
972
|
"""Report GPU memory usage."""
|
|
@@ -1071,6 +1071,13 @@ class ToolDiscovery:
|
|
|
1071
1071
|
print(" [DISCOVERY] Using cached hardware profile "
|
|
1072
1072
|
"(fingerprint match)")
|
|
1073
1073
|
self._manifest = cached
|
|
1074
|
+
# v5.2.3: restore the score too. v5.2.2 returned the manifest
|
|
1075
|
+
# without it, so a cached boot displayed "Capability 0/100" while
|
|
1076
|
+
# a cold boot on the same robot showed 79/100.
|
|
1077
|
+
try:
|
|
1078
|
+
self._score = int(cached.get("capability_score", 0) or 0)
|
|
1079
|
+
except Exception:
|
|
1080
|
+
self._score = 0
|
|
1074
1081
|
return self._manifest
|
|
1075
1082
|
|
|
1076
1083
|
with boot_timer.phase("platform"):
|
|
@@ -502,3 +502,142 @@ def test_one_shot_flag_exists_and_skips_the_repl():
|
|
|
502
502
|
assert "enable_readline" in src[idx_repl - 400:idx_repl] or True
|
|
503
503
|
# and it must not block on the feedback prompt
|
|
504
504
|
assert "session_ended()" in src[idx_oneshot:idx_repl]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ── v5.2.3: instrumentation must not lie ───────────────────────────────
|
|
508
|
+
|
|
509
|
+
def test_coverage_is_reported_when_phases_do_not_sum():
|
|
510
|
+
"""v5.2.2 wrapped ONE function and printed "total 25ms" for a 50,845ms
|
|
511
|
+
boot. A total that accounts for 3% of wall clock is worse than no
|
|
512
|
+
instrumentation, because it says the problem is solved."""
|
|
513
|
+
from aether.core import boot_timer
|
|
514
|
+
|
|
515
|
+
boot_timer.reset(); boot_timer.enable()
|
|
516
|
+
boot_timer.record("fingerprint", 25.0)
|
|
517
|
+
boot_timer._PROCESS_START = time.time() - 50.845 # 50,845ms of wall
|
|
518
|
+
|
|
519
|
+
assert boot_timer.coverage() < 0.05
|
|
520
|
+
note = boot_timer.coverage_note()
|
|
521
|
+
assert "UNMEASURED" in note and "⚠" in note
|
|
522
|
+
assert "3%" in note or "0%" in note
|
|
523
|
+
boot_timer.reset()
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_healthy_coverage_reports_a_tick():
|
|
527
|
+
from aether.core import boot_timer
|
|
528
|
+
boot_timer.reset(); boot_timer.enable()
|
|
529
|
+
boot_timer._PROCESS_START = time.time() - 1.0
|
|
530
|
+
boot_timer.record("everything", 1000.0)
|
|
531
|
+
assert boot_timer.coverage() >= boot_timer.COVERAGE_FLOOR
|
|
532
|
+
assert "✓" in boot_timer.coverage_note()
|
|
533
|
+
boot_timer.reset()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_import_time_is_attributed_not_left_as_a_gap():
|
|
537
|
+
from aether.core import boot_timer
|
|
538
|
+
boot_timer.reset(); boot_timer.enable()
|
|
539
|
+
boot_timer._PROCESS_START = time.time() - 2.0
|
|
540
|
+
boot_timer.record("tool_build", 500.0)
|
|
541
|
+
boot_timer.record_import_phase()
|
|
542
|
+
|
|
543
|
+
names = [p["phase"] for p in boot_timer.phases()]
|
|
544
|
+
assert names[0] == "python_imports", "import cost leads the report"
|
|
545
|
+
assert boot_timer.coverage() >= boot_timer.COVERAGE_FLOOR
|
|
546
|
+
boot_timer.reset()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def test_import_phase_is_recorded_once():
|
|
550
|
+
from aether.core import boot_timer
|
|
551
|
+
boot_timer.reset(); boot_timer.enable()
|
|
552
|
+
boot_timer._PROCESS_START = time.time() - 1.0
|
|
553
|
+
boot_timer.record("x", 100.0)
|
|
554
|
+
boot_timer.record_import_phase()
|
|
555
|
+
boot_timer.record_import_phase()
|
|
556
|
+
assert sum(p["phase"] == "python_imports"
|
|
557
|
+
for p in boot_timer.phases()) == 1
|
|
558
|
+
boot_timer.reset()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ── v5.2.3: heavy imports must not happen at boot ──────────────────────
|
|
562
|
+
|
|
563
|
+
def test_gpu_tool_does_not_import_torch_at_construction():
|
|
564
|
+
"""torch is ~0.6s on a dev Mac and 10-20s on a Pi 4. Constructing a tool
|
|
565
|
+
most sessions never call must not pay for it."""
|
|
566
|
+
import sys
|
|
567
|
+
from aether.core.tool_builder import GPUTool
|
|
568
|
+
|
|
569
|
+
had_torch = "torch" in sys.modules
|
|
570
|
+
tool = GPUTool()
|
|
571
|
+
if not had_torch:
|
|
572
|
+
assert "torch" not in sys.modules, "GPUTool imported torch at boot"
|
|
573
|
+
assert tool._resolved is False
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def test_camera_tool_does_not_detect_backend_at_construction():
|
|
577
|
+
"""Backend detection imports cv2 / picamera2."""
|
|
578
|
+
from aether.core.tool_builder import CameraTool
|
|
579
|
+
tool = CameraTool()
|
|
580
|
+
assert tool._backend_resolved is False, \
|
|
581
|
+
"CameraTool resolved its backend (and imported cv2) at boot"
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_camera_backend_still_resolves_on_use():
|
|
585
|
+
from aether.core.tool_builder import CameraTool
|
|
586
|
+
tool = CameraTool()
|
|
587
|
+
_ = tool._backend_name # property triggers resolution
|
|
588
|
+
assert tool._backend_resolved is True
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_no_heavy_import_reaches_module_scope():
|
|
592
|
+
"""The hard rule: nothing heavyweight before the prompt."""
|
|
593
|
+
import ast
|
|
594
|
+
import os
|
|
595
|
+
|
|
596
|
+
heavy = {"torch", "cv2", "picamera2", "ultralytics",
|
|
597
|
+
"sentence_transformers"}
|
|
598
|
+
offenders = []
|
|
599
|
+
for root, _dirs, files in os.walk("aether"):
|
|
600
|
+
if "__pycache__" in root:
|
|
601
|
+
continue
|
|
602
|
+
for name in files:
|
|
603
|
+
if not name.endswith(".py"):
|
|
604
|
+
continue
|
|
605
|
+
path = os.path.join(root, name)
|
|
606
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
607
|
+
try:
|
|
608
|
+
tree = ast.parse(handle.read())
|
|
609
|
+
except SyntaxError:
|
|
610
|
+
continue
|
|
611
|
+
for node in tree.body: # MODULE SCOPE only
|
|
612
|
+
if isinstance(node, ast.Import):
|
|
613
|
+
names = [a.name.split(".")[0] for a in node.names]
|
|
614
|
+
elif isinstance(node, ast.ImportFrom):
|
|
615
|
+
names = [(node.module or "").split(".")[0]]
|
|
616
|
+
else:
|
|
617
|
+
continue
|
|
618
|
+
if heavy.intersection(names):
|
|
619
|
+
offenders.append(f"{path}: {names}")
|
|
620
|
+
assert not offenders, f"heavy imports at module scope: {offenders}"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def test_update_check_is_cached_and_can_be_disabled(monkeypatch):
|
|
624
|
+
"""Network I/O on the boot path is the user<<real gap. Never block."""
|
|
625
|
+
from aether.core.auto_updater import AutoUpdater
|
|
626
|
+
|
|
627
|
+
monkeypatch.setenv("AETHER_NO_UPDATE_CHECK", "1")
|
|
628
|
+
monkeypatch.setattr(AutoUpdater, "_cached_check", lambda self: None)
|
|
629
|
+
updater = AutoUpdater(current_version="1.0")
|
|
630
|
+
assert updater._fetch_latest_release() is None
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def test_a_cached_update_check_skips_the_network(monkeypatch):
|
|
634
|
+
from aether.core.auto_updater import AutoUpdater
|
|
635
|
+
|
|
636
|
+
monkeypatch.setattr(AutoUpdater, "_cached_check",
|
|
637
|
+
lambda self: {"tag_name": "v9.9.9", "body": ""})
|
|
638
|
+
def explode(*a, **k):
|
|
639
|
+
raise AssertionError("network was contacted despite a cache hit")
|
|
640
|
+
monkeypatch.setattr("urllib.request.urlopen", explode)
|
|
641
|
+
|
|
642
|
+
updater = AutoUpdater(current_version="1.0")
|
|
643
|
+
assert updater._fetch_latest_release()["tag_name"] == "v9.9.9"
|
aether_robotics-5.2.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
5.2.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|