foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport Abstraction Layer for Robot Communication.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for communicating with robots
|
|
5
|
+
across different physical transports (BLE, USB Serial, WiFi, etc.).
|
|
6
|
+
|
|
7
|
+
The key insight: Users shouldn't need to know HOW a robot communicates,
|
|
8
|
+
only WHAT they want it to do.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
from ate.transports import connect
|
|
12
|
+
|
|
13
|
+
# Auto-discovers available transports
|
|
14
|
+
robot = await connect("mechdog")
|
|
15
|
+
|
|
16
|
+
# These work regardless of underlying transport
|
|
17
|
+
await robot.walk_forward()
|
|
18
|
+
await robot.gripper_open() # Automatically routes to USB if BLE can't do it
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from .base import (
|
|
24
|
+
RobotTransport,
|
|
25
|
+
TransportCapability,
|
|
26
|
+
TransportState,
|
|
27
|
+
ConnectionInfo,
|
|
28
|
+
CommandResult,
|
|
29
|
+
HiWonderProtocol,
|
|
30
|
+
)
|
|
31
|
+
from .ble import BLETransport
|
|
32
|
+
from .serial import SerialTransport
|
|
33
|
+
from .hybrid import HybridTransport
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def connect(
|
|
37
|
+
address: Optional[str] = None,
|
|
38
|
+
name: Optional[str] = None,
|
|
39
|
+
transport_type: str = "auto"
|
|
40
|
+
) -> HybridTransport:
|
|
41
|
+
"""
|
|
42
|
+
Connect to a robot with automatic transport detection.
|
|
43
|
+
|
|
44
|
+
This is the main entry point for the transport layer.
|
|
45
|
+
It discovers available transports and creates a hybrid connection.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
address: Specific address to connect to (BLE MAC/UUID or serial port)
|
|
49
|
+
name: Robot name hint for discovery (e.g., "mechdog")
|
|
50
|
+
transport_type: "auto", "ble", "serial", or "hybrid"
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Connected HybridTransport instance
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
# Auto-discover and connect
|
|
57
|
+
robot = await connect(name="mechdog")
|
|
58
|
+
await robot.walk_forward()
|
|
59
|
+
|
|
60
|
+
# Connect to specific BLE device
|
|
61
|
+
robot = await connect(address="FAF198B6-D9A4-CC27-7BBA-3159F63A61A9")
|
|
62
|
+
|
|
63
|
+
# Connect to specific serial port
|
|
64
|
+
robot = await connect(address="/dev/cu.usbmodem1234")
|
|
65
|
+
"""
|
|
66
|
+
robot = HybridTransport()
|
|
67
|
+
|
|
68
|
+
if address:
|
|
69
|
+
# Direct address provided
|
|
70
|
+
await robot.connect(address, name=name)
|
|
71
|
+
else:
|
|
72
|
+
# Discover and connect to first matching device
|
|
73
|
+
connections = await HybridTransport.discover(timeout=5.0)
|
|
74
|
+
|
|
75
|
+
ble_device = None
|
|
76
|
+
serial_device = None
|
|
77
|
+
|
|
78
|
+
for conn in connections:
|
|
79
|
+
if name and conn.name and name.lower() not in conn.name.lower():
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if conn.transport_type == "ble" and not ble_device:
|
|
83
|
+
ble_device = conn
|
|
84
|
+
elif conn.transport_type == "serial" and not serial_device:
|
|
85
|
+
serial_device = conn
|
|
86
|
+
|
|
87
|
+
if ble_device:
|
|
88
|
+
await robot.connect_ble(ble_device.address)
|
|
89
|
+
if serial_device:
|
|
90
|
+
await robot.connect_serial(serial_device.address)
|
|
91
|
+
|
|
92
|
+
return robot
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def discover(timeout: float = 5.0) -> list[ConnectionInfo]:
|
|
96
|
+
"""
|
|
97
|
+
Discover all available robots across all transports.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of discovered robot connections
|
|
101
|
+
"""
|
|
102
|
+
return await HybridTransport.discover(timeout)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
# Core types
|
|
107
|
+
"RobotTransport",
|
|
108
|
+
"TransportCapability",
|
|
109
|
+
"TransportState",
|
|
110
|
+
"ConnectionInfo",
|
|
111
|
+
"CommandResult",
|
|
112
|
+
# Protocol adapters
|
|
113
|
+
"HiWonderProtocol",
|
|
114
|
+
# Transport implementations
|
|
115
|
+
"BLETransport",
|
|
116
|
+
"SerialTransport",
|
|
117
|
+
"HybridTransport",
|
|
118
|
+
# Convenience functions
|
|
119
|
+
"connect",
|
|
120
|
+
"discover",
|
|
121
|
+
]
|
ate/transports/base.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Transport Interface.
|
|
3
|
+
|
|
4
|
+
Defines the abstract interface that all robot transports must implement.
|
|
5
|
+
This allows the rest of the codebase to work with robots without knowing
|
|
6
|
+
the underlying communication protocol.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from typing import Optional, Callable, Awaitable, Any
|
|
13
|
+
import asyncio
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TransportState(Enum):
|
|
17
|
+
"""Current state of a transport connection."""
|
|
18
|
+
DISCONNECTED = auto()
|
|
19
|
+
CONNECTING = auto()
|
|
20
|
+
CONNECTED = auto()
|
|
21
|
+
ERROR = auto()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TransportCapability(Enum):
|
|
25
|
+
"""
|
|
26
|
+
Capabilities that a transport may or may not support.
|
|
27
|
+
|
|
28
|
+
This is CRITICAL for the abstraction layer - we discovered that:
|
|
29
|
+
- BLE can do locomotion but NOT arm control on MechDog
|
|
30
|
+
- USB Serial can do arm control but connection is finicky
|
|
31
|
+
- Different firmware versions support different commands
|
|
32
|
+
|
|
33
|
+
By probing capabilities, we can automatically route commands
|
|
34
|
+
to the right transport.
|
|
35
|
+
"""
|
|
36
|
+
# Locomotion
|
|
37
|
+
WALK = auto()
|
|
38
|
+
TURN = auto()
|
|
39
|
+
STAND = auto()
|
|
40
|
+
SIT = auto()
|
|
41
|
+
POSTURE = auto()
|
|
42
|
+
|
|
43
|
+
# Manipulation
|
|
44
|
+
ARM_POSITION = auto()
|
|
45
|
+
GRIPPER = auto()
|
|
46
|
+
|
|
47
|
+
# Sensors
|
|
48
|
+
ULTRASONIC = auto()
|
|
49
|
+
IMU = auto()
|
|
50
|
+
BATTERY = auto()
|
|
51
|
+
|
|
52
|
+
# Actions (pre-programmed sequences)
|
|
53
|
+
ACTION_GROUPS = auto()
|
|
54
|
+
|
|
55
|
+
# LED/Visual
|
|
56
|
+
RGB_LED = auto()
|
|
57
|
+
|
|
58
|
+
# Low-level
|
|
59
|
+
RAW_SERVO = auto()
|
|
60
|
+
|
|
61
|
+
# Meta
|
|
62
|
+
BIDIRECTIONAL = auto() # Can receive responses
|
|
63
|
+
STREAMING = auto() # Can stream continuous data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ConnectionInfo:
|
|
68
|
+
"""Information about a discovered or connected transport."""
|
|
69
|
+
transport_type: str # "ble", "serial", "wifi"
|
|
70
|
+
address: str # MAC address, port, or IP
|
|
71
|
+
name: Optional[str] = None
|
|
72
|
+
rssi: Optional[int] = None # Signal strength for wireless
|
|
73
|
+
metadata: dict = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CommandResult:
|
|
78
|
+
"""Result of sending a command."""
|
|
79
|
+
success: bool
|
|
80
|
+
response: Optional[str] = None
|
|
81
|
+
raw_data: Optional[bytes] = None
|
|
82
|
+
error: Optional[str] = None
|
|
83
|
+
latency_ms: Optional[float] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RobotTransport(ABC):
|
|
87
|
+
"""
|
|
88
|
+
Abstract base class for robot communication transports.
|
|
89
|
+
|
|
90
|
+
Every transport (BLE, Serial, WiFi) implements this interface,
|
|
91
|
+
allowing higher-level code to work with any robot without
|
|
92
|
+
caring about the underlying protocol.
|
|
93
|
+
|
|
94
|
+
Key design decisions:
|
|
95
|
+
1. Async-first: Robot communication is inherently async
|
|
96
|
+
2. Capability-based: Not all transports support all features
|
|
97
|
+
3. Response handling: Can register callbacks for async responses
|
|
98
|
+
4. Connection lifecycle: Connect/disconnect/reconnect handling
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self._state = TransportState.DISCONNECTED
|
|
103
|
+
self._capabilities: set[TransportCapability] = set()
|
|
104
|
+
self._response_handlers: list[Callable[[bytes], Awaitable[None]]] = []
|
|
105
|
+
self._connection_info: Optional[ConnectionInfo] = None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def state(self) -> TransportState:
|
|
109
|
+
"""Current connection state."""
|
|
110
|
+
return self._state
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_connected(self) -> bool:
|
|
114
|
+
"""Whether transport is currently connected."""
|
|
115
|
+
return self._state == TransportState.CONNECTED
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def capabilities(self) -> set[TransportCapability]:
|
|
119
|
+
"""Set of capabilities this transport supports."""
|
|
120
|
+
return self._capabilities.copy()
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def connection_info(self) -> Optional[ConnectionInfo]:
|
|
124
|
+
"""Information about the current connection."""
|
|
125
|
+
return self._connection_info
|
|
126
|
+
|
|
127
|
+
def has_capability(self, cap: TransportCapability) -> bool:
|
|
128
|
+
"""Check if this transport supports a specific capability."""
|
|
129
|
+
return cap in self._capabilities
|
|
130
|
+
|
|
131
|
+
# ========== Abstract Methods ==========
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def connect(self, address: str, **kwargs) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Connect to a robot at the given address.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
address: Transport-specific address (MAC, port, IP)
|
|
140
|
+
**kwargs: Transport-specific options
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if connection successful
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
async def disconnect(self) -> None:
|
|
149
|
+
"""Disconnect from the robot."""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
async def send(self, data: bytes) -> CommandResult:
|
|
154
|
+
"""
|
|
155
|
+
Send raw data to the robot.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data: Raw bytes to send
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
CommandResult with success status and any response
|
|
162
|
+
"""
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
async def send_command(self, command: str) -> CommandResult:
|
|
167
|
+
"""
|
|
168
|
+
Send a formatted command string to the robot.
|
|
169
|
+
|
|
170
|
+
This handles protocol-specific formatting (e.g., HiWonder CMD|...|$)
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
command: Command string in transport's native format
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
CommandResult with success status and any response
|
|
177
|
+
"""
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
@abstractmethod
|
|
182
|
+
async def discover(cls, timeout: float = 5.0) -> list[ConnectionInfo]:
|
|
183
|
+
"""
|
|
184
|
+
Discover available robots using this transport.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
timeout: How long to scan in seconds
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of discovered connection info
|
|
191
|
+
"""
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
async def probe_capabilities(self) -> set[TransportCapability]:
|
|
196
|
+
"""
|
|
197
|
+
Probe the connected robot to discover what it can do.
|
|
198
|
+
|
|
199
|
+
This is where we send test commands and see what works.
|
|
200
|
+
Essential for handling firmware fragmentation.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Set of capabilities that work on this connection
|
|
204
|
+
"""
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# ========== Response Handling ==========
|
|
208
|
+
|
|
209
|
+
def add_response_handler(
|
|
210
|
+
self,
|
|
211
|
+
handler: Callable[[bytes], Awaitable[None]]
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Register a callback for async responses from robot."""
|
|
214
|
+
self._response_handlers.append(handler)
|
|
215
|
+
|
|
216
|
+
def remove_response_handler(
|
|
217
|
+
self,
|
|
218
|
+
handler: Callable[[bytes], Awaitable[None]]
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Remove a response handler."""
|
|
221
|
+
if handler in self._response_handlers:
|
|
222
|
+
self._response_handlers.remove(handler)
|
|
223
|
+
|
|
224
|
+
async def _notify_handlers(self, data: bytes) -> None:
|
|
225
|
+
"""Notify all registered handlers of received data."""
|
|
226
|
+
for handler in self._response_handlers:
|
|
227
|
+
try:
|
|
228
|
+
await handler(data)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
# Don't let one handler crash others
|
|
231
|
+
print(f"Handler error: {e}")
|
|
232
|
+
|
|
233
|
+
# ========== Context Manager ==========
|
|
234
|
+
|
|
235
|
+
async def __aenter__(self):
|
|
236
|
+
"""Async context manager entry."""
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
240
|
+
"""Async context manager exit - ensures cleanup."""
|
|
241
|
+
await self.disconnect()
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ProtocolAdapter(ABC):
|
|
246
|
+
"""
|
|
247
|
+
Adapts high-level commands to protocol-specific format.
|
|
248
|
+
|
|
249
|
+
Different robots use different command formats:
|
|
250
|
+
- HiWonder: CMD|function|sub|data|$
|
|
251
|
+
- Unitree: Binary protocol
|
|
252
|
+
- Some: JSON over WebSocket
|
|
253
|
+
|
|
254
|
+
The adapter translates between canonical commands and
|
|
255
|
+
protocol-specific formats.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
@abstractmethod
|
|
259
|
+
def encode_command(self, command: str, *args) -> bytes:
|
|
260
|
+
"""
|
|
261
|
+
Encode a canonical command into protocol-specific bytes.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
command: Canonical command name (e.g., "walk_forward")
|
|
265
|
+
*args: Command arguments
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Protocol-specific bytes to send
|
|
269
|
+
"""
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
@abstractmethod
|
|
273
|
+
def decode_response(self, data: bytes) -> dict[str, Any]:
|
|
274
|
+
"""
|
|
275
|
+
Decode protocol-specific response into canonical format.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
data: Raw bytes received from robot
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Decoded response as a dictionary
|
|
282
|
+
"""
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class HiWonderProtocol(ProtocolAdapter):
|
|
287
|
+
"""
|
|
288
|
+
Protocol adapter for HiWonder robots (MechDog, etc).
|
|
289
|
+
|
|
290
|
+
Command format: CMD|function|subfunction|data...|$
|
|
291
|
+
Response format: CMD|function|data...|$
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
# Canonical command -> (function, subfunction, args_formatter)
|
|
295
|
+
COMMAND_MAP = {
|
|
296
|
+
# Posture commands (Function 1)
|
|
297
|
+
"posture_normal": (1, 0, None),
|
|
298
|
+
"posture_high": (1, 1, None),
|
|
299
|
+
"posture_low": (1, 2, None),
|
|
300
|
+
"turn_left": (1, 3, None),
|
|
301
|
+
"turn_right": (1, 4, None),
|
|
302
|
+
"posture_reset": (1, 5, None),
|
|
303
|
+
"sit": (1, 7, None),
|
|
304
|
+
"lie_down": (1, 8, None),
|
|
305
|
+
"low_crawl": (1, 9, None),
|
|
306
|
+
"high_crawl": (1, 10, None),
|
|
307
|
+
"twist": (1, 11, None),
|
|
308
|
+
"wave": (1, 12, None),
|
|
309
|
+
"handshake": (1, 13, None),
|
|
310
|
+
|
|
311
|
+
# Action groups (Function 2)
|
|
312
|
+
"action_group": (2, 1, lambda group_id: [group_id]),
|
|
313
|
+
|
|
314
|
+
# Movement (Function 3)
|
|
315
|
+
"move": (3, None, lambda x, y, yaw: [x, y, yaw]),
|
|
316
|
+
"walk_forward": (3, None, lambda: [100, 0, 0]),
|
|
317
|
+
"walk_backward": (3, None, lambda: [-100, 0, 0]),
|
|
318
|
+
"strafe_left": (3, None, lambda: [0, -100, 0]),
|
|
319
|
+
"strafe_right": (3, None, lambda: [0, 100, 0]),
|
|
320
|
+
"rotate_left": (3, None, lambda: [0, 0, 50]),
|
|
321
|
+
"rotate_right": (3, None, lambda: [0, 0, -50]),
|
|
322
|
+
"stop": (3, None, lambda: [0, 0, 0]),
|
|
323
|
+
|
|
324
|
+
# Sensors (Function 4)
|
|
325
|
+
"ultrasonic_query": (4, 1, None),
|
|
326
|
+
"rgb_led": (4, 3, lambda r, g, b: [r, g, b]),
|
|
327
|
+
|
|
328
|
+
# Battery (Function 6)
|
|
329
|
+
"battery_query": (6, None, None),
|
|
330
|
+
|
|
331
|
+
# Arm control (Function 7) - May not work on all firmware
|
|
332
|
+
"arm_action_1": (7, 1, None),
|
|
333
|
+
"arm_action_2": (7, 2, None),
|
|
334
|
+
"arm_joint_a_plus": (7, 3, lambda: [1]),
|
|
335
|
+
"arm_joint_a_minus": (7, 3, lambda: [-1]),
|
|
336
|
+
"arm_joint_b_plus": (7, 4, lambda: [1]),
|
|
337
|
+
"arm_joint_b_minus": (7, 4, lambda: [-1]),
|
|
338
|
+
"gripper_open": (7, 6, None),
|
|
339
|
+
"gripper_close": (7, 7, None),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def encode_command(self, command: str, *args) -> bytes:
|
|
343
|
+
"""Encode canonical command to HiWonder format."""
|
|
344
|
+
if command not in self.COMMAND_MAP:
|
|
345
|
+
raise ValueError(f"Unknown command: {command}")
|
|
346
|
+
|
|
347
|
+
func, sub, args_formatter = self.COMMAND_MAP[command]
|
|
348
|
+
|
|
349
|
+
parts = ["CMD", str(func)]
|
|
350
|
+
|
|
351
|
+
if sub is not None:
|
|
352
|
+
parts.append(str(sub))
|
|
353
|
+
|
|
354
|
+
if args_formatter:
|
|
355
|
+
formatted_args = args_formatter(*args) if args else args_formatter()
|
|
356
|
+
parts.extend(str(a) for a in formatted_args)
|
|
357
|
+
|
|
358
|
+
parts.append("$")
|
|
359
|
+
return "|".join(parts).encode('ascii')
|
|
360
|
+
|
|
361
|
+
def decode_response(self, data: bytes) -> dict[str, Any]:
|
|
362
|
+
"""Decode HiWonder response."""
|
|
363
|
+
try:
|
|
364
|
+
text = data.decode('ascii').strip()
|
|
365
|
+
if not text.startswith("CMD|"):
|
|
366
|
+
return {"raw": text}
|
|
367
|
+
|
|
368
|
+
parts = text.split("|")
|
|
369
|
+
func = int(parts[1])
|
|
370
|
+
|
|
371
|
+
result = {
|
|
372
|
+
"function": func,
|
|
373
|
+
"raw": text,
|
|
374
|
+
"parts": parts,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Parse known response types
|
|
378
|
+
if func == 4 and len(parts) >= 3:
|
|
379
|
+
# Ultrasonic response: CMD|4|<mm>|$
|
|
380
|
+
try:
|
|
381
|
+
result["ultrasonic_mm"] = int(parts[2])
|
|
382
|
+
except ValueError:
|
|
383
|
+
pass
|
|
384
|
+
elif func == 6 and len(parts) >= 3:
|
|
385
|
+
# Battery response: CMD|6|<voltage>|$ (voltage in 0.01V)
|
|
386
|
+
try:
|
|
387
|
+
result["battery_voltage"] = float(parts[2])
|
|
388
|
+
except ValueError:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
except Exception as e:
|
|
394
|
+
return {"error": str(e), "raw_bytes": data}
|