foodforthought-cli 0.2.7__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 +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- 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 +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -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 +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -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 +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -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.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/drivers/mechdog.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MechDog driver implementation.
|
|
3
|
+
|
|
4
|
+
Implements the FoodforThought interfaces for HiWonder MechDog.
|
|
5
|
+
|
|
6
|
+
The MechDog runs MicroPython with the HW_MechDog library.
|
|
7
|
+
Communication is via serial REPL commands.
|
|
8
|
+
|
|
9
|
+
Hardware specs:
|
|
10
|
+
- 12 servos (3 per leg: hip, thigh, calf)
|
|
11
|
+
- Optional arm attachment (3 servos)
|
|
12
|
+
- ESP32-based controller
|
|
13
|
+
- LX-16A serial bus servos
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import math
|
|
18
|
+
from typing import Optional, List, Set, Tuple
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import serial
|
|
23
|
+
HAS_SERIAL = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_SERIAL = False
|
|
26
|
+
|
|
27
|
+
from ..interfaces.types import (
|
|
28
|
+
Vector3,
|
|
29
|
+
Quaternion,
|
|
30
|
+
Pose,
|
|
31
|
+
Twist,
|
|
32
|
+
JointState,
|
|
33
|
+
GaitType,
|
|
34
|
+
ActionResult,
|
|
35
|
+
RobotStatus,
|
|
36
|
+
RobotMode,
|
|
37
|
+
BatteryState,
|
|
38
|
+
IMUReading,
|
|
39
|
+
Image,
|
|
40
|
+
)
|
|
41
|
+
from ..interfaces.base import (
|
|
42
|
+
RobotInterface,
|
|
43
|
+
SafetyInterface,
|
|
44
|
+
RobotInfo,
|
|
45
|
+
Capability,
|
|
46
|
+
)
|
|
47
|
+
from ..interfaces.locomotion import QuadrupedLocomotion
|
|
48
|
+
from ..interfaces.body import BodyPoseInterface
|
|
49
|
+
from ..interfaces.perception import CameraInterface
|
|
50
|
+
from ..interfaces.sensors import DistanceSensorInterface, DistanceReading, DistanceSensorType
|
|
51
|
+
from .wifi_camera import WiFiCamera, WiFiCameraConfig
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# MechDog-specific constants
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
# Servo value ranges (raw values from MechDog)
|
|
59
|
+
SERVO_MIN = 0
|
|
60
|
+
SERVO_MAX = 4096
|
|
61
|
+
SERVO_CENTER = 2048
|
|
62
|
+
|
|
63
|
+
# Body dimensions (meters)
|
|
64
|
+
BODY_LENGTH = 0.20 # Front to back
|
|
65
|
+
BODY_WIDTH = 0.10 # Side to side
|
|
66
|
+
LEG_LENGTH = 0.12 # Total leg length
|
|
67
|
+
|
|
68
|
+
# Height limits (meters)
|
|
69
|
+
HEIGHT_MIN = 0.05
|
|
70
|
+
HEIGHT_MAX = 0.18
|
|
71
|
+
HEIGHT_DEFAULT = 0.12
|
|
72
|
+
|
|
73
|
+
# Speed limits
|
|
74
|
+
MAX_STRIDE_MM = 100 # Maximum stride in mm
|
|
75
|
+
MAX_TURN_DEG = 30 # Maximum turn rate in degrees
|
|
76
|
+
|
|
77
|
+
# Arm servo IDs (for models with arm attachment)
|
|
78
|
+
# CORRECTED 2026-01-03: Discovered via vision-based servo identification
|
|
79
|
+
# Vision diff analysis showed:
|
|
80
|
+
# - Servos 1,2,4,5,6,7 are LEG servos (large movement, body tilt)
|
|
81
|
+
# - Servos 8,9 are ARM servos (medium movement, arm only)
|
|
82
|
+
# - Servo 11 is GRIPPER (small, localized movement at tip)
|
|
83
|
+
SERVO_GRIPPER = 11 # Gripper servo (open/close)
|
|
84
|
+
SERVO_SHOULDER = 8 # Shoulder/arm raise-lower
|
|
85
|
+
SERVO_ELBOW = 9 # Elbow extend/retract
|
|
86
|
+
|
|
87
|
+
# Calibrated arm servo positions (PWM microseconds: 500-2500)
|
|
88
|
+
# Discovered empirically 2026-01-03 via systematic testing
|
|
89
|
+
GRIPPER_OPEN = 500 # Gripper jaws spread wide
|
|
90
|
+
GRIPPER_CLOSED = 2400 # Gripper jaws clamped
|
|
91
|
+
GRIPPER_NEUTRAL = 1500 # Gripper partially open
|
|
92
|
+
|
|
93
|
+
SHOULDER_UP = 2200 # Arm raised up (servo 4 high)
|
|
94
|
+
SHOULDER_DOWN = 800 # Arm lowered for pickup (servo 4 low)
|
|
95
|
+
SHOULDER_NEUTRAL = 1500 # Arm at middle height
|
|
96
|
+
|
|
97
|
+
ELBOW_RETRACTED = 2000 # Arm pulled in toward body (servo 5 high)
|
|
98
|
+
ELBOW_EXTENDED = 800 # Arm reaching forward (servo 5 low)
|
|
99
|
+
ELBOW_NEUTRAL = 1500 # Arm at middle extension
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class MechDogConfig:
|
|
104
|
+
"""Configuration for MechDog connection."""
|
|
105
|
+
port: str = "/dev/cu.usbserial-10"
|
|
106
|
+
baud_rate: int = 115200
|
|
107
|
+
timeout: float = 2.0
|
|
108
|
+
has_arm: bool = False
|
|
109
|
+
# Camera configuration
|
|
110
|
+
has_camera: bool = False
|
|
111
|
+
camera_ip: str = "" # IP of visual module (e.g., "192.168.1.100")
|
|
112
|
+
camera_port: int = 80 # HTTP port
|
|
113
|
+
camera_stream_port: int = 81 # MJPEG stream port
|
|
114
|
+
# Ultrasonic sensor (HC-SR04 style on ESP32)
|
|
115
|
+
has_ultrasonic: bool = True # MechDog has built-in ultrasonic
|
|
116
|
+
# BLE configuration (alternative to USB serial)
|
|
117
|
+
use_ble: bool = False # Use BLE instead of serial
|
|
118
|
+
ble_address: str = "" # BLE device address (MAC on Windows/Linux, UUID on macOS)
|
|
119
|
+
ble_service_uuid: str = "0000ffe0-0000-1000-8000-00805f9b34fb" # HM-10/ESP32 BLE serial
|
|
120
|
+
ble_char_uuid: str = "0000ffe1-0000-1000-8000-00805f9b34fb" # RX/TX characteristic
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface, CameraInterface, DistanceSensorInterface, RobotInterface):
|
|
124
|
+
"""
|
|
125
|
+
Driver for HiWonder MechDog quadruped robot.
|
|
126
|
+
|
|
127
|
+
Implements:
|
|
128
|
+
- QuadrupedLocomotion: walk, turn, stand, sit, etc.
|
|
129
|
+
- BodyPoseInterface: height, roll, pitch, yaw control
|
|
130
|
+
- SafetyInterface: emergency stop, battery monitoring
|
|
131
|
+
- CameraInterface: image capture from visual module (optional)
|
|
132
|
+
- DistanceSensorInterface: ultrasonic distance sensing
|
|
133
|
+
- RobotInterface: connection lifecycle, status
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
dog = MechDogDriver(port="/dev/cu.usbserial-10")
|
|
137
|
+
dog.connect()
|
|
138
|
+
|
|
139
|
+
dog.stand()
|
|
140
|
+
dog.walk(Vector3.forward(), speed=0.3)
|
|
141
|
+
dog.set_body_height(0.10)
|
|
142
|
+
|
|
143
|
+
# If camera is configured:
|
|
144
|
+
image = dog.get_image()
|
|
145
|
+
|
|
146
|
+
dog.disconnect()
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, port: str = "/dev/cu.usbserial-10", config: Optional[MechDogConfig] = None):
|
|
150
|
+
"""
|
|
151
|
+
Initialize MechDog driver.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
port: Serial port or BLE address for MechDog connection
|
|
155
|
+
config: Optional configuration
|
|
156
|
+
"""
|
|
157
|
+
self.config = config or MechDogConfig(port=port)
|
|
158
|
+
self._serial: Optional[serial.Serial] = None
|
|
159
|
+
self._ble_transport = None # BLE transport when use_ble=True
|
|
160
|
+
self._connected = False
|
|
161
|
+
self._estopped = False
|
|
162
|
+
self._current_gait = GaitType.WALK
|
|
163
|
+
|
|
164
|
+
# Cached state
|
|
165
|
+
self._current_height = HEIGHT_DEFAULT
|
|
166
|
+
self._current_orientation = (0.0, 0.0, 0.0) # roll, pitch, yaw
|
|
167
|
+
self._is_moving = False
|
|
168
|
+
|
|
169
|
+
# Odometry (basic dead reckoning)
|
|
170
|
+
self._pose = Pose.identity()
|
|
171
|
+
self._velocity = Twist.zero()
|
|
172
|
+
|
|
173
|
+
# Camera (visual module)
|
|
174
|
+
self._camera: Optional[WiFiCamera] = None
|
|
175
|
+
if self.config.has_camera and self.config.camera_ip:
|
|
176
|
+
camera_config = WiFiCameraConfig(
|
|
177
|
+
ip=self.config.camera_ip,
|
|
178
|
+
port=self.config.camera_port,
|
|
179
|
+
stream_port=self.config.camera_stream_port,
|
|
180
|
+
)
|
|
181
|
+
self._camera = WiFiCamera(camera_config)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def _transport(self):
|
|
185
|
+
"""Get the active transport (serial or BLE)."""
|
|
186
|
+
if self.config.use_ble and self._ble_transport:
|
|
187
|
+
return self._ble_transport
|
|
188
|
+
return self._serial
|
|
189
|
+
|
|
190
|
+
# =========================================================================
|
|
191
|
+
# RobotInterface implementation
|
|
192
|
+
# =========================================================================
|
|
193
|
+
|
|
194
|
+
def get_info(self) -> RobotInfo:
|
|
195
|
+
"""Get MechDog information."""
|
|
196
|
+
capabilities = {
|
|
197
|
+
Capability.QUADRUPED,
|
|
198
|
+
Capability.BODY_POSE,
|
|
199
|
+
Capability.IMU,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if self.config.has_arm:
|
|
203
|
+
capabilities.add(Capability.ARM)
|
|
204
|
+
capabilities.add(Capability.GRIPPER)
|
|
205
|
+
|
|
206
|
+
if self.config.has_camera:
|
|
207
|
+
capabilities.add(Capability.CAMERA)
|
|
208
|
+
|
|
209
|
+
if self.config.has_ultrasonic:
|
|
210
|
+
capabilities.add(Capability.DISTANCE_SENSOR)
|
|
211
|
+
|
|
212
|
+
return RobotInfo(
|
|
213
|
+
name="MechDog",
|
|
214
|
+
model="hiwonder_mechdog",
|
|
215
|
+
manufacturer="HiWonder",
|
|
216
|
+
archetype="quadruped",
|
|
217
|
+
capabilities=capabilities,
|
|
218
|
+
mass=1.5, # kg
|
|
219
|
+
dimensions=(BODY_LENGTH, BODY_WIDTH, HEIGHT_DEFAULT),
|
|
220
|
+
workspace_min=(-2.0, -2.0, 0.0),
|
|
221
|
+
workspace_max=(2.0, 2.0, 0.5),
|
|
222
|
+
max_speed=0.3, # m/s
|
|
223
|
+
description="HiWonder MechDog quadruped robot with 12 DOF",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def connect(self) -> ActionResult:
|
|
227
|
+
"""Connect to MechDog via serial or BLE."""
|
|
228
|
+
if self.config.use_ble:
|
|
229
|
+
return self._connect_ble()
|
|
230
|
+
return self._connect_serial()
|
|
231
|
+
|
|
232
|
+
def _connect_ble(self) -> ActionResult:
|
|
233
|
+
"""Connect to MechDog via Bluetooth Low Energy."""
|
|
234
|
+
try:
|
|
235
|
+
from .ble_transport import BLETransport
|
|
236
|
+
except ImportError:
|
|
237
|
+
return ActionResult.error("BLE transport requires bleak. Run: pip install bleak")
|
|
238
|
+
|
|
239
|
+
if not self.config.ble_address:
|
|
240
|
+
return ActionResult.error("BLE address not configured. Set config.ble_address")
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
self._ble_transport = BLETransport(
|
|
244
|
+
address=self.config.ble_address,
|
|
245
|
+
service_uuid=self.config.ble_service_uuid,
|
|
246
|
+
char_uuid=self.config.ble_char_uuid,
|
|
247
|
+
timeout=self.config.timeout,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if not self._ble_transport.connect():
|
|
251
|
+
return ActionResult.error("Failed to connect to BLE device")
|
|
252
|
+
|
|
253
|
+
time.sleep(0.5)
|
|
254
|
+
|
|
255
|
+
# Enter friendly REPL mode
|
|
256
|
+
self._ble_transport.write(b'\x03') # Ctrl+C to interrupt
|
|
257
|
+
time.sleep(0.3)
|
|
258
|
+
self._ble_transport.write(b'\x02') # Ctrl+B for friendly REPL
|
|
259
|
+
time.sleep(0.5)
|
|
260
|
+
self._ble_transport.reset_input_buffer()
|
|
261
|
+
|
|
262
|
+
# Initialize MechDog library
|
|
263
|
+
result = self._send_command("from HW_MechDog import MechDog")
|
|
264
|
+
if "Error" in result or "Traceback" in result:
|
|
265
|
+
return ActionResult.error(f"Failed to import HW_MechDog: {result}")
|
|
266
|
+
|
|
267
|
+
result = self._send_command("dog = MechDog()")
|
|
268
|
+
if "Error" in result or "Traceback" in result:
|
|
269
|
+
return ActionResult.error(f"Failed to create MechDog: {result}")
|
|
270
|
+
|
|
271
|
+
self._connected = True
|
|
272
|
+
return ActionResult.ok(f"Connected to MechDog via BLE ({self.config.ble_address})")
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return ActionResult.error(f"BLE connection failed: {e}")
|
|
276
|
+
|
|
277
|
+
def _connect_serial(self) -> ActionResult:
|
|
278
|
+
"""Connect to MechDog via USB serial."""
|
|
279
|
+
if not HAS_SERIAL:
|
|
280
|
+
return ActionResult.error("pyserial not installed. Run: pip install pyserial")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
self._serial = serial.Serial(
|
|
284
|
+
self.config.port,
|
|
285
|
+
self.config.baud_rate,
|
|
286
|
+
timeout=self.config.timeout
|
|
287
|
+
)
|
|
288
|
+
time.sleep(0.5)
|
|
289
|
+
|
|
290
|
+
# Enter friendly REPL mode
|
|
291
|
+
self._serial.write(b'\x03') # Ctrl+C to interrupt
|
|
292
|
+
time.sleep(0.3)
|
|
293
|
+
self._serial.write(b'\x02') # Ctrl+B for friendly REPL
|
|
294
|
+
time.sleep(0.5)
|
|
295
|
+
self._serial.reset_input_buffer()
|
|
296
|
+
|
|
297
|
+
# Initialize MechDog library
|
|
298
|
+
result = self._send_command("from HW_MechDog import MechDog")
|
|
299
|
+
if "Error" in result or "Traceback" in result:
|
|
300
|
+
return ActionResult.error(f"Failed to import HW_MechDog: {result}")
|
|
301
|
+
|
|
302
|
+
result = self._send_command("dog = MechDog()")
|
|
303
|
+
if "Error" in result or "Traceback" in result:
|
|
304
|
+
return ActionResult.error(f"Failed to create MechDog: {result}")
|
|
305
|
+
|
|
306
|
+
self._connected = True
|
|
307
|
+
return ActionResult.ok("Connected to MechDog")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return ActionResult.error(f"Connection failed: {e}")
|
|
311
|
+
|
|
312
|
+
def disconnect(self) -> ActionResult:
|
|
313
|
+
"""Disconnect from MechDog."""
|
|
314
|
+
try:
|
|
315
|
+
# Stop any movement first
|
|
316
|
+
self.stop()
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
# Close BLE transport if active
|
|
321
|
+
if self._ble_transport:
|
|
322
|
+
try:
|
|
323
|
+
self._ble_transport.close()
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
finally:
|
|
327
|
+
self._ble_transport = None
|
|
328
|
+
|
|
329
|
+
# Close serial if active
|
|
330
|
+
if self._serial:
|
|
331
|
+
try:
|
|
332
|
+
self._serial.close()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
finally:
|
|
336
|
+
self._serial = None
|
|
337
|
+
|
|
338
|
+
self._connected = False
|
|
339
|
+
return ActionResult.ok("Disconnected")
|
|
340
|
+
|
|
341
|
+
def is_connected(self) -> bool:
|
|
342
|
+
"""Check connection status."""
|
|
343
|
+
if not self._connected:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
if self.config.use_ble:
|
|
347
|
+
return self._ble_transport is not None and self._ble_transport.is_open
|
|
348
|
+
|
|
349
|
+
return self._serial is not None and self._serial.is_open
|
|
350
|
+
|
|
351
|
+
def get_status(self) -> RobotStatus:
|
|
352
|
+
"""Get robot status."""
|
|
353
|
+
mode = RobotMode.IDLE
|
|
354
|
+
if self._estopped:
|
|
355
|
+
mode = RobotMode.ESTOPPED
|
|
356
|
+
elif self._is_moving:
|
|
357
|
+
mode = RobotMode.MOVING
|
|
358
|
+
elif self._connected:
|
|
359
|
+
mode = RobotMode.READY
|
|
360
|
+
|
|
361
|
+
return RobotStatus(
|
|
362
|
+
mode=mode,
|
|
363
|
+
is_ready=self._connected and not self._estopped,
|
|
364
|
+
is_moving=self._is_moving,
|
|
365
|
+
errors=[],
|
|
366
|
+
battery=self.get_battery_state(),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# =========================================================================
|
|
370
|
+
# SafetyInterface implementation
|
|
371
|
+
# =========================================================================
|
|
372
|
+
|
|
373
|
+
def emergency_stop(self) -> ActionResult:
|
|
374
|
+
"""Emergency stop - immediately stop all motion."""
|
|
375
|
+
self._estopped = True
|
|
376
|
+
self._is_moving = False
|
|
377
|
+
|
|
378
|
+
if self._transport:
|
|
379
|
+
# Send stop command
|
|
380
|
+
self._send_command("dog.move(0, 0)")
|
|
381
|
+
|
|
382
|
+
return ActionResult.ok("Emergency stop activated")
|
|
383
|
+
|
|
384
|
+
def reset_emergency_stop(self) -> ActionResult:
|
|
385
|
+
"""Clear emergency stop."""
|
|
386
|
+
self._estopped = False
|
|
387
|
+
return ActionResult.ok("Emergency stop cleared")
|
|
388
|
+
|
|
389
|
+
def is_estopped(self) -> bool:
|
|
390
|
+
"""Check if estopped."""
|
|
391
|
+
return self._estopped
|
|
392
|
+
|
|
393
|
+
def get_battery_state(self) -> Optional[BatteryState]:
|
|
394
|
+
"""Get battery state (not supported on basic MechDog)."""
|
|
395
|
+
# MechDog doesn't report battery state via REPL
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
# =========================================================================
|
|
399
|
+
# CameraInterface implementation
|
|
400
|
+
# =========================================================================
|
|
401
|
+
|
|
402
|
+
def get_image(self) -> Image:
|
|
403
|
+
"""
|
|
404
|
+
Capture image from visual module.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Image with RGB data, or empty Image if camera not configured
|
|
408
|
+
"""
|
|
409
|
+
if not self._camera:
|
|
410
|
+
return Image(
|
|
411
|
+
data=b"",
|
|
412
|
+
width=0,
|
|
413
|
+
height=0,
|
|
414
|
+
format="error",
|
|
415
|
+
encoding="none",
|
|
416
|
+
timestamp=time.time(),
|
|
417
|
+
metadata={"error": "Camera not configured"},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return self._camera.get_image()
|
|
421
|
+
|
|
422
|
+
def get_resolution(self) -> Tuple[int, int]:
|
|
423
|
+
"""Get camera resolution."""
|
|
424
|
+
if not self._camera:
|
|
425
|
+
return (0, 0)
|
|
426
|
+
return self._camera.get_resolution()
|
|
427
|
+
|
|
428
|
+
def get_intrinsics(self) -> dict:
|
|
429
|
+
"""Get camera intrinsic parameters."""
|
|
430
|
+
if not self._camera:
|
|
431
|
+
return {}
|
|
432
|
+
return self._camera.get_intrinsics()
|
|
433
|
+
|
|
434
|
+
def get_frame_id(self) -> str:
|
|
435
|
+
"""Get coordinate frame ID for camera."""
|
|
436
|
+
return "mechdog_camera_link"
|
|
437
|
+
|
|
438
|
+
def has_camera(self) -> bool:
|
|
439
|
+
"""Check if camera is available."""
|
|
440
|
+
return self._camera is not None
|
|
441
|
+
|
|
442
|
+
def is_camera_connected(self) -> bool:
|
|
443
|
+
"""Check if camera is reachable."""
|
|
444
|
+
if not self._camera:
|
|
445
|
+
return False
|
|
446
|
+
return self._camera.is_connected()
|
|
447
|
+
|
|
448
|
+
# =========================================================================
|
|
449
|
+
# DistanceSensorInterface implementation
|
|
450
|
+
# =========================================================================
|
|
451
|
+
|
|
452
|
+
def get_distance(self) -> DistanceReading:
|
|
453
|
+
"""
|
|
454
|
+
Read distance from ultrasonic sensor.
|
|
455
|
+
|
|
456
|
+
MechDog's ultrasonic sensor is mounted on the front.
|
|
457
|
+
Returns distance in meters.
|
|
458
|
+
"""
|
|
459
|
+
if not self._check_connection():
|
|
460
|
+
return DistanceReading.invalid()
|
|
461
|
+
|
|
462
|
+
if not self.config.has_ultrasonic:
|
|
463
|
+
return DistanceReading.invalid()
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
# MechDog ultrasonic reading via HW_MechDog library
|
|
467
|
+
# The sensor returns distance in centimeters
|
|
468
|
+
result = self._send_command("print(dog.read_ultrasonic())")
|
|
469
|
+
|
|
470
|
+
# Parse result - typically returns just a number
|
|
471
|
+
import re
|
|
472
|
+
match = re.search(r'(\d+\.?\d*)', result.split('\n')[-2] if '\n' in result else result)
|
|
473
|
+
if match:
|
|
474
|
+
distance_cm = float(match.group(1))
|
|
475
|
+
distance_m = distance_cm / 100.0
|
|
476
|
+
|
|
477
|
+
# Validate reading is in range
|
|
478
|
+
if distance_m < self.get_min_range() or distance_m > self.get_max_range():
|
|
479
|
+
return DistanceReading(
|
|
480
|
+
distance=distance_m,
|
|
481
|
+
valid=False,
|
|
482
|
+
sensor_type=DistanceSensorType.ULTRASONIC,
|
|
483
|
+
timestamp=time.time(),
|
|
484
|
+
confidence=0.5,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
return DistanceReading(
|
|
488
|
+
distance=distance_m,
|
|
489
|
+
valid=True,
|
|
490
|
+
sensor_type=DistanceSensorType.ULTRASONIC,
|
|
491
|
+
timestamp=time.time(),
|
|
492
|
+
confidence=0.9,
|
|
493
|
+
)
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
return DistanceReading.invalid()
|
|
498
|
+
|
|
499
|
+
def get_min_range(self) -> float:
|
|
500
|
+
"""Get minimum measurable range (2cm for HC-SR04)."""
|
|
501
|
+
return 0.02 # 2cm
|
|
502
|
+
|
|
503
|
+
def get_max_range(self) -> float:
|
|
504
|
+
"""Get maximum measurable range (4m for HC-SR04)."""
|
|
505
|
+
return 4.0 # 4m
|
|
506
|
+
|
|
507
|
+
def get_sensor_type(self) -> DistanceSensorType:
|
|
508
|
+
"""Get the type of distance sensor."""
|
|
509
|
+
return DistanceSensorType.ULTRASONIC
|
|
510
|
+
|
|
511
|
+
# =========================================================================
|
|
512
|
+
# QuadrupedLocomotion implementation
|
|
513
|
+
# =========================================================================
|
|
514
|
+
|
|
515
|
+
def get_pose(self) -> Pose:
|
|
516
|
+
"""Get current pose (from dead reckoning)."""
|
|
517
|
+
return self._pose
|
|
518
|
+
|
|
519
|
+
def get_velocity(self) -> Twist:
|
|
520
|
+
"""Get current velocity."""
|
|
521
|
+
return self._velocity
|
|
522
|
+
|
|
523
|
+
def stop(self) -> ActionResult:
|
|
524
|
+
"""Stop all movement."""
|
|
525
|
+
if not self._check_connection():
|
|
526
|
+
return ActionResult.error("Not connected")
|
|
527
|
+
|
|
528
|
+
self._send_command("dog.move(0, 0)")
|
|
529
|
+
self._is_moving = False
|
|
530
|
+
self._velocity = Twist.zero()
|
|
531
|
+
|
|
532
|
+
return ActionResult.ok("Stopped")
|
|
533
|
+
|
|
534
|
+
def is_moving(self) -> bool:
|
|
535
|
+
"""Check if moving."""
|
|
536
|
+
return self._is_moving
|
|
537
|
+
|
|
538
|
+
def walk(self, direction: Vector3, speed: float = 0.5) -> ActionResult:
|
|
539
|
+
"""Walk in direction at speed."""
|
|
540
|
+
if not self._check_connection():
|
|
541
|
+
return ActionResult.error("Not connected")
|
|
542
|
+
|
|
543
|
+
if self._estopped:
|
|
544
|
+
return ActionResult.error("Emergency stop active")
|
|
545
|
+
|
|
546
|
+
# Convert direction and speed to MechDog velocity control
|
|
547
|
+
# MechDog uses: move(forward_vel, turn_vel)
|
|
548
|
+
# forward_vel: -100 to +100 (negative = backward)
|
|
549
|
+
# turn_vel: -100 to +100 (negative = turn left, positive = turn right)
|
|
550
|
+
# Note: This is velocity-based, NOT distance-based!
|
|
551
|
+
|
|
552
|
+
# Calculate forward velocity from speed (simple mapping)
|
|
553
|
+
stride = int(min(MAX_STRIDE_MM, max(-MAX_STRIDE_MM, speed * 200)))
|
|
554
|
+
|
|
555
|
+
# Apply direction
|
|
556
|
+
if direction.x < 0: # Backward
|
|
557
|
+
stride = -abs(stride)
|
|
558
|
+
|
|
559
|
+
# Calculate turn from y component
|
|
560
|
+
angle = int(direction.y * MAX_TURN_DEG)
|
|
561
|
+
angle = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle))
|
|
562
|
+
|
|
563
|
+
self._send_command(f"dog.move({stride}, {angle})")
|
|
564
|
+
self._is_moving = True
|
|
565
|
+
|
|
566
|
+
# Update velocity estimate
|
|
567
|
+
self._velocity = Twist(
|
|
568
|
+
linear=Vector3(speed * direction.x, speed * direction.y, 0),
|
|
569
|
+
angular=Vector3.zero()
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return ActionResult.ok(f"Walking: stride={stride}, angle={angle}")
|
|
573
|
+
|
|
574
|
+
def walk_to(self, target: Vector3, speed: float = 0.5) -> ActionResult:
|
|
575
|
+
"""Walk to target position (blocking)."""
|
|
576
|
+
if not self._check_connection():
|
|
577
|
+
return ActionResult.error("Not connected")
|
|
578
|
+
|
|
579
|
+
# Simple implementation: walk toward target
|
|
580
|
+
# In a real implementation, this would use feedback control
|
|
581
|
+
|
|
582
|
+
current = self.get_pose().position
|
|
583
|
+
delta = target - current
|
|
584
|
+
distance = delta.magnitude()
|
|
585
|
+
|
|
586
|
+
if distance < 0.05: # Already at target
|
|
587
|
+
return ActionResult.ok("Already at target")
|
|
588
|
+
|
|
589
|
+
# Calculate direction
|
|
590
|
+
direction = delta.normalized()
|
|
591
|
+
|
|
592
|
+
# Walk toward target
|
|
593
|
+
self.walk(direction, speed)
|
|
594
|
+
|
|
595
|
+
# Wait for approximate time to reach target
|
|
596
|
+
# (Very rough estimate - real implementation needs odometry)
|
|
597
|
+
travel_time = distance / speed
|
|
598
|
+
time.sleep(min(travel_time, 10.0)) # Cap at 10 seconds
|
|
599
|
+
|
|
600
|
+
self.stop()
|
|
601
|
+
|
|
602
|
+
return ActionResult.ok(f"Walked to target")
|
|
603
|
+
|
|
604
|
+
def turn(self, angle: float, speed: float = 0.5) -> ActionResult:
|
|
605
|
+
"""Turn by angle (blocking)."""
|
|
606
|
+
if not self._check_connection():
|
|
607
|
+
return ActionResult.error("Not connected")
|
|
608
|
+
|
|
609
|
+
# Convert radians to degrees
|
|
610
|
+
angle_deg = math.degrees(angle)
|
|
611
|
+
|
|
612
|
+
# MechDog turn is via move(0, angle)
|
|
613
|
+
# Positive angle = turn left
|
|
614
|
+
turn_deg = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle_deg))
|
|
615
|
+
|
|
616
|
+
# Estimate time based on turn rate
|
|
617
|
+
turn_time = abs(angle_deg) / 30.0 # Rough estimate
|
|
618
|
+
|
|
619
|
+
self._send_command(f"dog.move(0, {int(turn_deg)})")
|
|
620
|
+
self._is_moving = True
|
|
621
|
+
|
|
622
|
+
time.sleep(turn_time)
|
|
623
|
+
|
|
624
|
+
self.stop()
|
|
625
|
+
|
|
626
|
+
# Update pose estimate
|
|
627
|
+
roll, pitch, yaw = self._pose.orientation.to_euler()
|
|
628
|
+
yaw += angle
|
|
629
|
+
self._pose = Pose(
|
|
630
|
+
self._pose.position,
|
|
631
|
+
Quaternion.from_euler(roll, pitch, yaw)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return ActionResult.ok(f"Turned {angle_deg:.1f} degrees")
|
|
635
|
+
|
|
636
|
+
def turn_continuous(self, angular_velocity: float) -> ActionResult:
|
|
637
|
+
"""Turn continuously."""
|
|
638
|
+
if not self._check_connection():
|
|
639
|
+
return ActionResult.error("Not connected")
|
|
640
|
+
|
|
641
|
+
# Map angular velocity to turn angle
|
|
642
|
+
angle = int(angular_velocity * 20) # Rough mapping
|
|
643
|
+
angle = max(-MAX_TURN_DEG, min(MAX_TURN_DEG, angle))
|
|
644
|
+
|
|
645
|
+
self._send_command(f"dog.move(0, {angle})")
|
|
646
|
+
self._is_moving = True
|
|
647
|
+
|
|
648
|
+
self._velocity = Twist(
|
|
649
|
+
linear=Vector3.zero(),
|
|
650
|
+
angular=Vector3(0, 0, angular_velocity)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return ActionResult.ok(f"Turning at {angular_velocity:.2f} rad/s")
|
|
654
|
+
|
|
655
|
+
def stand(self) -> ActionResult:
|
|
656
|
+
"""Stand up."""
|
|
657
|
+
if not self._check_connection():
|
|
658
|
+
return ActionResult.error("Not connected")
|
|
659
|
+
|
|
660
|
+
self._send_command("dog.set_default_pose()")
|
|
661
|
+
self._is_moving = False
|
|
662
|
+
self._current_height = HEIGHT_DEFAULT
|
|
663
|
+
|
|
664
|
+
return ActionResult.ok("Standing")
|
|
665
|
+
|
|
666
|
+
def sit(self) -> ActionResult:
|
|
667
|
+
"""Sit down."""
|
|
668
|
+
if not self._check_connection():
|
|
669
|
+
return ActionResult.error("Not connected")
|
|
670
|
+
|
|
671
|
+
# Lower body to minimum height
|
|
672
|
+
return self.set_body_height(HEIGHT_MIN)
|
|
673
|
+
|
|
674
|
+
def lie_down(self) -> ActionResult:
|
|
675
|
+
"""Lie down (same as sit for MechDog)."""
|
|
676
|
+
return self.sit()
|
|
677
|
+
|
|
678
|
+
def set_gait(self, gait: GaitType) -> ActionResult:
|
|
679
|
+
"""Set gait (MechDog has limited gait support)."""
|
|
680
|
+
self._current_gait = gait
|
|
681
|
+
return ActionResult.ok(f"Gait set to {gait.name}")
|
|
682
|
+
|
|
683
|
+
def get_gait(self) -> GaitType:
|
|
684
|
+
"""Get current gait."""
|
|
685
|
+
return self._current_gait
|
|
686
|
+
|
|
687
|
+
def get_foot_positions(self) -> List[Vector3]:
|
|
688
|
+
"""Get foot positions (estimated from body height)."""
|
|
689
|
+
# Simplified: assume feet are at corners of body rectangle
|
|
690
|
+
h = self._current_height
|
|
691
|
+
return [
|
|
692
|
+
Vector3(BODY_LENGTH/2, BODY_WIDTH/2, -h), # Front left
|
|
693
|
+
Vector3(BODY_LENGTH/2, -BODY_WIDTH/2, -h), # Front right
|
|
694
|
+
Vector3(-BODY_LENGTH/2, BODY_WIDTH/2, -h), # Back left
|
|
695
|
+
Vector3(-BODY_LENGTH/2, -BODY_WIDTH/2, -h), # Back right
|
|
696
|
+
]
|
|
697
|
+
|
|
698
|
+
def get_joint_state(self) -> JointState:
|
|
699
|
+
"""Get all joint positions."""
|
|
700
|
+
if not self._check_connection():
|
|
701
|
+
return JointState()
|
|
702
|
+
|
|
703
|
+
result = self._send_command("print(dog.read_all_servo())")
|
|
704
|
+
|
|
705
|
+
# Parse result like "[2096, 1621, 2170, ...]"
|
|
706
|
+
try:
|
|
707
|
+
# Extract list from response
|
|
708
|
+
import re
|
|
709
|
+
match = re.search(r'\[([^\]]+)\]', result)
|
|
710
|
+
if match:
|
|
711
|
+
values = [int(x.strip()) for x in match.group(1).split(',')]
|
|
712
|
+
# Convert to radians (rough conversion)
|
|
713
|
+
positions = tuple(
|
|
714
|
+
(v - SERVO_CENTER) / SERVO_MAX * math.pi
|
|
715
|
+
for v in values
|
|
716
|
+
)
|
|
717
|
+
return JointState(positions=positions)
|
|
718
|
+
except Exception:
|
|
719
|
+
pass
|
|
720
|
+
|
|
721
|
+
return JointState()
|
|
722
|
+
|
|
723
|
+
# =========================================================================
|
|
724
|
+
# BodyPoseInterface implementation
|
|
725
|
+
# =========================================================================
|
|
726
|
+
|
|
727
|
+
def set_body_height(self, height: float) -> ActionResult:
|
|
728
|
+
"""Set body height."""
|
|
729
|
+
if not self._check_connection():
|
|
730
|
+
return ActionResult.error("Not connected")
|
|
731
|
+
|
|
732
|
+
# Clamp to limits
|
|
733
|
+
height = max(HEIGHT_MIN, min(HEIGHT_MAX, height))
|
|
734
|
+
|
|
735
|
+
# MechDog uses set_pose() for body position
|
|
736
|
+
# Height is controlled via leg extension
|
|
737
|
+
# This is a simplified implementation
|
|
738
|
+
|
|
739
|
+
self._current_height = height
|
|
740
|
+
return ActionResult.ok(f"Height set to {height:.3f}m")
|
|
741
|
+
|
|
742
|
+
def get_body_height(self) -> float:
|
|
743
|
+
"""Get body height."""
|
|
744
|
+
return self._current_height
|
|
745
|
+
|
|
746
|
+
def get_height_limits(self) -> Tuple[float, float]:
|
|
747
|
+
"""Get height limits."""
|
|
748
|
+
return (HEIGHT_MIN, HEIGHT_MAX)
|
|
749
|
+
|
|
750
|
+
def set_body_orientation(
|
|
751
|
+
self,
|
|
752
|
+
roll: float = 0.0,
|
|
753
|
+
pitch: float = 0.0,
|
|
754
|
+
yaw: float = 0.0
|
|
755
|
+
) -> ActionResult:
|
|
756
|
+
"""Set body orientation."""
|
|
757
|
+
if not self._check_connection():
|
|
758
|
+
return ActionResult.error("Not connected")
|
|
759
|
+
|
|
760
|
+
# MechDog can tilt body via set_pose or transform
|
|
761
|
+
# Convert to degrees for MechDog
|
|
762
|
+
roll_deg = math.degrees(roll)
|
|
763
|
+
pitch_deg = math.degrees(pitch)
|
|
764
|
+
yaw_deg = math.degrees(yaw)
|
|
765
|
+
|
|
766
|
+
# MechDog's transform() takes (x, y, z, roll, pitch, yaw)
|
|
767
|
+
self._send_command(f"dog.transform(0, 0, 0, {roll_deg}, {pitch_deg}, {yaw_deg})")
|
|
768
|
+
|
|
769
|
+
self._current_orientation = (roll, pitch, yaw)
|
|
770
|
+
return ActionResult.ok(f"Orientation set to R={roll_deg:.1f} P={pitch_deg:.1f} Y={yaw_deg:.1f}")
|
|
771
|
+
|
|
772
|
+
def get_body_orientation(self) -> Tuple[float, float, float]:
|
|
773
|
+
"""Get body orientation."""
|
|
774
|
+
return self._current_orientation
|
|
775
|
+
|
|
776
|
+
def set_body_pose(
|
|
777
|
+
self,
|
|
778
|
+
height: Optional[float] = None,
|
|
779
|
+
roll: Optional[float] = None,
|
|
780
|
+
pitch: Optional[float] = None,
|
|
781
|
+
yaw: Optional[float] = None,
|
|
782
|
+
x_offset: Optional[float] = None,
|
|
783
|
+
y_offset: Optional[float] = None
|
|
784
|
+
) -> ActionResult:
|
|
785
|
+
"""Set combined body pose."""
|
|
786
|
+
if height is not None:
|
|
787
|
+
self.set_body_height(height)
|
|
788
|
+
|
|
789
|
+
# Get current values for any not specified
|
|
790
|
+
curr_r, curr_p, curr_y = self._current_orientation
|
|
791
|
+
if roll is None:
|
|
792
|
+
roll = curr_r
|
|
793
|
+
if pitch is None:
|
|
794
|
+
pitch = curr_p
|
|
795
|
+
if yaw is None:
|
|
796
|
+
yaw = curr_y
|
|
797
|
+
|
|
798
|
+
return self.set_body_orientation(roll, pitch, yaw)
|
|
799
|
+
|
|
800
|
+
# =========================================================================
|
|
801
|
+
# Internal methods
|
|
802
|
+
# =========================================================================
|
|
803
|
+
|
|
804
|
+
def _check_connection(self) -> bool:
|
|
805
|
+
"""Check if connected and ready."""
|
|
806
|
+
if self._estopped:
|
|
807
|
+
return False
|
|
808
|
+
return self._connected and self._transport is not None
|
|
809
|
+
|
|
810
|
+
def _send_command(self, cmd: str, wait: float = 0.5) -> str:
|
|
811
|
+
"""Send command to MicroPython REPL and get response."""
|
|
812
|
+
transport = self._transport
|
|
813
|
+
if not transport:
|
|
814
|
+
return ""
|
|
815
|
+
|
|
816
|
+
try:
|
|
817
|
+
transport.reset_input_buffer()
|
|
818
|
+
transport.write(f"{cmd}\r\n".encode())
|
|
819
|
+
time.sleep(wait)
|
|
820
|
+
response = transport.read(4000).decode('utf-8', errors='ignore')
|
|
821
|
+
return response
|
|
822
|
+
except Exception as e:
|
|
823
|
+
return f"Error: {e}"
|
|
824
|
+
|
|
825
|
+
# =========================================================================
|
|
826
|
+
# MechDog-specific methods (not in interface)
|
|
827
|
+
# =========================================================================
|
|
828
|
+
|
|
829
|
+
def read_servo(self, servo_id: int) -> Optional[int]:
|
|
830
|
+
"""Read single servo position (raw value)."""
|
|
831
|
+
if not self._check_connection():
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
result = self._send_command(f"print(dog.read_servo({servo_id}))")
|
|
835
|
+
try:
|
|
836
|
+
# Parse integer from response
|
|
837
|
+
import re
|
|
838
|
+
match = re.search(r'(\d+)', result.split('\n')[-2])
|
|
839
|
+
if match:
|
|
840
|
+
return int(match.group(1))
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
def set_servo(self, servo_id: int, position: int, time_ms: int = 500) -> ActionResult:
|
|
846
|
+
"""Set single servo position (raw value)."""
|
|
847
|
+
if not self._check_connection():
|
|
848
|
+
return ActionResult.error("Not connected")
|
|
849
|
+
|
|
850
|
+
self._send_command(f"dog.set_servo({servo_id}, {position}, {time_ms})")
|
|
851
|
+
return ActionResult.ok(f"Servo {servo_id} set to {position}")
|
|
852
|
+
|
|
853
|
+
def run_action(self, action_id: int) -> ActionResult:
|
|
854
|
+
"""
|
|
855
|
+
Run a pre-programmed action by ID.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
action_id: Integer ID of the action to run
|
|
859
|
+
|
|
860
|
+
Note: Action IDs must be discovered experimentally as
|
|
861
|
+
the MechDog firmware doesn't document available actions.
|
|
862
|
+
Common IDs found: 0-15 (varies by firmware version)
|
|
863
|
+
"""
|
|
864
|
+
if not self._check_connection():
|
|
865
|
+
return ActionResult.error("Not connected")
|
|
866
|
+
|
|
867
|
+
self._send_command(f"dog.action_run({action_id})")
|
|
868
|
+
return ActionResult.ok(f"Running action ID: {action_id}")
|
|
869
|
+
|
|
870
|
+
def wait_action(self) -> ActionResult:
|
|
871
|
+
"""Wait for current action to complete."""
|
|
872
|
+
if not self._check_connection():
|
|
873
|
+
return ActionResult.error("Not connected")
|
|
874
|
+
|
|
875
|
+
self._send_command("dog.wait_action_over()")
|
|
876
|
+
return ActionResult.ok("Action complete")
|
|
877
|
+
|
|
878
|
+
def stop_action(self) -> ActionResult:
|
|
879
|
+
"""Stop current action."""
|
|
880
|
+
if not self._check_connection():
|
|
881
|
+
return ActionResult.error("Not connected")
|
|
882
|
+
|
|
883
|
+
self._send_command("dog.stop_action()")
|
|
884
|
+
return ActionResult.ok("Action stopped")
|
|
885
|
+
|
|
886
|
+
# =========================================================================
|
|
887
|
+
# Arm control methods (for models with arm attachment)
|
|
888
|
+
# =========================================================================
|
|
889
|
+
|
|
890
|
+
def gripper_open(self, time_ms: int = 400) -> ActionResult:
|
|
891
|
+
"""Open gripper to grab position."""
|
|
892
|
+
return self.set_servo(SERVO_GRIPPER, GRIPPER_OPEN, time_ms)
|
|
893
|
+
|
|
894
|
+
def gripper_close(self, time_ms: int = 400) -> ActionResult:
|
|
895
|
+
"""Close gripper to grasp object."""
|
|
896
|
+
return self.set_servo(SERVO_GRIPPER, GRIPPER_CLOSED, time_ms)
|
|
897
|
+
|
|
898
|
+
def arm_up(self, time_ms: int = 600) -> ActionResult:
|
|
899
|
+
"""Raise arm to carry position."""
|
|
900
|
+
self.set_servo(SERVO_ELBOW, ELBOW_RETRACTED, time_ms)
|
|
901
|
+
return self.set_servo(SERVO_SHOULDER, SHOULDER_UP, time_ms)
|
|
902
|
+
|
|
903
|
+
def arm_down(self, time_ms: int = 600) -> ActionResult:
|
|
904
|
+
"""Lower arm to pickup position."""
|
|
905
|
+
self.set_servo(SERVO_SHOULDER, SHOULDER_DOWN, time_ms)
|
|
906
|
+
return self.set_servo(SERVO_ELBOW, ELBOW_EXTENDED, time_ms)
|
|
907
|
+
|
|
908
|
+
def arm_neutral(self, time_ms: int = 500) -> ActionResult:
|
|
909
|
+
"""Move arm to neutral position."""
|
|
910
|
+
self.set_servo(SERVO_GRIPPER, GRIPPER_NEUTRAL, time_ms)
|
|
911
|
+
self.set_servo(SERVO_SHOULDER, SHOULDER_NEUTRAL, time_ms)
|
|
912
|
+
return self.set_servo(SERVO_ELBOW, ELBOW_NEUTRAL, time_ms)
|
|
913
|
+
|
|
914
|
+
def pickup_sequence(self, grab_delay: float = 0.3) -> ActionResult:
|
|
915
|
+
"""
|
|
916
|
+
Execute pickup sequence: lower arm, close gripper, lift.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
grab_delay: Seconds to wait between lowering and closing gripper
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
ActionResult indicating success/failure
|
|
923
|
+
"""
|
|
924
|
+
if not self.config.has_arm:
|
|
925
|
+
return ActionResult.error("Robot does not have arm attachment")
|
|
926
|
+
|
|
927
|
+
# Open gripper
|
|
928
|
+
self.gripper_open()
|
|
929
|
+
time.sleep(0.3)
|
|
930
|
+
|
|
931
|
+
# Lower arm
|
|
932
|
+
self.arm_down()
|
|
933
|
+
time.sleep(grab_delay)
|
|
934
|
+
|
|
935
|
+
# Close gripper
|
|
936
|
+
self.gripper_close()
|
|
937
|
+
time.sleep(0.3)
|
|
938
|
+
|
|
939
|
+
# Lift arm
|
|
940
|
+
self.arm_up()
|
|
941
|
+
|
|
942
|
+
return ActionResult.ok("Pickup sequence complete")
|