aether-robotics 3.9.2__tar.gz → 3.9.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.9.2/aether_robotics.egg-info → aether_robotics-3.9.4}/PKG-INFO +1 -1
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/__init__.py +1 -1
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/sunfounder_picarx.py +71 -2
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/skills/drive_until_obstacle.py +79 -4
- {aether_robotics-3.9.2 → aether_robotics-3.9.4/aether_robotics.egg-info}/PKG-INFO +1 -1
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/pyproject.toml +1 -1
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_drive_until_obstacle.py +100 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_sunfounder_picarx_adapter.py +141 -1
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/LICENSE +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/MANIFEST.in +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/README.md +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/VERSION +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/__main__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/actions/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/_generator.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/power_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/task_manager.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/app.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/auto_installer.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/auto_updater.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/banner.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/calibration.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/executor.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/feedback.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/genome.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/goal_parser.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/llm_planner.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/mapper.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/memory.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/message_bus.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/metrics.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/navigation_engine.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/planner.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/tool_builder.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/tool_discovery.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/tool_registry.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/core/visualizer.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/faults/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/parallel/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/parallel/executor.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/simulation/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/simulation/environment.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether/skills/__init__.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/SOURCES.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/drone_v1.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/rover_v1.json +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/context/aether_definitions.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/install.sh +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/main.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/requirements.txt +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/setup.cfg +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_adapter_preflight.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_atexit_adapter_shutdown.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_auto_updater.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_boot_resolver_flow.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_calibration.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_chaos.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_correction_agent.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_entrypoint.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_genome.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_mapper.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_parallel_executor.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_security.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_tier1_motion_cleanup.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_tool_builder.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/tests/test_yolo_integration.py +0 -0
- {aether_robotics-3.9.2 → aether_robotics-3.9.4}/weights/fault_agent.npy +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "3.9.
|
|
1
|
+
__version__ = "3.9.4"
|
|
2
2
|
__author__ = "Chahel Paatur"
|
|
@@ -13,6 +13,38 @@ from typing import Dict
|
|
|
13
13
|
|
|
14
14
|
_ADAPTER_ID = "sunfounder_picarx"
|
|
15
15
|
|
|
16
|
+
# Module-level reference to the live Picarx instance, set during register().
|
|
17
|
+
# Used ONLY by the skill-internal continuous-motion primitives below — never
|
|
18
|
+
# exposed as an LLM-callable tool.
|
|
19
|
+
_picarx_instance = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _continuous_forward(speed: float) -> None:
|
|
23
|
+
"""Start the motor running forward at `speed`. Does NOT auto-stop.
|
|
24
|
+
|
|
25
|
+
INTERNAL: only callable by parallel skills that wrap the call in their
|
|
26
|
+
own try/finally with _continuous_stop() in the finally block. NOT
|
|
27
|
+
registered as an LLM-callable tool because it breaks the motion-cleanup
|
|
28
|
+
invariant. The adapter's atexit emergency_stop (v3.7.5) still neutralizes
|
|
29
|
+
the motor on AETHER exit.
|
|
30
|
+
"""
|
|
31
|
+
global _picarx_instance
|
|
32
|
+
if _picarx_instance is None:
|
|
33
|
+
raise RuntimeError("sunfounder_picarx not initialized")
|
|
34
|
+
_picarx_instance.forward(float(speed))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _continuous_stop() -> None:
|
|
38
|
+
"""Stop the motor immediately. Safe to call multiple times (idempotent
|
|
39
|
+
at the Robot HAT level). Called by parallel skills in their try/finally."""
|
|
40
|
+
global _picarx_instance
|
|
41
|
+
if _picarx_instance is None:
|
|
42
|
+
return # nothing to stop, no init
|
|
43
|
+
try:
|
|
44
|
+
_picarx_instance.stop()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass # never raise in cleanup paths
|
|
47
|
+
|
|
16
48
|
# Config files robot_hat tries to write at runtime. When these are not
|
|
17
49
|
# writable by the current user, robot_hat's filedb.py / config.py invoke
|
|
18
50
|
# os.popen('sudo chmod ...') / os.popen('sudo chown ...'), which pops a
|
|
@@ -367,6 +399,39 @@ def register(registry, manifest: dict) -> None:
|
|
|
367
399
|
print(f" [ADAPTER] sunfounder_picarx: cali_dir_value inversion "
|
|
368
400
|
f"failed: {e}")
|
|
369
401
|
|
|
402
|
+
# Expose the live instance to the skill-internal continuous-motion
|
|
403
|
+
# primitives (_continuous_forward / _continuous_stop). These are NOT
|
|
404
|
+
# registered as tools — only parallel skills with their own try/finally
|
|
405
|
+
# cleanup may call them. See the module-level helpers above.
|
|
406
|
+
global _picarx_instance
|
|
407
|
+
_picarx_instance = car
|
|
408
|
+
|
|
409
|
+
# v3.9.4: surface ultrasonic faults early so the user knows before running
|
|
410
|
+
# skills. This is detection only — it does NOT attempt to fix the sensor.
|
|
411
|
+
try:
|
|
412
|
+
# Take 3 quick reads; if all are -1, warn loudly
|
|
413
|
+
bad = 0
|
|
414
|
+
for _ in range(3):
|
|
415
|
+
d = car.ultrasonic.read()
|
|
416
|
+
if d is None or d < 0 or d > 400:
|
|
417
|
+
bad += 1
|
|
418
|
+
time.sleep(0.05)
|
|
419
|
+
if bad == 3:
|
|
420
|
+
print(" [ADAPTER] WARNING: ultrasonic sensor returned bad reads "
|
|
421
|
+
"on 3/3 probes — drive_until_obstacle may fail.")
|
|
422
|
+
print(" [ADAPTER] Check sensor wiring (TRIG=D2, ECHO=D3, "
|
|
423
|
+
"GND/3V3 power) or run:")
|
|
424
|
+
print(" [ADAPTER] python3 -c 'from picarx import Picarx; "
|
|
425
|
+
"print(Picarx().ultrasonic.read())'")
|
|
426
|
+
elif bad > 0:
|
|
427
|
+
print(f" [ADAPTER] NOTE: ultrasonic returned {bad}/3 bad reads "
|
|
428
|
+
f"— sensor may be intermittent.")
|
|
429
|
+
else:
|
|
430
|
+
print(" [ADAPTER] sunfounder_picarx: ultrasonic sensor OK")
|
|
431
|
+
except Exception:
|
|
432
|
+
# Never fail boot on a sensor probe — just skip
|
|
433
|
+
pass
|
|
434
|
+
|
|
370
435
|
_meta = {"adapter_source": "tier1", "adapter_id": _ADAPTER_ID}
|
|
371
436
|
|
|
372
437
|
def emergency_stop(**kwargs):
|
|
@@ -411,7 +476,9 @@ def register(registry, manifest: dict) -> None:
|
|
|
411
476
|
speed = float(kwargs.get("speed", 40))
|
|
412
477
|
duration = kwargs.get("duration")
|
|
413
478
|
if duration is None:
|
|
414
|
-
|
|
479
|
+
# v3.9.4: 1.0s produced too little heading change on the bench;
|
|
480
|
+
# turn longer before recentering. LLM can still override.
|
|
481
|
+
duration = 2.0
|
|
415
482
|
try:
|
|
416
483
|
car.set_dir_servo_angle(-30)
|
|
417
484
|
car.forward(speed)
|
|
@@ -425,7 +492,9 @@ def register(registry, manifest: dict) -> None:
|
|
|
425
492
|
speed = float(kwargs.get("speed", 40))
|
|
426
493
|
duration = kwargs.get("duration")
|
|
427
494
|
if duration is None:
|
|
428
|
-
|
|
495
|
+
# v3.9.4: 1.0s produced too little heading change on the bench;
|
|
496
|
+
# turn longer before recentering. LLM can still override.
|
|
497
|
+
duration = 2.0
|
|
429
498
|
try:
|
|
430
499
|
car.set_dir_servo_angle(30)
|
|
431
500
|
car.forward(speed)
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
"""AETHER v3.9.
|
|
1
|
+
"""AETHER v3.9.3 — drive_until_obstacle parallel skill.
|
|
2
2
|
|
|
3
3
|
Drives the robot forward while a second thread polls the ultrasonic sensor,
|
|
4
4
|
stopping automatically when an obstacle is within target_cm. True threaded
|
|
5
|
-
concurrency: motor
|
|
5
|
+
concurrency: motor drive and sensor reads run on separate threads that share
|
|
6
6
|
a stop event.
|
|
7
7
|
|
|
8
|
+
v3.9.3: the motor thread now drives CONTINUOUSLY (via the adapter's
|
|
9
|
+
skill-internal _continuous_forward / _continuous_stop primitives) and only
|
|
10
|
+
re-issues a command when the approach-speed band changes — eliminating the
|
|
11
|
+
start-stop jitter and inconsistent stopping distance of the pulsed path. The
|
|
12
|
+
pulsed path is retained as a fallback for adapters that don't expose the
|
|
13
|
+
continuous primitives.
|
|
14
|
+
|
|
8
15
|
Demo objective this satisfies (verbatim): "Go forward and stop ~5 cm in front
|
|
9
16
|
of the wall — keep going forward and use the ultrasonic sensor in parallel to
|
|
10
17
|
detect the wall, and stop when it's within 5 cm."
|
|
@@ -85,11 +92,66 @@ def drive_until_obstacle(target_cm: float = 5.0, max_speed: float = 25.0,
|
|
|
85
92
|
"final_distance_cm": None}
|
|
86
93
|
|
|
87
94
|
def motor_task(should_stop):
|
|
88
|
-
"""
|
|
95
|
+
"""Drive the motor until told to stop; always stop on exit.
|
|
96
|
+
|
|
97
|
+
Prefers CONTINUOUS motion (v3.9.3): the motor runs without pause and
|
|
98
|
+
we only re-issue a forward command when the approach-speed band
|
|
99
|
+
changes. This eliminates the start-stop-start jitter the pulsed path
|
|
100
|
+
produced (10 accel/decel cycles per second) and makes the stopping
|
|
101
|
+
distance consistent. Falls back to the pulsed v3.9.1 behavior when the
|
|
102
|
+
adapter doesn't expose continuous primitives (e.g. a Tier 2 generated
|
|
103
|
+
adapter), preserving backward compatibility.
|
|
89
104
|
|
|
90
105
|
Speed follows an approach curve based on the most recent sensor
|
|
91
106
|
reading — full speed when far, easing to ~35% near the target — so
|
|
92
|
-
|
|
107
|
+
deceleration begins early and overshoot stays small."""
|
|
108
|
+
# Try to import the continuous primitives from the adapter. If the
|
|
109
|
+
# adapter doesn't expose them, fall back to the pulsed approach.
|
|
110
|
+
#
|
|
111
|
+
# Gate on _picarx_instance too: the sunfounder module is ALWAYS
|
|
112
|
+
# importable in-process, so import success alone does not mean the
|
|
113
|
+
# sunfounder adapter is the live one. A Tier 2 generated adapter
|
|
114
|
+
# leaves _picarx_instance None — in that case the primitives would
|
|
115
|
+
# raise RuntimeError, so we must fall back to the pulsed path. Only
|
|
116
|
+
# use continuous motion when the adapter is actually initialized.
|
|
117
|
+
try:
|
|
118
|
+
from aether.adapters.sunfounder_picarx import (
|
|
119
|
+
_continuous_forward, _continuous_stop, _picarx_instance
|
|
120
|
+
)
|
|
121
|
+
use_continuous = _picarx_instance is not None
|
|
122
|
+
except (ImportError, AttributeError):
|
|
123
|
+
use_continuous = False
|
|
124
|
+
|
|
125
|
+
if use_continuous:
|
|
126
|
+
try:
|
|
127
|
+
current_speed = None
|
|
128
|
+
while not should_halt(should_stop):
|
|
129
|
+
with state_lock:
|
|
130
|
+
current_dist = state.get("last_distance_cm")
|
|
131
|
+
target_speed = _approach_speed(
|
|
132
|
+
current_dist if current_dist is not None else 1000.0,
|
|
133
|
+
target_cm, max_speed)
|
|
134
|
+
# Only re-issue the forward command when speed actually
|
|
135
|
+
# changes. Motion stays continuous; we just adjust the
|
|
136
|
+
# speed setpoint — no stop between bands, no jitter.
|
|
137
|
+
if target_speed != current_speed:
|
|
138
|
+
with _MOTOR_LOCK:
|
|
139
|
+
if should_halt(should_stop):
|
|
140
|
+
break
|
|
141
|
+
_continuous_forward(target_speed)
|
|
142
|
+
current_speed = target_speed
|
|
143
|
+
time.sleep(_POLL_S) # poll cadence; motor keeps running
|
|
144
|
+
finally:
|
|
145
|
+
# Skill-owned cleanup: any exception (including a stop signal)
|
|
146
|
+
# halts the motor here. A daemon kill at interpreter exit skips
|
|
147
|
+
# this — the adapter emergency_stop from app's exit handler is
|
|
148
|
+
# the authoritative kill in that case.
|
|
149
|
+
with _MOTOR_LOCK:
|
|
150
|
+
_continuous_stop()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Pulsed fallback (v3.9.1 behavior) for adapters without
|
|
154
|
+
# continuous-motion support.
|
|
93
155
|
try:
|
|
94
156
|
while not should_halt(should_stop):
|
|
95
157
|
with state_lock:
|
|
@@ -137,6 +199,19 @@ def drive_until_obstacle(target_cm: float = 5.0, max_speed: float = 25.0,
|
|
|
137
199
|
if failures > _MAX_SENSOR_FAILURES:
|
|
138
200
|
with state_lock:
|
|
139
201
|
state["stop_reason"] = "sensor failing"
|
|
202
|
+
# v3.9.4: surface the fault with actionable wiring guidance
|
|
203
|
+
# (detection only — no software fix for the sensor).
|
|
204
|
+
print(
|
|
205
|
+
f"\n [SENSOR FAULT] Ultrasonic sensor returned -1 or "
|
|
206
|
+
f"out-of-range {state['sensor_failures']} times during "
|
|
207
|
+
f"drive_until_obstacle.\n"
|
|
208
|
+
f" This usually means a wiring or hardware issue:\n"
|
|
209
|
+
f" • Check the ultrasonic sensor's 4-pin cable is firmly seated\n"
|
|
210
|
+
f" • TRIG should be on D2, ECHO on D3 (Robot HAT)\n"
|
|
211
|
+
f" • Verify power: red=3V3/5V, black=GND\n"
|
|
212
|
+
f" • Try: python3 -c 'from picarx import Picarx; print(Picarx().ultrasonic.read())'\n"
|
|
213
|
+
f" Skill halted safely for safety.\n"
|
|
214
|
+
)
|
|
140
215
|
should_stop.set()
|
|
141
216
|
return
|
|
142
217
|
time.sleep(_POLL_S)
|
|
@@ -205,3 +205,103 @@ def test_demo_drive_until_obstacle_at_5cm_motor_ceases_after_stop():
|
|
|
205
205
|
time.sleep(0.4)
|
|
206
206
|
assert reg.move_calls == calls_at_return, "motor kept pulsing after stop"
|
|
207
207
|
assert reg.stop_calls >= 1
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── v3.9.3: continuous-motion motor_task ───────────────────────────────
|
|
211
|
+
#
|
|
212
|
+
# These exercise the path where the sunfounder adapter is LIVE (its
|
|
213
|
+
# _picarx_instance is wired) so the skill drives continuously instead of
|
|
214
|
+
# pulsing. The existing FakeRegistry tests above run with _picarx_instance
|
|
215
|
+
# None (autouse fixture below), so they keep exercising the pulsed path —
|
|
216
|
+
# this is the exact Tier 2 / generic-adapter backward-compat fallback.
|
|
217
|
+
import aether.adapters.sunfounder_picarx as sp # noqa: E402
|
|
218
|
+
from unittest.mock import MagicMock # noqa: E402
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.fixture(autouse=True)
|
|
222
|
+
def _picarx_instance_none(monkeypatch):
|
|
223
|
+
"""Default every test to 'sunfounder adapter not live' → pulsed path.
|
|
224
|
+
Continuous tests opt in by setting _picarx_instance themselves."""
|
|
225
|
+
monkeypatch.setattr(sp, "_picarx_instance", None, raising=False)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_motor_task_uses_continuous_when_available(monkeypatch):
|
|
229
|
+
"""With the adapter live, the motor issues only a FEW forward commands
|
|
230
|
+
(one per approach-speed band change), NOT a pulse 10x/second."""
|
|
231
|
+
fwd, stop = MagicMock(), MagicMock()
|
|
232
|
+
monkeypatch.setattr(sp, "_continuous_forward", fwd)
|
|
233
|
+
monkeypatch.setattr(sp, "_continuous_stop", stop)
|
|
234
|
+
monkeypatch.setattr(sp, "_picarx_instance", object()) # adapter is live
|
|
235
|
+
|
|
236
|
+
reg = FakeRegistry([60, 50, 40, 30, 20, 12, 8, 5, 5, 5])
|
|
237
|
+
result = drive_until_obstacle(target_cm=5, max_speed=30, timeout_s=8, registry=reg)
|
|
238
|
+
|
|
239
|
+
assert "obstacle at" in result["stop_reason"]
|
|
240
|
+
# Continuous: forward issued once per band (max → 60% → 35%), never spammed.
|
|
241
|
+
assert 1 <= fwd.call_count <= 3, f"expected ≤3 forward calls, got {fwd.call_count}"
|
|
242
|
+
# The pulsed registry path must NOT have been used at all.
|
|
243
|
+
assert reg.move_calls == 0
|
|
244
|
+
# Speeds applied via the approach curve, monotonically non-increasing.
|
|
245
|
+
speeds = [c.args[0] for c in fwd.call_args_list]
|
|
246
|
+
assert speeds == sorted(speeds, reverse=True), speeds
|
|
247
|
+
stop.assert_called() # skill-owned cleanup fired
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_motor_task_falls_back_to_pulsed_when_no_continuous(monkeypatch):
|
|
251
|
+
"""Adapter without continuous primitives (e.g. Tier 2) → pulsed
|
|
252
|
+
move_forward, repeatedly (backward compatibility)."""
|
|
253
|
+
monkeypatch.delattr(sp, "_continuous_forward", raising=False)
|
|
254
|
+
monkeypatch.delattr(sp, "_continuous_stop", raising=False)
|
|
255
|
+
|
|
256
|
+
reg = FakeRegistry([100]) # wall never appears → keeps pulsing
|
|
257
|
+
result = drive_until_obstacle(target_cm=5, max_speed=30, timeout_s=1.0, registry=reg)
|
|
258
|
+
|
|
259
|
+
assert result["timed_out"] is True
|
|
260
|
+
assert reg.move_calls >= 2, f"pulsed path should re-issue; got {reg.move_calls}"
|
|
261
|
+
assert reg.stop_calls >= 1 # pulsed path's finally stop
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_continuous_motor_task_always_stops_on_exception(monkeypatch):
|
|
265
|
+
"""An exception mid-loop (e.g. HAT fault during a forward) must still stop
|
|
266
|
+
the motor via the skill's finally → _continuous_stop."""
|
|
267
|
+
boom = MagicMock(side_effect=RuntimeError("HAT fault mid-drive"))
|
|
268
|
+
stop = MagicMock()
|
|
269
|
+
monkeypatch.setattr(sp, "_continuous_forward", boom)
|
|
270
|
+
monkeypatch.setattr(sp, "_continuous_stop", stop)
|
|
271
|
+
monkeypatch.setattr(sp, "_picarx_instance", object())
|
|
272
|
+
|
|
273
|
+
reg = FakeRegistry([100])
|
|
274
|
+
drive_until_obstacle(target_cm=5, max_speed=30, timeout_s=1.0, registry=reg)
|
|
275
|
+
|
|
276
|
+
boom.assert_called() # the failing forward ran
|
|
277
|
+
stop.assert_called() # finally still stopped the motor
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_continuous_motor_task_stops_on_should_stop(monkeypatch):
|
|
281
|
+
"""When the sensor confirms an obstacle and sets should_stop, the motor's
|
|
282
|
+
finally fires _continuous_stop."""
|
|
283
|
+
fwd, stop = MagicMock(), MagicMock()
|
|
284
|
+
monkeypatch.setattr(sp, "_continuous_forward", fwd)
|
|
285
|
+
monkeypatch.setattr(sp, "_continuous_stop", stop)
|
|
286
|
+
monkeypatch.setattr(sp, "_picarx_instance", object())
|
|
287
|
+
|
|
288
|
+
reg = FakeRegistry([6, 4, 4, 4]) # obstacle confirmed quickly
|
|
289
|
+
result = drive_until_obstacle(target_cm=5, max_speed=30, timeout_s=5, registry=reg)
|
|
290
|
+
|
|
291
|
+
assert "obstacle at" in result["stop_reason"]
|
|
292
|
+
stop.assert_called()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ── v3.9.4: clearer sensor-fault surfacing ─────────────────────────────
|
|
296
|
+
|
|
297
|
+
def test_sensor_fault_log_includes_wiring_guidance(capsys):
|
|
298
|
+
"""When the sensor fails (>20 bad reads), the skill halts safely AND prints
|
|
299
|
+
actionable wiring guidance. Behavior is unchanged — only the log is richer."""
|
|
300
|
+
reg = FakeRegistry([-1]) # no echo, ever → sensor failing
|
|
301
|
+
result = drive_until_obstacle(target_cm=5, max_speed=30, timeout_s=5, registry=reg)
|
|
302
|
+
|
|
303
|
+
assert result["stop_reason"] == "sensor failing" # unchanged behavior
|
|
304
|
+
out = capsys.readouterr().out
|
|
305
|
+
assert "SENSOR FAULT" in out
|
|
306
|
+
assert "TRIG" in out and "ECHO" in out
|
|
307
|
+
assert "wiring" in out
|
|
@@ -125,7 +125,7 @@ def test_turn_left_recenters_servo_on_exception():
|
|
|
125
125
|
def test_turn_left_default_duration_when_none_provided():
|
|
126
126
|
reg, car = _register()
|
|
127
127
|
_, sleep_mock = _call(reg, "turn_left") # no kwargs
|
|
128
|
-
sleep_mock.assert_called_once_with(1.0
|
|
128
|
+
sleep_mock.assert_called_once_with(2.0) # v3.9.4: 1.0 → 2.0
|
|
129
129
|
# and it still recenters
|
|
130
130
|
assert ("set_dir_servo_angle", 0) in car.calls
|
|
131
131
|
|
|
@@ -476,3 +476,143 @@ def test_is_gpio_busy_error_matches_varied_types():
|
|
|
476
476
|
assert _is_gpio_busy_error(Exception("gpio line is busy"))
|
|
477
477
|
assert not _is_gpio_busy_error(ValueError("bad pin number"))
|
|
478
478
|
assert not _is_gpio_busy_error(ImportError("no module named picarx"))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ── v3.9.3: skill-internal continuous-motion primitives ────────────────
|
|
482
|
+
|
|
483
|
+
@pytest.fixture(autouse=True)
|
|
484
|
+
def _reset_picarx_instance():
|
|
485
|
+
"""register() sets the module-global _picarx_instance; isolate tests."""
|
|
486
|
+
saved = sp._picarx_instance
|
|
487
|
+
sp._picarx_instance = None
|
|
488
|
+
yield
|
|
489
|
+
sp._picarx_instance = saved
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_continuous_primitives_not_in_registry():
|
|
493
|
+
"""The continuous primitives must NEVER be LLM-callable — they break the
|
|
494
|
+
motion-cleanup invariant by design and are only safe inside a skill's own
|
|
495
|
+
try/finally. register() must not expose them as tools."""
|
|
496
|
+
reg, car = _register()
|
|
497
|
+
assert "_continuous_forward" not in reg.tools
|
|
498
|
+
assert "_continuous_stop" not in reg.tools
|
|
499
|
+
# And no alias under a non-underscore name either.
|
|
500
|
+
assert "continuous_forward" not in reg.tools
|
|
501
|
+
assert "continuous_stop" not in reg.tools
|
|
502
|
+
# The standard 12-tool vocabulary is still present.
|
|
503
|
+
assert "move_forward" in reg.tools
|
|
504
|
+
assert "emergency_stop" in reg.tools
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_continuous_forward_requires_init():
|
|
508
|
+
"""_continuous_forward before register() (no instance) must raise rather
|
|
509
|
+
than silently no-op — a skill calling it pre-init is a bug."""
|
|
510
|
+
sp._picarx_instance = None
|
|
511
|
+
with pytest.raises(RuntimeError):
|
|
512
|
+
sp._continuous_forward(20.0)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_continuous_stop_idempotent():
|
|
516
|
+
"""_continuous_stop must be safe to call repeatedly and never raise —
|
|
517
|
+
it lives in cleanup paths."""
|
|
518
|
+
fake = FakePicarx()
|
|
519
|
+
sp._picarx_instance = fake
|
|
520
|
+
sp._continuous_stop()
|
|
521
|
+
sp._continuous_stop() # second call must not raise
|
|
522
|
+
assert fake.calls.count(("stop",)) == 2
|
|
523
|
+
|
|
524
|
+
# Also safe with no instance (returns early, no raise).
|
|
525
|
+
sp._picarx_instance = None
|
|
526
|
+
sp._continuous_stop()
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_continuous_stop_never_raises_when_hat_errors():
|
|
530
|
+
"""If the underlying car.stop() throws, _continuous_stop swallows it."""
|
|
531
|
+
class BoomStop(FakePicarx):
|
|
532
|
+
def stop(self):
|
|
533
|
+
raise RuntimeError("HAT I2C error")
|
|
534
|
+
sp._picarx_instance = BoomStop()
|
|
535
|
+
sp._continuous_stop() # must not propagate
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_register_sets_picarx_instance():
|
|
539
|
+
"""After register(), the module-global instance is wired so the skill's
|
|
540
|
+
continuous primitives can drive it."""
|
|
541
|
+
assert sp._picarx_instance is None # reset by fixture
|
|
542
|
+
reg, car = _register()
|
|
543
|
+
assert sp._picarx_instance is not None
|
|
544
|
+
assert sp._picarx_instance is car
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_continuous_forward_drives_instance():
|
|
548
|
+
"""_continuous_forward issues a forward() WITHOUT a following stop()."""
|
|
549
|
+
fake = FakePicarx()
|
|
550
|
+
sp._picarx_instance = fake
|
|
551
|
+
sp._continuous_forward(33.0)
|
|
552
|
+
assert ("forward", 33.0) in fake.calls
|
|
553
|
+
assert ("stop",) not in fake.calls # continuous: no auto-stop
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# ── v3.9.4: turn default duration 1.0 → 2.0 ────────────────────────────
|
|
557
|
+
|
|
558
|
+
def test_turn_left_default_duration_is_2s():
|
|
559
|
+
reg, car = _register()
|
|
560
|
+
_, sleep_mock = _call(reg, "turn_left") # no duration → default
|
|
561
|
+
sleep_mock.assert_called_once_with(2.0)
|
|
562
|
+
assert ("set_dir_servo_angle", 0) in car.calls # still recenters
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_turn_right_default_duration_is_2s():
|
|
566
|
+
reg, car = _register()
|
|
567
|
+
_, sleep_mock = _call(reg, "turn_right")
|
|
568
|
+
sleep_mock.assert_called_once_with(2.0)
|
|
569
|
+
assert ("set_dir_servo_angle", 0) in car.calls
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_turn_left_explicit_duration_still_works():
|
|
573
|
+
reg, car = _register()
|
|
574
|
+
_, sleep_mock = _call(reg, "turn_left", duration=0.5)
|
|
575
|
+
sleep_mock.assert_called_once_with(0.5) # LLM override unaffected
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ── v3.9.4: boot-time ultrasonic health probe (detection only) ─────────
|
|
579
|
+
|
|
580
|
+
def _picarx_cls_with_reads(reads):
|
|
581
|
+
"""A FakePicarx subclass whose ultrasonic.read() yields `reads`, then -1.
|
|
582
|
+
The boot probe reads exactly 3 times."""
|
|
583
|
+
class _USPicarx(FakePicarx):
|
|
584
|
+
def __init__(self):
|
|
585
|
+
super().__init__()
|
|
586
|
+
seq = list(reads)
|
|
587
|
+
us = MagicMock()
|
|
588
|
+
us.read.side_effect = lambda: seq.pop(0) if seq else -1
|
|
589
|
+
self.ultrasonic = us
|
|
590
|
+
return _USPicarx
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_register_warns_on_all_bad_sensor_reads(capsys):
|
|
594
|
+
_register(picarx_cls=_picarx_cls_with_reads([-1, -1, -1]))
|
|
595
|
+
out = capsys.readouterr().out
|
|
596
|
+
assert "WARNING: ultrasonic sensor returned bad reads" in out
|
|
597
|
+
assert "TRIG=D2" in out and "ECHO=D3" in out
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def test_register_quiet_on_all_good_sensor_reads(capsys):
|
|
601
|
+
_register(picarx_cls=_picarx_cls_with_reads([50, 50, 50]))
|
|
602
|
+
out = capsys.readouterr().out
|
|
603
|
+
assert "ultrasonic sensor OK" in out
|
|
604
|
+
assert "WARNING" not in out
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_register_notes_intermittent_sensor(capsys):
|
|
608
|
+
_register(picarx_cls=_picarx_cls_with_reads([50, -1, 50]))
|
|
609
|
+
out = capsys.readouterr().out
|
|
610
|
+
assert "NOTE: ultrasonic returned 1/3 bad reads" in out
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def test_register_never_fails_boot_when_ultrasonic_attr_missing():
|
|
614
|
+
"""Base FakePicarx has no `ultrasonic` attr — the probe must swallow the
|
|
615
|
+
AttributeError and register the full tool vocabulary anyway."""
|
|
616
|
+
reg, car = _register() # default FakePicarx, no ultrasonic
|
|
617
|
+
assert "move_forward" in reg.tools
|
|
618
|
+
assert "get_distance" in reg.tools
|
|
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
|
|
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.9.2 → aether_robotics-3.9.4}/aether_robotics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260331_121709.json
RENAMED
|
File without changes
|
{aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260331_122446.json
RENAMED
|
File without changes
|
{aether_robotics-3.9.2 → aether_robotics-3.9.4}/configs/calibrated_camera_only_20260401_094544.json
RENAMED
|
File without changes
|
{aether_robotics-3.9.2 → aether_robotics-3.9.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|