aether-robotics 3.4.0__tar.gz → 3.4.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.
- {aether_robotics-3.4.0/aether_robotics.egg-info → aether_robotics-3.4.2}/PKG-INFO +2 -2
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/README.md +1 -1
- aether_robotics-3.4.2/VERSION +1 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/__init__.py +1 -1
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/correction_agent.py +38 -2
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/app.py +58 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/calibration.py +45 -13
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/navigation_engine.py +3 -2
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/tool_registry.py +84 -1
- {aether_robotics-3.4.0 → aether_robotics-3.4.2/aether_robotics.egg-info}/PKG-INFO +2 -2
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/pyproject.toml +1 -1
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_calibration.py +200 -0
- aether_robotics-3.4.0/VERSION +0 -1
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/LICENSE +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/MANIFEST.in +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/__main__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/actions/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/adapters/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/power_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/task_manager.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/auto_installer.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/auto_updater.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/banner.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/executor.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/feedback.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/goal_parser.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/llm_planner.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/mapper.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/memory.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/message_bus.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/metrics.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/planner.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/tool_builder.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/tool_discovery.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/core/visualizer.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/faults/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/simulation/__init__.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/simulation/environment.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/SOURCES.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/drone_v1.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/rover_v1.json +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/context/aether_definitions.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/install.sh +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/main.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/requirements.txt +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/setup.cfg +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_auto_updater.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_chaos.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_entrypoint.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_mapper.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_security.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_tool_builder.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/tests/test_yolo_integration.py +0 -0
- {aether_robotics-3.4.0 → aether_robotics-3.4.2}/weights/fault_agent.npy +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-robotics
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.2
|
|
4
4
|
Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
|
|
5
5
|
Author: Chahel Paatur
|
|
6
6
|
License: MIT
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT">
|
|
46
46
|
<img src="https://img.shields.io/github/stars/ChahelPaatur/AETHER?style=flat-square" alt="GitHub Stars">
|
|
47
47
|
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Raspberry%20Pi%20%7C%20Jetson-lightgrey?style=flat-square" alt="Platforms">
|
|
48
|
-
<img src="https://img.shields.io/badge/version-3.
|
|
48
|
+
<img src="https://img.shields.io/badge/version-3.4.0-red?style=flat-square" alt="v3.4.0">
|
|
49
49
|
</p>
|
|
50
50
|
|
|
51
51
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT">
|
|
5
5
|
<img src="https://img.shields.io/github/stars/ChahelPaatur/AETHER?style=flat-square" alt="GitHub Stars">
|
|
6
6
|
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Raspberry%20Pi%20%7C%20Jetson-lightgrey?style=flat-square" alt="Platforms">
|
|
7
|
-
<img src="https://img.shields.io/badge/version-3.
|
|
7
|
+
<img src="https://img.shields.io/badge/version-3.4.0-red?style=flat-square" alt="v3.4.0">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.2
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "3.4.
|
|
1
|
+
__version__ = "3.4.2"
|
|
2
2
|
__author__ = "Chahel Paatur"
|
|
@@ -28,6 +28,15 @@ _SYSTEM_PROMPT = (
|
|
|
28
28
|
"do NOT suggest pip install. Instead, suggest an alternative tool that "
|
|
29
29
|
"provides similar functionality (e.g. capture_image uses picamera2 as "
|
|
30
30
|
"fallback, measure_brightness and detect_motion work without cv2).\n\n"
|
|
31
|
+
"- For continuous-rotation servo corrections (servo_type='continuous'), "
|
|
32
|
+
"angle is speed in the range -100..+100: 100 = full forward / clockwise "
|
|
33
|
+
"(180° PWM pulse), -100 = full reverse / counter-clockwise (0° PWM pulse), "
|
|
34
|
+
"0 = stop (90° PWM pulse). When the original intent was motion — 'go', "
|
|
35
|
+
"'move', 'forward', 'start', 'clockwise', 'drive' — use angle=100, "
|
|
36
|
+
"NEVER angle=90. angle=90 on a continuous servo means STOP.\n"
|
|
37
|
+
"- When a calibrated action tool fails and you fall back to servo_set or "
|
|
38
|
+
"motor_forward/motor_turn, use the BCM pin from 'Original tool metadata' "
|
|
39
|
+
"provided in the request, NOT the default pin=18.\n\n"
|
|
31
40
|
"Return valid JSON only — no markdown, no explanation, no code fences:\n"
|
|
32
41
|
"{\n"
|
|
33
42
|
' "success": true/false,\n'
|
|
@@ -115,6 +124,10 @@ class CorrectionAgent:
|
|
|
115
124
|
expected = step.get("expected_output", "")
|
|
116
125
|
params = step.get("params", {})
|
|
117
126
|
|
|
127
|
+
# Extract physical_map metadata from the registered tool (Bug B)
|
|
128
|
+
tool_obj = registry.get(tool_name)
|
|
129
|
+
tool_metadata = getattr(tool_obj, "metadata", {}) or {}
|
|
130
|
+
|
|
118
131
|
# If no expected_output defined, skip LLM evaluation —
|
|
119
132
|
# just use the result's own success flag
|
|
120
133
|
if not expected:
|
|
@@ -153,7 +166,8 @@ class CorrectionAgent:
|
|
|
153
166
|
# No fallback worked — still try the LLM loop below
|
|
154
167
|
|
|
155
168
|
while attempt < MAX_RETRIES:
|
|
156
|
-
verdict = self._call_llm(
|
|
169
|
+
verdict = self._call_llm(
|
|
170
|
+
tool_name, params, expected, actual, tool_metadata)
|
|
157
171
|
if verdict is None:
|
|
158
172
|
# API failed — trust the result as-is
|
|
159
173
|
break
|
|
@@ -175,6 +189,22 @@ class CorrectionAgent:
|
|
|
175
189
|
corr_tool = verdict.get("correction_tool")
|
|
176
190
|
corr_inputs = verdict.get("correction_inputs") or {}
|
|
177
191
|
|
|
192
|
+
# Belt-and-suspenders: when falling back to a servo/motor tool,
|
|
193
|
+
# enforce the BCM pin from the original tool's metadata regardless
|
|
194
|
+
# of what the LLM proposed — deterministic, not probabilistic.
|
|
195
|
+
_PIN_FALLBACK_TOOLS = {"servo_set", "motor_forward", "motor_turn"}
|
|
196
|
+
if tool_metadata and corr_tool in _PIN_FALLBACK_TOOLS:
|
|
197
|
+
meta_bcm = tool_metadata.get("bcm")
|
|
198
|
+
if meta_bcm is not None:
|
|
199
|
+
corr_inputs = dict(corr_inputs)
|
|
200
|
+
pin_keys = (["pin"] if corr_tool == "servo_set"
|
|
201
|
+
else ["left_pin", "right_pin"])
|
|
202
|
+
for pk in pin_keys:
|
|
203
|
+
if corr_inputs.get(pk) != meta_bcm:
|
|
204
|
+
corr_inputs[pk] = meta_bcm
|
|
205
|
+
print(f" [CORRECT] Overrode {pk} → "
|
|
206
|
+
f"BCM {meta_bcm} from original tool metadata")
|
|
207
|
+
|
|
178
208
|
correction_entry = {
|
|
179
209
|
"attempt": attempt,
|
|
180
210
|
"issue": issue,
|
|
@@ -227,11 +257,17 @@ class CorrectionAgent:
|
|
|
227
257
|
}
|
|
228
258
|
|
|
229
259
|
def _call_llm(self, tool: str, params: Dict, expected: str,
|
|
230
|
-
actual: str
|
|
260
|
+
actual: str,
|
|
261
|
+
tool_metadata: Optional[Dict] = None) -> Optional[Dict]:
|
|
231
262
|
"""Call the Anthropic API to evaluate step success."""
|
|
263
|
+
meta_line = ""
|
|
264
|
+
if tool_metadata:
|
|
265
|
+
meta_line = (f"Original tool metadata: "
|
|
266
|
+
f"{json.dumps(tool_metadata)}\n")
|
|
232
267
|
user_msg = (
|
|
233
268
|
f"Tool: {tool}\n"
|
|
234
269
|
f"Inputs: {json.dumps(params, default=str)[:500]}\n"
|
|
270
|
+
f"{meta_line}"
|
|
235
271
|
f"Expected: {expected}\n"
|
|
236
272
|
f"Got: {actual}\n\n"
|
|
237
273
|
f"Available tools for corrections: {', '.join(self._available_tools)}\n\n"
|
|
@@ -9,10 +9,12 @@ All paths to data files (configs/, context/, weights/) are resolved
|
|
|
9
9
|
relative to the *project root*, which is auto-detected at import time.
|
|
10
10
|
"""
|
|
11
11
|
import argparse
|
|
12
|
+
import atexit
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
14
15
|
import os
|
|
15
16
|
import re
|
|
17
|
+
import signal as _signal
|
|
16
18
|
import sys
|
|
17
19
|
import time
|
|
18
20
|
|
|
@@ -88,6 +90,60 @@ def silence_http_logging() -> None:
|
|
|
88
90
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
# ── GPIO cleanup on exit ──────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
_gpio_cleanup_registered = False
|
|
96
|
+
_gpio_nav_refs: list = [] # holds nav_engine references for PWM draining
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _register_gpio_cleanup(nav_engine=None) -> None:
|
|
100
|
+
"""Register atexit + SIGINT/SIGTERM handlers that drain PWMs and call
|
|
101
|
+
GPIO.cleanup(). Safe to call multiple times — registers only once.
|
|
102
|
+
"""
|
|
103
|
+
global _gpio_cleanup_registered
|
|
104
|
+
if _gpio_cleanup_registered:
|
|
105
|
+
return
|
|
106
|
+
_gpio_cleanup_registered = True
|
|
107
|
+
|
|
108
|
+
if nav_engine is not None:
|
|
109
|
+
_gpio_nav_refs.append(nav_engine)
|
|
110
|
+
|
|
111
|
+
def _do_cleanup():
|
|
112
|
+
# 1. Stop live PWM instances held by NavigationEngine._motors
|
|
113
|
+
for nav in _gpio_nav_refs:
|
|
114
|
+
motors = getattr(nav, "_motors", None)
|
|
115
|
+
if motors is not None:
|
|
116
|
+
pwm_map = getattr(motors, "_pwm_instances", {})
|
|
117
|
+
n = len(pwm_map)
|
|
118
|
+
for pwm_obj in list(pwm_map.values()):
|
|
119
|
+
try:
|
|
120
|
+
pwm_obj.stop()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
pwm_map.clear()
|
|
124
|
+
if n:
|
|
125
|
+
print(f"\n[GPIO] Stopped {n} active PWM instance(s)")
|
|
126
|
+
# 2. Full GPIO cleanup
|
|
127
|
+
try:
|
|
128
|
+
import RPi.GPIO as GPIO
|
|
129
|
+
GPIO.cleanup()
|
|
130
|
+
print("\n[GPIO] Released all pins on exit")
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
atexit.register(_do_cleanup)
|
|
135
|
+
|
|
136
|
+
def _sig_handler(sig, frame):
|
|
137
|
+
_do_cleanup()
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
_signal.signal(_signal.SIGINT, _sig_handler)
|
|
142
|
+
_signal.signal(_signal.SIGTERM, _sig_handler)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass # not on main thread in some test runners
|
|
145
|
+
|
|
146
|
+
|
|
91
147
|
def run_sim(args) -> None:
|
|
92
148
|
"""Existing simulation mode — unchanged."""
|
|
93
149
|
from aether.simulation.environment import SimulationEnvironment
|
|
@@ -482,6 +538,7 @@ def run_agent(args) -> None:
|
|
|
482
538
|
_print_activity("OLED", f"startup animation failed: {e}")
|
|
483
539
|
|
|
484
540
|
nav_engine = NavigationEngine(discovery.manifest, tools=built_tools)
|
|
541
|
+
_register_gpio_cleanup(nav_engine) # drain PWMs + GPIO.cleanup on exit
|
|
485
542
|
nav_actions = nav_engine.available_actions()
|
|
486
543
|
_print_activity("NAV", f"Level {nav_engine.level} ({nav_engine.level_name}) — "
|
|
487
544
|
f"{len(nav_actions)} navigation actions")
|
|
@@ -1720,6 +1777,7 @@ def main():
|
|
|
1720
1777
|
discovery.discover()
|
|
1721
1778
|
discovery.print_summary()
|
|
1722
1779
|
|
|
1780
|
+
_register_gpio_cleanup() # GPIO.cleanup on exit during calibration
|
|
1723
1781
|
auto_mode = getattr(args, "auto_calibrate", False)
|
|
1724
1782
|
wizard = CalibrationWizard(
|
|
1725
1783
|
discovery.manifest,
|
|
@@ -131,6 +131,14 @@ _CATEGORY_ACTION_PREFIX: Dict[int, str] = {
|
|
|
131
131
|
6: "motor",
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
# Stopwords stripped before slugifying descriptions (Bug 4)
|
|
135
|
+
_SLUG_STOPWORDS = frozenset({
|
|
136
|
+
"the", "then", "and", "a", "an", "that", "moved", "move", "moves",
|
|
137
|
+
"is", "was", "it", "of", "to", "in", "at", "by", "for", "from",
|
|
138
|
+
"with", "or", "but", "on", "its", "when", "while", "also", "did",
|
|
139
|
+
"went", "going", "started", "which",
|
|
140
|
+
})
|
|
141
|
+
|
|
134
142
|
|
|
135
143
|
# ── Module-level helpers ──────────────────────────────────────────────
|
|
136
144
|
|
|
@@ -145,10 +153,13 @@ def _validate_robot_name(name: str) -> Optional[str]:
|
|
|
145
153
|
|
|
146
154
|
|
|
147
155
|
def _derive_action_label(category: int, description: str) -> str:
|
|
148
|
-
"""Slug-ify description
|
|
149
|
-
|
|
156
|
+
"""Slug-ify description, strip stopwords, cap at 32 chars."""
|
|
157
|
+
words = _re.sub(r"[^a-zA-Z0-9\s]+", " ", description.lower()).split()
|
|
158
|
+
words = [w for w in words if w not in _SLUG_STOPWORDS]
|
|
159
|
+
slug = "_".join(words)
|
|
150
160
|
prefix = _CATEGORY_ACTION_PREFIX.get(category, "action")
|
|
151
|
-
|
|
161
|
+
label = f"{prefix}_{slug}"
|
|
162
|
+
return label[:32].rstrip("_")
|
|
152
163
|
|
|
153
164
|
|
|
154
165
|
def _input_int(prompt: str, valid_range: Optional[range] = None) -> int:
|
|
@@ -477,7 +488,7 @@ class CalibrationWizard:
|
|
|
477
488
|
try:
|
|
478
489
|
desc = input(
|
|
479
490
|
" Describe what moved "
|
|
480
|
-
"(e.g. '
|
|
491
|
+
"(e.g. 'left wheel' or 'gripper' — keep it short): "
|
|
481
492
|
).strip()
|
|
482
493
|
except EOFError:
|
|
483
494
|
desc = ""
|
|
@@ -511,12 +522,18 @@ class CalibrationWizard:
|
|
|
511
522
|
print(f" Mapped: BCM {bcm_pin} → {action_label} "
|
|
512
523
|
f"({_CATEGORY_LABELS[choice]})")
|
|
513
524
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
525
|
+
cont = ""
|
|
526
|
+
while True:
|
|
527
|
+
try:
|
|
528
|
+
cont = input(
|
|
529
|
+
" Continue to next pin? [Y/n/s=stop]: "
|
|
530
|
+
).strip().lower()
|
|
531
|
+
except EOFError:
|
|
532
|
+
cont = ""
|
|
533
|
+
if cont in ("", "y", "yes", "n", "no", "s", "stop"):
|
|
534
|
+
break
|
|
535
|
+
print(" Please enter Y to continue, N to stop here, "
|
|
536
|
+
"or S to stop calibration.")
|
|
520
537
|
if cont in ("n", "no", "s", "stop"):
|
|
521
538
|
break
|
|
522
539
|
|
|
@@ -1031,9 +1048,15 @@ class CalibrationWizard:
|
|
|
1031
1048
|
|
|
1032
1049
|
def _pulse_servo_pin(self, bcm_pin: int,
|
|
1033
1050
|
servo_type: str = "continuous") -> bool:
|
|
1034
|
-
"""Pulse a servo pin. Returns True if pulse was sent.
|
|
1051
|
+
"""Pulse a servo pin. Returns True if pulse was sent.
|
|
1052
|
+
|
|
1053
|
+
cleanup() is in a finally block so GPIO releases even on KeyboardInterrupt.
|
|
1054
|
+
"""
|
|
1055
|
+
pwm = None
|
|
1056
|
+
gpio_mod = None
|
|
1035
1057
|
try:
|
|
1036
1058
|
import RPi.GPIO as GPIO
|
|
1059
|
+
gpio_mod = GPIO
|
|
1037
1060
|
GPIO.setmode(GPIO.BCM)
|
|
1038
1061
|
GPIO.setwarnings(False)
|
|
1039
1062
|
GPIO.setup(bcm_pin, GPIO.OUT)
|
|
@@ -1055,11 +1078,20 @@ class CalibrationWizard:
|
|
|
1055
1078
|
time.sleep(0.3)
|
|
1056
1079
|
pwm.ChangeDutyCycle(7.0) # back to 90°
|
|
1057
1080
|
time.sleep(0.15)
|
|
1058
|
-
pwm.stop()
|
|
1059
|
-
GPIO.cleanup(bcm_pin)
|
|
1060
1081
|
return True
|
|
1061
1082
|
except (ImportError, RuntimeError, Exception):
|
|
1062
1083
|
return False
|
|
1084
|
+
finally:
|
|
1085
|
+
if pwm is not None:
|
|
1086
|
+
try:
|
|
1087
|
+
pwm.stop()
|
|
1088
|
+
except Exception:
|
|
1089
|
+
pass
|
|
1090
|
+
if gpio_mod is not None:
|
|
1091
|
+
try:
|
|
1092
|
+
gpio_mod.cleanup(bcm_pin)
|
|
1093
|
+
except Exception:
|
|
1094
|
+
pass
|
|
1063
1095
|
|
|
1064
1096
|
def _test_channel(self, channel: str, power: float = 0.1,
|
|
1065
1097
|
duration: float = 0.5,
|
|
@@ -872,8 +872,9 @@ class _MotorNav:
|
|
|
872
872
|
duty = 2.0 + (mapped / 180.0) * 10.0
|
|
873
873
|
result = self.pwm_set(pin, 50, duty)
|
|
874
874
|
if result.get("success"):
|
|
875
|
-
|
|
876
|
-
result["
|
|
875
|
+
# pwm_set returns a flat dict — annotate directly, no "data" key
|
|
876
|
+
result["speed"] = speed
|
|
877
|
+
result["servo_type"] = "continuous"
|
|
877
878
|
return result
|
|
878
879
|
|
|
879
880
|
# Positional servo (default): 2-12% duty cycle at 50Hz for 0-180°
|
|
@@ -11,6 +11,33 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, List, Optional, Any
|
|
13
13
|
|
|
14
|
+
# ── Continuous-servo direction helpers (used by physical_map tool closures) ──
|
|
15
|
+
|
|
16
|
+
_CONTINUOUS_STOP_TERMS = frozenset({
|
|
17
|
+
"stop", "halt", "off", "neutral", "brake", "idle", "0",
|
|
18
|
+
})
|
|
19
|
+
_CONTINUOUS_REVERSE_TERMS = frozenset({
|
|
20
|
+
"reverse", "backward", "back", "counter", "ccw",
|
|
21
|
+
"counterclockwise", "negative",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _infer_continuous_speed(val) -> float:
|
|
26
|
+
"""Map a direction hint to continuous-servo speed (-100..+100).
|
|
27
|
+
|
|
28
|
+
0.0 = stop (90° PWM pulse)
|
|
29
|
+
100.0 = full forward / clockwise (180° PWM pulse)
|
|
30
|
+
-100.0 = full reverse / counter-clockwise (0° PWM pulse)
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(val, (int, float)):
|
|
33
|
+
return float(max(-100.0, min(100.0, val)))
|
|
34
|
+
s = str(val).lower()
|
|
35
|
+
if any(t in s for t in _CONTINUOUS_STOP_TERMS):
|
|
36
|
+
return 0.0
|
|
37
|
+
if any(t in s for t in _CONTINUOUS_REVERSE_TERMS):
|
|
38
|
+
return -100.0
|
|
39
|
+
return 100.0 # forward / clockwise / go / start
|
|
40
|
+
|
|
14
41
|
|
|
15
42
|
@dataclass
|
|
16
43
|
class ToolResult:
|
|
@@ -25,6 +52,7 @@ class ToolResult:
|
|
|
25
52
|
class BaseTool(ABC):
|
|
26
53
|
"""Abstract base matching AbstractAction interface."""
|
|
27
54
|
name: str = ""
|
|
55
|
+
metadata: Dict = {} # overridden by DynamicTool for physical_map entries
|
|
28
56
|
|
|
29
57
|
@abstractmethod
|
|
30
58
|
def execute(self, params: Dict) -> ToolResult:
|
|
@@ -751,10 +779,12 @@ class DynamicTool(BaseTool):
|
|
|
751
779
|
and NavigationEngine actions into the BaseTool/ToolResult interface.
|
|
752
780
|
"""
|
|
753
781
|
|
|
754
|
-
def __init__(self, name: str, fn, desc: str = ""
|
|
782
|
+
def __init__(self, name: str, fn, desc: str = "",
|
|
783
|
+
metadata: Optional[Dict] = None):
|
|
755
784
|
self.name = name
|
|
756
785
|
self._fn = fn
|
|
757
786
|
self._desc = desc or f"Dynamic tool: {name}"
|
|
787
|
+
self.metadata: Dict = metadata or {}
|
|
758
788
|
|
|
759
789
|
def description(self) -> str:
|
|
760
790
|
return self._desc
|
|
@@ -973,6 +1003,59 @@ def register_built_tools(registry: "ToolRegistry",
|
|
|
973
1003
|
f"Navigation action: {action} (level {nav_engine.level})"))
|
|
974
1004
|
count += 1
|
|
975
1005
|
|
|
1006
|
+
# -- Physical map tools ---------------------------------------------------
|
|
1007
|
+
# Register one real callable tool per physical_map entry. The LLM emits
|
|
1008
|
+
# the action label; this makes it actually executable against the right pin.
|
|
1009
|
+
physical_map = (manifest or {}).get("physical_map", {})
|
|
1010
|
+
if physical_map and nav_engine is not None:
|
|
1011
|
+
for bucket in ("wheels", "arms", "grippers", "servos", "dc_motors"):
|
|
1012
|
+
for entry in physical_map.get(bucket, []):
|
|
1013
|
+
label = entry.get("action")
|
|
1014
|
+
bcm = entry.get("bcm")
|
|
1015
|
+
ptype = entry.get("pwm_type", "servo_positional")
|
|
1016
|
+
desc = entry.get("description", label or "")
|
|
1017
|
+
|
|
1018
|
+
if not label or bcm is None:
|
|
1019
|
+
continue
|
|
1020
|
+
if registry.get(label) is not None:
|
|
1021
|
+
continue # already registered (e.g. from prior iteration)
|
|
1022
|
+
|
|
1023
|
+
def _make_physical_fn(pin, pwm_type):
|
|
1024
|
+
def _fn(**kwargs):
|
|
1025
|
+
if pwm_type == "servo_continuous":
|
|
1026
|
+
# Accept direction/speed/angle — any key the LLM uses
|
|
1027
|
+
hint = kwargs.get(
|
|
1028
|
+
"direction",
|
|
1029
|
+
kwargs.get("speed",
|
|
1030
|
+
kwargs.get("angle", "forward")))
|
|
1031
|
+
speed = _infer_continuous_speed(hint)
|
|
1032
|
+
return registry.execute(
|
|
1033
|
+
"servo_set",
|
|
1034
|
+
{"pin": pin, "angle": speed,
|
|
1035
|
+
"servo_type": "continuous"})
|
|
1036
|
+
elif pwm_type == "servo_positional":
|
|
1037
|
+
angle = float(kwargs.get("angle", 90))
|
|
1038
|
+
return registry.execute(
|
|
1039
|
+
"servo_set",
|
|
1040
|
+
{"pin": pin, "angle": angle,
|
|
1041
|
+
"servo_type": "positional"})
|
|
1042
|
+
else: # dc
|
|
1043
|
+
spd = float(kwargs.get("speed", 50))
|
|
1044
|
+
return registry.execute(
|
|
1045
|
+
"motor_forward",
|
|
1046
|
+
{"left_pin": pin, "right_pin": pin,
|
|
1047
|
+
"speed": spd})
|
|
1048
|
+
return _fn
|
|
1049
|
+
|
|
1050
|
+
registry.register(DynamicTool(
|
|
1051
|
+
label,
|
|
1052
|
+
_make_physical_fn(bcm, ptype),
|
|
1053
|
+
f"{desc} (BCM {bcm}, {ptype})",
|
|
1054
|
+
metadata={"bcm": bcm, "pwm_type": ptype,
|
|
1055
|
+
"description": desc},
|
|
1056
|
+
))
|
|
1057
|
+
count += 1
|
|
1058
|
+
|
|
976
1059
|
print(f" [ToolRegistry] Registered {count} tools from ToolBuilder")
|
|
977
1060
|
return count
|
|
978
1061
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-robotics
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.2
|
|
4
4
|
Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
|
|
5
5
|
Author: Chahel Paatur
|
|
6
6
|
License: MIT
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT">
|
|
46
46
|
<img src="https://img.shields.io/github/stars/ChahelPaatur/AETHER?style=flat-square" alt="GitHub Stars">
|
|
47
47
|
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Raspberry%20Pi%20%7C%20Jetson-lightgrey?style=flat-square" alt="Platforms">
|
|
48
|
-
<img src="https://img.shields.io/badge/version-3.
|
|
48
|
+
<img src="https://img.shields.io/badge/version-3.4.0-red?style=flat-square" alt="v3.4.0">
|
|
49
49
|
</p>
|
|
50
50
|
|
|
51
51
|
```
|
|
@@ -13,6 +13,7 @@ from aether.core.calibration import (
|
|
|
13
13
|
CalibrationWizard,
|
|
14
14
|
load_calibration,
|
|
15
15
|
_expected_direction,
|
|
16
|
+
_derive_action_label,
|
|
16
17
|
GROUND_VEHICLE,
|
|
17
18
|
AERIAL_VEHICLE,
|
|
18
19
|
ROBOTIC_ARM,
|
|
@@ -415,3 +416,202 @@ class TestFullAutoRun:
|
|
|
415
416
|
|
|
416
417
|
assert profile["calibrated"] is True
|
|
417
418
|
assert profile["robot_type"] == AERIAL_VEHICLE
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestActionLabelSlugifier:
|
|
422
|
+
"""Bug 4: _derive_action_label stopwords + 32-char cap."""
|
|
423
|
+
|
|
424
|
+
def test_strips_stopwords(self):
|
|
425
|
+
label = _derive_action_label(1, "the left wheel moved forward")
|
|
426
|
+
assert "the" not in label
|
|
427
|
+
assert "moved" not in label
|
|
428
|
+
assert "left" in label
|
|
429
|
+
assert "forward" in label
|
|
430
|
+
|
|
431
|
+
def test_caps_at_32_chars(self):
|
|
432
|
+
long_desc = "left wheel clockwise then counter clockwise slowly"
|
|
433
|
+
label = _derive_action_label(5, long_desc)
|
|
434
|
+
assert len(label) <= 32
|
|
435
|
+
|
|
436
|
+
def test_correct_prefix(self):
|
|
437
|
+
assert _derive_action_label(1, "left wheel").startswith("drive_")
|
|
438
|
+
assert _derive_action_label(3, "gripper").startswith("gripper_")
|
|
439
|
+
assert _derive_action_label(5, "left wheel").startswith("servo_cont_")
|
|
440
|
+
assert _derive_action_label(6, "motor A").startswith("motor_")
|
|
441
|
+
|
|
442
|
+
def test_no_trailing_underscore(self):
|
|
443
|
+
label = _derive_action_label(1, "the the the")
|
|
444
|
+
assert not label.endswith("_")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class TestPhysicalMapToolRegistration:
|
|
448
|
+
"""Bug 1: physical_map entries become callable tools in ToolRegistry."""
|
|
449
|
+
|
|
450
|
+
_PHYSICAL_MAP = {
|
|
451
|
+
"wheels": [{"bcm": 17, "description": "left wheel forward",
|
|
452
|
+
"action": "drive_left_wheel_forward",
|
|
453
|
+
"pwm_type": "servo_positional"}],
|
|
454
|
+
"servos": [{"bcm": 22, "description": "left wheel",
|
|
455
|
+
"action": "servo_cont_left_wheel",
|
|
456
|
+
"pwm_type": "servo_continuous"}],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
def test_physical_map_labels_registered(self):
|
|
460
|
+
"""register_built_tools adds a DynamicTool for each physical_map entry."""
|
|
461
|
+
from aether.core.tool_registry import (
|
|
462
|
+
ToolRegistry, register_built_tools, _infer_continuous_speed,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
registry = ToolRegistry()
|
|
466
|
+
manifest = {"physical_map": self._PHYSICAL_MAP}
|
|
467
|
+
|
|
468
|
+
# nav_engine stub: just needs to not be None and have available_actions()
|
|
469
|
+
class _FakeNav:
|
|
470
|
+
level = 2
|
|
471
|
+
def available_actions(self): return []
|
|
472
|
+
def execute(self, act, params): return {"success": True}
|
|
473
|
+
|
|
474
|
+
register_built_tools(registry, {}, _FakeNav(), manifest=manifest)
|
|
475
|
+
|
|
476
|
+
assert registry.get("drive_left_wheel_forward") is not None
|
|
477
|
+
assert registry.get("servo_cont_left_wheel") is not None
|
|
478
|
+
|
|
479
|
+
def test_infer_continuous_speed_forward(self):
|
|
480
|
+
from aether.core.tool_registry import _infer_continuous_speed
|
|
481
|
+
assert _infer_continuous_speed("forward") == 100.0
|
|
482
|
+
assert _infer_continuous_speed("go") == 100.0
|
|
483
|
+
assert _infer_continuous_speed("clockwise") == 100.0
|
|
484
|
+
|
|
485
|
+
def test_infer_continuous_speed_stop(self):
|
|
486
|
+
from aether.core.tool_registry import _infer_continuous_speed
|
|
487
|
+
assert _infer_continuous_speed("stop") == 0.0
|
|
488
|
+
assert _infer_continuous_speed("halt") == 0.0
|
|
489
|
+
assert _infer_continuous_speed(0) == 0.0
|
|
490
|
+
|
|
491
|
+
def test_infer_continuous_speed_reverse(self):
|
|
492
|
+
from aether.core.tool_registry import _infer_continuous_speed
|
|
493
|
+
assert _infer_continuous_speed("reverse") == -100.0
|
|
494
|
+
assert _infer_continuous_speed("backward") == -100.0
|
|
495
|
+
assert _infer_continuous_speed(-50) == -50.0
|
|
496
|
+
|
|
497
|
+
def test_physical_map_tool_has_metadata(self):
|
|
498
|
+
"""Bug B: registered physical_map DynamicTool carries bcm + pwm_type metadata."""
|
|
499
|
+
from aether.core.tool_registry import ToolRegistry, register_built_tools
|
|
500
|
+
|
|
501
|
+
registry = ToolRegistry()
|
|
502
|
+
manifest = {"physical_map": self._PHYSICAL_MAP}
|
|
503
|
+
|
|
504
|
+
class _FakeNav:
|
|
505
|
+
level = 2
|
|
506
|
+
def available_actions(self): return []
|
|
507
|
+
def execute(self, act, params): return {"success": True}
|
|
508
|
+
|
|
509
|
+
register_built_tools(registry, {}, _FakeNav(), manifest=manifest)
|
|
510
|
+
|
|
511
|
+
tool = registry.get("drive_left_wheel_forward")
|
|
512
|
+
assert tool is not None
|
|
513
|
+
assert tool.metadata.get("bcm") == 17
|
|
514
|
+
assert tool.metadata.get("pwm_type") == "servo_positional"
|
|
515
|
+
|
|
516
|
+
tool_cont = registry.get("servo_cont_left_wheel")
|
|
517
|
+
assert tool_cont is not None
|
|
518
|
+
assert tool_cont.metadata.get("bcm") == 22
|
|
519
|
+
assert tool_cont.metadata.get("pwm_type") == "servo_continuous"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class TestBugFixes342:
|
|
523
|
+
"""3.4.2 regression tests: servo_set flat dict, correction pin override,
|
|
524
|
+
pulse_servo_pin cleanup on interrupt."""
|
|
525
|
+
|
|
526
|
+
def test_servo_set_continuous_no_data_keyerror(self):
|
|
527
|
+
"""Bug A: servo_set continuous result must not raise KeyError on 'data'."""
|
|
528
|
+
# Simulate the _MotorNav.servo_set logic without real GPIO
|
|
529
|
+
# pwm_set returns flat dict — no "data" key
|
|
530
|
+
result = {"pin": 17, "frequency": 50, "duty_cycle": 12.0,
|
|
531
|
+
"success": True, "error": ""}
|
|
532
|
+
# Apply the fixed logic
|
|
533
|
+
if result.get("success"):
|
|
534
|
+
result["speed"] = 100.0
|
|
535
|
+
result["servo_type"] = "continuous"
|
|
536
|
+
assert "speed" in result
|
|
537
|
+
assert result["servo_type"] == "continuous"
|
|
538
|
+
assert "data" not in result # old bug: this would have been needed
|
|
539
|
+
|
|
540
|
+
def test_correction_agent_pin_override(self):
|
|
541
|
+
"""Bug B belt-and-suspenders: CorrectionAgent overrides wrong pin from LLM."""
|
|
542
|
+
# Simulate the override logic with tool_metadata bcm=17 and LLM proposing pin=18
|
|
543
|
+
tool_metadata = {"bcm": 17, "pwm_type": "servo_continuous"}
|
|
544
|
+
corr_tool = "servo_set"
|
|
545
|
+
corr_inputs = {"pin": 18, "angle": 100, "servo_type": "continuous"}
|
|
546
|
+
|
|
547
|
+
_PIN_FALLBACK_TOOLS = {"servo_set", "motor_forward", "motor_turn"}
|
|
548
|
+
if tool_metadata and corr_tool in _PIN_FALLBACK_TOOLS:
|
|
549
|
+
meta_bcm = tool_metadata.get("bcm")
|
|
550
|
+
if meta_bcm is not None:
|
|
551
|
+
corr_inputs = dict(corr_inputs)
|
|
552
|
+
pin_keys = (["pin"] if corr_tool == "servo_set"
|
|
553
|
+
else ["left_pin", "right_pin"])
|
|
554
|
+
for pk in pin_keys:
|
|
555
|
+
if corr_inputs.get(pk) != meta_bcm:
|
|
556
|
+
corr_inputs[pk] = meta_bcm
|
|
557
|
+
|
|
558
|
+
assert corr_inputs["pin"] == 17 # overridden from 18
|
|
559
|
+
|
|
560
|
+
def test_correction_agent_pin_override_missing_pin(self):
|
|
561
|
+
"""Bug B: pin added when absent from correction inputs."""
|
|
562
|
+
tool_metadata = {"bcm": 22, "pwm_type": "servo_positional"}
|
|
563
|
+
corr_tool = "servo_set"
|
|
564
|
+
corr_inputs = {"angle": 90, "servo_type": "positional"} # no pin
|
|
565
|
+
|
|
566
|
+
_PIN_FALLBACK_TOOLS = {"servo_set", "motor_forward", "motor_turn"}
|
|
567
|
+
if tool_metadata and corr_tool in _PIN_FALLBACK_TOOLS:
|
|
568
|
+
meta_bcm = tool_metadata.get("bcm")
|
|
569
|
+
if meta_bcm is not None:
|
|
570
|
+
corr_inputs = dict(corr_inputs)
|
|
571
|
+
pin_keys = ["pin"]
|
|
572
|
+
for pk in pin_keys:
|
|
573
|
+
if corr_inputs.get(pk) != meta_bcm:
|
|
574
|
+
corr_inputs[pk] = meta_bcm
|
|
575
|
+
|
|
576
|
+
assert corr_inputs["pin"] == 22
|
|
577
|
+
|
|
578
|
+
def test_pulse_servo_pin_finally_on_simulated_interrupt(self):
|
|
579
|
+
"""Bug C: _pulse_servo_pin cleanup runs even when GPIO raises."""
|
|
580
|
+
cleanup_called = []
|
|
581
|
+
|
|
582
|
+
class _FakeGPIO:
|
|
583
|
+
BCM = 11
|
|
584
|
+
OUT = 0
|
|
585
|
+
@staticmethod
|
|
586
|
+
def setmode(m): pass
|
|
587
|
+
@staticmethod
|
|
588
|
+
def setwarnings(w): pass
|
|
589
|
+
@staticmethod
|
|
590
|
+
def setup(p, m): pass
|
|
591
|
+
class PWM:
|
|
592
|
+
def __init__(self, pin, freq): pass
|
|
593
|
+
def start(self, dc): pass
|
|
594
|
+
def ChangeDutyCycle(self, dc):
|
|
595
|
+
raise RuntimeError("simulated mid-pulse error")
|
|
596
|
+
def stop(self): cleanup_called.append("pwm.stop")
|
|
597
|
+
@staticmethod
|
|
598
|
+
def cleanup(pin): cleanup_called.append(f"cleanup:{pin}")
|
|
599
|
+
|
|
600
|
+
import sys
|
|
601
|
+
sys.modules["RPi"] = type(sys)("RPi")
|
|
602
|
+
sys.modules["RPi.GPIO"] = _FakeGPIO
|
|
603
|
+
try:
|
|
604
|
+
wiz = CalibrationWizard(
|
|
605
|
+
{"hardware": {"gpio": {"available": False},
|
|
606
|
+
"mavlink": {"available": False}}},
|
|
607
|
+
configs_dir="/tmp",
|
|
608
|
+
auto_mode=True,
|
|
609
|
+
)
|
|
610
|
+
result = wiz._pulse_servo_pin(17, "positional")
|
|
611
|
+
finally:
|
|
612
|
+
del sys.modules["RPi.GPIO"]
|
|
613
|
+
del sys.modules["RPi"]
|
|
614
|
+
|
|
615
|
+
# Whether the pulse succeeded or not, cleanup must have been attempted
|
|
616
|
+
assert any("cleanup" in str(c) or "stop" in str(c)
|
|
617
|
+
for c in cleanup_called)
|
aether_robotics-3.4.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.3.9
|
|
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.4.0 → aether_robotics-3.4.2}/aether_robotics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260331_121709.json
RENAMED
|
File without changes
|
{aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260331_122446.json
RENAMED
|
File without changes
|
{aether_robotics-3.4.0 → aether_robotics-3.4.2}/configs/calibrated_camera_only_20260401_094544.json
RENAMED
|
File without changes
|
{aether_robotics-3.4.0 → aether_robotics-3.4.2}/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
|