openmotion-sdk 1.5.5__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.
- omotion/CommInterface.py +370 -0
- omotion/CommandError.py +12 -0
- omotion/Console.py +2459 -0
- omotion/ConsoleTelemetry.py +369 -0
- omotion/DFUProgrammer.py +346 -0
- omotion/DualMotionComposite.py +198 -0
- omotion/FPGAProgrammer.py +567 -0
- omotion/GitHubReleases.py +158 -0
- omotion/Interface.py +441 -0
- omotion/MotionComposite.py +137 -0
- omotion/MotionConfig.py +180 -0
- omotion/MotionProcessing.py +1779 -0
- omotion/MotionSignal.py +45 -0
- omotion/MotionUart.py +467 -0
- omotion/ScanWorkflow.py +1166 -0
- omotion/Sensor.py +1063 -0
- omotion/StreamInterface.py +369 -0
- omotion/USBInterfaceBase.py +41 -0
- omotion/UartPacket.py +108 -0
- omotion/__init__.py +51 -0
- omotion/_vendor/libusb/windows/x64/libusb-1.0.dll +0 -0
- omotion/_vendor/libusb/windows/x86/libusb-1.0.dll +0 -0
- omotion/config.py +219 -0
- omotion/connection_state.py +9 -0
- omotion/dfu-util/AUTHORS +49 -0
- omotion/dfu-util/COPYING +340 -0
- omotion/dfu-util/ChangeLog +143 -0
- omotion/dfu-util/README +21 -0
- omotion/dfu-util/README-bin.txt +16 -0
- omotion/dfu-util/darwin-x86_64/dfu-prefix +0 -0
- omotion/dfu-util/darwin-x86_64/dfu-suffix +0 -0
- omotion/dfu-util/darwin-x86_64/dfu-util +0 -0
- omotion/dfu-util/darwin-x86_64/libusb-1.0.0.dylib +0 -0
- omotion/dfu-util/darwin-x86_64/libusb-1.0.a +0 -0
- omotion/dfu-util/darwin-x86_64/libusb-1.0.dylib +0 -0
- omotion/dfu-util/darwin-x86_64/libusb-1.0.la +41 -0
- omotion/dfu-util/darwin-x86_64/lsusb +0 -0
- omotion/dfu-util/darwin-x86_64/usbhid-dump +0 -0
- omotion/dfu-util/dfu-prefix.1.man.pdf +0 -0
- omotion/dfu-util/dfu-suffix.1.man.pdf +0 -0
- omotion/dfu-util/dfu-util.1.man.pdf +0 -0
- omotion/dfu-util/dfuse-pack.py +352 -0
- omotion/dfu-util/linux-amd64/dfu-prefix +0 -0
- omotion/dfu-util/linux-amd64/dfu-suffix +0 -0
- omotion/dfu-util/linux-amd64/dfu-util +0 -0
- omotion/dfu-util/linux-amd64/dfu-util-static +0 -0
- omotion/dfu-util/lsusb_build_on_mingw.patch +225 -0
- omotion/dfu-util/win32/dfu-prefix.exe +0 -0
- omotion/dfu-util/win32/dfu-suffix.exe +0 -0
- omotion/dfu-util/win32/dfu-util-static.exe +0 -0
- omotion/dfu-util/win32/dfu-util.exe +0 -0
- omotion/dfu-util/win32/libusb-1.0.a +0 -0
- omotion/dfu-util/win32/libusb-1.0.dll +0 -0
- omotion/dfu-util/win32/libusb-1.0.dll.a +0 -0
- omotion/dfu-util/win32/libusb-1.0.la +41 -0
- omotion/dfu-util/win32/lsusb-static.exe +0 -0
- omotion/dfu-util/win32/lsusb.exe +0 -0
- omotion/dfu-util/win64/dfu-prefix.exe +0 -0
- omotion/dfu-util/win64/dfu-suffix.exe +0 -0
- omotion/dfu-util/win64/dfu-util-static.exe +0 -0
- omotion/dfu-util/win64/dfu-util.exe +0 -0
- omotion/dfu-util/win64/libusb-1.0.a +0 -0
- omotion/dfu-util/win64/libusb-1.0.dll +0 -0
- omotion/dfu-util/win64/libusb-1.0.dll.a +0 -0
- omotion/dfu-util/win64/libusb-1.0.la +41 -0
- omotion/dfu-util/win64/lsusb-static.exe +0 -0
- omotion/dfu-util/win64/lsusb.exe +0 -0
- omotion/i2c_data_packet.py +134 -0
- omotion/i2c_packet.py +104 -0
- omotion/i2c_status_packet.py +82 -0
- omotion/jedecParser.py +313 -0
- omotion/signal_wrapper.py +40 -0
- omotion/usb_backend.py +51 -0
- omotion/utils.py +324 -0
- openmotion_sdk-1.5.5.dist-info/METADATA +51 -0
- openmotion_sdk-1.5.5.dist-info/RECORD +79 -0
- openmotion_sdk-1.5.5.dist-info/WHEEL +5 -0
- openmotion_sdk-1.5.5.dist-info/licenses/LICENSE +661 -0
- openmotion_sdk-1.5.5.dist-info/top_level.txt +1 -0
omotion/CommInterface.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import omotion.config as config
|
|
3
|
+
from omotion.UartPacket import UartPacket
|
|
4
|
+
from omotion.config import (
|
|
5
|
+
OW_ACK,
|
|
6
|
+
OW_CMD_NOP,
|
|
7
|
+
OW_END_BYTE,
|
|
8
|
+
OW_START_BYTE,
|
|
9
|
+
OW_DATA,
|
|
10
|
+
OW_CMD_ECHO,
|
|
11
|
+
)
|
|
12
|
+
import usb.core
|
|
13
|
+
import usb.util
|
|
14
|
+
import time
|
|
15
|
+
import threading
|
|
16
|
+
import queue
|
|
17
|
+
from omotion.USBInterfaceBase import USBInterfaceBase
|
|
18
|
+
from omotion import _log_root
|
|
19
|
+
|
|
20
|
+
# Max data_len we accept (sanity check to avoid runaway buffer)
|
|
21
|
+
OW_MAX_PACKET_DATA_LEN = 4096 * 2
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(
|
|
24
|
+
f"{_log_root}.CommInterface" if _log_root else "CommInterface"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Max data_len we accept (sanity check to avoid runaway buffer)
|
|
28
|
+
OW_MAX_PACKET_DATA_LEN = 4096 * 2
|
|
29
|
+
|
|
30
|
+
_PACKET_TYPE_NAMES = {
|
|
31
|
+
value: name
|
|
32
|
+
for name, value in vars(config).items()
|
|
33
|
+
if name.startswith("OW_") and name.isupper() and isinstance(value, int)
|
|
34
|
+
}
|
|
35
|
+
_CMD_NAMES = {
|
|
36
|
+
"OW_CMD": {
|
|
37
|
+
value: name
|
|
38
|
+
for name, value in vars(config).items()
|
|
39
|
+
if name.startswith("OW_CMD_")
|
|
40
|
+
},
|
|
41
|
+
"OW_CONTROLLER": {
|
|
42
|
+
value: name
|
|
43
|
+
for name, value in vars(config).items()
|
|
44
|
+
if name.startswith("OW_CTRL_")
|
|
45
|
+
},
|
|
46
|
+
"OW_FPGA": {
|
|
47
|
+
value: name
|
|
48
|
+
for name, value in vars(config).items()
|
|
49
|
+
if name.startswith("OW_FPGA_")
|
|
50
|
+
},
|
|
51
|
+
"OW_CAMERA": {
|
|
52
|
+
value: name
|
|
53
|
+
for name, value in vars(config).items()
|
|
54
|
+
if name.startswith("OW_CAMERA_")
|
|
55
|
+
},
|
|
56
|
+
"OW_IMU": {
|
|
57
|
+
value: name
|
|
58
|
+
for name, value in vars(config).items()
|
|
59
|
+
if name.startswith("OW_IMU_")
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_named(value: int, name_map: dict[int, str], width: int = 2) -> str:
|
|
65
|
+
name = name_map.get(value)
|
|
66
|
+
if name:
|
|
67
|
+
return f"{name}(0x{value:0{width}X})"
|
|
68
|
+
return f"0x{value:0{width}X}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =========================================
|
|
72
|
+
# Comm Interface (IN + OUT + threads)
|
|
73
|
+
# =========================================
|
|
74
|
+
class CommInterface(USBInterfaceBase):
|
|
75
|
+
def __init__(self, dev, interface_index, desc="Comm", async_mode=False):
|
|
76
|
+
super().__init__(dev, interface_index, desc)
|
|
77
|
+
self.read_thread = None
|
|
78
|
+
self.stop_event = threading.Event()
|
|
79
|
+
# Contiguous byte buffer: USB reader extends the end, packet parser chops from the front
|
|
80
|
+
self._read_buffer = bytearray()
|
|
81
|
+
self._buffer_lock = threading.Lock()
|
|
82
|
+
self._buffer_condition = threading.Condition(self._buffer_lock)
|
|
83
|
+
self.packet_count = 0
|
|
84
|
+
self.async_mode = async_mode
|
|
85
|
+
self.on_disconnect = None
|
|
86
|
+
self._disconnect_notified = False
|
|
87
|
+
self._io_lock = threading.RLock()
|
|
88
|
+
self._send_lock = threading.Lock()
|
|
89
|
+
if self.async_mode:
|
|
90
|
+
self.response_queue = queue.Queue()
|
|
91
|
+
self.response_thread = threading.Thread(
|
|
92
|
+
target=self._process_responses, daemon=True
|
|
93
|
+
)
|
|
94
|
+
self.response_thread.start()
|
|
95
|
+
|
|
96
|
+
def claim(self):
|
|
97
|
+
super().claim()
|
|
98
|
+
intf = self.dev.get_active_configuration()[(self.interface_index, 0)]
|
|
99
|
+
self.ep_out = usb.util.find_descriptor(
|
|
100
|
+
intf,
|
|
101
|
+
custom_match=lambda e: (
|
|
102
|
+
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
if not self.ep_out:
|
|
106
|
+
raise RuntimeError(f"{self.desc}: No OUT endpoint found")
|
|
107
|
+
|
|
108
|
+
def send_packet(
|
|
109
|
+
self,
|
|
110
|
+
id=None,
|
|
111
|
+
packetType=OW_ACK,
|
|
112
|
+
command=OW_CMD_NOP,
|
|
113
|
+
addr=0,
|
|
114
|
+
reserved=0,
|
|
115
|
+
data=None,
|
|
116
|
+
timeout=10.0,
|
|
117
|
+
max_retries=0,
|
|
118
|
+
) -> UartPacket:
|
|
119
|
+
with self._send_lock:
|
|
120
|
+
if id is None:
|
|
121
|
+
self.packet_count = (self.packet_count + 1) & 0xFFFF or 1
|
|
122
|
+
id = self.packet_count
|
|
123
|
+
|
|
124
|
+
if data:
|
|
125
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
126
|
+
raise ValueError("Data must be bytes or bytearray")
|
|
127
|
+
payload = data
|
|
128
|
+
else:
|
|
129
|
+
payload = b""
|
|
130
|
+
|
|
131
|
+
uart_packet = UartPacket(
|
|
132
|
+
id=id,
|
|
133
|
+
packet_type=packetType,
|
|
134
|
+
command=command,
|
|
135
|
+
addr=addr,
|
|
136
|
+
reserved=reserved,
|
|
137
|
+
data=payload,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
tx_bytes = uart_packet.to_bytes()
|
|
141
|
+
packet_type_name = _PACKET_TYPE_NAMES.get(packetType)
|
|
142
|
+
cmd_names = _CMD_NAMES.get(packet_type_name, {})
|
|
143
|
+
logger.debug(
|
|
144
|
+
f"{self.desc}: TX id=0x{id:04X} "
|
|
145
|
+
f"type={_format_named(packetType, _PACKET_TYPE_NAMES)} "
|
|
146
|
+
f"cmd={_format_named(command, cmd_names)} "
|
|
147
|
+
f"addr=0x{addr:02X} reserved=0x{reserved:02X} len={len(payload)} data={tx_bytes.hex()}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self.write(tx_bytes)
|
|
151
|
+
time.sleep(0.0005)
|
|
152
|
+
|
|
153
|
+
if not self.async_mode:
|
|
154
|
+
start = time.monotonic()
|
|
155
|
+
data = bytearray()
|
|
156
|
+
with self._io_lock:
|
|
157
|
+
while time.monotonic() - start < timeout:
|
|
158
|
+
try:
|
|
159
|
+
resp = self.receive()
|
|
160
|
+
time.sleep(0.005)
|
|
161
|
+
if resp:
|
|
162
|
+
data.extend(resp)
|
|
163
|
+
if data and data[-1] == OW_END_BYTE:
|
|
164
|
+
return UartPacket(buffer=data)
|
|
165
|
+
except usb.core.USBError:
|
|
166
|
+
continue
|
|
167
|
+
last_error = TimeoutError("No response")
|
|
168
|
+
else:
|
|
169
|
+
start_time = time.monotonic()
|
|
170
|
+
while time.monotonic() - start_time < timeout:
|
|
171
|
+
if self.response_queue.empty():
|
|
172
|
+
time.sleep(0.0005)
|
|
173
|
+
else:
|
|
174
|
+
time.sleep(0.001)
|
|
175
|
+
pkt = self.response_queue.get()
|
|
176
|
+
if pkt.id != id:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"%s: discarding stale response id=0x%04X (expected 0x%04X)",
|
|
179
|
+
self.desc, pkt.id, id,
|
|
180
|
+
)
|
|
181
|
+
continue
|
|
182
|
+
return pkt
|
|
183
|
+
raise TimeoutError(f"No response in async mode, packet id 0x{id:04X}")
|
|
184
|
+
|
|
185
|
+
def clear_buffer(self):
|
|
186
|
+
with self._buffer_lock:
|
|
187
|
+
self._read_buffer.clear()
|
|
188
|
+
|
|
189
|
+
def write(self, data, timeout=100, _retries=5):
|
|
190
|
+
with self._io_lock:
|
|
191
|
+
for attempt in range(1 + _retries):
|
|
192
|
+
try:
|
|
193
|
+
return self.dev.write(self.ep_out.bEndpointAddress, data, timeout=timeout)
|
|
194
|
+
except usb.core.USBError as e:
|
|
195
|
+
# Firmware back-pressure: the device's OUT FIFO is temporarily
|
|
196
|
+
# full. Back off briefly and retry so callers don't have to
|
|
197
|
+
# care about transient busy periods (e.g. after program_fpga).
|
|
198
|
+
if e.errno in (110, 10060): # ETIMEDOUT / WSAETIMEDOUT
|
|
199
|
+
if attempt < _retries:
|
|
200
|
+
delay = 0.05 * (attempt + 1) # 50 ms, 100 ms, 150 ms …
|
|
201
|
+
logger.warning(
|
|
202
|
+
"%s: write timeout (attempt %d/%d), retrying in %.0f ms",
|
|
203
|
+
self.desc, attempt + 1, 1 + _retries, delay * 1000,
|
|
204
|
+
)
|
|
205
|
+
time.sleep(delay)
|
|
206
|
+
continue
|
|
207
|
+
logger.error("%s: write timed out after %d attempts", self.desc, 1 + _retries)
|
|
208
|
+
raise
|
|
209
|
+
# A stalled endpoint (EPIPE / broken-pipe) can be recovered by
|
|
210
|
+
# issuing a CLEAR_HALT control transfer. Try once; if it works
|
|
211
|
+
# re-send the original data. Any other USB error is re-raised
|
|
212
|
+
# so callers and _read_loop disconnect logic see it normally.
|
|
213
|
+
if e.errno in (32, -9): # EPIPE on Linux; LIBUSB_ERROR_PIPE cross-platform
|
|
214
|
+
logger.warning("%s: OUT endpoint stalled, attempting clear_halt", self.desc)
|
|
215
|
+
try:
|
|
216
|
+
usb.util.clear_halt(self.dev, self.ep_out)
|
|
217
|
+
return self.dev.write(self.ep_out.bEndpointAddress, data, timeout=timeout)
|
|
218
|
+
except Exception as recovery_err:
|
|
219
|
+
logger.error("%s: clear_halt recovery failed: %s", self.desc, recovery_err)
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
def receive(self, length=512, timeout=100):
|
|
223
|
+
with self._io_lock:
|
|
224
|
+
data = self.dev.read(self.ep_in.bEndpointAddress, length, timeout=timeout)
|
|
225
|
+
logger.debug(f"Received {len(data)} bytes.")
|
|
226
|
+
return data
|
|
227
|
+
|
|
228
|
+
def start_read_thread(self):
|
|
229
|
+
if self.read_thread and self.read_thread.is_alive():
|
|
230
|
+
logger.info(f"{self.desc}: Read thread already running")
|
|
231
|
+
return
|
|
232
|
+
self.stop_event.clear()
|
|
233
|
+
self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
|
|
234
|
+
self.read_thread.start()
|
|
235
|
+
logger.info(f"{self.desc}: Read thread started")
|
|
236
|
+
|
|
237
|
+
def stop_read_thread(self):
|
|
238
|
+
self.stop_event.set()
|
|
239
|
+
# We're intentionally shutting down; prevent the read loop from
|
|
240
|
+
# triggering disconnect callbacks/logging if the device disappears.
|
|
241
|
+
self._disconnect_notified = True
|
|
242
|
+
if self.read_thread:
|
|
243
|
+
# Safety net: _trigger_disconnect now dispatches on_disconnect to a
|
|
244
|
+
# separate daemon thread, so this guard should never fire in normal
|
|
245
|
+
# operation. It stays here to prevent a hang if anyone calls
|
|
246
|
+
# stop_read_thread() from a context where the read thread is on the
|
|
247
|
+
# call stack (joining the current thread raises RuntimeError).
|
|
248
|
+
if threading.current_thread() is not self.read_thread:
|
|
249
|
+
self.read_thread.join(timeout=2.0)
|
|
250
|
+
logger.info(f"{self.desc}: Read thread stopped")
|
|
251
|
+
|
|
252
|
+
def _trigger_disconnect(self, error):
|
|
253
|
+
# During an intentional shutdown, ignore disconnect triggers.
|
|
254
|
+
if self.stop_event.is_set():
|
|
255
|
+
return
|
|
256
|
+
if self._disconnect_notified:
|
|
257
|
+
return
|
|
258
|
+
self._disconnect_notified = True
|
|
259
|
+
logger.error(f"{self.desc}: triggering disconnect due to USB error: {error}")
|
|
260
|
+
self.stop_event.set()
|
|
261
|
+
if callable(self.on_disconnect):
|
|
262
|
+
# Dispatch the disconnect callback to a new daemon thread rather than
|
|
263
|
+
# calling it directly from the read thread. on_disconnect typically
|
|
264
|
+
# calls MotionComposite.disconnect() → stop_read_thread() → join(),
|
|
265
|
+
# which would deadlock if run on the read thread itself. By handing
|
|
266
|
+
# off to a separate thread the read loop exits naturally (stop_event
|
|
267
|
+
# is already set) and the join in stop_read_thread() completes quickly.
|
|
268
|
+
t = threading.Thread(
|
|
269
|
+
target=self.on_disconnect,
|
|
270
|
+
args=(self.desc, error),
|
|
271
|
+
daemon=True,
|
|
272
|
+
name=f"{self.desc}-disconnect",
|
|
273
|
+
)
|
|
274
|
+
t.start()
|
|
275
|
+
|
|
276
|
+
def _read_loop(self):
|
|
277
|
+
while not self.stop_event.is_set():
|
|
278
|
+
try:
|
|
279
|
+
data = self.dev.read(
|
|
280
|
+
self.ep_in.bEndpointAddress, self.ep_in.wMaxPacketSize, timeout=100
|
|
281
|
+
)
|
|
282
|
+
if data:
|
|
283
|
+
data_bytes = bytes(data)
|
|
284
|
+
with self._buffer_condition:
|
|
285
|
+
self._read_buffer.extend(data_bytes)
|
|
286
|
+
self._buffer_condition.notify()
|
|
287
|
+
logger.debug(f"Read {len(data)} bytes.")
|
|
288
|
+
time.sleep(0.001)
|
|
289
|
+
except usb.core.USBError as e:
|
|
290
|
+
# If we're shutting down, USB errors here are expected and should not be noisy.
|
|
291
|
+
if self.stop_event.is_set():
|
|
292
|
+
break
|
|
293
|
+
if e.errno == 110:
|
|
294
|
+
pass
|
|
295
|
+
elif e.errno == 10060:
|
|
296
|
+
pass
|
|
297
|
+
elif e.errno == 32:
|
|
298
|
+
# Only log at ERROR when this is unexpected (not a clean shutdown).
|
|
299
|
+
if not self._disconnect_notified:
|
|
300
|
+
logger.error(f"{self.desc} read error: DISCONNECT{e}")
|
|
301
|
+
self._trigger_disconnect(e)
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
elif e.errno == 19 or e.errno == 5:
|
|
305
|
+
# errno 19 = ENODEV (device unplugged or GC'd during shutdown).
|
|
306
|
+
# errno 5 = EIO (device I/O error).
|
|
307
|
+
# Both are expected when the app is tearing down — only log at
|
|
308
|
+
# ERROR when the disconnect is genuinely unintentional.
|
|
309
|
+
if not self._disconnect_notified:
|
|
310
|
+
logger.error(f"{self.desc} read error: IO Error{e}")
|
|
311
|
+
self._trigger_disconnect(e)
|
|
312
|
+
break
|
|
313
|
+
else:
|
|
314
|
+
if not self._disconnect_notified:
|
|
315
|
+
logger.error(f"{self.desc} read error: Unknown Error{e}")
|
|
316
|
+
self._trigger_disconnect(e)
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
def _process_responses(self):
|
|
320
|
+
while not self.stop_event.is_set():
|
|
321
|
+
with self._buffer_condition:
|
|
322
|
+
if not self._read_buffer:
|
|
323
|
+
self._buffer_condition.wait(timeout=0.1)
|
|
324
|
+
continue
|
|
325
|
+
buf = self._read_buffer
|
|
326
|
+
# Align to start of packet: discard leading bytes until OW_START_BYTE
|
|
327
|
+
if buf[0] != OW_START_BYTE:
|
|
328
|
+
try:
|
|
329
|
+
start_idx = buf.index(OW_START_BYTE)
|
|
330
|
+
except ValueError:
|
|
331
|
+
start_idx = len(buf)
|
|
332
|
+
del self._read_buffer[:start_idx]
|
|
333
|
+
if start_idx == len(buf):
|
|
334
|
+
continue
|
|
335
|
+
buf = self._read_buffer
|
|
336
|
+
# Need at least 9 bytes to read data_len (bytes 7:9)
|
|
337
|
+
if len(buf) < 9:
|
|
338
|
+
continue
|
|
339
|
+
data_len = int.from_bytes(buf[7:9], "big")
|
|
340
|
+
if data_len > OW_MAX_PACKET_DATA_LEN:
|
|
341
|
+
del self._read_buffer[:1]
|
|
342
|
+
continue
|
|
343
|
+
packet_len = 12 + data_len # header(11) + data + crc(2) + end(1)
|
|
344
|
+
if len(buf) < packet_len:
|
|
345
|
+
continue
|
|
346
|
+
if buf[packet_len - 1] != OW_END_BYTE:
|
|
347
|
+
del self._read_buffer[:1]
|
|
348
|
+
continue
|
|
349
|
+
packet_bytes = bytes(buf[:packet_len])
|
|
350
|
+
del self._read_buffer[:packet_len]
|
|
351
|
+
try:
|
|
352
|
+
uart_packet = UartPacket(buffer=packet_bytes)
|
|
353
|
+
except ValueError:
|
|
354
|
+
continue
|
|
355
|
+
if (
|
|
356
|
+
uart_packet.id == 0
|
|
357
|
+
and uart_packet.packet_type == OW_DATA
|
|
358
|
+
and uart_packet.command == OW_CMD_ECHO
|
|
359
|
+
):
|
|
360
|
+
_raw = bytes(uart_packet.data[:uart_packet.data_len]) if uart_packet.data_len > 0 else b""
|
|
361
|
+
try:
|
|
362
|
+
_text = _raw.decode("utf-8", errors="replace").rstrip("\x00").strip()
|
|
363
|
+
except Exception:
|
|
364
|
+
_text = ""
|
|
365
|
+
if _text:
|
|
366
|
+
logger.warning("[%s PRINTF] %s", self.desc, _text)
|
|
367
|
+
else:
|
|
368
|
+
logger.warning("[%s] MCU echo: data=%s", self.desc, _raw.hex() if _raw else "")
|
|
369
|
+
else:
|
|
370
|
+
self.response_queue.put(uart_packet)
|
omotion/CommandError.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CommandError(RuntimeError):
|
|
5
|
+
"""
|
|
6
|
+
Raised when the hardware returns a non-OK response (NAK, BAD_CRC, etc.)
|
|
7
|
+
or when the response payload cannot be decoded.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, response: Optional[Any] = None) -> None:
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.response = response
|