aether-robotics 3.7.2__tar.gz → 3.7.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-3.7.2/aether_robotics.egg-info → aether_robotics-3.7.4}/PKG-INFO +2 -2
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/README.md +1 -1
- aether_robotics-3.7.4/VERSION +1 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/__init__.py +1 -1
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/__init__.py +45 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/app.py +40 -3
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/tool_discovery.py +50 -6
- {aether_robotics-3.7.2 → aether_robotics-3.7.4/aether_robotics.egg-info}/PKG-INFO +2 -2
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/SOURCES.txt +1 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/pyproject.toml +1 -1
- aether_robotics-3.7.4/tests/test_boot_resolver_flow.py +125 -0
- aether_robotics-3.7.4/tests/test_tool_discovery.py +133 -0
- aether_robotics-3.7.2/VERSION +0 -1
- aether_robotics-3.7.2/tests/test_tool_discovery.py +0 -46
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/LICENSE +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/MANIFEST.in +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/__main__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/actions/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/_generator.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/sunfounder_picarx.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/power_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/task_manager.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/auto_installer.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/auto_updater.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/banner.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/calibration.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/executor.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/feedback.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/genome.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/goal_parser.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/llm_planner.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/mapper.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/memory.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/message_bus.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/metrics.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/navigation_engine.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/planner.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/tool_builder.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/tool_registry.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/core/visualizer.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/faults/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/simulation/__init__.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/simulation/environment.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/drone_v1.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/rover_v1.json +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/context/aether_definitions.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/install.sh +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/main.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/requirements.txt +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/setup.cfg +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_auto_updater.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_calibration.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_chaos.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_entrypoint.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_genome.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_mapper.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_security.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_tool_builder.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/tests/test_yolo_integration.py +0 -0
- {aether_robotics-3.7.2 → aether_robotics-3.7.4}/weights/fault_agent.npy +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-robotics
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.4
|
|
4
4
|
Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
|
|
5
5
|
Author: Chahel Paatur
|
|
6
6
|
License: MIT
|
|
@@ -57,7 +57,7 @@ Dynamic: license-file
|
|
|
57
57
|
████████████████████████████████
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
# AETHER v3.7.
|
|
60
|
+
# AETHER v3.7.4 — Autonomous Robotics Operating System
|
|
61
61
|
|
|
62
62
|
> AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
|
|
63
63
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
████████████████████████████████
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
# AETHER v3.7.
|
|
19
|
+
# AETHER v3.7.4 — Autonomous Robotics Operating System
|
|
20
20
|
|
|
21
21
|
> AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
|
|
22
22
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.7.4
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "3.7.
|
|
1
|
+
__version__ = "3.7.4"
|
|
2
2
|
__author__ = "Chahel Paatur"
|
|
@@ -136,6 +136,51 @@ def _build_vendor_lib_list() -> List[str]:
|
|
|
136
136
|
return present
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def detect_adapter(manifest: Dict, i2c_devices: Optional[List] = None,
|
|
140
|
+
include_user_dir: bool = True) -> Optional[str]:
|
|
141
|
+
"""Detection-only: would any adapter claim this hardware? No registration.
|
|
142
|
+
|
|
143
|
+
Mirrors the match logic of load_tier1 (signature_matches on each Tier 1
|
|
144
|
+
adapter) plus the presence of any user-dir generated/guided adapter,
|
|
145
|
+
WITHOUT calling register() or touching the registry. Used at boot to
|
|
146
|
+
decide whether the interactive calibration wizard should run: the
|
|
147
|
+
resolver's verdict must be known BEFORE the (blocking) wizard fires,
|
|
148
|
+
otherwise the wizard preempts the resolver on a fresh profile.
|
|
149
|
+
|
|
150
|
+
Returns the adapter_id that would match, or None.
|
|
151
|
+
"""
|
|
152
|
+
i2c_devices = i2c_devices or (
|
|
153
|
+
manifest.get("hardware", {}).get("i2c", {}).get("devices", []))
|
|
154
|
+
vendor_libs = _build_vendor_lib_list()
|
|
155
|
+
|
|
156
|
+
for path in sorted(glob.glob(os.path.join(_ADAPTER_DIR, "*.py"))):
|
|
157
|
+
basename = os.path.basename(path)
|
|
158
|
+
if basename.startswith("_") or basename in ("__init__.py",
|
|
159
|
+
"base_adapter.py", "rover_adapter.py",
|
|
160
|
+
"drone_adapter.py", "universal_adapter.py"):
|
|
161
|
+
continue
|
|
162
|
+
mod_name = f"aether.adapters._detect_{basename[:-3]}"
|
|
163
|
+
try:
|
|
164
|
+
spec = importlib.util.spec_from_file_location(mod_name, path)
|
|
165
|
+
mod = importlib.util.module_from_spec(spec)
|
|
166
|
+
spec.loader.exec_module(mod)
|
|
167
|
+
if mod.signature_matches(manifest, i2c_devices, vendor_libs):
|
|
168
|
+
return basename[:-3]
|
|
169
|
+
except Exception:
|
|
170
|
+
continue # a broken adapter never blocks detection
|
|
171
|
+
|
|
172
|
+
# A user-dir adapter (from a previous Tier 2/3 run) is loaded
|
|
173
|
+
# unconditionally by load_user_adapters, so its mere presence means the
|
|
174
|
+
# robot is already configured — count it as a match for the wizard gate.
|
|
175
|
+
if include_user_dir and os.path.isdir(_USER_ADAPTER_DIR):
|
|
176
|
+
for fname in sorted(os.listdir(_USER_ADAPTER_DIR)):
|
|
177
|
+
if fname.endswith(".py") and (
|
|
178
|
+
fname.startswith("generated_") or fname.startswith("guided_")):
|
|
179
|
+
return fname.replace(".py", "")
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
139
184
|
def load_tier1(manifest: Dict, registry, i2c_devices: Optional[List] = None) -> Optional[str]:
|
|
140
185
|
"""Iterate pre-built adapters. First match wins; returns adapter_id or None.
|
|
141
186
|
|
|
@@ -401,6 +401,22 @@ def _print_fault_alert(fault_type: str, details: str) -> None:
|
|
|
401
401
|
print(f" !! FAULT: {fault_type} — {details}")
|
|
402
402
|
|
|
403
403
|
|
|
404
|
+
def _should_run_autocal_wizard(pre_adapter_id, calibration_profile,
|
|
405
|
+
needs_calibration: bool) -> bool:
|
|
406
|
+
"""Auto-detect (non --calibrate) calibration-wizard gate.
|
|
407
|
+
|
|
408
|
+
The wizard fires only when NO adapter claims the hardware AND there is
|
|
409
|
+
no existing calibration profile AND the wizard reports new hardware.
|
|
410
|
+
An adapter match means the robot is already configured — the resolver
|
|
411
|
+
will build the genome from the adapter, so the wizard must be skipped.
|
|
412
|
+
"""
|
|
413
|
+
if pre_adapter_id is not None:
|
|
414
|
+
return False
|
|
415
|
+
if calibration_profile is not None:
|
|
416
|
+
return False
|
|
417
|
+
return bool(needs_calibration)
|
|
418
|
+
|
|
419
|
+
|
|
404
420
|
def _adapter_resolver(manifest, registry, genome,
|
|
405
421
|
calibrating: bool = False,
|
|
406
422
|
no_auto: bool = False):
|
|
@@ -592,16 +608,37 @@ def run_agent(args) -> None:
|
|
|
592
608
|
auto_mode=auto_cal,
|
|
593
609
|
force_remap=recalibrate,
|
|
594
610
|
)
|
|
611
|
+
|
|
612
|
+
# Adapter-resolver verdict BEFORE the (blocking, interactive) wizard.
|
|
613
|
+
# If a pre-built or previously-generated adapter claims this hardware,
|
|
614
|
+
# the robot is already configured and the auto-detect wizard must be
|
|
615
|
+
# skipped — otherwise it preempts the resolver whenever no calibration
|
|
616
|
+
# profile is present. This is a side-effect-free detection pass; the
|
|
617
|
+
# full resolver (which registers the tools) still runs below at the
|
|
618
|
+
# Auto-Adapter step and prints the [ADAPTER] match line.
|
|
619
|
+
from aether.adapters import detect_adapter
|
|
620
|
+
no_auto = getattr(args, "no_auto_adapter", False)
|
|
621
|
+
pre_adapter_id = None
|
|
622
|
+
if not (calibrate or recalibrate):
|
|
623
|
+
pre_adapter_id = detect_adapter(
|
|
624
|
+
discovery.manifest, include_user_dir=(not no_auto))
|
|
625
|
+
if pre_adapter_id:
|
|
626
|
+
_print_activity("ADAPTER",
|
|
627
|
+
f"Detected '{pre_adapter_id}' — using adapter, skipping "
|
|
628
|
+
f"calibration wizard")
|
|
629
|
+
|
|
595
630
|
if calibrate or recalibrate:
|
|
596
631
|
if recalibrate or wizard.needs_calibration(force=True):
|
|
597
632
|
calibration_profile = wizard.run()
|
|
598
633
|
else:
|
|
599
|
-
# Auto-detect: load existing
|
|
634
|
+
# Auto-detect: load existing profile, else run the wizard ONLY if no
|
|
635
|
+
# adapter matched and new hardware is present.
|
|
600
636
|
calibration_profile = load_calibration()
|
|
601
637
|
if calibration_profile:
|
|
602
638
|
_print_activity("CAL", f"Loaded: {calibration_profile['robot_name']} "
|
|
603
639
|
f"({calibration_profile['robot_type']})")
|
|
604
|
-
elif
|
|
640
|
+
elif _should_run_autocal_wizard(pre_adapter_id, calibration_profile,
|
|
641
|
+
wizard.needs_calibration()):
|
|
605
642
|
_print_activity("CAL", "New hardware detected — running calibration wizard")
|
|
606
643
|
calibration_profile = wizard.run()
|
|
607
644
|
|
|
@@ -710,7 +747,7 @@ def run_agent(args) -> None:
|
|
|
710
747
|
_print_activity("MAVLINK", f"Registered {n_mav} MAVLink capability tools on {mav_port}")
|
|
711
748
|
|
|
712
749
|
# ── Auto-Adapter System (Tier 1 / Tier 2 / Tier 3) ───────────────
|
|
713
|
-
no_auto
|
|
750
|
+
# no_auto already resolved above (before the wizard gate).
|
|
714
751
|
adapter_id, adapter_tools = _adapter_resolver(
|
|
715
752
|
manifest, registry, genome,
|
|
716
753
|
calibrating=(calibrate or recalibrate),
|
|
@@ -97,13 +97,49 @@ def _probe_gpio_gpiozero() -> Tuple[bool, Dict]:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def _probe_i2c() -> Tuple[bool, Dict]:
|
|
100
|
-
"""
|
|
100
|
+
"""Enumerate I2C devices on bus 1 so adapter signature_matches can
|
|
101
|
+
detect HATs by address (e.g. SunFounder Robot HAT at 0x14).
|
|
102
|
+
|
|
103
|
+
smbus2 (shipped in the `pi` install extra) is the authoritative
|
|
104
|
+
enumerator: opening SMBus(1) proves the kernel I2C bus is enabled and
|
|
105
|
+
the user has permission, and read_byte() ACK-probes each address.
|
|
106
|
+
This replaces the old Blinka-only check, which required board+busio
|
|
107
|
+
and so reported "not detected" on real Pi HATs that work fine over
|
|
108
|
+
smbus2.
|
|
109
|
+
|
|
110
|
+
Returns (True, {"type", "available": True, "bus": 1, "devices": [...]})
|
|
111
|
+
on success, or (False, {"available": False, "devices": []}) on any
|
|
112
|
+
failure. The "devices" field is ALWAYS a list, never None. The whole
|
|
113
|
+
body is wrapped so a bug here can never crash boot.
|
|
114
|
+
"""
|
|
101
115
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
try:
|
|
117
|
+
import smbus2
|
|
118
|
+
except ImportError:
|
|
119
|
+
# No enumerator available — report I2C absent rather than
|
|
120
|
+
# crash. (Blinka was the old path; smbus2 is required for the
|
|
121
|
+
# actual device scan, so its absence means no enumeration.)
|
|
122
|
+
return False, {"available": False, "devices": []}
|
|
123
|
+
|
|
124
|
+
bus = smbus2.SMBus(1)
|
|
125
|
+
devices: List[int] = []
|
|
126
|
+
try:
|
|
127
|
+
for addr in range(0x03, 0x78):
|
|
128
|
+
try:
|
|
129
|
+
bus.read_byte(addr)
|
|
130
|
+
devices.append(addr)
|
|
131
|
+
except OSError:
|
|
132
|
+
pass # no device ACKed at this address
|
|
133
|
+
finally:
|
|
134
|
+
bus.close()
|
|
135
|
+
|
|
136
|
+
print(f" [I2C] Scanned bus 1: found {len(devices)} device(s): "
|
|
137
|
+
f"{[hex(d) for d in devices]}")
|
|
138
|
+
return True, {"type": "smbus2_i2c", "available": True,
|
|
139
|
+
"bus": 1, "devices": devices}
|
|
140
|
+
except Exception:
|
|
141
|
+
# No /dev/i2c-1, permission error, or any unexpected failure.
|
|
142
|
+
return False, {"available": False, "devices": []}
|
|
107
143
|
|
|
108
144
|
|
|
109
145
|
def _probe_mavlink_serial() -> Tuple[bool, Dict]:
|
|
@@ -1203,6 +1239,14 @@ class ToolDiscovery:
|
|
|
1203
1239
|
if key == "oled":
|
|
1204
1240
|
iface = info.get("interface", "spi").upper()
|
|
1205
1241
|
detail = f"{hw_type} {resolution} {iface}"
|
|
1242
|
+
if key == "i2c":
|
|
1243
|
+
devs = info.get("devices", [])
|
|
1244
|
+
bus = info.get("bus", 1)
|
|
1245
|
+
if devs:
|
|
1246
|
+
addrs = ", ".join(hex(d) for d in devs)
|
|
1247
|
+
detail = f"bus {bus}, {len(devs)} device(s) at {addrs}"
|
|
1248
|
+
else:
|
|
1249
|
+
detail = f"bus {bus}, no devices found"
|
|
1206
1250
|
else:
|
|
1207
1251
|
if key == "oled":
|
|
1208
1252
|
if info.get("spi_not_enabled"):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-robotics
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.4
|
|
4
4
|
Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
|
|
5
5
|
Author: Chahel Paatur
|
|
6
6
|
License: MIT
|
|
@@ -57,7 +57,7 @@ Dynamic: license-file
|
|
|
57
57
|
████████████████████████████████
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
# AETHER v3.7.
|
|
60
|
+
# AETHER v3.7.4 — Autonomous Robotics Operating System
|
|
61
61
|
|
|
62
62
|
> AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
|
|
63
63
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Regression tests for the v3.7.4 boot-flow fix.
|
|
2
|
+
|
|
3
|
+
Bug: the calibration wizard's auto-detect branch ran wizard.run() before
|
|
4
|
+
the adapter resolver was reached, so on a fresh profile (no robot.json /
|
|
5
|
+
no calibrated_*.json) the wizard preempted the resolver entirely — the
|
|
6
|
+
[ADAPTER] match line never printed and a known HAT (e.g. SunFounder
|
|
7
|
+
PiCar-X at I2C 0x14) was never auto-configured.
|
|
8
|
+
|
|
9
|
+
Fix: a side-effect-free detect_adapter() pass runs BEFORE the wizard, and
|
|
10
|
+
_should_run_autocal_wizard() skips the wizard whenever an adapter claims
|
|
11
|
+
the hardware. These tests pin both halves.
|
|
12
|
+
"""
|
|
13
|
+
import importlib.machinery
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from aether.adapters import detect_adapter
|
|
23
|
+
from aether.app import _should_run_autocal_wizard
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _manifest(i2c_devices=None, gpio=False):
|
|
27
|
+
return {
|
|
28
|
+
"hardware": {
|
|
29
|
+
"gpio": {"available": gpio},
|
|
30
|
+
"i2c": {"available": bool(i2c_devices),
|
|
31
|
+
"devices": i2c_devices or []},
|
|
32
|
+
"mavlink": {},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── detect_adapter: the resolver verdict that gates the wizard ─────────
|
|
38
|
+
|
|
39
|
+
def test_resolver_runs_when_no_calibration_profile():
|
|
40
|
+
"""Detection is independent of calibration state — it runs and returns
|
|
41
|
+
a verdict whether or not a profile exists. (No profile is read here.)"""
|
|
42
|
+
# PiCar-X HAT present → a Tier 1 adapter must claim it...
|
|
43
|
+
assert detect_adapter(_manifest(i2c_devices=[0x14])) is not None
|
|
44
|
+
# ...and bare hardware with no matching HAT yields no adapter, so the
|
|
45
|
+
# caller (boot flow) will fall through to the wizard.
|
|
46
|
+
with patch("aether.adapters._build_vendor_lib_list", return_value=[]):
|
|
47
|
+
verdict = detect_adapter(_manifest(gpio=False), include_user_dir=False)
|
|
48
|
+
assert verdict is None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_resolver_matches_sunfounder_picarx_with_i2c_0x14():
|
|
52
|
+
"""The SunFounder Robot HAT at 0x14 must be detected as picarx."""
|
|
53
|
+
with patch("aether.adapters._build_vendor_lib_list", return_value=[]):
|
|
54
|
+
adapter_id = detect_adapter(_manifest(i2c_devices=[0x14]),
|
|
55
|
+
include_user_dir=False)
|
|
56
|
+
assert adapter_id == "sunfounder_picarx"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_resolver_matches_user_dir_adapter_when_picarx_lib_present():
|
|
60
|
+
"""Brief req #6 case 2: a generated user-dir adapter present AND picarx
|
|
61
|
+
importable → detection returns a match (non-None) so the wizard is
|
|
62
|
+
skipped. With picarx importable the Tier 1 sunfounder adapter claims it
|
|
63
|
+
first; that still satisfies the contract (an adapter loads, no wizard)."""
|
|
64
|
+
with tempfile.TemporaryDirectory() as udir:
|
|
65
|
+
with open(os.path.join(udir, "generated_picarx_test.py"), "w") as f:
|
|
66
|
+
f.write("# generated adapter stub\n")
|
|
67
|
+
fake_picarx = ModuleType("picarx")
|
|
68
|
+
fake_picarx.__spec__ = importlib.machinery.ModuleSpec("picarx", loader=None)
|
|
69
|
+
fake_picarx.Picarx = MagicMock()
|
|
70
|
+
with patch("aether.adapters._USER_ADAPTER_DIR", udir), \
|
|
71
|
+
patch.dict(sys.modules, {"picarx": fake_picarx}):
|
|
72
|
+
adapter_id = detect_adapter(_manifest(), include_user_dir=True)
|
|
73
|
+
assert adapter_id is not None # an adapter claims it → wizard skipped
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_resolver_matches_user_dir_adapter_when_no_tier1_match():
|
|
77
|
+
"""User-dir path in isolation: no Tier 1 adapter matches (no picarx, no
|
|
78
|
+
i2c, no vendor libs) → the generated user-dir adapter is the match."""
|
|
79
|
+
with tempfile.TemporaryDirectory() as udir:
|
|
80
|
+
with open(os.path.join(udir, "generated_picarx_test.py"), "w") as f:
|
|
81
|
+
f.write("# generated adapter stub\n")
|
|
82
|
+
with patch("aether.adapters._USER_ADAPTER_DIR", udir), \
|
|
83
|
+
patch("aether.adapters._build_vendor_lib_list", return_value=[]):
|
|
84
|
+
adapter_id = detect_adapter(_manifest(), include_user_dir=True)
|
|
85
|
+
assert adapter_id == "generated_picarx_test"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_user_dir_ignored_when_no_auto():
|
|
89
|
+
"""include_user_dir=False (i.e. --no-auto-adapter) must not count
|
|
90
|
+
user-dir adapters toward the wizard-skip decision."""
|
|
91
|
+
with tempfile.TemporaryDirectory() as udir:
|
|
92
|
+
with open(os.path.join(udir, "generated_picarx_test.py"), "w") as f:
|
|
93
|
+
f.write("# stub\n")
|
|
94
|
+
with patch("aether.adapters._USER_ADAPTER_DIR", udir), \
|
|
95
|
+
patch("aether.adapters._build_vendor_lib_list", return_value=[]):
|
|
96
|
+
adapter_id = detect_adapter(_manifest(), include_user_dir=False)
|
|
97
|
+
assert adapter_id is None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── _should_run_autocal_wizard: the wizard gate ────────────────────────
|
|
101
|
+
|
|
102
|
+
def test_calibration_wizard_skipped_when_adapter_matches():
|
|
103
|
+
"""Adapter matched → wizard must NOT fire, even with new hardware and
|
|
104
|
+
no profile."""
|
|
105
|
+
assert _should_run_autocal_wizard(
|
|
106
|
+
"sunfounder_picarx", None, needs_calibration=True) is False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_calibration_wizard_runs_when_no_adapter_and_no_profile():
|
|
110
|
+
"""No adapter, no profile, new hardware → wizard fires (the today path)."""
|
|
111
|
+
assert _should_run_autocal_wizard(
|
|
112
|
+
None, None, needs_calibration=True) is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_calibration_wizard_skipped_when_profile_present():
|
|
116
|
+
"""No adapter but an existing profile → load profile, no wizard."""
|
|
117
|
+
profile = {"robot_name": "rover", "robot_type": "differential_drive"}
|
|
118
|
+
assert _should_run_autocal_wizard(
|
|
119
|
+
None, profile, needs_calibration=True) is False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_calibration_wizard_skipped_when_no_new_hardware():
|
|
123
|
+
"""No adapter, no profile, but wizard reports no new hardware → no wizard."""
|
|
124
|
+
assert _should_run_autocal_wizard(
|
|
125
|
+
None, None, needs_calibration=False) is False
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests for ToolDiscovery: platform detection, manifest structure."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
10
|
+
|
|
11
|
+
from aether.core.tool_discovery import ToolDiscovery, _probe_i2c
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(scope="module")
|
|
15
|
+
def manifest():
|
|
16
|
+
d = ToolDiscovery()
|
|
17
|
+
d.discover()
|
|
18
|
+
return d.manifest
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestToolDiscovery:
|
|
22
|
+
def test_platform_detected(self, manifest):
|
|
23
|
+
assert "platform" in manifest
|
|
24
|
+
assert manifest["platform"] != ""
|
|
25
|
+
|
|
26
|
+
def test_camera_found_or_missing(self, manifest):
|
|
27
|
+
hw = manifest.get("hardware", {})
|
|
28
|
+
cam = hw.get("camera", {})
|
|
29
|
+
# camera key must exist with an 'available' field
|
|
30
|
+
assert "available" in cam
|
|
31
|
+
|
|
32
|
+
def test_manifest_has_capability_score(self, manifest):
|
|
33
|
+
assert "capability_score" in manifest
|
|
34
|
+
score = manifest["capability_score"]
|
|
35
|
+
assert isinstance(score, (int, float))
|
|
36
|
+
assert 0 <= score <= 100
|
|
37
|
+
|
|
38
|
+
def test_available_tools_non_empty(self, manifest):
|
|
39
|
+
tools = manifest.get("available_tools", [])
|
|
40
|
+
assert isinstance(tools, list)
|
|
41
|
+
assert len(tools) > 0
|
|
42
|
+
|
|
43
|
+
def test_missing_capabilities_is_list(self, manifest):
|
|
44
|
+
missing = manifest.get("missing_capabilities", [])
|
|
45
|
+
assert isinstance(missing, list)
|
|
46
|
+
# On a dev machine at least some capabilities will be missing
|
|
47
|
+
# (e.g., GPIO on macOS), so the list should be non-empty
|
|
48
|
+
assert len(missing) > 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── I2C device enumeration (v3.7.3) ────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fake_smbus2(ack_addrs=None, instantiation_error=None):
|
|
55
|
+
"""Build a fake smbus2 module whose SMBus(1).read_byte ACKs (returns)
|
|
56
|
+
only at ack_addrs and raises OSError elsewhere. If instantiation_error
|
|
57
|
+
is set, SMBus(bus) raises it (simulates a missing /dev/i2c-N)."""
|
|
58
|
+
ack = set(ack_addrs or [])
|
|
59
|
+
mod = ModuleType("smbus2")
|
|
60
|
+
|
|
61
|
+
class _SMBus:
|
|
62
|
+
def __init__(self, bus):
|
|
63
|
+
if instantiation_error is not None:
|
|
64
|
+
raise instantiation_error
|
|
65
|
+
self.bus = bus
|
|
66
|
+
|
|
67
|
+
def read_byte(self, addr):
|
|
68
|
+
if addr in ack:
|
|
69
|
+
return 0
|
|
70
|
+
raise OSError("no device ACKed")
|
|
71
|
+
|
|
72
|
+
def close(self):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
mod.SMBus = _SMBus
|
|
76
|
+
return mod
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_probe_i2c_returns_devices_list_on_success():
|
|
80
|
+
fake = _fake_smbus2(ack_addrs=[0x14, 0x40])
|
|
81
|
+
with patch.dict(sys.modules, {"smbus2": fake}):
|
|
82
|
+
ok, info = _probe_i2c()
|
|
83
|
+
assert ok is True
|
|
84
|
+
assert info["available"] is True
|
|
85
|
+
assert info["bus"] == 1
|
|
86
|
+
assert info["type"] == "smbus2_i2c"
|
|
87
|
+
assert info["devices"] == [0x14, 0x40] # ascending scan order
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_probe_i2c_returns_empty_list_on_no_devices():
|
|
91
|
+
fake = _fake_smbus2(ack_addrs=[]) # nothing ACKs
|
|
92
|
+
with patch.dict(sys.modules, {"smbus2": fake}):
|
|
93
|
+
ok, info = _probe_i2c()
|
|
94
|
+
assert ok is True # bus opened fine, just no peripherals
|
|
95
|
+
assert info["devices"] == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_probe_i2c_returns_false_on_no_bus():
|
|
99
|
+
# SMBus(1) raises FileNotFoundError — /dev/i2c-1 absent / I2C disabled
|
|
100
|
+
fake = _fake_smbus2(instantiation_error=FileNotFoundError("/dev/i2c-1"))
|
|
101
|
+
with patch.dict(sys.modules, {"smbus2": fake}):
|
|
102
|
+
ok, info = _probe_i2c()
|
|
103
|
+
assert ok is False
|
|
104
|
+
assert info["available"] is False
|
|
105
|
+
assert info["devices"] == []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_probe_i2c_never_crashes_boot():
|
|
109
|
+
# smbus2 import raises ImportError — must return False cleanly, no crash
|
|
110
|
+
with patch.dict(sys.modules, {"smbus2": None}):
|
|
111
|
+
ok, info = _probe_i2c()
|
|
112
|
+
assert ok is False
|
|
113
|
+
assert info["available"] is False
|
|
114
|
+
assert info["devices"] == []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_capability_discovery_printer_shows_i2c_with_devices(capsys):
|
|
118
|
+
"""With a device enumerated, the summary's i2c line must read OK with
|
|
119
|
+
the address — never the old '--- not detected'."""
|
|
120
|
+
with patch("aether.core.tool_discovery._probe_i2c",
|
|
121
|
+
return_value=(True, {"type": "smbus2_i2c", "available": True,
|
|
122
|
+
"bus": 1, "devices": [0x14]})):
|
|
123
|
+
d = ToolDiscovery()
|
|
124
|
+
d.discover()
|
|
125
|
+
d.print_summary()
|
|
126
|
+
|
|
127
|
+
out = capsys.readouterr().out
|
|
128
|
+
i2c_line = next(ln for ln in out.splitlines()
|
|
129
|
+
if ln.strip().startswith(("[+] i2c", "[-] i2c")))
|
|
130
|
+
assert "[+]" in i2c_line
|
|
131
|
+
assert "OK" in i2c_line
|
|
132
|
+
assert "0x14" in i2c_line
|
|
133
|
+
assert "not detected" not in i2c_line
|
aether_robotics-3.7.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.7.2
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""Tests for ToolDiscovery: platform detection, manifest structure."""
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
8
|
-
|
|
9
|
-
from aether.core.tool_discovery import ToolDiscovery
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture(scope="module")
|
|
13
|
-
def manifest():
|
|
14
|
-
d = ToolDiscovery()
|
|
15
|
-
d.discover()
|
|
16
|
-
return d.manifest
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestToolDiscovery:
|
|
20
|
-
def test_platform_detected(self, manifest):
|
|
21
|
-
assert "platform" in manifest
|
|
22
|
-
assert manifest["platform"] != ""
|
|
23
|
-
|
|
24
|
-
def test_camera_found_or_missing(self, manifest):
|
|
25
|
-
hw = manifest.get("hardware", {})
|
|
26
|
-
cam = hw.get("camera", {})
|
|
27
|
-
# camera key must exist with an 'available' field
|
|
28
|
-
assert "available" in cam
|
|
29
|
-
|
|
30
|
-
def test_manifest_has_capability_score(self, manifest):
|
|
31
|
-
assert "capability_score" in manifest
|
|
32
|
-
score = manifest["capability_score"]
|
|
33
|
-
assert isinstance(score, (int, float))
|
|
34
|
-
assert 0 <= score <= 100
|
|
35
|
-
|
|
36
|
-
def test_available_tools_non_empty(self, manifest):
|
|
37
|
-
tools = manifest.get("available_tools", [])
|
|
38
|
-
assert isinstance(tools, list)
|
|
39
|
-
assert len(tools) > 0
|
|
40
|
-
|
|
41
|
-
def test_missing_capabilities_is_list(self, manifest):
|
|
42
|
-
missing = manifest.get("missing_capabilities", [])
|
|
43
|
-
assert isinstance(missing, list)
|
|
44
|
-
# On a dev machine at least some capabilities will be missing
|
|
45
|
-
# (e.g., GPIO on macOS), so the list should be non-empty
|
|
46
|
-
assert len(missing) > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aether_robotics-3.7.2 → aether_robotics-3.7.4}/aether_robotics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260331_121709.json
RENAMED
|
File without changes
|
{aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260331_122446.json
RENAMED
|
File without changes
|
{aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260401_094544.json
RENAMED
|
File without changes
|
{aether_robotics-3.7.2 → aether_robotics-3.7.4}/configs/calibrated_camera_only_20260401_094554.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|