foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.1__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 +402 -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.1.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.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.1.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BLE Transport Layer for Robot Drivers.
|
|
3
|
+
|
|
4
|
+
Provides a serial-compatible interface over Bluetooth Low Energy.
|
|
5
|
+
Uses the bleak library for cross-platform BLE support.
|
|
6
|
+
|
|
7
|
+
This enables wireless control of robots that support BLE serial
|
|
8
|
+
(ESP32, HM-10, nRF52, etc.) using the same driver code as USB serial.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from ate.drivers.ble_transport import BLETransport
|
|
12
|
+
|
|
13
|
+
# Discover BLE devices
|
|
14
|
+
devices = await BLETransport.discover()
|
|
15
|
+
|
|
16
|
+
# Connect to a device
|
|
17
|
+
transport = BLETransport(
|
|
18
|
+
address="AA:BB:CC:DD:EE:FF",
|
|
19
|
+
service_uuid="0000ffe0-0000-1000-8000-00805f9b34fb",
|
|
20
|
+
char_uuid="0000ffe1-0000-1000-8000-00805f9b34fb",
|
|
21
|
+
)
|
|
22
|
+
await transport.connect()
|
|
23
|
+
|
|
24
|
+
# Use like serial
|
|
25
|
+
transport.write(b"command\\r\\n")
|
|
26
|
+
response = transport.read(100)
|
|
27
|
+
|
|
28
|
+
await transport.disconnect()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import queue
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import Optional, List, Callable, Any
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from bleak import BleakClient, BleakScanner
|
|
40
|
+
from bleak.backends.device import BLEDevice
|
|
41
|
+
HAS_BLEAK = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
HAS_BLEAK = False
|
|
44
|
+
BleakClient = None
|
|
45
|
+
BleakScanner = None
|
|
46
|
+
BLEDevice = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class BLEDeviceInfo:
|
|
51
|
+
"""Information about a discovered BLE device."""
|
|
52
|
+
address: str
|
|
53
|
+
name: str
|
|
54
|
+
rssi: int
|
|
55
|
+
services: List[str]
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f"{self.name or 'Unknown'} ({self.address}) RSSI: {self.rssi}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BLETransport:
|
|
62
|
+
"""
|
|
63
|
+
BLE transport that provides a serial-compatible interface.
|
|
64
|
+
|
|
65
|
+
This allows robot drivers to use BLE with minimal changes.
|
|
66
|
+
The interface mimics pyserial's Serial class.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Default ESP32/HM-10 BLE serial UUIDs
|
|
70
|
+
DEFAULT_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb"
|
|
71
|
+
DEFAULT_WRITE_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb" # Write characteristic
|
|
72
|
+
DEFAULT_NOTIFY_CHAR_UUID = "0000ffe2-0000-1000-8000-00805f9b34fb" # Notify characteristic
|
|
73
|
+
# Legacy single-char UUID for devices that use same char for both
|
|
74
|
+
DEFAULT_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
address: str,
|
|
79
|
+
service_uuid: str = None,
|
|
80
|
+
char_uuid: str = None,
|
|
81
|
+
write_char_uuid: str = None,
|
|
82
|
+
notify_char_uuid: str = None,
|
|
83
|
+
timeout: float = 2.0,
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Initialize BLE transport.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
address: BLE device address (MAC on Windows/Linux, UUID on macOS)
|
|
90
|
+
service_uuid: BLE service UUID (default: HM-10 compatible)
|
|
91
|
+
char_uuid: BLE characteristic UUID for read/write (legacy, single char)
|
|
92
|
+
write_char_uuid: BLE characteristic UUID for writing (ffe1)
|
|
93
|
+
notify_char_uuid: BLE characteristic UUID for notifications (ffe2)
|
|
94
|
+
timeout: Read timeout in seconds
|
|
95
|
+
"""
|
|
96
|
+
if not HAS_BLEAK:
|
|
97
|
+
raise ImportError("bleak is required for BLE. Install with: pip install bleak")
|
|
98
|
+
|
|
99
|
+
self.address = address
|
|
100
|
+
self.service_uuid = service_uuid or self.DEFAULT_SERVICE_UUID
|
|
101
|
+
|
|
102
|
+
# Support both legacy single-char and separate write/notify chars
|
|
103
|
+
if write_char_uuid or notify_char_uuid:
|
|
104
|
+
self.write_char_uuid = write_char_uuid or self.DEFAULT_WRITE_CHAR_UUID
|
|
105
|
+
self.notify_char_uuid = notify_char_uuid or self.DEFAULT_NOTIFY_CHAR_UUID
|
|
106
|
+
elif char_uuid:
|
|
107
|
+
# Legacy: same char for both
|
|
108
|
+
self.write_char_uuid = char_uuid
|
|
109
|
+
self.notify_char_uuid = char_uuid
|
|
110
|
+
else:
|
|
111
|
+
# Default: use separate chars (MechDog style)
|
|
112
|
+
self.write_char_uuid = self.DEFAULT_WRITE_CHAR_UUID
|
|
113
|
+
self.notify_char_uuid = self.DEFAULT_NOTIFY_CHAR_UUID
|
|
114
|
+
|
|
115
|
+
# Keep char_uuid for backwards compatibility
|
|
116
|
+
self.char_uuid = self.write_char_uuid
|
|
117
|
+
self.timeout = timeout
|
|
118
|
+
|
|
119
|
+
self._client: Optional[BleakClient] = None
|
|
120
|
+
self._connected = False
|
|
121
|
+
self._rx_buffer = queue.Queue()
|
|
122
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
123
|
+
self._thread: Optional[threading.Thread] = None
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
async def discover(
|
|
127
|
+
timeout: float = 10.0,
|
|
128
|
+
name_filter: str = None,
|
|
129
|
+
) -> List[BLEDeviceInfo]:
|
|
130
|
+
"""
|
|
131
|
+
Discover BLE devices.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
timeout: Scan duration in seconds
|
|
135
|
+
name_filter: Only return devices whose name contains this string
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of discovered BLE devices
|
|
139
|
+
"""
|
|
140
|
+
if not HAS_BLEAK:
|
|
141
|
+
raise ImportError("bleak is required for BLE. Install with: pip install bleak")
|
|
142
|
+
|
|
143
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
144
|
+
|
|
145
|
+
result = []
|
|
146
|
+
for d in devices:
|
|
147
|
+
if name_filter and (not d.name or name_filter.lower() not in d.name.lower()):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
info = BLEDeviceInfo(
|
|
151
|
+
address=d.address,
|
|
152
|
+
name=d.name or "Unknown",
|
|
153
|
+
rssi=d.rssi if hasattr(d, 'rssi') else -100,
|
|
154
|
+
services=[], # Would need to connect to enumerate
|
|
155
|
+
)
|
|
156
|
+
result.append(info)
|
|
157
|
+
|
|
158
|
+
# Sort by signal strength
|
|
159
|
+
result.sort(key=lambda x: x.rssi, reverse=True)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def discover_sync(timeout: float = 10.0, name_filter: str = None) -> List[BLEDeviceInfo]:
|
|
164
|
+
"""Synchronous wrapper for discover()."""
|
|
165
|
+
return asyncio.run(BLETransport.discover(timeout, name_filter))
|
|
166
|
+
|
|
167
|
+
def _notification_handler(self, sender: int, data: bytearray):
|
|
168
|
+
"""Handle incoming BLE notifications."""
|
|
169
|
+
self._rx_buffer.put(bytes(data))
|
|
170
|
+
|
|
171
|
+
async def _connect_async(self) -> bool:
|
|
172
|
+
"""Async connect implementation."""
|
|
173
|
+
try:
|
|
174
|
+
self._client = BleakClient(self.address)
|
|
175
|
+
await self._client.connect()
|
|
176
|
+
|
|
177
|
+
# Subscribe to notifications on the NOTIFY characteristic (ffe2)
|
|
178
|
+
await self._client.start_notify(self.notify_char_uuid, self._notification_handler)
|
|
179
|
+
|
|
180
|
+
self._connected = True
|
|
181
|
+
return True
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"BLE connect error: {e}")
|
|
184
|
+
self._connected = False
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
async def _disconnect_async(self):
|
|
188
|
+
"""Async disconnect implementation."""
|
|
189
|
+
if self._client and self._connected:
|
|
190
|
+
try:
|
|
191
|
+
await self._client.stop_notify(self.notify_char_uuid)
|
|
192
|
+
await self._client.disconnect()
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
self._connected = False
|
|
196
|
+
self._client = None
|
|
197
|
+
|
|
198
|
+
async def _write_async(self, data: bytes):
|
|
199
|
+
"""Async write implementation."""
|
|
200
|
+
if not self._connected or not self._client:
|
|
201
|
+
raise IOError("Not connected")
|
|
202
|
+
|
|
203
|
+
# Split into chunks if needed (BLE has MTU limits)
|
|
204
|
+
chunk_size = 20 # Safe default, could be negotiated higher
|
|
205
|
+
for i in range(0, len(data), chunk_size):
|
|
206
|
+
chunk = data[i:i + chunk_size]
|
|
207
|
+
# Write to the WRITE characteristic (ffe1), without response for speed
|
|
208
|
+
await self._client.write_gatt_char(self.write_char_uuid, chunk, response=False)
|
|
209
|
+
|
|
210
|
+
def _run_async_loop(self):
|
|
211
|
+
"""Run the async event loop in a background thread."""
|
|
212
|
+
self._loop = asyncio.new_event_loop()
|
|
213
|
+
asyncio.set_event_loop(self._loop)
|
|
214
|
+
self._loop.run_forever()
|
|
215
|
+
|
|
216
|
+
def _run_coroutine(self, coro) -> Any:
|
|
217
|
+
"""Run a coroutine from sync code."""
|
|
218
|
+
if self._loop is None:
|
|
219
|
+
# Start the async loop in a background thread
|
|
220
|
+
self._thread = threading.Thread(target=self._run_async_loop, daemon=True)
|
|
221
|
+
self._thread.start()
|
|
222
|
+
time.sleep(0.1) # Give the loop time to start
|
|
223
|
+
|
|
224
|
+
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
225
|
+
return future.result(timeout=self.timeout * 2)
|
|
226
|
+
|
|
227
|
+
def connect(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Connect to the BLE device.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if connected successfully
|
|
233
|
+
"""
|
|
234
|
+
return self._run_coroutine(self._connect_async())
|
|
235
|
+
|
|
236
|
+
def disconnect(self):
|
|
237
|
+
"""Disconnect from the BLE device."""
|
|
238
|
+
if self._loop:
|
|
239
|
+
self._run_coroutine(self._disconnect_async())
|
|
240
|
+
|
|
241
|
+
if self._loop:
|
|
242
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
243
|
+
if self._thread:
|
|
244
|
+
self._thread.join(timeout=1.0)
|
|
245
|
+
|
|
246
|
+
def write(self, data: bytes) -> int:
|
|
247
|
+
"""
|
|
248
|
+
Write data to the BLE device.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
data: Bytes to send
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Number of bytes written
|
|
255
|
+
"""
|
|
256
|
+
self._run_coroutine(self._write_async(data))
|
|
257
|
+
return len(data)
|
|
258
|
+
|
|
259
|
+
def read(self, size: int = 1) -> bytes:
|
|
260
|
+
"""
|
|
261
|
+
Read data from the BLE device.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
size: Maximum bytes to read
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Received bytes (may be less than size)
|
|
268
|
+
"""
|
|
269
|
+
result = b""
|
|
270
|
+
deadline = time.time() + self.timeout
|
|
271
|
+
|
|
272
|
+
while len(result) < size and time.time() < deadline:
|
|
273
|
+
try:
|
|
274
|
+
chunk = self._rx_buffer.get(timeout=0.1)
|
|
275
|
+
result += chunk
|
|
276
|
+
except queue.Empty:
|
|
277
|
+
if result: # Got some data, return it
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
return result[:size]
|
|
281
|
+
|
|
282
|
+
def read_until(self, terminator: bytes = b"\n") -> bytes:
|
|
283
|
+
"""
|
|
284
|
+
Read until terminator is found.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
terminator: Bytes to read until
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Data including terminator
|
|
291
|
+
"""
|
|
292
|
+
result = b""
|
|
293
|
+
deadline = time.time() + self.timeout
|
|
294
|
+
|
|
295
|
+
while time.time() < deadline:
|
|
296
|
+
try:
|
|
297
|
+
chunk = self._rx_buffer.get(timeout=0.1)
|
|
298
|
+
result += chunk
|
|
299
|
+
if terminator in result:
|
|
300
|
+
break
|
|
301
|
+
except queue.Empty:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
def readline(self) -> bytes:
|
|
307
|
+
"""Read a line (until newline)."""
|
|
308
|
+
return self.read_until(b"\n")
|
|
309
|
+
|
|
310
|
+
def flush(self):
|
|
311
|
+
"""Flush buffers (no-op for BLE)."""
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
def reset_input_buffer(self):
|
|
315
|
+
"""Clear the input buffer."""
|
|
316
|
+
while not self._rx_buffer.empty():
|
|
317
|
+
try:
|
|
318
|
+
self._rx_buffer.get_nowait()
|
|
319
|
+
except queue.Empty:
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
def reset_output_buffer(self):
|
|
323
|
+
"""Clear the output buffer (no-op for BLE)."""
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def is_open(self) -> bool:
|
|
328
|
+
"""Check if connection is open."""
|
|
329
|
+
return self._connected
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def in_waiting(self) -> int:
|
|
333
|
+
"""Number of bytes in input buffer."""
|
|
334
|
+
return self._rx_buffer.qsize() * 20 # Approximate
|
|
335
|
+
|
|
336
|
+
def close(self):
|
|
337
|
+
"""Close the connection."""
|
|
338
|
+
self.disconnect()
|
|
339
|
+
|
|
340
|
+
def __enter__(self):
|
|
341
|
+
self.connect()
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
def __exit__(self, *args):
|
|
345
|
+
self.close()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def discover_ble_robots(
|
|
349
|
+
timeout: float = 10.0,
|
|
350
|
+
name_patterns: List[str] = None,
|
|
351
|
+
) -> List[BLEDeviceInfo]:
|
|
352
|
+
"""
|
|
353
|
+
Discover BLE robots by scanning for known device names.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
timeout: Scan duration
|
|
357
|
+
name_patterns: Device name patterns to match (e.g., ["MechDog", "ESP32"])
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of discovered robot devices
|
|
361
|
+
"""
|
|
362
|
+
if not HAS_BLEAK:
|
|
363
|
+
print("Warning: bleak not installed. Run: pip install bleak")
|
|
364
|
+
return []
|
|
365
|
+
|
|
366
|
+
if name_patterns is None:
|
|
367
|
+
name_patterns = [
|
|
368
|
+
"MechDog",
|
|
369
|
+
"HiWonder",
|
|
370
|
+
"ESP32",
|
|
371
|
+
"ESP-",
|
|
372
|
+
"BT",
|
|
373
|
+
"HC-",
|
|
374
|
+
"HM-",
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
all_devices = BLETransport.discover_sync(timeout=timeout)
|
|
378
|
+
|
|
379
|
+
# Filter by name patterns
|
|
380
|
+
robots = []
|
|
381
|
+
for device in all_devices:
|
|
382
|
+
if not device.name:
|
|
383
|
+
continue
|
|
384
|
+
for pattern in name_patterns:
|
|
385
|
+
if pattern.lower() in device.name.lower():
|
|
386
|
+
robots.append(device)
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
return robots
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
# Test discovery
|
|
394
|
+
print("Scanning for BLE devices...")
|
|
395
|
+
devices = BLETransport.discover_sync(timeout=5.0)
|
|
396
|
+
|
|
397
|
+
print(f"\nFound {len(devices)} devices:")
|
|
398
|
+
for d in devices:
|
|
399
|
+
print(f" {d}")
|
|
400
|
+
|
|
401
|
+
# Filter for potential robots
|
|
402
|
+
print("\nPotential robots:")
|
|
403
|
+
robots = discover_ble_robots(timeout=5.0)
|
|
404
|
+
for r in robots:
|
|
405
|
+
print(f" {r}")
|