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/robot/ble_capture.py
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BLE Protocol Capture and Analysis Tools.
|
|
3
|
+
|
|
4
|
+
Automates the workflow for reverse-engineering BLE robot protocols by:
|
|
5
|
+
1. Capturing traffic from phone apps (iOS PacketLogger / Android HCI snoop)
|
|
6
|
+
2. Analyzing captures to decode commands
|
|
7
|
+
3. Generating Python code for protocol replay
|
|
8
|
+
|
|
9
|
+
This addresses a critical gap in open-source robotics: many robots have
|
|
10
|
+
undocumented BLE protocols with only proprietary phone apps for control.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
ate robot ble-capture --platform ios --output capture.pklg
|
|
14
|
+
ate robot ble-analyze capture.pklg
|
|
15
|
+
ate robot ble-inspect <address>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Dict, List, Optional, Tuple
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class BLECommand:
|
|
33
|
+
"""Represents a decoded BLE command."""
|
|
34
|
+
frame: int
|
|
35
|
+
opcode: str # 'write' or 'notify'
|
|
36
|
+
handle: int
|
|
37
|
+
raw_hex: str
|
|
38
|
+
decoded: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_write(self) -> bool:
|
|
42
|
+
return 'write' in self.opcode.lower() or self.opcode == '0x12'
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_notify(self) -> bool:
|
|
46
|
+
return 'notif' in self.opcode.lower() or self.opcode == '0x1b'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CaptureAnalysis:
|
|
51
|
+
"""Results of analyzing a BLE capture."""
|
|
52
|
+
file_path: str
|
|
53
|
+
write_handle: Optional[int] = None
|
|
54
|
+
notify_handle: Optional[int] = None
|
|
55
|
+
commands: List[BLECommand] = field(default_factory=list)
|
|
56
|
+
unique_writes: Dict[str, int] = field(default_factory=dict) # cmd -> count
|
|
57
|
+
unique_notifies: Dict[str, int] = field(default_factory=dict)
|
|
58
|
+
protocol_detected: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def summary(self) -> str:
|
|
61
|
+
"""Generate a human-readable summary."""
|
|
62
|
+
lines = [
|
|
63
|
+
"=" * 60,
|
|
64
|
+
"BLE CAPTURE ANALYSIS",
|
|
65
|
+
"=" * 60,
|
|
66
|
+
f"File: {self.file_path}",
|
|
67
|
+
f"Total packets: {len(self.commands)}",
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if self.write_handle:
|
|
72
|
+
lines.append(f"WRITE Handle: 0x{self.write_handle:04x} ({self.write_handle})")
|
|
73
|
+
if self.notify_handle:
|
|
74
|
+
lines.append(f"NOTIFY Handle: 0x{self.notify_handle:04x} ({self.notify_handle})")
|
|
75
|
+
|
|
76
|
+
if self.protocol_detected:
|
|
77
|
+
lines.append(f"Protocol: {self.protocol_detected}")
|
|
78
|
+
|
|
79
|
+
lines.append("")
|
|
80
|
+
lines.append("-" * 60)
|
|
81
|
+
lines.append("WRITE COMMANDS (App → Robot):")
|
|
82
|
+
lines.append("-" * 60)
|
|
83
|
+
|
|
84
|
+
for cmd, count in sorted(self.unique_writes.items(), key=lambda x: -x[1]):
|
|
85
|
+
lines.append(f" {cmd:30s} ({count}x)")
|
|
86
|
+
|
|
87
|
+
if self.unique_notifies:
|
|
88
|
+
lines.append("")
|
|
89
|
+
lines.append("-" * 60)
|
|
90
|
+
lines.append("NOTIFICATIONS (Robot → App):")
|
|
91
|
+
lines.append("-" * 60)
|
|
92
|
+
|
|
93
|
+
for cmd, count in sorted(self.unique_notifies.items(), key=lambda x: -x[1]):
|
|
94
|
+
lines.append(f" {cmd:30s} ({count}x)")
|
|
95
|
+
|
|
96
|
+
lines.append("")
|
|
97
|
+
lines.append("=" * 60)
|
|
98
|
+
|
|
99
|
+
return "\n".join(lines)
|
|
100
|
+
|
|
101
|
+
def generate_python_code(self) -> str:
|
|
102
|
+
"""Generate Python code to replay the captured protocol."""
|
|
103
|
+
code_lines = [
|
|
104
|
+
'"""',
|
|
105
|
+
f'Auto-generated BLE protocol from: {os.path.basename(self.file_path)}',
|
|
106
|
+
'',
|
|
107
|
+
'Usage:',
|
|
108
|
+
' python this_file.py',
|
|
109
|
+
'"""',
|
|
110
|
+
'',
|
|
111
|
+
'import asyncio',
|
|
112
|
+
'from bleak import BleakClient, BleakScanner',
|
|
113
|
+
'',
|
|
114
|
+
'# Device configuration',
|
|
115
|
+
'DEVICE_NAME = "your_device_name" # Update this',
|
|
116
|
+
'',
|
|
117
|
+
'# Characteristic UUIDs (common ESP32/HM-10 serial)',
|
|
118
|
+
'FFE1_WRITE = "0000ffe1-0000-1000-8000-00805f9b34fb"',
|
|
119
|
+
'FFE2_NOTIFY = "0000ffe2-0000-1000-8000-00805f9b34fb"',
|
|
120
|
+
'',
|
|
121
|
+
'# Discovered commands',
|
|
122
|
+
'COMMANDS = {',
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# Add discovered commands
|
|
126
|
+
for cmd in sorted(self.unique_writes.keys()):
|
|
127
|
+
# Try to infer command purpose
|
|
128
|
+
purpose = self._infer_purpose(cmd)
|
|
129
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', purpose.lower())
|
|
130
|
+
code_lines.append(f' "{safe_name}": b"{cmd}",')
|
|
131
|
+
|
|
132
|
+
code_lines.extend([
|
|
133
|
+
'}',
|
|
134
|
+
'',
|
|
135
|
+
'',
|
|
136
|
+
'def notification_handler(sender, data):',
|
|
137
|
+
' """Handle responses from the device."""',
|
|
138
|
+
' try:',
|
|
139
|
+
' decoded = data.decode("ascii")',
|
|
140
|
+
' print(f"Response: {decoded}")',
|
|
141
|
+
' except:',
|
|
142
|
+
' print(f"Response (hex): {data.hex()}")',
|
|
143
|
+
'',
|
|
144
|
+
'',
|
|
145
|
+
'async def main():',
|
|
146
|
+
' print("Scanning for device...")',
|
|
147
|
+
' ',
|
|
148
|
+
' device = None',
|
|
149
|
+
' devices = await BleakScanner.discover(timeout=5.0)',
|
|
150
|
+
' for d in devices:',
|
|
151
|
+
' if d.name and DEVICE_NAME.lower() in d.name.lower():',
|
|
152
|
+
' device = d',
|
|
153
|
+
' break',
|
|
154
|
+
' ',
|
|
155
|
+
' if not device:',
|
|
156
|
+
' print(f"Device {DEVICE_NAME} not found!")',
|
|
157
|
+
' return',
|
|
158
|
+
' ',
|
|
159
|
+
' print(f"Found: {device.name}")',
|
|
160
|
+
' ',
|
|
161
|
+
' async with BleakClient(device, timeout=15.0) as client:',
|
|
162
|
+
' print("Connected!")',
|
|
163
|
+
' ',
|
|
164
|
+
' # Subscribe to notifications',
|
|
165
|
+
' await client.start_notify(FFE2_NOTIFY, notification_handler)',
|
|
166
|
+
' await asyncio.sleep(0.3)',
|
|
167
|
+
' ',
|
|
168
|
+
' # Test each command',
|
|
169
|
+
' for name, cmd in COMMANDS.items():',
|
|
170
|
+
' print(f"\\nSending: {name}")',
|
|
171
|
+
' await client.write_gatt_char(FFE1_WRITE, cmd)',
|
|
172
|
+
' await asyncio.sleep(1.0)',
|
|
173
|
+
' ',
|
|
174
|
+
' await client.stop_notify(FFE2_NOTIFY)',
|
|
175
|
+
' print("\\nDone!")',
|
|
176
|
+
'',
|
|
177
|
+
'',
|
|
178
|
+
'if __name__ == "__main__":',
|
|
179
|
+
' asyncio.run(main())',
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
return "\n".join(code_lines)
|
|
183
|
+
|
|
184
|
+
def _infer_purpose(self, cmd: str) -> str:
|
|
185
|
+
"""Try to infer the purpose of a command."""
|
|
186
|
+
# Common patterns for HiWonder CMD protocol
|
|
187
|
+
if cmd.startswith("CMD|"):
|
|
188
|
+
parts = cmd.rstrip("|$").split("|")
|
|
189
|
+
if len(parts) >= 2:
|
|
190
|
+
func = parts[1]
|
|
191
|
+
if func == "0":
|
|
192
|
+
return "init"
|
|
193
|
+
elif func == "1":
|
|
194
|
+
if len(parts) >= 3:
|
|
195
|
+
sub = parts[2]
|
|
196
|
+
if sub == "1":
|
|
197
|
+
return "pitch"
|
|
198
|
+
elif sub == "2":
|
|
199
|
+
return "roll"
|
|
200
|
+
elif sub == "3":
|
|
201
|
+
return "balance"
|
|
202
|
+
elif sub == "4":
|
|
203
|
+
return "height"
|
|
204
|
+
elif sub == "5":
|
|
205
|
+
return "reset_posture"
|
|
206
|
+
return "posture"
|
|
207
|
+
elif func == "2":
|
|
208
|
+
return f"action_group_{parts[3] if len(parts) > 3 else '1'}"
|
|
209
|
+
elif func == "3":
|
|
210
|
+
if len(parts) >= 3:
|
|
211
|
+
sub = parts[2]
|
|
212
|
+
mapping = {
|
|
213
|
+
"0": "stop",
|
|
214
|
+
"1": "turn_right_small",
|
|
215
|
+
"3": "forward",
|
|
216
|
+
"4": "turn_left_large",
|
|
217
|
+
"5": "turn_left_small",
|
|
218
|
+
"7": "backward",
|
|
219
|
+
}
|
|
220
|
+
return mapping.get(sub, f"locomotion_{sub}")
|
|
221
|
+
return "locomotion"
|
|
222
|
+
elif func == "6":
|
|
223
|
+
return "battery_query"
|
|
224
|
+
|
|
225
|
+
# Default: use sanitized command
|
|
226
|
+
return cmd[:20].replace("|", "_").replace("$", "")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def check_tshark() -> Optional[str]:
|
|
230
|
+
"""Check if tshark is available and return its path."""
|
|
231
|
+
# Common paths
|
|
232
|
+
paths = [
|
|
233
|
+
"/opt/homebrew/bin/tshark", # Homebrew on Apple Silicon
|
|
234
|
+
"/usr/local/bin/tshark", # Homebrew on Intel Mac
|
|
235
|
+
"/usr/bin/tshark", # Linux
|
|
236
|
+
shutil.which("tshark"), # PATH lookup
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
for path in paths:
|
|
240
|
+
if path and os.path.exists(path):
|
|
241
|
+
return path
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def check_adb() -> Optional[str]:
|
|
247
|
+
"""Check if adb is available and return its path."""
|
|
248
|
+
paths = [
|
|
249
|
+
"/opt/homebrew/bin/adb",
|
|
250
|
+
"/usr/local/bin/adb",
|
|
251
|
+
shutil.which("adb"),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for path in paths:
|
|
255
|
+
if path and os.path.exists(path):
|
|
256
|
+
return path
|
|
257
|
+
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def check_ios_device() -> Tuple[bool, str]:
|
|
262
|
+
"""Check if an iOS device is connected via USB."""
|
|
263
|
+
try:
|
|
264
|
+
result = subprocess.run(
|
|
265
|
+
["system_profiler", "SPUSBDataType"],
|
|
266
|
+
capture_output=True,
|
|
267
|
+
text=True,
|
|
268
|
+
timeout=10,
|
|
269
|
+
)
|
|
270
|
+
if "iPhone" in result.stdout or "iPad" in result.stdout:
|
|
271
|
+
return True, "iOS device detected via USB"
|
|
272
|
+
return False, "No iOS device found. Connect iPhone via USB cable."
|
|
273
|
+
except Exception as e:
|
|
274
|
+
return False, f"Could not check for iOS device: {e}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def check_android_device() -> Tuple[bool, str]:
|
|
278
|
+
"""Check if an Android device is connected via ADB."""
|
|
279
|
+
adb = check_adb()
|
|
280
|
+
if not adb:
|
|
281
|
+
return False, "adb not found. Install with: brew install android-platform-tools"
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
result = subprocess.run(
|
|
285
|
+
[adb, "devices"],
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
timeout=10,
|
|
289
|
+
)
|
|
290
|
+
lines = result.stdout.strip().split("\n")
|
|
291
|
+
# Skip header line, look for devices
|
|
292
|
+
devices = [l for l in lines[1:] if l.strip() and "device" in l]
|
|
293
|
+
if devices:
|
|
294
|
+
return True, f"Android device connected: {devices[0].split()[0]}"
|
|
295
|
+
return False, "No Android device found. Enable USB debugging and connect via USB."
|
|
296
|
+
except Exception as e:
|
|
297
|
+
return False, f"Could not check for Android device: {e}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def capture_ios_interactive(output_path: str, duration: int = 60) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Guide user through iOS BLE capture using PacketLogger.
|
|
303
|
+
|
|
304
|
+
Returns True if capture was successful.
|
|
305
|
+
"""
|
|
306
|
+
print("=" * 60)
|
|
307
|
+
print("iOS BLE CAPTURE WORKFLOW")
|
|
308
|
+
print("=" * 60)
|
|
309
|
+
print()
|
|
310
|
+
|
|
311
|
+
# Check prerequisites
|
|
312
|
+
print("Checking prerequisites...")
|
|
313
|
+
|
|
314
|
+
# Check for iOS device
|
|
315
|
+
ios_ok, ios_msg = check_ios_device()
|
|
316
|
+
if ios_ok:
|
|
317
|
+
print(f" ✓ {ios_msg}")
|
|
318
|
+
else:
|
|
319
|
+
print(f" ✗ {ios_msg}")
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
# Check for PacketLogger
|
|
323
|
+
packet_logger_paths = [
|
|
324
|
+
"/Applications/PacketLogger.app",
|
|
325
|
+
os.path.expanduser("~/Applications/PacketLogger.app"),
|
|
326
|
+
"/Applications/Additional Tools/Hardware/PacketLogger.app",
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
packet_logger = None
|
|
330
|
+
for path in packet_logger_paths:
|
|
331
|
+
if os.path.exists(path):
|
|
332
|
+
packet_logger = path
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
if packet_logger:
|
|
336
|
+
print(f" ✓ PacketLogger found at {packet_logger}")
|
|
337
|
+
else:
|
|
338
|
+
print(" ✗ PacketLogger not found")
|
|
339
|
+
print()
|
|
340
|
+
print("To install PacketLogger:")
|
|
341
|
+
print(" 1. Go to: https://developer.apple.com/download/all/")
|
|
342
|
+
print(" 2. Search for 'Additional Tools for Xcode'")
|
|
343
|
+
print(" 3. Download and open the DMG")
|
|
344
|
+
print(" 4. Copy PacketLogger.app from Hardware folder to /Applications")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
# Check for tshark (for analysis)
|
|
348
|
+
tshark = check_tshark()
|
|
349
|
+
if tshark:
|
|
350
|
+
print(f" ✓ tshark found at {tshark}")
|
|
351
|
+
else:
|
|
352
|
+
print(" ⚠ tshark not found (needed for analysis)")
|
|
353
|
+
print(" Install with: brew install wireshark")
|
|
354
|
+
|
|
355
|
+
print()
|
|
356
|
+
print("-" * 60)
|
|
357
|
+
print("IMPORTANT: Bluetooth Logging Profile Required")
|
|
358
|
+
print("-" * 60)
|
|
359
|
+
print("""
|
|
360
|
+
If you haven't already, install the Bluetooth logging profile on your iPhone:
|
|
361
|
+
|
|
362
|
+
1. On your iPhone, open Safari
|
|
363
|
+
2. Go to: https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth
|
|
364
|
+
3. Sign in with your Apple ID (developer account required)
|
|
365
|
+
4. Download and install the 'Bluetooth for iOS' profile
|
|
366
|
+
5. Go to Settings > General > VPN & Device Management
|
|
367
|
+
6. Tap the Bluetooth profile and install it
|
|
368
|
+
7. RESTART YOUR IPHONE (required!)
|
|
369
|
+
|
|
370
|
+
A pulsing Bluetooth icon in the status bar indicates logging is active.
|
|
371
|
+
""")
|
|
372
|
+
|
|
373
|
+
input("Press Enter when your iPhone is ready with the logging profile installed...")
|
|
374
|
+
|
|
375
|
+
print()
|
|
376
|
+
print("-" * 60)
|
|
377
|
+
print("CAPTURE INSTRUCTIONS")
|
|
378
|
+
print("-" * 60)
|
|
379
|
+
print(f"""
|
|
380
|
+
1. PacketLogger will now open
|
|
381
|
+
2. Go to File > New iOS Trace (or Cmd+Shift+I)
|
|
382
|
+
3. Select your iPhone from the device list
|
|
383
|
+
4. Click 'Start' to begin capturing
|
|
384
|
+
5. On your iPhone:
|
|
385
|
+
- Open your robot's control app (e.g., WonderBot)
|
|
386
|
+
- Connect to the robot
|
|
387
|
+
- Perform the actions you want to capture
|
|
388
|
+
- Disconnect from the robot
|
|
389
|
+
6. In PacketLogger, click 'Stop'
|
|
390
|
+
7. Save the capture to: {output_path}
|
|
391
|
+
(File > Save As...)
|
|
392
|
+
""")
|
|
393
|
+
|
|
394
|
+
# Open PacketLogger
|
|
395
|
+
print("Opening PacketLogger...")
|
|
396
|
+
subprocess.run(["open", packet_logger])
|
|
397
|
+
|
|
398
|
+
print()
|
|
399
|
+
input(f"Press Enter after you've saved the capture to {output_path}...")
|
|
400
|
+
|
|
401
|
+
# Verify the file exists
|
|
402
|
+
if os.path.exists(output_path):
|
|
403
|
+
size = os.path.getsize(output_path)
|
|
404
|
+
print(f"\n✓ Capture saved: {output_path} ({size:,} bytes)")
|
|
405
|
+
return True
|
|
406
|
+
else:
|
|
407
|
+
print(f"\n✗ Capture file not found at {output_path}")
|
|
408
|
+
print(" Make sure you saved the file to the correct location.")
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def capture_android_interactive(output_path: str) -> bool:
|
|
413
|
+
"""
|
|
414
|
+
Guide user through Android BLE capture using HCI snoop log.
|
|
415
|
+
|
|
416
|
+
Returns True if capture was successful.
|
|
417
|
+
"""
|
|
418
|
+
print("=" * 60)
|
|
419
|
+
print("ANDROID BLE CAPTURE WORKFLOW")
|
|
420
|
+
print("=" * 60)
|
|
421
|
+
print()
|
|
422
|
+
|
|
423
|
+
# Check prerequisites
|
|
424
|
+
print("Checking prerequisites...")
|
|
425
|
+
|
|
426
|
+
adb = check_adb()
|
|
427
|
+
if adb:
|
|
428
|
+
print(f" ✓ adb found at {adb}")
|
|
429
|
+
else:
|
|
430
|
+
print(" ✗ adb not found")
|
|
431
|
+
print(" Install with: brew install android-platform-tools")
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
android_ok, android_msg = check_android_device()
|
|
435
|
+
if android_ok:
|
|
436
|
+
print(f" ✓ {android_msg}")
|
|
437
|
+
else:
|
|
438
|
+
print(f" ✗ {android_msg}")
|
|
439
|
+
print()
|
|
440
|
+
print("To enable USB debugging:")
|
|
441
|
+
print(" 1. Go to Settings > About Phone")
|
|
442
|
+
print(" 2. Tap 'Build Number' 7 times")
|
|
443
|
+
print(" 3. Go to Settings > Developer Options")
|
|
444
|
+
print(" 4. Enable 'USB Debugging'")
|
|
445
|
+
print(" 5. Connect phone and accept the debugging prompt")
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
tshark = check_tshark()
|
|
449
|
+
if tshark:
|
|
450
|
+
print(f" ✓ tshark found at {tshark}")
|
|
451
|
+
else:
|
|
452
|
+
print(" ⚠ tshark not found (needed for analysis)")
|
|
453
|
+
print(" Install with: brew install wireshark")
|
|
454
|
+
|
|
455
|
+
print()
|
|
456
|
+
print("-" * 60)
|
|
457
|
+
print("ENABLE BLUETOOTH HCI SNOOP LOG")
|
|
458
|
+
print("-" * 60)
|
|
459
|
+
print("""
|
|
460
|
+
On your Android phone:
|
|
461
|
+
1. Go to Settings > Developer Options
|
|
462
|
+
2. Find 'Enable Bluetooth HCI snoop log'
|
|
463
|
+
(may be under 'Networking' section)
|
|
464
|
+
3. ENABLE it
|
|
465
|
+
4. RESTART your phone (required for logging to start!)
|
|
466
|
+
|
|
467
|
+
After restart, the logging will be active.
|
|
468
|
+
""")
|
|
469
|
+
|
|
470
|
+
input("Press Enter when you've enabled HCI snoop log and restarted...")
|
|
471
|
+
|
|
472
|
+
print()
|
|
473
|
+
print("-" * 60)
|
|
474
|
+
print("CAPTURE INSTRUCTIONS")
|
|
475
|
+
print("-" * 60)
|
|
476
|
+
print("""
|
|
477
|
+
Now perform the actions you want to capture:
|
|
478
|
+
|
|
479
|
+
1. Open your robot's control app (e.g., WonderBot)
|
|
480
|
+
2. Connect to the robot
|
|
481
|
+
3. Perform the actions you want to capture
|
|
482
|
+
4. Disconnect from the robot
|
|
483
|
+
5. DISABLE Bluetooth HCI snoop log in Developer Options
|
|
484
|
+
(This stops logging and finalizes the file)
|
|
485
|
+
""")
|
|
486
|
+
|
|
487
|
+
input("Press Enter when you've finished capturing and disabled the snoop log...")
|
|
488
|
+
|
|
489
|
+
print()
|
|
490
|
+
print("Extracting capture via adb bugreport...")
|
|
491
|
+
print("(This may take 1-2 minutes)")
|
|
492
|
+
|
|
493
|
+
# Create temp directory for extraction
|
|
494
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
495
|
+
bugreport_zip = os.path.join(tmpdir, "bugreport.zip")
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
# Get bugreport
|
|
499
|
+
result = subprocess.run(
|
|
500
|
+
[adb, "bugreport", bugreport_zip],
|
|
501
|
+
capture_output=True,
|
|
502
|
+
text=True,
|
|
503
|
+
timeout=180, # 3 minutes timeout
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if not os.path.exists(bugreport_zip):
|
|
507
|
+
print(f"✗ Failed to get bugreport: {result.stderr}")
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
print(f" ✓ Bugreport saved ({os.path.getsize(bugreport_zip):,} bytes)")
|
|
511
|
+
|
|
512
|
+
# Extract and find btsnoop file
|
|
513
|
+
extract_dir = os.path.join(tmpdir, "extracted")
|
|
514
|
+
subprocess.run(
|
|
515
|
+
["unzip", "-q", bugreport_zip, "-d", extract_dir],
|
|
516
|
+
check=True,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Find btsnoop files
|
|
520
|
+
btsnoop_files = []
|
|
521
|
+
for root, dirs, files in os.walk(extract_dir):
|
|
522
|
+
for f in files:
|
|
523
|
+
if "btsnoop" in f.lower() or f.endswith(".cfa"):
|
|
524
|
+
btsnoop_files.append(os.path.join(root, f))
|
|
525
|
+
|
|
526
|
+
if not btsnoop_files:
|
|
527
|
+
print("✗ No btsnoop file found in bugreport")
|
|
528
|
+
print(" Make sure you enabled HCI snoop log before capturing")
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
# Use the largest file (most data)
|
|
532
|
+
btsnoop_file = max(btsnoop_files, key=os.path.getsize)
|
|
533
|
+
print(f" ✓ Found: {os.path.basename(btsnoop_file)}")
|
|
534
|
+
|
|
535
|
+
# Copy to output
|
|
536
|
+
shutil.copy2(btsnoop_file, output_path)
|
|
537
|
+
print(f" ✓ Saved to: {output_path}")
|
|
538
|
+
|
|
539
|
+
return True
|
|
540
|
+
|
|
541
|
+
except subprocess.TimeoutExpired:
|
|
542
|
+
print("✗ Timeout waiting for bugreport")
|
|
543
|
+
return False
|
|
544
|
+
except Exception as e:
|
|
545
|
+
print(f"✗ Error: {e}")
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def analyze_capture(capture_path: str) -> Optional[CaptureAnalysis]:
|
|
550
|
+
"""
|
|
551
|
+
Analyze a BLE capture file (.pklg or .btsnoop).
|
|
552
|
+
|
|
553
|
+
Uses tshark to extract BLE ATT protocol data.
|
|
554
|
+
"""
|
|
555
|
+
if not os.path.exists(capture_path):
|
|
556
|
+
print(f"Error: File not found: {capture_path}")
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
tshark = check_tshark()
|
|
560
|
+
if not tshark:
|
|
561
|
+
print("Error: tshark not found. Install with: brew install wireshark")
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
print(f"Analyzing: {capture_path}")
|
|
565
|
+
print(f"Size: {os.path.getsize(capture_path):,} bytes")
|
|
566
|
+
print()
|
|
567
|
+
|
|
568
|
+
# Extract BLE ATT data
|
|
569
|
+
try:
|
|
570
|
+
result = subprocess.run(
|
|
571
|
+
[
|
|
572
|
+
tshark,
|
|
573
|
+
"-r", capture_path,
|
|
574
|
+
"-Y", "btatt.value",
|
|
575
|
+
"-T", "fields",
|
|
576
|
+
"-e", "frame.number",
|
|
577
|
+
"-e", "btatt.opcode",
|
|
578
|
+
"-e", "btatt.handle",
|
|
579
|
+
"-e", "btatt.value",
|
|
580
|
+
],
|
|
581
|
+
capture_output=True,
|
|
582
|
+
text=True,
|
|
583
|
+
timeout=60,
|
|
584
|
+
)
|
|
585
|
+
except subprocess.TimeoutExpired:
|
|
586
|
+
print("Error: tshark timeout")
|
|
587
|
+
return None
|
|
588
|
+
except Exception as e:
|
|
589
|
+
print(f"Error running tshark: {e}")
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
if result.returncode != 0:
|
|
593
|
+
print(f"tshark error: {result.stderr}")
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
# Parse output
|
|
597
|
+
analysis = CaptureAnalysis(file_path=capture_path)
|
|
598
|
+
|
|
599
|
+
for line in result.stdout.strip().split("\n"):
|
|
600
|
+
if not line.strip():
|
|
601
|
+
continue
|
|
602
|
+
|
|
603
|
+
parts = line.split("\t")
|
|
604
|
+
if len(parts) < 4:
|
|
605
|
+
continue
|
|
606
|
+
|
|
607
|
+
frame, opcode, handle_str, value_hex = parts[0], parts[1], parts[2], parts[3]
|
|
608
|
+
|
|
609
|
+
if not value_hex:
|
|
610
|
+
continue
|
|
611
|
+
|
|
612
|
+
# Parse handle
|
|
613
|
+
try:
|
|
614
|
+
if handle_str.startswith("0x"):
|
|
615
|
+
handle = int(handle_str, 16)
|
|
616
|
+
else:
|
|
617
|
+
handle = int(handle_str)
|
|
618
|
+
except:
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
# Decode value
|
|
622
|
+
try:
|
|
623
|
+
value_bytes = bytes.fromhex(value_hex.replace(":", ""))
|
|
624
|
+
decoded = value_bytes.decode("ascii")
|
|
625
|
+
except:
|
|
626
|
+
decoded = None
|
|
627
|
+
|
|
628
|
+
cmd = BLECommand(
|
|
629
|
+
frame=int(frame),
|
|
630
|
+
opcode=opcode,
|
|
631
|
+
handle=handle,
|
|
632
|
+
raw_hex=value_hex,
|
|
633
|
+
decoded=decoded,
|
|
634
|
+
)
|
|
635
|
+
analysis.commands.append(cmd)
|
|
636
|
+
|
|
637
|
+
# Track writes and notifies
|
|
638
|
+
display = decoded if decoded else f"[hex:{value_hex[:20]}...]"
|
|
639
|
+
|
|
640
|
+
if cmd.is_write:
|
|
641
|
+
if analysis.write_handle is None:
|
|
642
|
+
analysis.write_handle = handle
|
|
643
|
+
analysis.unique_writes[display] = analysis.unique_writes.get(display, 0) + 1
|
|
644
|
+
elif cmd.is_notify:
|
|
645
|
+
if analysis.notify_handle is None:
|
|
646
|
+
analysis.notify_handle = handle
|
|
647
|
+
analysis.unique_notifies[display] = analysis.unique_notifies.get(display, 0) + 1
|
|
648
|
+
|
|
649
|
+
# Detect protocol
|
|
650
|
+
for cmd in analysis.unique_writes.keys():
|
|
651
|
+
if cmd.startswith("CMD|") and cmd.endswith("|$"):
|
|
652
|
+
analysis.protocol_detected = "HiWonder CMD Protocol"
|
|
653
|
+
break
|
|
654
|
+
elif cmd.startswith("[hex:5555"):
|
|
655
|
+
analysis.protocol_detected = "Binary Servo Protocol (0x55 0x55)"
|
|
656
|
+
break
|
|
657
|
+
|
|
658
|
+
return analysis
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def inspect_ble_device(address: str) -> bool:
|
|
662
|
+
"""
|
|
663
|
+
Inspect a BLE device's services and characteristics.
|
|
664
|
+
|
|
665
|
+
Shows which characteristics support write vs notify to help
|
|
666
|
+
users understand the correct configuration.
|
|
667
|
+
"""
|
|
668
|
+
try:
|
|
669
|
+
import asyncio
|
|
670
|
+
from bleak import BleakClient, BleakScanner
|
|
671
|
+
except ImportError:
|
|
672
|
+
print("Error: bleak not installed. Run: pip install bleak")
|
|
673
|
+
return False
|
|
674
|
+
|
|
675
|
+
async def _inspect():
|
|
676
|
+
print(f"Scanning for device: {address}")
|
|
677
|
+
|
|
678
|
+
# Try to find device
|
|
679
|
+
device = None
|
|
680
|
+
devices = await BleakScanner.discover(timeout=5.0)
|
|
681
|
+
for d in devices:
|
|
682
|
+
if d.address == address or (d.name and address.lower() in d.name.lower()):
|
|
683
|
+
device = d
|
|
684
|
+
break
|
|
685
|
+
|
|
686
|
+
if not device:
|
|
687
|
+
print(f"Device not found: {address}")
|
|
688
|
+
print("\nAvailable devices:")
|
|
689
|
+
for d in devices:
|
|
690
|
+
if d.name:
|
|
691
|
+
print(f" {d.name}: {d.address}")
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
print(f"Found: {device.name} ({device.address})")
|
|
695
|
+
print()
|
|
696
|
+
|
|
697
|
+
async with BleakClient(device, timeout=15.0) as client:
|
|
698
|
+
print("=" * 60)
|
|
699
|
+
print("BLE SERVICE & CHARACTERISTIC MAP")
|
|
700
|
+
print("=" * 60)
|
|
701
|
+
print()
|
|
702
|
+
|
|
703
|
+
for service in client.services:
|
|
704
|
+
# Shorten UUID for display
|
|
705
|
+
uuid = str(service.uuid)
|
|
706
|
+
short_uuid = uuid.split("-")[0] if "-" in uuid else uuid
|
|
707
|
+
print(f"Service: {short_uuid}")
|
|
708
|
+
|
|
709
|
+
for char in service.characteristics:
|
|
710
|
+
char_uuid = str(char.uuid)
|
|
711
|
+
char_short = char_uuid.split("-")[0] if "-" in char_uuid else char_uuid
|
|
712
|
+
|
|
713
|
+
props = char.properties
|
|
714
|
+
prop_str = ", ".join(props)
|
|
715
|
+
|
|
716
|
+
# Determine usage
|
|
717
|
+
usage = []
|
|
718
|
+
if "write" in props or "write-without-response" in props:
|
|
719
|
+
usage.append("← WRITE commands here")
|
|
720
|
+
if "notify" in props or "indicate" in props:
|
|
721
|
+
usage.append("→ SUBSCRIBE for responses")
|
|
722
|
+
if "read" in props:
|
|
723
|
+
usage.append("? Can read")
|
|
724
|
+
|
|
725
|
+
print(f" ├── {char_short}")
|
|
726
|
+
print(f" │ Handle: 0x{char.handle:04x} ({char.handle})")
|
|
727
|
+
print(f" │ Properties: [{prop_str}]")
|
|
728
|
+
if usage:
|
|
729
|
+
print(f" │ {' | '.join(usage)}")
|
|
730
|
+
print(f" │")
|
|
731
|
+
|
|
732
|
+
print()
|
|
733
|
+
print("=" * 60)
|
|
734
|
+
print("RECOMMENDED CONFIGURATION")
|
|
735
|
+
print("=" * 60)
|
|
736
|
+
|
|
737
|
+
# Find write and notify characteristics
|
|
738
|
+
write_chars = []
|
|
739
|
+
notify_chars = []
|
|
740
|
+
|
|
741
|
+
for service in client.services:
|
|
742
|
+
for char in service.characteristics:
|
|
743
|
+
if "write" in char.properties or "write-without-response" in char.properties:
|
|
744
|
+
write_chars.append(char)
|
|
745
|
+
if "notify" in char.properties or "indicate" in char.properties:
|
|
746
|
+
notify_chars.append(char)
|
|
747
|
+
|
|
748
|
+
if write_chars:
|
|
749
|
+
print(f"\nWrite commands to:")
|
|
750
|
+
for c in write_chars:
|
|
751
|
+
print(f" {c.uuid}")
|
|
752
|
+
|
|
753
|
+
if notify_chars:
|
|
754
|
+
print(f"\nSubscribe for responses on:")
|
|
755
|
+
for c in notify_chars:
|
|
756
|
+
print(f" {c.uuid}")
|
|
757
|
+
|
|
758
|
+
if write_chars and notify_chars:
|
|
759
|
+
print(f"""
|
|
760
|
+
Example Python code:
|
|
761
|
+
|
|
762
|
+
FFE1_WRITE = "{write_chars[0].uuid}"
|
|
763
|
+
FFE2_NOTIFY = "{notify_chars[0].uuid}"
|
|
764
|
+
|
|
765
|
+
await client.start_notify(FFE2_NOTIFY, handler)
|
|
766
|
+
await client.write_gatt_char(FFE1_WRITE, b"your_command")
|
|
767
|
+
""")
|
|
768
|
+
|
|
769
|
+
return True
|
|
770
|
+
|
|
771
|
+
return asyncio.run(_inspect())
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
async def ble_pickup_sequence(address: str) -> bool:
|
|
775
|
+
"""
|
|
776
|
+
Execute a ball pickup sequence using pure BLE commands.
|
|
777
|
+
|
|
778
|
+
Uses:
|
|
779
|
+
- Action groups (CMD|2|1|X|$) for arm control (if available)
|
|
780
|
+
- Locomotion (CMD|3|X|$) for walking
|
|
781
|
+
|
|
782
|
+
Note: Action groups are pre-programmed sequences on the robot.
|
|
783
|
+
Not all robots have pickup-related action groups. If the arm
|
|
784
|
+
doesn't respond, use USB serial for direct servo control.
|
|
785
|
+
|
|
786
|
+
Returns True if sequence completed, False if failed.
|
|
787
|
+
"""
|
|
788
|
+
try:
|
|
789
|
+
import asyncio
|
|
790
|
+
from bleak import BleakClient, BleakScanner
|
|
791
|
+
except ImportError:
|
|
792
|
+
print("Error: bleak not installed. Run: pip install bleak")
|
|
793
|
+
return False
|
|
794
|
+
|
|
795
|
+
FFE1_WRITE = "0000ffe1-0000-1000-8000-00805f9b34fb"
|
|
796
|
+
FFE2_NOTIFY = "0000ffe2-0000-1000-8000-00805f9b34fb"
|
|
797
|
+
|
|
798
|
+
responses = []
|
|
799
|
+
|
|
800
|
+
def notification_handler(sender, data):
|
|
801
|
+
try:
|
|
802
|
+
decoded = data.decode('ascii')
|
|
803
|
+
responses.append(decoded)
|
|
804
|
+
print(f" ✓ Response: {decoded}")
|
|
805
|
+
except:
|
|
806
|
+
responses.append(data.hex())
|
|
807
|
+
|
|
808
|
+
print(f"Scanning for device: {address}...")
|
|
809
|
+
|
|
810
|
+
# Find device
|
|
811
|
+
devices = await BleakScanner.discover(timeout=5.0)
|
|
812
|
+
device = None
|
|
813
|
+
for d in devices:
|
|
814
|
+
if d.address == address or (d.name and address.lower() in (d.name or "").lower()):
|
|
815
|
+
device = d
|
|
816
|
+
break
|
|
817
|
+
|
|
818
|
+
if not device:
|
|
819
|
+
print(f"Device not found: {address}")
|
|
820
|
+
print("Make sure the robot is powered on and not connected to another app.")
|
|
821
|
+
return False
|
|
822
|
+
|
|
823
|
+
print(f"Found: {device.name} ({device.address})")
|
|
824
|
+
|
|
825
|
+
try:
|
|
826
|
+
async with BleakClient(device, timeout=15.0) as client:
|
|
827
|
+
print(f"Connected: {client.is_connected}")
|
|
828
|
+
|
|
829
|
+
# Subscribe to notifications
|
|
830
|
+
await client.start_notify(FFE2_NOTIFY, notification_handler)
|
|
831
|
+
await asyncio.sleep(0.3)
|
|
832
|
+
|
|
833
|
+
# Battery check
|
|
834
|
+
print("\n--- Battery Check ---")
|
|
835
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|6|$")
|
|
836
|
+
await asyncio.sleep(0.5)
|
|
837
|
+
|
|
838
|
+
for resp in responses:
|
|
839
|
+
if "CMD|6|" in resp:
|
|
840
|
+
parts = resp.split("|")
|
|
841
|
+
if len(parts) >= 3:
|
|
842
|
+
try:
|
|
843
|
+
mv = int(parts[2])
|
|
844
|
+
print(f" Battery: {mv/1000:.2f}V")
|
|
845
|
+
except:
|
|
846
|
+
pass
|
|
847
|
+
|
|
848
|
+
# Try action groups to find arm-related sequences
|
|
849
|
+
# These are pre-programmed on the robot
|
|
850
|
+
print("\n--- Testing Action Groups (looking for arm movement) ---")
|
|
851
|
+
print("Watch the robot's arm for any movement...")
|
|
852
|
+
|
|
853
|
+
# Action groups 1-5 - one might be arm-related
|
|
854
|
+
for action_id in [1, 2, 3, 4, 5]:
|
|
855
|
+
cmd = f"CMD|2|1|{action_id}|$".encode()
|
|
856
|
+
print(f"\n Action Group {action_id}: {cmd.decode()}")
|
|
857
|
+
await client.write_gatt_char(FFE1_WRITE, cmd)
|
|
858
|
+
await asyncio.sleep(3.0)
|
|
859
|
+
|
|
860
|
+
# Now do the locomotion sequence
|
|
861
|
+
print("\n--- Locomotion Sequence ---")
|
|
862
|
+
|
|
863
|
+
# Walk forward
|
|
864
|
+
print("\n Walking forward (1.5s)...")
|
|
865
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|3|3|$")
|
|
866
|
+
await asyncio.sleep(1.5)
|
|
867
|
+
|
|
868
|
+
# Stop
|
|
869
|
+
print(" Stopping...")
|
|
870
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|3|0|$")
|
|
871
|
+
await asyncio.sleep(0.5)
|
|
872
|
+
|
|
873
|
+
# Walk backward
|
|
874
|
+
print(" Walking backward (1.0s)...")
|
|
875
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|3|7|$")
|
|
876
|
+
await asyncio.sleep(1.0)
|
|
877
|
+
|
|
878
|
+
# Stop
|
|
879
|
+
print(" Stopping...")
|
|
880
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|3|0|$")
|
|
881
|
+
await asyncio.sleep(0.5)
|
|
882
|
+
|
|
883
|
+
# Reset posture
|
|
884
|
+
print(" Resetting posture...")
|
|
885
|
+
await client.write_gatt_char(FFE1_WRITE, b"CMD|1|5|$")
|
|
886
|
+
await asyncio.sleep(1.0)
|
|
887
|
+
|
|
888
|
+
await client.stop_notify(FFE2_NOTIFY)
|
|
889
|
+
|
|
890
|
+
print("\n--- Sequence Complete ---")
|
|
891
|
+
print("\nIf the arm didn't move during action groups, the robot may")
|
|
892
|
+
print("not have pre-programmed arm sequences. Use USB serial for")
|
|
893
|
+
print("direct servo control: ate robot test mechdog --port <port>")
|
|
894
|
+
|
|
895
|
+
return True
|
|
896
|
+
|
|
897
|
+
except Exception as e:
|
|
898
|
+
print(f"Error: {e}")
|
|
899
|
+
return False
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
# CLI Integration
|
|
903
|
+
def register_cli_commands(cli):
|
|
904
|
+
"""Register BLE capture commands with the CLI."""
|
|
905
|
+
import click
|
|
906
|
+
|
|
907
|
+
@cli.group()
|
|
908
|
+
def ble():
|
|
909
|
+
"""BLE protocol capture and analysis tools."""
|
|
910
|
+
pass
|
|
911
|
+
|
|
912
|
+
@ble.command("capture")
|
|
913
|
+
@click.option("--platform", "-p", type=click.Choice(["ios", "android", "auto"]),
|
|
914
|
+
default="auto", help="Mobile platform")
|
|
915
|
+
@click.option("--output", "-o", default="capture.pklg", help="Output file path")
|
|
916
|
+
def ble_capture(platform: str, output: str):
|
|
917
|
+
"""Capture BLE traffic from phone app for protocol analysis."""
|
|
918
|
+
if platform == "auto":
|
|
919
|
+
# Try to detect
|
|
920
|
+
ios_ok, _ = check_ios_device()
|
|
921
|
+
android_ok, _ = check_android_device()
|
|
922
|
+
|
|
923
|
+
if ios_ok:
|
|
924
|
+
platform = "ios"
|
|
925
|
+
elif android_ok:
|
|
926
|
+
platform = "android"
|
|
927
|
+
else:
|
|
928
|
+
print("No mobile device detected.")
|
|
929
|
+
print("Connect your iPhone or Android phone via USB.")
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
if platform == "ios":
|
|
933
|
+
success = capture_ios_interactive(output)
|
|
934
|
+
else:
|
|
935
|
+
success = capture_android_interactive(output)
|
|
936
|
+
|
|
937
|
+
if success:
|
|
938
|
+
print()
|
|
939
|
+
print(f"Next step: Analyze the capture with:")
|
|
940
|
+
print(f" ate ble analyze {output}")
|
|
941
|
+
|
|
942
|
+
@ble.command("analyze")
|
|
943
|
+
@click.argument("capture_file")
|
|
944
|
+
@click.option("--generate", "-g", is_flag=True, help="Generate Python replay code")
|
|
945
|
+
@click.option("--output", "-o", help="Output file for generated code")
|
|
946
|
+
def ble_analyze(capture_file: str, generate: bool, output: str):
|
|
947
|
+
"""Analyze a BLE capture file to decode the protocol."""
|
|
948
|
+
analysis = analyze_capture(capture_file)
|
|
949
|
+
|
|
950
|
+
if not analysis:
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
print(analysis.summary())
|
|
954
|
+
|
|
955
|
+
if generate or output:
|
|
956
|
+
code = analysis.generate_python_code()
|
|
957
|
+
|
|
958
|
+
if output:
|
|
959
|
+
with open(output, "w") as f:
|
|
960
|
+
f.write(code)
|
|
961
|
+
print(f"\nGenerated code saved to: {output}")
|
|
962
|
+
else:
|
|
963
|
+
print("\n" + "=" * 60)
|
|
964
|
+
print("GENERATED PYTHON CODE")
|
|
965
|
+
print("=" * 60)
|
|
966
|
+
print(code)
|
|
967
|
+
|
|
968
|
+
@ble.command("inspect")
|
|
969
|
+
@click.argument("address")
|
|
970
|
+
def ble_inspect(address: str):
|
|
971
|
+
"""Inspect a BLE device's characteristics and capabilities."""
|
|
972
|
+
inspect_ble_device(address)
|
|
973
|
+
|
|
974
|
+
return ble
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
if __name__ == "__main__":
|
|
978
|
+
# Quick test
|
|
979
|
+
import sys
|
|
980
|
+
|
|
981
|
+
if len(sys.argv) > 1:
|
|
982
|
+
if sys.argv[1] == "analyze" and len(sys.argv) > 2:
|
|
983
|
+
analysis = analyze_capture(sys.argv[2])
|
|
984
|
+
if analysis:
|
|
985
|
+
print(analysis.summary())
|
|
986
|
+
print("\n" + analysis.generate_python_code())
|
|
987
|
+
elif sys.argv[1] == "inspect" and len(sys.argv) > 2:
|
|
988
|
+
inspect_ble_device(sys.argv[2])
|
|
989
|
+
elif sys.argv[1] == "capture":
|
|
990
|
+
platform = sys.argv[2] if len(sys.argv) > 2 else "ios"
|
|
991
|
+
output = sys.argv[3] if len(sys.argv) > 3 else "capture.pklg"
|
|
992
|
+
if platform == "ios":
|
|
993
|
+
capture_ios_interactive(output)
|
|
994
|
+
else:
|
|
995
|
+
capture_android_interactive(output)
|
|
996
|
+
else:
|
|
997
|
+
print("Usage:")
|
|
998
|
+
print(" python ble_capture.py capture [ios|android] [output.pklg]")
|
|
999
|
+
print(" python ble_capture.py analyze <capture_file>")
|
|
1000
|
+
print(" python ble_capture.py inspect <device_address>")
|