aether-robotics 5.2.2__tar.gz → 5.2.4__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.4}/PKG-INFO +17 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/README.md +16 -0
- aether_robotics-5.2.4/VERSION +1 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/__init__.py +8 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/app.py +104 -18
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/auto_updater.py +30 -2
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/boot_timer.py +59 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/embeddings.py +102 -5
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/memory.py +16 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/navigation_engine.py +31 -9
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/plan_cache.py +72 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_builder.py +64 -17
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_discovery.py +7 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4/aether_robotics.egg-info}/PKG-INFO +17 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/pyproject.toml +1 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_plan_cache.py +300 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_retrieval_planner_contract.py +1 -1
- aether_robotics-5.2.2/VERSION +0 -1
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/LICENSE +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/MANIFEST.in +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/__main__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/actions/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/_generator.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/gopigo3.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/sunfounder_picarx.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/sunfounder_pidog.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/yahboom_robot_hat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/power_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/task_manager.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/analytics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/calibrate.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/freenove_4wd.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/gopigo3.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/sunfounder_picarx.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/sunfounder_pidog.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/yahboom_robot_hat.yaml +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/memory.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/analytics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/auto_installer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/banner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/calibration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/discovery_cache.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/feedback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/feedback_prompt.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/firstrun.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/genome.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/goal_parser.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/goals.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/intent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/json_repair.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/llm_planner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/mapper.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/message_bus.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/metrics.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/net_guard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/overrides.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/parallel_safety.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/patches.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/permissions.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/planner.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/_cache.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/aether_cloud.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/anthropic.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/base.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/gemini.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/groq.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/ollama.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/openai_compat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/openrouter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/quiet.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/security.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/telemetry_config.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_registry.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/ui.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/visualizer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/parallel/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/parallel/executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/environment.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/watch_distance.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/app.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/__init__.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/common.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/SOURCES.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/drone_v1.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/rover_v1.json +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/context/aether_definitions.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/install.sh +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/main.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/requirements.txt +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/setup.cfg +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_gpio_manifests.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_override_wiring.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_preflight.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_resolver_v397.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_module.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_wiring.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_atexit_adapter_shutdown.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_atexit_reaches_stop_all.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_auto_updater.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_loop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_loop_parallel.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_manifest_parity.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_no_between_step_emergency_stop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_boot_resolver_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibrate_wizard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_chaos.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_doctor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_patches.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_v3_10_6.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_v3_10_8.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_visibility.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_kits.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_consent_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_console_script_smoke.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_correction_agent.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_avoids_legacy.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_reactive_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_display_bug_fix.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_doctor_gpio_allocation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_doctor_multi_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_drive_until_obstacle.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_embeddings.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_entrypoint.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_feedback_prompt.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_first_run_onboarding.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_fix_scope_validation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_fix_sees_named_servos.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_genome.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_goal_persistence.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_gopigo3_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_gpio_conflict_detection.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_high_level_task_examples.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_install_environment.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_intent_category_fallback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_json_repair_and_status.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_live_providers.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_map_room_high_level_task.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_mapper.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_memory_cli.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_memory_retrieval.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_migration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_adapter_boot_flow.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_adapter_registration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_discovery.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_named_servo_registration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_oled_false_positive.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides_multi_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides_schema_v1_2.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_decomposer.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_executor.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_safety_hardware.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_validator.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_emergency_stop.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_emergency_stop_hardware.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_hard_off.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_pwm_primitives.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_wake_sequence.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_stop_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_uses_retrieval.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_posthog_integration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_primary_adapter_designation.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_provider_model_resolution.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_provider_outage_visibility.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_providers.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_quiet.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_robot_json_v1_1.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_security.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_security_v520.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_set_servo_angle_duration.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_stop_full_off_behavior.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_stop_servo_sleep_fallback.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_sunfounder_picarx_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_sunfounder_pidog_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_synthesizer_fidelity_check.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tier1_motion_cleanup.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_builder.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_namespacing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_app.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_backward_compat.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_widgets.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_autonomous.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_dashboard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_module.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_wizard.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v511_hardware_bugs.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v511_ui.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v512_feedback_routing.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_validator_new_tools.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_validator_rejects_legacy.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_watch_distance.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_yahboom_robot_hat_adapter.py +0 -0
- {aether_robotics-5.2.2 → aether_robotics-5.2.4}/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.4
|
|
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,20 @@ Full engineering detail, including the algorithms and where they came from: [doc
|
|
|
144
144
|
|
|
145
145
|
## What's New
|
|
146
146
|
|
|
147
|
+
### v5.2.4 — The 22-second embedding load, and the warm-up that never ran
|
|
148
|
+
|
|
149
|
+
Loading MiniLM takes **~22 s on a Pi 4**. Everything else was noise.
|
|
150
|
+
|
|
151
|
+
- **The async warm-up had been dead since v5.1.0.** Its only call chain was `warm_up_async ← auto_migrate_on_boot ← print_startup`, and v5.1.0 moved `print_startup()` inside `if _verbose:` — so no normal boot warmed the model. It is now started explicitly from boot, nice'd, and instrumented (`[EMBED] warm-up loaded in 21840ms`).
|
|
152
|
+
- **`available()` was loading the model.** It was `get_model() is not None`, so asking "are embeddings available?" performed the full load — on every `plan_cache.enabled()`, and in the guard in front of the warm-up itself. One level down, `is_installed()` did `import sentence_transformers`, which pulls torch. Both are now genuinely cheap (`find_spec`, 30 ms).
|
|
153
|
+
- **Exact-match cache tier, no model required.** Repeating an objective verbatim — the common case — is now a normalized string comparison. Embedding similarity remains only as the fallback for genuinely reworded objectives.
|
|
154
|
+
- **`AETHER_RAG_MODE=auto|always|off`** (default `auto`): while the model is cold, retrieval is *skipped* and planning proceeds normally. An optimisation that costs 22 s to save 1 s must yield, not block.
|
|
155
|
+
- **`get_model()` is now lock-serialised.** Without it the main thread never waited for the warm-up thread — it started a *second* concurrent 22 s load.
|
|
156
|
+
- **`--objective` one-shot was broken**: it used the autonomous path, which has no plan-cache lookup, and discarded the result. It now checks the cache and prints the outcome.
|
|
157
|
+
- Never stall silently: `[MEM] Loading embedding model (first use, ~20s on a Pi)...`
|
|
158
|
+
|
|
159
|
+
**A correction:** the v5.2.2 and v5.2.3 latency claims were measured on a macOS dev box and extrapolated. The import-time work in v5.2.3 was real but was never the 50 s. This one was found by measuring on the hardware that had the problem.
|
|
160
|
+
|
|
147
161
|
### v5.2.2 — Boot is the latency now, so boot got measured
|
|
148
162
|
|
|
149
163
|
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:
|
|
@@ -187,6 +201,8 @@ Five sit above 0.88. And the discriminator isn't "contains a number" — *open*
|
|
|
187
201
|
|
|
188
202
|
**Faster.** A repeat objective no longer calls the LLM at all. On real hardware `get_distance` executes in 4-6 ms while planning takes ~1 s, so for sensor-only work the model was ~99% of wall clock — and users repeat themselves constantly.
|
|
189
203
|
|
|
204
|
+
> **Note on the `[rag]` extra:** the embedding model costs **~22 s to load once per process on a Pi 4**. AETHER warms it in the background and, by default (`AETHER_RAG_MODE=auto`), skips retrieval entirely while it is still cold — so a cold first objective plans normally rather than waiting. Exact repeats of a previous objective are served from cache without touching the model at all.
|
|
205
|
+
|
|
190
206
|
- **Semantic plan cache.** A successful plan is stored against the embedding of the objective that produced it; a semantically equivalent objective replays it. **~6 ms instead of ~1000 ms — roughly 158× on a repeat.** Every entry is keyed on the robot's genome hash and a mismatch is a *hard* miss: a cached plan is instructions for moving a physical machine and must never replay against different hardware. Parameters are replayed verbatim, never interpolated — 0.95 cosine still admits "2 seconds" vs "20 seconds". Needs the `[rag]` extra; degrades to normal planning without it.
|
|
191
207
|
- **Anthropic prompt caching** on the static system prefix (~71 tool descriptions), plus **latency instrumentation** on every planning call: `[PLAN] via anthropic in 847ms (cached prefix: 4200 tok)`.
|
|
192
208
|
- **Provider detection persisted** for 24 h against a credential-presence fingerprint, so boot stays off the detection path.
|
|
@@ -96,6 +96,20 @@ Full engineering detail, including the algorithms and where they came from: [doc
|
|
|
96
96
|
|
|
97
97
|
## What's New
|
|
98
98
|
|
|
99
|
+
### v5.2.4 — The 22-second embedding load, and the warm-up that never ran
|
|
100
|
+
|
|
101
|
+
Loading MiniLM takes **~22 s on a Pi 4**. Everything else was noise.
|
|
102
|
+
|
|
103
|
+
- **The async warm-up had been dead since v5.1.0.** Its only call chain was `warm_up_async ← auto_migrate_on_boot ← print_startup`, and v5.1.0 moved `print_startup()` inside `if _verbose:` — so no normal boot warmed the model. It is now started explicitly from boot, nice'd, and instrumented (`[EMBED] warm-up loaded in 21840ms`).
|
|
104
|
+
- **`available()` was loading the model.** It was `get_model() is not None`, so asking "are embeddings available?" performed the full load — on every `plan_cache.enabled()`, and in the guard in front of the warm-up itself. One level down, `is_installed()` did `import sentence_transformers`, which pulls torch. Both are now genuinely cheap (`find_spec`, 30 ms).
|
|
105
|
+
- **Exact-match cache tier, no model required.** Repeating an objective verbatim — the common case — is now a normalized string comparison. Embedding similarity remains only as the fallback for genuinely reworded objectives.
|
|
106
|
+
- **`AETHER_RAG_MODE=auto|always|off`** (default `auto`): while the model is cold, retrieval is *skipped* and planning proceeds normally. An optimisation that costs 22 s to save 1 s must yield, not block.
|
|
107
|
+
- **`get_model()` is now lock-serialised.** Without it the main thread never waited for the warm-up thread — it started a *second* concurrent 22 s load.
|
|
108
|
+
- **`--objective` one-shot was broken**: it used the autonomous path, which has no plan-cache lookup, and discarded the result. It now checks the cache and prints the outcome.
|
|
109
|
+
- Never stall silently: `[MEM] Loading embedding model (first use, ~20s on a Pi)...`
|
|
110
|
+
|
|
111
|
+
**A correction:** the v5.2.2 and v5.2.3 latency claims were measured on a macOS dev box and extrapolated. The import-time work in v5.2.3 was real but was never the 50 s. This one was found by measuring on the hardware that had the problem.
|
|
112
|
+
|
|
99
113
|
### v5.2.2 — Boot is the latency now, so boot got measured
|
|
100
114
|
|
|
101
115
|
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:
|
|
@@ -139,6 +153,8 @@ Five sit above 0.88. And the discriminator isn't "contains a number" — *open*
|
|
|
139
153
|
|
|
140
154
|
**Faster.** A repeat objective no longer calls the LLM at all. On real hardware `get_distance` executes in 4-6 ms while planning takes ~1 s, so for sensor-only work the model was ~99% of wall clock — and users repeat themselves constantly.
|
|
141
155
|
|
|
156
|
+
> **Note on the `[rag]` extra:** the embedding model costs **~22 s to load once per process on a Pi 4**. AETHER warms it in the background and, by default (`AETHER_RAG_MODE=auto`), skips retrieval entirely while it is still cold — so a cold first objective plans normally rather than waiting. Exact repeats of a previous objective are served from cache without touching the model at all.
|
|
157
|
+
|
|
142
158
|
- **Semantic plan cache.** A successful plan is stored against the embedding of the objective that produced it; a semantically equivalent objective replays it. **~6 ms instead of ~1000 ms — roughly 158× on a repeat.** Every entry is keyed on the robot's genome hash and a mismatch is a *hard* miss: a cached plan is instructions for moving a physical machine and must never replay against different hardware. Parameters are replayed verbatim, never interpolated — 0.95 cosine still admits "2 seconds" vs "20 seconds". Needs the `[rag]` extra; degrades to normal planning without it.
|
|
143
159
|
- **Anthropic prompt caching** on the static system prefix (~71 tool descriptions), plus **latency instrumentation** on every planning call: `[PLAN] via anthropic in 847ms (cached prefix: 4200 tok)`.
|
|
144
160
|
- **Provider detection persisted** for 24 h against a credential-presence fingerprint, so boot stays off the detection path.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5.2.4
|
|
@@ -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.4"
|
|
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}) — "
|
|
@@ -1218,6 +1224,19 @@ def run_agent(args) -> None:
|
|
|
1218
1224
|
# Load persistent memory
|
|
1219
1225
|
recent_memory = _load_agent_memory(5)
|
|
1220
1226
|
|
|
1227
|
+
# v5.2.4 (BUG 1): start the embedding warm-up EXPLICITLY. Its only call
|
|
1228
|
+
# chain was warm_up_async <- auto_migrate_on_boot <- print_startup, and
|
|
1229
|
+
# v5.1.0 moved print_startup() inside `if _verbose:` — so every normal
|
|
1230
|
+
# boot since has had no warm-up at all, and the first objective paid the
|
|
1231
|
+
# full ~22s model load on a Pi. Called here so it cannot be disconnected
|
|
1232
|
+
# again by a change to what boot prints.
|
|
1233
|
+
try:
|
|
1234
|
+
from aether.core import embeddings as _embeddings
|
|
1235
|
+
if _embeddings.available():
|
|
1236
|
+
_embeddings.warm_up_async()
|
|
1237
|
+
except Exception:
|
|
1238
|
+
pass
|
|
1239
|
+
|
|
1221
1240
|
# v5.1.0 FIX 6: the 71-line tool dump is a debugging aid, not an
|
|
1222
1241
|
# onboarding experience. It moves behind `tools` / --verbose, and a
|
|
1223
1242
|
# first-time user gets three things they can actually type instead.
|
|
@@ -1260,7 +1279,10 @@ def run_agent(args) -> None:
|
|
|
1260
1279
|
status = "OK" if mem.get("outcome") == "SUCCESS" else "FAIL"
|
|
1261
1280
|
print(f" [{status}] {mem.get('objective', '?')[:60]}")
|
|
1262
1281
|
|
|
1263
|
-
|
|
1282
|
+
_is_one_shot = bool((getattr(args, "objective", "") or "").strip())
|
|
1283
|
+
if _is_one_shot:
|
|
1284
|
+
pass # v5.2.4: scripted use gets no REPL banner
|
|
1285
|
+
elif _firstrun.check_and_mark(_firstrun.QUICKSTART_SHOWN):
|
|
1264
1286
|
_print_quickstart(available)
|
|
1265
1287
|
else:
|
|
1266
1288
|
print("\nType an objective ('tools' to list tools, "
|
|
@@ -1320,11 +1342,16 @@ def run_agent(args) -> None:
|
|
|
1320
1342
|
# invocation must not touch shell history or block waiting on input.
|
|
1321
1343
|
_one_shot = (getattr(args, "objective", "") or "").strip()
|
|
1322
1344
|
if _one_shot:
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1345
|
+
# v5.2.4 (BUG 2): _execute_objective is the autonomous/TUI path and
|
|
1346
|
+
# has NO plan-cache lookup, so one-shot mode always paid a full API
|
|
1347
|
+
# call for an objective the REPL would have served from cache — and
|
|
1348
|
+
# its return value was discarded, so nothing was printed either. Route
|
|
1349
|
+
# through the shared helper the REPL uses instead.
|
|
1350
|
+
_result = _run_one_shot(_one_shot, registry, llm_planner, corrector,
|
|
1351
|
+
parser, manifest, _load_agent_memory(5),
|
|
1352
|
+
available)
|
|
1326
1353
|
_feedback.session_ended()
|
|
1327
|
-
|
|
1354
|
+
sys.exit(0 if _result.get("status") in ("SUCCESS", "DEGRADED") else 1)
|
|
1328
1355
|
|
|
1329
1356
|
_ui.enable_readline() # v5.1.1 UI 2: history + line editing
|
|
1330
1357
|
|
|
@@ -1397,10 +1424,6 @@ def run_agent(args) -> None:
|
|
|
1397
1424
|
plan = None
|
|
1398
1425
|
plan_source = "keyword"
|
|
1399
1426
|
|
|
1400
|
-
# Inject planning hints from past experience
|
|
1401
|
-
hints = _persistent_memory.planning_hints(objective)
|
|
1402
|
-
if hints:
|
|
1403
|
-
_print_activity("MEM", "Found similar past objectives")
|
|
1404
1427
|
|
|
1405
1428
|
if oled_tool is not None:
|
|
1406
1429
|
try:
|
|
@@ -1413,7 +1436,19 @@ def run_agent(args) -> None:
|
|
|
1413
1436
|
# the genome hash — a plan built for one robot must never run on
|
|
1414
1437
|
# another, so a hash mismatch is a hard miss, not a fuzzy match.
|
|
1415
1438
|
from aether.core import plan_cache as _plan_cache
|
|
1416
|
-
|
|
1439
|
+
# v5.2.4 (BUG 1): the cache is consulted FIRST. On a hit the planner
|
|
1440
|
+
# is never called, so building its retrieval context beforehand was
|
|
1441
|
+
# pure waste — and on a cold model it was a ~22s stall to produce
|
|
1442
|
+
# hints nothing would read.
|
|
1443
|
+
_cache_hit = (_plan_cache.lookup_exact(objective, _genome_hash(),
|
|
1444
|
+
available)
|
|
1445
|
+
or _plan_cache.lookup(objective, _genome_hash(),
|
|
1446
|
+
available))
|
|
1447
|
+
hints = None
|
|
1448
|
+
if _cache_hit is None:
|
|
1449
|
+
hints = _persistent_memory.planning_hints(objective)
|
|
1450
|
+
if hints:
|
|
1451
|
+
_print_activity("MEM", "Found similar past objectives")
|
|
1417
1452
|
if _cache_hit is not None:
|
|
1418
1453
|
_entry, _similarity = _cache_hit
|
|
1419
1454
|
plan = [dict(step) for step in _entry["plan"]]
|
|
@@ -2301,6 +2336,57 @@ def _summarize_skill_output(output) -> Optional[str]:
|
|
|
2301
2336
|
return None
|
|
2302
2337
|
|
|
2303
2338
|
|
|
2339
|
+
def _run_one_shot(objective, registry, llm_planner, corrector, parser,
|
|
2340
|
+
manifest, recent_memory, available):
|
|
2341
|
+
"""Execute one objective and PRINT what happened (v5.2.4 BUG 2).
|
|
2342
|
+
|
|
2343
|
+
Shares _execute_objective with autonomous mode, but adds the two things
|
|
2344
|
+
one-shot mode was missing: a plan-cache lookup before planning, and
|
|
2345
|
+
printing the result rather than exiting silently.
|
|
2346
|
+
"""
|
|
2347
|
+
from aether.core import plan_cache as _plan_cache
|
|
2348
|
+
|
|
2349
|
+
hit = (_plan_cache.lookup_exact(objective, _genome_hash(), available)
|
|
2350
|
+
or _plan_cache.lookup(objective, _genome_hash(), available))
|
|
2351
|
+
if hit is not None:
|
|
2352
|
+
entry, similarity = hit
|
|
2353
|
+
print(f" [CACHE] Reusing plan (similarity {similarity:.2f}) — "
|
|
2354
|
+
f"skipped the planning call")
|
|
2355
|
+
llm_planner = _CachedPlanner(entry["plan"], llm_planner)
|
|
2356
|
+
|
|
2357
|
+
result = _execute_objective(objective, registry, llm_planner, corrector,
|
|
2358
|
+
parser, manifest, recent_memory, available)
|
|
2359
|
+
|
|
2360
|
+
outcome = result.get("status", "?")
|
|
2361
|
+
print(f"\n {'✓' if outcome == 'SUCCESS' else '!'} {outcome} · "
|
|
2362
|
+
f"{len(result.get('actions', []))} action(s) · "
|
|
2363
|
+
f"{result.get('faults', 0)} fault(s)")
|
|
2364
|
+
payload = result.get("result")
|
|
2365
|
+
if payload:
|
|
2366
|
+
text = _summarize_output(payload)
|
|
2367
|
+
print(f" {text}")
|
|
2368
|
+
return result
|
|
2369
|
+
|
|
2370
|
+
|
|
2371
|
+
class _CachedPlanner:
|
|
2372
|
+
"""Serves a cached plan in place of an API call, keeping the planner's
|
|
2373
|
+
interface so _execute_objective is unchanged."""
|
|
2374
|
+
|
|
2375
|
+
def __init__(self, plan, wrapped):
|
|
2376
|
+
self._plan = [dict(step) for step in plan]
|
|
2377
|
+
self._wrapped = wrapped
|
|
2378
|
+
self.last_raw_response = ""
|
|
2379
|
+
self.last_provider_error = ""
|
|
2380
|
+
self.last_latency_ms = 0.0
|
|
2381
|
+
|
|
2382
|
+
@property
|
|
2383
|
+
def available(self):
|
|
2384
|
+
return True
|
|
2385
|
+
|
|
2386
|
+
def plan(self, objective, manifest, memory=None):
|
|
2387
|
+
return self._plan
|
|
2388
|
+
|
|
2389
|
+
|
|
2304
2390
|
def _execute_objective(objective: str, registry, llm_planner, corrector,
|
|
2305
2391
|
parser_obj, manifest, recent_memory,
|
|
2306
2392
|
available, fix_mode: bool = False,
|
|
@@ -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
|
|
@@ -55,14 +55,54 @@ def reset_model() -> None:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def is_installed() -> bool:
|
|
58
|
-
"""True when
|
|
58
|
+
"""True when sentence-transformers is present — WITHOUT importing it.
|
|
59
|
+
|
|
60
|
+
v5.2.4: this used to `import sentence_transformers`, which pulls torch —
|
|
61
|
+
~0.6s on a dev Mac and 10-20s on a Pi 4. So the cheap-looking capability
|
|
62
|
+
check was itself the expensive thing, one level below available(). find_spec
|
|
63
|
+
locates the package without executing it.
|
|
64
|
+
"""
|
|
59
65
|
try:
|
|
60
|
-
import
|
|
61
|
-
return
|
|
66
|
+
import importlib.util
|
|
67
|
+
return importlib.util.find_spec("sentence_transformers") is not None
|
|
62
68
|
except Exception:
|
|
63
69
|
return False
|
|
64
70
|
|
|
65
71
|
|
|
72
|
+
_LOAD_LOCK = None
|
|
73
|
+
_warm_started = 0.0
|
|
74
|
+
_warm_elapsed = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _lock():
|
|
78
|
+
"""One lock, created lazily so import stays cheap."""
|
|
79
|
+
global _LOAD_LOCK
|
|
80
|
+
if _LOAD_LOCK is None:
|
|
81
|
+
import threading
|
|
82
|
+
_LOAD_LOCK = threading.Lock()
|
|
83
|
+
return _LOAD_LOCK
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_loaded() -> bool:
|
|
87
|
+
"""True when the model is resident and embedding is effectively free.
|
|
88
|
+
|
|
89
|
+
v5.2.4: callers use this to DECIDE whether an optional embedding is worth
|
|
90
|
+
it. On a Pi 4 a cold load is ~22s, so retrieval — an optimisation for a
|
|
91
|
+
~1s planning call — must never be the thing that blocks.
|
|
92
|
+
"""
|
|
93
|
+
return _model is not None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def warm_state() -> dict:
|
|
97
|
+
"""Diagnostics for the warm-up thread (v5.2.4)."""
|
|
98
|
+
import threading
|
|
99
|
+
alive = any(t.name == "aether-embed-warmup" and t.is_alive()
|
|
100
|
+
for t in threading.enumerate())
|
|
101
|
+
return {"loaded": is_loaded(), "failed": _load_failed,
|
|
102
|
+
"thread_alive": alive, "started_at": _warm_started,
|
|
103
|
+
"elapsed_ms": _warm_elapsed}
|
|
104
|
+
|
|
105
|
+
|
|
66
106
|
def get_model():
|
|
67
107
|
"""Return the cached SentenceTransformer, loading it on first use.
|
|
68
108
|
|
|
@@ -74,6 +114,27 @@ def get_model():
|
|
|
74
114
|
return _model
|
|
75
115
|
if _load_failed:
|
|
76
116
|
return None
|
|
117
|
+
|
|
118
|
+
# v5.2.4: without this lock the main thread did NOT wait for the warm-up
|
|
119
|
+
# thread — it saw _model is None and started a SECOND concurrent load.
|
|
120
|
+
# Two 22-second torch loads competing for four Pi cores is worse than one.
|
|
121
|
+
with _lock():
|
|
122
|
+
if _model is not None:
|
|
123
|
+
return _model
|
|
124
|
+
if _load_failed:
|
|
125
|
+
return None
|
|
126
|
+
# v5.2.4: fourteen silent seconds reads as a hang. Announced on the
|
|
127
|
+
# foreground path only — the warm-up thread has its own [EMBED] line
|
|
128
|
+
# and should not print over the boot screen.
|
|
129
|
+
import threading as _th
|
|
130
|
+
if _th.current_thread().name != "aether-embed-warmup":
|
|
131
|
+
print(" [MEM] Loading embedding model "
|
|
132
|
+
"(first use, ~20s on a Pi)...", flush=True)
|
|
133
|
+
return _load_model_locked()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_model_locked():
|
|
137
|
+
global _model, _load_failed
|
|
77
138
|
try:
|
|
78
139
|
# Keep HF quiet: progress bars and telemetry would corrupt the v4.2.0
|
|
79
140
|
# TUI screen and add noise to classic CLI output.
|
|
@@ -106,8 +167,24 @@ def get_model():
|
|
|
106
167
|
|
|
107
168
|
|
|
108
169
|
def available() -> bool:
|
|
109
|
-
"""True when embeddings
|
|
110
|
-
|
|
170
|
+
"""True when embeddings COULD be computed — without loading the model.
|
|
171
|
+
|
|
172
|
+
v5.2.4: this used to be `get_model() is not None`, i.e. asking "are
|
|
173
|
+
embeddings available?" performed the full ~22s load on a Pi. Every caller
|
|
174
|
+
that thought it was making a cheap capability check was paying for it,
|
|
175
|
+
including plan_cache.enabled() on every objective and the guard in front
|
|
176
|
+
of warm_up_async — which meant deciding whether to warm the model
|
|
177
|
+
asynchronously first loaded it synchronously.
|
|
178
|
+
|
|
179
|
+
Availability is now package-installability plus "we have not already
|
|
180
|
+
failed", which is what callers actually meant. Use is_loaded() to ask
|
|
181
|
+
whether embedding is free RIGHT NOW.
|
|
182
|
+
"""
|
|
183
|
+
if _load_failed:
|
|
184
|
+
return False
|
|
185
|
+
if _model is not None:
|
|
186
|
+
return True
|
|
187
|
+
return is_installed()
|
|
111
188
|
|
|
112
189
|
|
|
113
190
|
# Single source of truth for "why is retrieval off?" so every surface gives
|
|
@@ -143,14 +220,34 @@ def warm_up_async() -> None:
|
|
|
143
220
|
import threading
|
|
144
221
|
|
|
145
222
|
def _load():
|
|
223
|
+
global _warm_elapsed
|
|
224
|
+
import time as _t
|
|
225
|
+
start = _t.time()
|
|
146
226
|
try:
|
|
227
|
+
# v5.2.4: nice the thread. The load is ~22s of CPU-bound torch
|
|
228
|
+
# work; on a 4-core Pi at default priority it contends with the
|
|
229
|
+
# boot path it is supposed to be hiding behind.
|
|
230
|
+
try:
|
|
231
|
+
os.nice(10)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
147
234
|
get_model()
|
|
148
235
|
except Exception:
|
|
149
236
|
pass
|
|
237
|
+
finally:
|
|
238
|
+
_warm_elapsed = round((_t.time() - start) * 1000.0, 1)
|
|
239
|
+
if os.environ.get("AETHER_VERBOSE"):
|
|
240
|
+
state = "loaded" if is_loaded() else "FAILED"
|
|
241
|
+
print(f" [EMBED] warm-up {state} in {_warm_elapsed:.0f}ms")
|
|
150
242
|
|
|
243
|
+
global _warm_started
|
|
151
244
|
try:
|
|
245
|
+
import time as _t
|
|
246
|
+
_warm_started = _t.time()
|
|
152
247
|
threading.Thread(target=_load, name="aether-embed-warmup",
|
|
153
248
|
daemon=True).start()
|
|
249
|
+
if os.environ.get("AETHER_VERBOSE"):
|
|
250
|
+
print(" [EMBED] warm-up thread started")
|
|
154
251
|
except Exception:
|
|
155
252
|
pass
|
|
156
253
|
|
|
@@ -561,7 +561,23 @@ def build_retrieval_block(objective: str, k: int = 3,
|
|
|
561
561
|
unavailable, or anything at all goes wrong — callers append the result
|
|
562
562
|
unconditionally, so an empty string means the prompt is byte-identical
|
|
563
563
|
to v4.2.0. Never injects empty scaffolding.
|
|
564
|
+
|
|
565
|
+
v5.2.4: in AETHER_RAG_MODE=auto (the default) this returns "" while the
|
|
566
|
+
embedding model is still cold. Retrieval improves a ~1s planning call; on
|
|
567
|
+
a Pi 4 loading MiniLM costs ~22s. An optimisation that is twenty times
|
|
568
|
+
more expensive than what it optimises must yield, not block.
|
|
564
569
|
"""
|
|
570
|
+
try:
|
|
571
|
+
from aether.core.plan_cache import embedding_ready, rag_mode
|
|
572
|
+
if not embedding_ready():
|
|
573
|
+
if rag_mode() == "auto":
|
|
574
|
+
import os as _os
|
|
575
|
+
if _os.environ.get("AETHER_VERBOSE"):
|
|
576
|
+
print(" [MEM] Embedding model still loading — "
|
|
577
|
+
"planning without retrieval this time")
|
|
578
|
+
return ""
|
|
579
|
+
except Exception:
|
|
580
|
+
pass
|
|
565
581
|
try:
|
|
566
582
|
# Resolve the default path at call time (not via the __init__ default
|
|
567
583
|
# arg, which binds at import) so tests can redirect it.
|
|
@@ -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]:
|