aether-robotics 5.2.1__tar.gz → 5.2.2__tar.gz

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