Barmaja-PyArduino 2.0.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.
- Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
- Barmaja_PyArduino/__init__.py +528 -0
- Barmaja_PyArduino/__init__.pyi +331 -0
- Barmaja_PyArduino/__main__.py +2 -0
- Barmaja_PyArduino/adapters/__init__.py +35 -0
- Barmaja_PyArduino/api_registry.py +459 -0
- Barmaja_PyArduino/backend/__init__.py +3 -0
- Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
- Barmaja_PyArduino/board.py +80 -0
- Barmaja_PyArduino/cli.py +223 -0
- Barmaja_PyArduino/compiler.py +1721 -0
- Barmaja_PyArduino/console.py +217 -0
- Barmaja_PyArduino/device_packs.py +367 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
- Barmaja_PyArduino/doctor.py +406 -0
- Barmaja_PyArduino/environment.py +595 -0
- Barmaja_PyArduino/errors.py +166 -0
- Barmaja_PyArduino/flow.py +754 -0
- Barmaja_PyArduino/frontend.py +611 -0
- Barmaja_PyArduino/installer.py +151 -0
- Barmaja_PyArduino/ir.py +660 -0
- Barmaja_PyArduino/library_adder.py +440 -0
- Barmaja_PyArduino/live.py +859 -0
- Barmaja_PyArduino/lowering.py +596 -0
- Barmaja_PyArduino/numeric.py +748 -0
- Barmaja_PyArduino/pack_builder.py +544 -0
- Barmaja_PyArduino/py.typed +0 -0
- Barmaja_PyArduino/serial_monitor.py +172 -0
- Barmaja_PyArduino/source.py +184 -0
- Barmaja_PyArduino/stubgen.py +148 -0
- Barmaja_PyArduino/symbols.py +646 -0
- Barmaja_PyArduino/target.py +1631 -0
- Barmaja_PyArduino/toolchain.py +506 -0
- Barmaja_PyArduino/typesys.py +917 -0
- Barmaja_PyArduino/workflow.py +213 -0
- barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
- barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
- barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
- barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
- barmaja_pyarduino-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"""Barmaja PyArduino LiveRun support.
|
|
2
|
+
|
|
3
|
+
LiveRun keeps the student's Arduino-style API but executes the Python program on
|
|
4
|
+
its host (normal Python or PictoBlox) while Telemetrix4Arduino runs on the Uno.
|
|
5
|
+
|
|
6
|
+
The public entry point is ``LiveRun()``. The rest of this module is deliberately
|
|
7
|
+
internal so the student surface remains small and consistent with standalone
|
|
8
|
+
Barmaja PyArduino programs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import atexit
|
|
14
|
+
import contextlib
|
|
15
|
+
import io
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LiveRunError(RuntimeError):
|
|
25
|
+
"""Raised when Barmaja PyArduino cannot start or use LiveRun."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LiveStudentError(LiveRunError):
|
|
29
|
+
"""Actionable LiveRun setup error; preserve the original cause for debugging."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LiveDependencyError(LiveRunError):
|
|
33
|
+
"""Required LiveRun Python support is not installed."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LiveConnectionError(LiveRunError):
|
|
37
|
+
"""The Uno did not answer as a Telemetrix LiveRun board."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LivePortBusyError(LiveConnectionError):
|
|
41
|
+
"""Another process has the serial port open; do not attempt to reflash."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LivePortMissingError(LiveConnectionError):
|
|
45
|
+
"""The selected COM port disappeared or was not present."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
EXPECTED_FIRMWARE = "5.4.4"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _port_is_busy(exc: BaseException) -> bool:
|
|
52
|
+
"""Identify a locked COM port in the underlying pyserial/Windows error.
|
|
53
|
+
|
|
54
|
+
A failed firmware identity check is expected after a standalone upload.
|
|
55
|
+
Access denied, on the other hand, is *not* evidence that the firmware is
|
|
56
|
+
wrong; trying avrdude in this case just gives a second misleading error.
|
|
57
|
+
"""
|
|
58
|
+
seen: set[int] = set()
|
|
59
|
+
current: Optional[BaseException] = exc
|
|
60
|
+
while current is not None and id(current) not in seen:
|
|
61
|
+
seen.add(id(current))
|
|
62
|
+
message = str(current).lower()
|
|
63
|
+
if any(word in message for word in (
|
|
64
|
+
"access is denied", "permissionerror(13", "port is busy",
|
|
65
|
+
"port busy", "resource busy", "device or resource busy",
|
|
66
|
+
"permission denied",
|
|
67
|
+
)):
|
|
68
|
+
return True
|
|
69
|
+
if isinstance(current, PermissionError):
|
|
70
|
+
return True
|
|
71
|
+
if isinstance(current, OSError) and getattr(current, "winerror", None) in (5, 32):
|
|
72
|
+
return True
|
|
73
|
+
current = current.__cause__ or current.__context__
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _port_is_missing(exc: BaseException) -> bool:
|
|
78
|
+
seen: set[int] = set()
|
|
79
|
+
current: Optional[BaseException] = exc
|
|
80
|
+
while current is not None and id(current) not in seen:
|
|
81
|
+
seen.add(id(current))
|
|
82
|
+
message = str(current).lower()
|
|
83
|
+
if (isinstance(current, FileNotFoundError) or
|
|
84
|
+
isinstance(current, OSError) and getattr(current, "winerror", None) in (2, 3) or
|
|
85
|
+
"file not found" in message or
|
|
86
|
+
"the system cannot find the file specified" in message):
|
|
87
|
+
return True
|
|
88
|
+
current = current.__cause__ or current.__context__
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _patch_pinned_telemetrix_identity_wait() -> None:
|
|
93
|
+
"""Give Telemetrix 1.43 enough time to dispatch the Uno identity reply.
|
|
94
|
+
|
|
95
|
+
The pinned client waits only 0.5 seconds after sending ARE_U_THERE. On
|
|
96
|
+
Windows, its background receiver/reporter threads can dispatch the valid
|
|
97
|
+
three-byte response just after that deadline. Keep the pinned protocol and
|
|
98
|
+
extend only this internal wait; future client versions are left untouched.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
from telemetrix import telemetrix as client
|
|
102
|
+
from telemetrix.private_constants import PrivateConstants
|
|
103
|
+
except Exception:
|
|
104
|
+
return
|
|
105
|
+
cls = client.Telemetrix
|
|
106
|
+
if getattr(cls, "_barmaja_identity_wait_patch", False):
|
|
107
|
+
return
|
|
108
|
+
if str(getattr(PrivateConstants, "TELEMETRIX_VERSION", "")) != "1.43":
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
def _get_arduino_id(self) -> None:
|
|
112
|
+
self._send_command([PrivateConstants.ARE_U_THERE])
|
|
113
|
+
deadline = time.monotonic() + 3.0
|
|
114
|
+
while self.reported_arduino_id is None and time.monotonic() < deadline:
|
|
115
|
+
time.sleep(0.05)
|
|
116
|
+
|
|
117
|
+
cls._get_arduino_id = _get_arduino_id
|
|
118
|
+
cls._barmaja_identity_wait_patch = True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _port_was_lost(exc: BaseException) -> bool:
|
|
122
|
+
"""True for errors that mean the USB serial link is closed or gone.
|
|
123
|
+
|
|
124
|
+
Covers pyserial ``PortNotOpenError`` ("Attempting to use a port that is not
|
|
125
|
+
open"), write/read failures after unplugging, and Windows ClearCommError.
|
|
126
|
+
"""
|
|
127
|
+
seen: set[int] = set()
|
|
128
|
+
current: Optional[BaseException] = exc
|
|
129
|
+
while current is not None and id(current) not in seen:
|
|
130
|
+
seen.add(id(current))
|
|
131
|
+
name = type(current).__name__.lower()
|
|
132
|
+
message = str(current).lower()
|
|
133
|
+
if ("portnotopen" in name or "not open" in message or "port is not open" in message
|
|
134
|
+
or "write fail in _send_command" in message or "shutdown failed" in message
|
|
135
|
+
or "clearcommerror" in message or "device not configured" in message
|
|
136
|
+
or "input/output error" in message or "device disconnected" in message
|
|
137
|
+
or "device attached to the system is not functioning" in message
|
|
138
|
+
or "writefile failed" in message or "readfile failed" in message
|
|
139
|
+
or "handle is invalid" in message):
|
|
140
|
+
return True
|
|
141
|
+
if _port_is_missing(current):
|
|
142
|
+
return True
|
|
143
|
+
current = current.__cause__ or current.__context__
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _GuardedSync:
|
|
148
|
+
"""Wrap Telemetrix calls so a lost USB port is reported ONCE and cleanly.
|
|
149
|
+
|
|
150
|
+
Without this the upstream client (``shutdown_on_exception=True``) keeps
|
|
151
|
+
calling into a closed serial handle and a student sees a long chain of
|
|
152
|
+
repeated ``PortNotOpenError`` tracebacks. Any error that proves the port
|
|
153
|
+
is gone stops LiveRun, releases everything and raises one short message.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, target: Any, owner: "_TelemetrixBackend") -> None:
|
|
157
|
+
object.__setattr__(self, "_target", target)
|
|
158
|
+
object.__setattr__(self, "_owner", owner)
|
|
159
|
+
|
|
160
|
+
def __getattr__(self, name: str) -> Any:
|
|
161
|
+
value = getattr(self._target, name)
|
|
162
|
+
if name in ("_context", "board"):
|
|
163
|
+
return _GuardedSync(value, self._owner)
|
|
164
|
+
if callable(value):
|
|
165
|
+
owner = self._owner
|
|
166
|
+
|
|
167
|
+
def guarded(*args: Any, **kwargs: Any) -> Any:
|
|
168
|
+
try:
|
|
169
|
+
return value(*args, **kwargs)
|
|
170
|
+
except LiveRunError:
|
|
171
|
+
raise
|
|
172
|
+
except BaseException as exc:
|
|
173
|
+
if isinstance(exc, (KeyboardInterrupt, SystemExit, GeneratorExit)):
|
|
174
|
+
raise
|
|
175
|
+
if owner._closed or _port_was_lost(exc):
|
|
176
|
+
owner._port_lost(exc)
|
|
177
|
+
raise
|
|
178
|
+
return guarded
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class LiveSession:
|
|
184
|
+
"""Small status object returned by ``LiveRun()``.
|
|
185
|
+
|
|
186
|
+
Students do not need to store this object; it is useful for diagnostics and
|
|
187
|
+
advanced code that wants to display the selected COM port or firmware.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
port: str
|
|
191
|
+
firmware_version: str
|
|
192
|
+
|
|
193
|
+
def shutdown(self) -> None:
|
|
194
|
+
LiveStop()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class _TelemetrixBackend:
|
|
198
|
+
def __init__(self, port: str) -> None:
|
|
199
|
+
try:
|
|
200
|
+
import telemetrix_Barmaja as tb
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
raise LiveDependencyError(
|
|
203
|
+
"LiveRun needs the Barmaja Telemetrix support package. "
|
|
204
|
+
"Install the Barmaja PyArduino LiveRun/Offline pack and run again."
|
|
205
|
+
) from exc
|
|
206
|
+
|
|
207
|
+
_patch_pinned_telemetrix_identity_wait()
|
|
208
|
+
self.tb = tb
|
|
209
|
+
capture = io.StringIO()
|
|
210
|
+
try:
|
|
211
|
+
# The upstream client prints a long banner and connection transcript.
|
|
212
|
+
# Keep the beginner-facing console clean; Barmaja prints a short status
|
|
213
|
+
# after a successful connection instead.
|
|
214
|
+
with contextlib.redirect_stdout(capture), contextlib.redirect_stderr(capture):
|
|
215
|
+
self.sync = tb.TelemetrixSync(
|
|
216
|
+
com_port=port,
|
|
217
|
+
arduino_wait=float(os.environ.get("BARMAJA_LIVE_ARDUINO_WAIT", "3.0")),
|
|
218
|
+
shutdown_on_exception=True,
|
|
219
|
+
)
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
if _port_is_busy(exc):
|
|
222
|
+
raise LivePortBusyError(
|
|
223
|
+
f"Arduino Uno found on {port}, but its port is busy. "
|
|
224
|
+
"Close other programs using this Arduino and try again."
|
|
225
|
+
) from exc
|
|
226
|
+
if _port_is_missing(exc):
|
|
227
|
+
raise LivePortMissingError(
|
|
228
|
+
f"Arduino Uno is no longer available on {port}. "
|
|
229
|
+
"Reconnect USB and try again."
|
|
230
|
+
) from exc
|
|
231
|
+
# The long upstream transcript belongs only in debugging tools;
|
|
232
|
+
# it is not helpful to students when a standalone sketch is running.
|
|
233
|
+
raise LiveConnectionError(
|
|
234
|
+
"Arduino is not running LiveRun firmware yet."
|
|
235
|
+
) from exc
|
|
236
|
+
|
|
237
|
+
self.port = port
|
|
238
|
+
try:
|
|
239
|
+
self._raw_board = self.sync._context.board
|
|
240
|
+
except Exception:
|
|
241
|
+
self._raw_board = None
|
|
242
|
+
# Telemetrix 1.43 with shutdown_on_exception=True recurses on a dead port:
|
|
243
|
+
# _send_command -> shutdown() -> _send_command ... Once connected, let
|
|
244
|
+
# BARMAJA own cleanup instead (see _release_port / _port_lost).
|
|
245
|
+
if self._raw_board is not None:
|
|
246
|
+
try:
|
|
247
|
+
self._raw_board.shutdown_on_exception = False
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
self._link_broken = False
|
|
251
|
+
self.sync = _GuardedSync(self.sync, self)
|
|
252
|
+
_install_thread_hook()
|
|
253
|
+
self._wire_address: Optional[int] = None
|
|
254
|
+
self._wire_tx: list[int] = []
|
|
255
|
+
self._wire_rx: list[int] = []
|
|
256
|
+
self._dht_started: set[tuple[int, int]] = set()
|
|
257
|
+
self._sonars: set[tuple[int, int]] = set()
|
|
258
|
+
self._closed = False
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def firmware_version(self) -> str:
|
|
262
|
+
try:
|
|
263
|
+
values = self.sync._context.board.firmware_version
|
|
264
|
+
if values and len(values) >= 3:
|
|
265
|
+
return ".".join(str(int(v)) for v in values[:3])
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
return "unknown"
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _analog_channel(pin: int) -> int:
|
|
272
|
+
"""Translate Barmaja's Arduino aliases A0..A5 (14..19) for Telemetrix."""
|
|
273
|
+
pin = int(pin)
|
|
274
|
+
if 14 <= pin <= 19:
|
|
275
|
+
return pin - 14
|
|
276
|
+
if 0 <= pin <= 5:
|
|
277
|
+
return pin
|
|
278
|
+
raise ValueError("analogRead() on Arduino Uno expects A0..A5.")
|
|
279
|
+
|
|
280
|
+
def _link_ok(self) -> bool:
|
|
281
|
+
"""Cheap health check: port open and the Telemetrix receiver thread alive.
|
|
282
|
+
|
|
283
|
+
Without this, cached readings (e.g. analogRead) silently repeat the last
|
|
284
|
+
value forever after the USB cable is pulled.
|
|
285
|
+
"""
|
|
286
|
+
if getattr(self, "_link_broken", False):
|
|
287
|
+
return False
|
|
288
|
+
board = getattr(self, "_raw_board", None)
|
|
289
|
+
if board is None:
|
|
290
|
+
return True
|
|
291
|
+
try:
|
|
292
|
+
port = getattr(board, "serial_port", None)
|
|
293
|
+
if port is not None and not getattr(port, "is_open", True):
|
|
294
|
+
return False
|
|
295
|
+
receiver = getattr(board, "the_data_receive_thread", None)
|
|
296
|
+
if receiver is not None and receiver.ident is not None and not receiver.is_alive():
|
|
297
|
+
return False
|
|
298
|
+
# Active probe (rate-limited). Telemetrix 1.43's receiver thread
|
|
299
|
+
# swallows OSError -- and pyserial's SerialException IS an OSError --
|
|
300
|
+
# so after a USB unplug it spins forever and the cached readings go
|
|
301
|
+
# stale silently. On Windows ``in_waiting`` raises
|
|
302
|
+
# "ClearCommError failed" for a removed device.
|
|
303
|
+
if port is not None:
|
|
304
|
+
now = time.monotonic()
|
|
305
|
+
if now - getattr(self, "_last_probe", 0.0) >= 0.1:
|
|
306
|
+
self._last_probe = now
|
|
307
|
+
try:
|
|
308
|
+
port.in_waiting
|
|
309
|
+
except Exception as exc:
|
|
310
|
+
if _port_was_lost(exc) or isinstance(exc, OSError):
|
|
311
|
+
return False
|
|
312
|
+
except Exception:
|
|
313
|
+
return False
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
def _check(self) -> None:
|
|
317
|
+
if self._closed:
|
|
318
|
+
raise LiveRunError("LiveRun has already been stopped. Call LiveRun() again.")
|
|
319
|
+
if not self._link_ok():
|
|
320
|
+
self._port_lost(LivePortMissingError(f"USB link lost on {self.port}"))
|
|
321
|
+
|
|
322
|
+
def _release_port(self) -> None:
|
|
323
|
+
"""Stop Telemetrix threads and ALWAYS close the COM handle, quietly.
|
|
324
|
+
|
|
325
|
+
Upstream shutdown() first sends STOP_ALL_REPORTS; on a dead port that
|
|
326
|
+
raises before serial_port.close(), leaving the COM port held open inside
|
|
327
|
+
IDLE/PictoBlox so the next LiveRun() reports "port busy".
|
|
328
|
+
"""
|
|
329
|
+
quiet = io.StringIO()
|
|
330
|
+
board = getattr(self, "_raw_board", None)
|
|
331
|
+
with contextlib.redirect_stdout(quiet), contextlib.redirect_stderr(quiet):
|
|
332
|
+
if board is None or self._link_ok():
|
|
333
|
+
try:
|
|
334
|
+
self.sync._target.shutdown()
|
|
335
|
+
except BaseException as exc:
|
|
336
|
+
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
|
|
337
|
+
raise
|
|
338
|
+
if board is not None:
|
|
339
|
+
try:
|
|
340
|
+
board.shutdown_flag = True
|
|
341
|
+
board._stop_threads()
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
port = getattr(board, "serial_port", None)
|
|
345
|
+
if port is not None:
|
|
346
|
+
try:
|
|
347
|
+
port.close()
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
try:
|
|
351
|
+
self.sync._target._context._closed = True
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
def _port_lost(self, exc: BaseException) -> None:
|
|
356
|
+
"""Stop LiveRun after USB loss: release once, raise one actionable error."""
|
|
357
|
+
global _active_backend, _active_session
|
|
358
|
+
already = self._closed
|
|
359
|
+
self._closed = True
|
|
360
|
+
if _active_backend is self:
|
|
361
|
+
_active_backend = None
|
|
362
|
+
_active_session = None
|
|
363
|
+
if not already:
|
|
364
|
+
self._link_broken = True
|
|
365
|
+
try:
|
|
366
|
+
self._release_port()
|
|
367
|
+
except BaseException:
|
|
368
|
+
pass
|
|
369
|
+
_enable_student_error_display()
|
|
370
|
+
error = LiveStudentError(
|
|
371
|
+
f"Arduino connection lost on {self.port}.\n"
|
|
372
|
+
"Reconnect the USB cable, close other programs using the Arduino, "
|
|
373
|
+
"and run the program again."
|
|
374
|
+
)
|
|
375
|
+
error._barmaja_student_message = True
|
|
376
|
+
raise error from None
|
|
377
|
+
|
|
378
|
+
# ---------- core Arduino-style API ----------
|
|
379
|
+
def pin_mode(self, pin: int, mode: int) -> None:
|
|
380
|
+
self._check()
|
|
381
|
+
self.sync.pinMode(int(pin), int(mode))
|
|
382
|
+
|
|
383
|
+
def digital_write(self, pin: int, value: int) -> None:
|
|
384
|
+
self._check()
|
|
385
|
+
self.sync.digitalWrite(int(pin), int(value))
|
|
386
|
+
|
|
387
|
+
def digital_read(self, pin: int) -> int:
|
|
388
|
+
self._check()
|
|
389
|
+
return int(self.sync.digitalRead(int(pin)))
|
|
390
|
+
|
|
391
|
+
def analog_read(self, pin: int) -> int:
|
|
392
|
+
self._check()
|
|
393
|
+
return int(self.sync.analogRead(self._analog_channel(pin)))
|
|
394
|
+
|
|
395
|
+
def analog_write(self, pin: int, value: int) -> None:
|
|
396
|
+
self._check()
|
|
397
|
+
self.sync.analogWrite(int(pin), int(value))
|
|
398
|
+
|
|
399
|
+
def delay(self, milliseconds: int) -> None:
|
|
400
|
+
self._check()
|
|
401
|
+
self.sync.delay(int(milliseconds))
|
|
402
|
+
|
|
403
|
+
def millis(self) -> int:
|
|
404
|
+
self._check()
|
|
405
|
+
return int(self.sync.millis())
|
|
406
|
+
|
|
407
|
+
# ---------- servo ----------
|
|
408
|
+
def servo_attach(self, pin: int, min_us: Optional[int] = None, max_us: Optional[int] = None) -> None:
|
|
409
|
+
self._check()
|
|
410
|
+
ctx = self.sync._context
|
|
411
|
+
pin = int(pin)
|
|
412
|
+
if ctx._set_servo is None:
|
|
413
|
+
raise LiveRunError("Servo is not supported by this Telemetrix firmware.")
|
|
414
|
+
if min_us is None:
|
|
415
|
+
try:
|
|
416
|
+
ctx._set_servo(pin)
|
|
417
|
+
except TypeError:
|
|
418
|
+
ctx._set_servo(pin, 544, 2400)
|
|
419
|
+
else:
|
|
420
|
+
try:
|
|
421
|
+
ctx._set_servo(pin, int(min_us), int(max_us))
|
|
422
|
+
except TypeError:
|
|
423
|
+
ctx._set_servo(pin)
|
|
424
|
+
ctx._servos.add(pin)
|
|
425
|
+
|
|
426
|
+
def servo_write(self, pin: int, angle: int) -> None:
|
|
427
|
+
self._check()
|
|
428
|
+
self.sync.servoWrite(int(pin), int(angle))
|
|
429
|
+
|
|
430
|
+
def servo_write_microseconds(self, pin: int, microseconds: int) -> None:
|
|
431
|
+
self._check()
|
|
432
|
+
pin = int(pin)
|
|
433
|
+
us = int(microseconds)
|
|
434
|
+
board = self.sync._context.board
|
|
435
|
+
method = getattr(board, "servo_write_microseconds", None)
|
|
436
|
+
if method is None:
|
|
437
|
+
# Official Telemetrix exposes degrees, not Servo.writeMicroseconds().
|
|
438
|
+
# Convert the standard 544..2400 us Arduino range to 0..180 degrees.
|
|
439
|
+
us = max(544, min(2400, us))
|
|
440
|
+
angle = round((us - 544) * 180 / (2400 - 544))
|
|
441
|
+
self.sync.servoWrite(pin, angle)
|
|
442
|
+
else:
|
|
443
|
+
method(pin, us)
|
|
444
|
+
|
|
445
|
+
def servo_detach(self, pin: int) -> None:
|
|
446
|
+
self._check()
|
|
447
|
+
pin = int(pin)
|
|
448
|
+
ctx = self.sync._context
|
|
449
|
+
method = getattr(ctx.board, "servo_detach", None)
|
|
450
|
+
if method is None:
|
|
451
|
+
raise LiveRunError("Servo detach is not supported by this Telemetrix build.")
|
|
452
|
+
method(pin)
|
|
453
|
+
ctx._servos.discard(pin)
|
|
454
|
+
|
|
455
|
+
def servo_attached(self, pin: int) -> bool:
|
|
456
|
+
self._check()
|
|
457
|
+
return int(pin) in self.sync._context._servos
|
|
458
|
+
|
|
459
|
+
# ---------- HC-SR04 ----------
|
|
460
|
+
def ultrasonic_begin(self, trigger_pin: int, echo_pin: int) -> None:
|
|
461
|
+
self._check()
|
|
462
|
+
key = (int(trigger_pin), int(echo_pin))
|
|
463
|
+
if key not in self._sonars:
|
|
464
|
+
self.sync.sonarBegin(*key)
|
|
465
|
+
self._sonars.add(key)
|
|
466
|
+
|
|
467
|
+
def ultrasonic_read(self, trigger_pin: int, echo_pin: int) -> float:
|
|
468
|
+
self.ultrasonic_begin(trigger_pin, echo_pin)
|
|
469
|
+
return float(self.sync.sonarReadCM(int(trigger_pin), int(echo_pin)))
|
|
470
|
+
|
|
471
|
+
# ---------- DHT ----------
|
|
472
|
+
def dht_begin(self, pin: int, sensor_type: int) -> None:
|
|
473
|
+
self._check()
|
|
474
|
+
key = (int(pin), int(sensor_type))
|
|
475
|
+
if key not in self._dht_started:
|
|
476
|
+
self.sync.dhtBegin(*key)
|
|
477
|
+
self._dht_started.add(key)
|
|
478
|
+
|
|
479
|
+
def dht_last(self, pin: int, sensor_type: int) -> tuple[Optional[float], Optional[float]]:
|
|
480
|
+
self.dht_begin(pin, sensor_type)
|
|
481
|
+
temperature, humidity = self.sync.dhtLast(int(pin))
|
|
482
|
+
return temperature, humidity
|
|
483
|
+
|
|
484
|
+
# ---------- I2C / Wire ----------
|
|
485
|
+
def wire_begin(self) -> None:
|
|
486
|
+
self._check()
|
|
487
|
+
self.sync.i2cBegin(0)
|
|
488
|
+
|
|
489
|
+
def wire_begin_transmission(self, address: int) -> None:
|
|
490
|
+
self.wire_begin()
|
|
491
|
+
self._wire_address = int(address)
|
|
492
|
+
self._wire_tx = []
|
|
493
|
+
|
|
494
|
+
def wire_write(self, value: int) -> int:
|
|
495
|
+
if self._wire_address is None:
|
|
496
|
+
raise LiveRunError("Call Wire.beginTransmission(address) before Wire.write().")
|
|
497
|
+
self._wire_tx.append(int(value) & 0xFF)
|
|
498
|
+
return 1
|
|
499
|
+
|
|
500
|
+
def wire_end_transmission(self) -> int:
|
|
501
|
+
if self._wire_address is None:
|
|
502
|
+
raise LiveRunError("Call Wire.beginTransmission(address) before Wire.endTransmission().")
|
|
503
|
+
self.sync.i2cWrite(self._wire_address, self._wire_tx, i2c_port=0)
|
|
504
|
+
self._wire_address = None
|
|
505
|
+
self._wire_tx = []
|
|
506
|
+
return 0
|
|
507
|
+
|
|
508
|
+
def wire_request_from(self, address: int, count: int) -> int:
|
|
509
|
+
self.wire_begin()
|
|
510
|
+
data = self.sync.i2cRead(
|
|
511
|
+
int(address), None, int(count), i2c_port=0, write_register=False
|
|
512
|
+
)
|
|
513
|
+
self._wire_rx = list(data)
|
|
514
|
+
return len(self._wire_rx)
|
|
515
|
+
|
|
516
|
+
def wire_available(self) -> int:
|
|
517
|
+
return len(self._wire_rx)
|
|
518
|
+
|
|
519
|
+
def wire_read(self) -> int:
|
|
520
|
+
if not self._wire_rx:
|
|
521
|
+
return -1
|
|
522
|
+
return int(self._wire_rx.pop(0))
|
|
523
|
+
|
|
524
|
+
# ---------- LCD ----------
|
|
525
|
+
def lcd_create(self, address: int, columns: int, rows: int):
|
|
526
|
+
self._check()
|
|
527
|
+
try:
|
|
528
|
+
lcd_cls = self.tb.LCD1602
|
|
529
|
+
except AttributeError as exc:
|
|
530
|
+
raise LiveRunError("LCD support is missing from telemetrix_Barmaja.") from exc
|
|
531
|
+
return lcd_cls(self.sync._context, address=int(address), columns=int(columns), rows=int(rows))
|
|
532
|
+
|
|
533
|
+
# ---------- lifecycle ----------
|
|
534
|
+
def shutdown(self) -> None:
|
|
535
|
+
if self._closed:
|
|
536
|
+
return
|
|
537
|
+
self._closed = True
|
|
538
|
+
# A port that is already gone must not produce a second traceback.
|
|
539
|
+
self._release_port()
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
_active_backend: Optional[_TelemetrixBackend] = None
|
|
543
|
+
_active_session: Optional[LiveSession] = None
|
|
544
|
+
_atexit_registered = False
|
|
545
|
+
_thread_hook_installed = False
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _install_thread_hook() -> None:
|
|
549
|
+
"""Silence Telemetrix background-thread tracebacks caused by a lost port.
|
|
550
|
+
|
|
551
|
+
When USB is unplugged the upstream receiver thread dies with a
|
|
552
|
+
SerialException/PortNotOpenError traceback printed by ``threading``. That
|
|
553
|
+
noise is replaced by ONE clean message from the next LiveRun call. Any
|
|
554
|
+
other thread error is passed to the previous hook unchanged.
|
|
555
|
+
"""
|
|
556
|
+
global _thread_hook_installed
|
|
557
|
+
if _thread_hook_installed:
|
|
558
|
+
return
|
|
559
|
+
import threading
|
|
560
|
+
previous = threading.excepthook
|
|
561
|
+
|
|
562
|
+
def hook(args) -> None:
|
|
563
|
+
backend_ = _active_backend
|
|
564
|
+
exc = args.exc_value
|
|
565
|
+
board = getattr(backend_, "_raw_board", None) if backend_ is not None else None
|
|
566
|
+
owned = board is not None and args.thread in (
|
|
567
|
+
getattr(board, "the_data_receive_thread", None),
|
|
568
|
+
getattr(board, "the_reporter_thread", None),
|
|
569
|
+
)
|
|
570
|
+
if exc is not None and (owned or "serial" in type(exc).__module__.lower()) and (
|
|
571
|
+
_port_was_lost(exc) or "serialexception" in type(exc).__name__.lower()):
|
|
572
|
+
if backend_ is not None:
|
|
573
|
+
backend_._link_broken = True
|
|
574
|
+
return
|
|
575
|
+
previous(args)
|
|
576
|
+
|
|
577
|
+
threading.excepthook = hook
|
|
578
|
+
_thread_hook_installed = True
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _say(text: str, *, error: bool = False, style: Optional[str] = None) -> None:
|
|
582
|
+
try:
|
|
583
|
+
from .console import write_styled
|
|
584
|
+
import sys
|
|
585
|
+
stream = sys.stderr if error else sys.stdout
|
|
586
|
+
write_styled(stream, text, style=style)
|
|
587
|
+
stream.flush()
|
|
588
|
+
except Exception:
|
|
589
|
+
import builtins
|
|
590
|
+
builtins.print(text)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _select_port(port: Optional[str]) -> str:
|
|
594
|
+
from .toolchain import board_for_port, detect_board
|
|
595
|
+
if port:
|
|
596
|
+
return board_for_port(port).port
|
|
597
|
+
return detect_board().port
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _telemetrix_sketch() -> Optional[Path]:
|
|
601
|
+
from .environment import private_arduino_library_roots
|
|
602
|
+
for library_root in private_arduino_library_roots():
|
|
603
|
+
sketch = (
|
|
604
|
+
library_root
|
|
605
|
+
/ "Telemetrix4Arduino"
|
|
606
|
+
/ "examples"
|
|
607
|
+
/ "Telemetrix4Arduino"
|
|
608
|
+
/ "Telemetrix4Arduino.ino"
|
|
609
|
+
)
|
|
610
|
+
if sketch.is_file():
|
|
611
|
+
return sketch
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _upload_live_firmware(port: str) -> None:
|
|
616
|
+
sketch = _telemetrix_sketch()
|
|
617
|
+
if sketch is None:
|
|
618
|
+
raise LiveRunError(
|
|
619
|
+
"Telemetrix4Arduino firmware is not installed in Barmaja's offline libraries. "
|
|
620
|
+
"Install/reinstall the Barmaja PyArduino Full or Light Offline pack."
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
from .toolchain import board_for_port, compile_existing_sketch, upload_compiled
|
|
624
|
+
|
|
625
|
+
_say("Preparing Arduino Uno for LiveRun...")
|
|
626
|
+
artifact = compile_existing_sketch(sketch.parent)
|
|
627
|
+
board = board_for_port(port)
|
|
628
|
+
# After a failed client identity check Windows can take a short moment to
|
|
629
|
+
# release the serial handle. Retry only a *busy-port upload*, never a
|
|
630
|
+
# compile failure or another type of avrdude failure.
|
|
631
|
+
for attempt in range(2):
|
|
632
|
+
try:
|
|
633
|
+
upload_compiled(artifact, board)
|
|
634
|
+
break
|
|
635
|
+
except Exception as exc:
|
|
636
|
+
if attempt != 0 or not _port_is_busy(exc):
|
|
637
|
+
raise
|
|
638
|
+
time.sleep(1.0)
|
|
639
|
+
# Give the Uno a short moment after avrdude closes the port. The Telemetrix
|
|
640
|
+
# client performs its own reset wait when it opens the connection.
|
|
641
|
+
time.sleep(0.5)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _try_connect(port: str) -> _TelemetrixBackend:
|
|
645
|
+
return _TelemetrixBackend(port)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _connect_after_firmware_upload(port: str) -> _TelemetrixBackend:
|
|
649
|
+
"""Let Uno USB reset / Telemetrix handshake settle after an AVR flash.
|
|
650
|
+
|
|
651
|
+
Do not compile or reflash again on these retries. A locked port must be
|
|
652
|
+
reported as locked instead of retried as if the firmware were missing.
|
|
653
|
+
"""
|
|
654
|
+
last_error: Optional[Exception] = None
|
|
655
|
+
for attempt in range(3):
|
|
656
|
+
if attempt:
|
|
657
|
+
time.sleep(1.0)
|
|
658
|
+
try:
|
|
659
|
+
return _try_connect(port)
|
|
660
|
+
except LivePortBusyError:
|
|
661
|
+
raise
|
|
662
|
+
except LiveConnectionError as exc:
|
|
663
|
+
last_error = exc
|
|
664
|
+
raise LiveRunError(
|
|
665
|
+
"Arduino firmware was uploaded, but the LiveRun connection did not start. "
|
|
666
|
+
"Unplug/reconnect USB, close other Arduino programs, and try again."
|
|
667
|
+
) from last_error
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _LiveRun_core(port: Optional[str] = None, *, auto_firmware: bool = True) -> LiveSession:
|
|
671
|
+
"""Start live PC/PictoBlox control of the connected Arduino Uno.
|
|
672
|
+
|
|
673
|
+
The first call connects to Telemetrix4Arduino. If the Uno is running a
|
|
674
|
+
normal standalone sketch and ``auto_firmware`` is true, Barmaja uploads the
|
|
675
|
+
pinned Telemetrix4Arduino firmware and reconnects automatically.
|
|
676
|
+
|
|
677
|
+
After this call, the usual Barmaja API (``pinMode``, ``analogRead``,
|
|
678
|
+
``digitalWrite``, ``Servo``, etc.) executes live instead of raising the
|
|
679
|
+
desktop guard. Normal ``print()`` remains ordinary Python output, so in
|
|
680
|
+
PictoBlox it appears in the Python console.
|
|
681
|
+
"""
|
|
682
|
+
global _active_backend, _active_session, _atexit_registered
|
|
683
|
+
|
|
684
|
+
if _active_backend is not None and not _active_backend._closed:
|
|
685
|
+
if port and str(port).strip().upper() != _active_backend.port.upper():
|
|
686
|
+
raise LiveRunError(
|
|
687
|
+
f"LiveRun is already connected on {_active_backend.port}. "
|
|
688
|
+
"Call LiveStop() before selecting another port."
|
|
689
|
+
)
|
|
690
|
+
return _active_session # type: ignore[return-value]
|
|
691
|
+
|
|
692
|
+
chosen_port = _select_port(port)
|
|
693
|
+
firmware_changed = False
|
|
694
|
+
try:
|
|
695
|
+
backend = _try_connect(chosen_port)
|
|
696
|
+
except LiveDependencyError:
|
|
697
|
+
raise
|
|
698
|
+
except LivePortBusyError:
|
|
699
|
+
# Do not flash a busy port. A locked COM3 is not evidence of missing
|
|
700
|
+
# firmware, and attempting upload only masks the actionable problem.
|
|
701
|
+
raise
|
|
702
|
+
except LivePortMissingError:
|
|
703
|
+
raise
|
|
704
|
+
except LiveConnectionError:
|
|
705
|
+
if not auto_firmware:
|
|
706
|
+
raise
|
|
707
|
+
_upload_live_firmware(chosen_port)
|
|
708
|
+
firmware_changed = True
|
|
709
|
+
backend = _connect_after_firmware_upload(chosen_port)
|
|
710
|
+
|
|
711
|
+
# A Telemetrix board may answer successfully while running an older server.
|
|
712
|
+
# LiveRun is validated against one pinned firmware version, so update it
|
|
713
|
+
# automatically instead of silently accepting an untested protocol.
|
|
714
|
+
if backend.firmware_version != EXPECTED_FIRMWARE:
|
|
715
|
+
if not auto_firmware:
|
|
716
|
+
backend.shutdown()
|
|
717
|
+
raise LiveRunError(
|
|
718
|
+
f"LiveRun requires Telemetrix4Arduino {EXPECTED_FIRMWARE}; "
|
|
719
|
+
f"the board reported {backend.firmware_version}."
|
|
720
|
+
)
|
|
721
|
+
backend.shutdown()
|
|
722
|
+
_upload_live_firmware(chosen_port)
|
|
723
|
+
firmware_changed = True
|
|
724
|
+
backend = _connect_after_firmware_upload(chosen_port)
|
|
725
|
+
if backend.firmware_version != EXPECTED_FIRMWARE:
|
|
726
|
+
backend.shutdown()
|
|
727
|
+
raise LiveRunError(
|
|
728
|
+
f"Arduino reported Telemetrix firmware {backend.firmware_version} after update; "
|
|
729
|
+
f"expected {EXPECTED_FIRMWARE}."
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
_active_backend = backend
|
|
733
|
+
_active_session = LiveSession(chosen_port, backend.firmware_version)
|
|
734
|
+
if not _atexit_registered:
|
|
735
|
+
atexit.register(LiveStop)
|
|
736
|
+
_atexit_registered = True
|
|
737
|
+
|
|
738
|
+
_say("Barmaja PyArduino LiveRun ready.", style="success")
|
|
739
|
+
_say(f"Arduino Uno: {chosen_port}")
|
|
740
|
+
if firmware_changed:
|
|
741
|
+
_say("LiveRun firmware was installed automatically.", style="success")
|
|
742
|
+
return _active_session
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _clean_liverun_error(exc: Exception, selected_port: Optional[str], auto_firmware: bool) -> str:
|
|
747
|
+
"""Map known connection/firmware failures to approved student help text.
|
|
748
|
+
|
|
749
|
+
Keep other error families out of this function so genuine programming bugs
|
|
750
|
+
are still surfaced by Python with their real tracebacks.
|
|
751
|
+
"""
|
|
752
|
+
from .errors import BoardDetectionError, ToolchainError
|
|
753
|
+
|
|
754
|
+
port = (selected_port or "").strip().upper()
|
|
755
|
+
if isinstance(exc, BoardDetectionError):
|
|
756
|
+
if exc.reason == "not-found":
|
|
757
|
+
return "Arduino Uno not detected.\nConnect the Arduino by USB and try again."
|
|
758
|
+
if exc.reason == "multiple":
|
|
759
|
+
options = ", ".join(exc.ports)
|
|
760
|
+
return (
|
|
761
|
+
"More than one Arduino Uno or possible board was detected.\n"
|
|
762
|
+
"Disconnect extra boards or choose a port with LiveRun(port='COM3')."
|
|
763
|
+
+ (" Available ports: " + options if options else "")
|
|
764
|
+
)
|
|
765
|
+
return "Could not scan for the Arduino Uno.\nCheck the USB connection and Barmaja offline installation."
|
|
766
|
+
if isinstance(exc, LivePortBusyError) or _port_is_busy(exc):
|
|
767
|
+
return (
|
|
768
|
+
"Arduino Uno found on {}, but the port is busy.\n"
|
|
769
|
+
"Close any program using {} and try again."
|
|
770
|
+
).format(port or "the selected port", port or "that port")
|
|
771
|
+
if isinstance(exc, LivePortMissingError) or _port_is_missing(exc):
|
|
772
|
+
return "Arduino connection lost.\nReconnect the Arduino and run the program again."
|
|
773
|
+
if isinstance(exc, LiveDependencyError):
|
|
774
|
+
return "LiveRun support is missing.\nReinstall the Barmaja PyArduino offline package."
|
|
775
|
+
if isinstance(exc, LiveConnectionError) and not auto_firmware:
|
|
776
|
+
return "Could not connect to the Arduino for LiveRun.\nCheck the firmware or enable automatic firmware preparation."
|
|
777
|
+
if isinstance(exc, (LiveRunError, ToolchainError)):
|
|
778
|
+
return "Could not prepare the Arduino for LiveRun.\nReconnect the board and try again."
|
|
779
|
+
raise TypeError("This is not a recognized LiveRun setup error")
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _enable_student_error_display() -> None:
|
|
783
|
+
"""Quiet only expected student errors in standard Python.
|
|
784
|
+
|
|
785
|
+
Other exception classes are delegated unchanged to the existing hook.
|
|
786
|
+
Some IDEs (notably embedded PictoBlox) own exception rendering, so a
|
|
787
|
+
physical PictoBlox check is still necessary.
|
|
788
|
+
"""
|
|
789
|
+
previous = sys.excepthook
|
|
790
|
+
if getattr(previous, "_barmaja_liverun_clean_hook", False):
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
def display(exc_type, exc, traceback):
|
|
794
|
+
if isinstance(exc, LiveStudentError) or getattr(exc, "_barmaja_student_message", False):
|
|
795
|
+
sys.stderr.write(str(exc).rstrip() + "\n")
|
|
796
|
+
sys.stderr.flush()
|
|
797
|
+
return
|
|
798
|
+
previous(exc_type, exc, traceback)
|
|
799
|
+
|
|
800
|
+
display._barmaja_liverun_clean_hook = True
|
|
801
|
+
sys.excepthook = display
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def LiveRun(port: Optional[str] = None, *, auto_firmware: bool = True,
|
|
805
|
+
debug: bool = False) -> LiveSession:
|
|
806
|
+
"""Start LiveRun. ``debug=True`` preserves original technical exceptions.
|
|
807
|
+
|
|
808
|
+
Normal startup failures use concise explanations. Technical causes remain
|
|
809
|
+
accessible through exception chaining and in debug mode.
|
|
810
|
+
"""
|
|
811
|
+
if debug:
|
|
812
|
+
return _LiveRun_core(port, auto_firmware=auto_firmware)
|
|
813
|
+
try:
|
|
814
|
+
return _LiveRun_core(port, auto_firmware=auto_firmware)
|
|
815
|
+
except LiveStudentError:
|
|
816
|
+
raise
|
|
817
|
+
except Exception as exc:
|
|
818
|
+
from .errors import BoardDetectionError, ToolchainError
|
|
819
|
+
if not isinstance(exc, (LiveRunError, BoardDetectionError, ToolchainError)):
|
|
820
|
+
raise
|
|
821
|
+
selected_port = port
|
|
822
|
+
# The selected port is known after detection in most cases, but do not
|
|
823
|
+
# trigger a second hardware scan just to improve an error message.
|
|
824
|
+
if not selected_port:
|
|
825
|
+
# LiveConnectionError / port errors sometimes carry a port name.
|
|
826
|
+
# Never guess COM3 if the user's board actually has another port.
|
|
827
|
+
import re
|
|
828
|
+
match = re.search(r"\bCOM[0-9]+\b", str(exc), re.IGNORECASE)
|
|
829
|
+
if match:
|
|
830
|
+
selected_port = match.group(0)
|
|
831
|
+
_enable_student_error_display()
|
|
832
|
+
message = _clean_liverun_error(exc, selected_port, auto_firmware)
|
|
833
|
+
# Preserve exception subclasses used by the recovery protocol and
|
|
834
|
+
# existing callers/tests. Only the displayed message is rewritten.
|
|
835
|
+
shown = type(exc)(message) if isinstance(exc, (LivePortBusyError, LivePortMissingError)) else LiveStudentError(message)
|
|
836
|
+
shown._barmaja_student_message = True
|
|
837
|
+
raise shown from None
|
|
838
|
+
|
|
839
|
+
def LiveStop() -> None:
|
|
840
|
+
"""Stop the current LiveRun connection, if one is active."""
|
|
841
|
+
global _active_backend, _active_session
|
|
842
|
+
backend = _active_backend
|
|
843
|
+
_active_backend = None
|
|
844
|
+
_active_session = None
|
|
845
|
+
if backend is not None:
|
|
846
|
+
try:
|
|
847
|
+
backend.shutdown()
|
|
848
|
+
except Exception:
|
|
849
|
+
pass
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def is_live() -> bool:
|
|
853
|
+
return _active_backend is not None and not _active_backend._closed
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def backend() -> _TelemetrixBackend:
|
|
857
|
+
if _active_backend is None or _active_backend._closed:
|
|
858
|
+
raise LiveRunError("LiveRun is not active. Add LiveRun() near the top of the program.")
|
|
859
|
+
return _active_backend
|