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,506 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .board import UNO, BoardProfile
|
|
12
|
+
from .environment import ensure_runtime_dirs, env_seconds, find_normal_arduino_cli, find_pictoblox_arduino_cli, invalidate_cache, run_text
|
|
13
|
+
from .errors import BoardDetectionError, HardwareError, InternalCompilerError, ToolchainError
|
|
14
|
+
|
|
15
|
+
# Common USB-UART bridges used on Uno-compatible classroom boards.
|
|
16
|
+
KNOWN_UNO_BRIDGES = {
|
|
17
|
+
("1a86", "7523"): "CH340/CH341",
|
|
18
|
+
("1a86", "5523"): "CH340/CH341",
|
|
19
|
+
("10c4", "ea60"): "CP210x",
|
|
20
|
+
("0403", "6001"): "FTDI",
|
|
21
|
+
}
|
|
22
|
+
GENUINE_UNO_IDS = {
|
|
23
|
+
("2341", "0043"), ("2341", "0001"), ("2a03", "0043"), ("2a03", "0001"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DetectedBoard:
|
|
28
|
+
port: str
|
|
29
|
+
fqbn: str
|
|
30
|
+
name: str
|
|
31
|
+
confidence: str = "exact" # exact / probable-clone
|
|
32
|
+
transport: str | None = None
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class BuildMemory:
|
|
36
|
+
flash_used: int | None = None
|
|
37
|
+
flash_total: int | None = None
|
|
38
|
+
sram_used: int | None = None
|
|
39
|
+
sram_total: int | None = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def flash_percent(self) -> float | None:
|
|
43
|
+
if self.flash_used is None or not self.flash_total:
|
|
44
|
+
return None
|
|
45
|
+
return self.flash_used * 100.0 / self.flash_total
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def sram_percent(self) -> float | None:
|
|
49
|
+
if self.sram_used is None or not self.sram_total:
|
|
50
|
+
return None
|
|
51
|
+
return self.sram_used * 100.0 / self.sram_total
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class CompileArtifact:
|
|
55
|
+
sketch_dir: Path
|
|
56
|
+
build_dir: Path
|
|
57
|
+
memory: BuildMemory
|
|
58
|
+
output: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def bundled_arduino_cli() -> str | None:
|
|
62
|
+
"""The pinned Barmaja runtime installed by the Full Offline Pack, if present."""
|
|
63
|
+
dirs = ensure_runtime_dirs()
|
|
64
|
+
names = ("arduino-cli.exe", "arduino-cli") if os.name == "nt" else ("arduino-cli",)
|
|
65
|
+
for name in names:
|
|
66
|
+
p = dirs["root"] / "runtime" / name
|
|
67
|
+
if p.exists():
|
|
68
|
+
return str(p)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find_arduino_cli() -> str:
|
|
73
|
+
"""Select the Arduino CLI used for every Barmaja compile/upload.
|
|
74
|
+
|
|
75
|
+
Order: explicit BARMAJA_ARDUINO_CLI override, then the pinned Barmaja runtime
|
|
76
|
+
(Full Offline Pack), then an existing Arduino installation (Light Pack reuse),
|
|
77
|
+
then a PictoBlox-bundled CLI. The pinned runtime wins over an unrelated
|
|
78
|
+
Arduino IDE so a classroom laptop always compiles with the tested toolchain,
|
|
79
|
+
even for Windows accounts that never opened the Arduino IDE.
|
|
80
|
+
"""
|
|
81
|
+
override = os.environ.get("BARMAJA_ARDUINO_CLI")
|
|
82
|
+
if override and Path(override).exists():
|
|
83
|
+
return str(Path(override).resolve())
|
|
84
|
+
bundled = bundled_arduino_cli()
|
|
85
|
+
if bundled:
|
|
86
|
+
return bundled
|
|
87
|
+
found = find_normal_arduino_cli()
|
|
88
|
+
if found:
|
|
89
|
+
return found
|
|
90
|
+
# PictoBlox is only reused when an actual Arduino CLI executable is discoverable.
|
|
91
|
+
# Capability/version validation is then performed by Smart Doctor.
|
|
92
|
+
pb = find_pictoblox_arduino_cli()
|
|
93
|
+
if pb:
|
|
94
|
+
return pb
|
|
95
|
+
raise ToolchainError(
|
|
96
|
+
"Arduino compilation environment was not found. Install Arduino support or use the Barmaja PyArduino Offline Pack."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run(args: list[str], *, timeout: float = 30.0, cwd: str | Path | None = None):
|
|
101
|
+
return run_text(args, timeout=timeout, cwd=cwd)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _extract_ports(payload) -> list[dict]:
|
|
105
|
+
if isinstance(payload, dict):
|
|
106
|
+
for key in ("detected_ports", "ports"):
|
|
107
|
+
if isinstance(payload.get(key), list):
|
|
108
|
+
return payload[key]
|
|
109
|
+
if "port" in payload or "address" in payload:
|
|
110
|
+
return [payload]
|
|
111
|
+
if isinstance(payload, list):
|
|
112
|
+
return payload
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _hex(v) -> str | None:
|
|
117
|
+
if v is None:
|
|
118
|
+
return None
|
|
119
|
+
if isinstance(v, int):
|
|
120
|
+
return f"{v:04x}"
|
|
121
|
+
s = str(v).lower().replace("0x", "").strip()
|
|
122
|
+
return s.zfill(4) if re.fullmatch(r"[0-9a-f]+", s or "x") else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _port_meta(item: dict) -> tuple[str | None, str | None, str | None, str | None]:
|
|
126
|
+
p = item.get("port", {}) if isinstance(item.get("port"), dict) else item
|
|
127
|
+
address = p.get("address") or item.get("address")
|
|
128
|
+
props = p.get("properties") or item.get("properties") or {}
|
|
129
|
+
vid = _hex(props.get("vid") or props.get("VID") or p.get("vid") or item.get("vid"))
|
|
130
|
+
pid = _hex(props.get("pid") or props.get("PID") or p.get("pid") or item.get("pid"))
|
|
131
|
+
protocol = p.get("protocol") or item.get("protocol")
|
|
132
|
+
return address, vid, pid, protocol
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def detect_board(profile: BoardProfile = UNO) -> DetectedBoard:
|
|
136
|
+
cli = find_arduino_cli()
|
|
137
|
+
result = _run([cli, "board", "list", "--format", "json"], timeout=12)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
invalidate_cache()
|
|
140
|
+
raise BoardDetectionError(
|
|
141
|
+
_translate_tool_failure(result.stderr or result.stdout, "Arduino CLI could not list connected boards."),
|
|
142
|
+
reason="list-failed",
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
payload = json.loads(result.stdout or "{}")
|
|
146
|
+
except json.JSONDecodeError as e:
|
|
147
|
+
raise BoardDetectionError("Arduino CLI returned unreadable board information.", reason="unreadable") from e
|
|
148
|
+
|
|
149
|
+
# Arduino CLI may exit 0 even when a discovery helper failed to start.
|
|
150
|
+
# When no ports are available, do not misreport that as "board not connected".
|
|
151
|
+
if isinstance(payload, dict) and not _extract_ports(payload):
|
|
152
|
+
warnings = payload.get("warnings")
|
|
153
|
+
if isinstance(warnings, list):
|
|
154
|
+
warning_text = " | ".join(str(w) for w in warnings if w)
|
|
155
|
+
elif warnings:
|
|
156
|
+
warning_text = str(warnings)
|
|
157
|
+
else:
|
|
158
|
+
warning_text = ""
|
|
159
|
+
if warning_text and "discovery" in warning_text.lower():
|
|
160
|
+
invalidate_cache()
|
|
161
|
+
raise BoardDetectionError(
|
|
162
|
+
"Barmaja could not scan USB ports because the Arduino port scanner failed. "
|
|
163
|
+
"Run Smart Doctor or reinstall the Full Offline package.",
|
|
164
|
+
reason="list-failed",
|
|
165
|
+
details=warning_text,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
exact: list[DetectedBoard] = []
|
|
169
|
+
fallback: list[DetectedBoard] = []
|
|
170
|
+
seen_ports: list[str] = []
|
|
171
|
+
for item in _extract_ports(payload):
|
|
172
|
+
if not isinstance(item, dict):
|
|
173
|
+
continue
|
|
174
|
+
address, vid, pid, protocol = _port_meta(item)
|
|
175
|
+
if address:
|
|
176
|
+
seen_ports.append(address)
|
|
177
|
+
boards = item.get("matching_boards") or item.get("boards") or []
|
|
178
|
+
for board in boards:
|
|
179
|
+
fqbn = board.get("fqbn")
|
|
180
|
+
name = board.get("name", "Arduino")
|
|
181
|
+
if address and fqbn == profile.fqbn:
|
|
182
|
+
exact.append(DetectedBoard(address, fqbn, name, "exact", protocol))
|
|
183
|
+
if address and vid and pid:
|
|
184
|
+
key = (vid, pid)
|
|
185
|
+
if key in GENUINE_UNO_IDS:
|
|
186
|
+
fallback.append(DetectedBoard(address, profile.fqbn, profile.name, "exact", protocol))
|
|
187
|
+
elif key in KNOWN_UNO_BRIDGES:
|
|
188
|
+
fallback.append(DetectedBoard(address, profile.fqbn, f"Uno-compatible via {KNOWN_UNO_BRIDGES[key]}", "probable-clone", protocol))
|
|
189
|
+
|
|
190
|
+
exact_unique = {b.port: b for b in exact}
|
|
191
|
+
if len(exact_unique) == 1:
|
|
192
|
+
return next(iter(exact_unique.values()))
|
|
193
|
+
if len(exact_unique) > 1:
|
|
194
|
+
ports = tuple(sorted(exact_unique))
|
|
195
|
+
raise BoardDetectionError(
|
|
196
|
+
f"More than one {profile.name} is connected: {', '.join(ports)}. Disconnect extra boards, or choose one with: pyarduino upload <file.py> --port {ports[0]}",
|
|
197
|
+
reason="multiple", ports=ports,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Safe clone fallback only when exactly one known USB-UART candidate exists.
|
|
201
|
+
fallback_unique = {b.port: b for b in fallback}
|
|
202
|
+
if len(fallback_unique) == 1:
|
|
203
|
+
return next(iter(fallback_unique.values()))
|
|
204
|
+
if len(fallback_unique) > 1:
|
|
205
|
+
ports = ", ".join(sorted(fallback_unique))
|
|
206
|
+
choices = tuple(sorted(fallback_unique))
|
|
207
|
+
raise BoardDetectionError(
|
|
208
|
+
f"Several possible Uno-compatible serial devices were detected: {ports}. Barmaja will not guess which one to upload to. Choose one with: pyarduino upload <file.py> --port {choices[0]}",
|
|
209
|
+
reason="multiple", ports=choices,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if seen_ports:
|
|
213
|
+
ports = tuple(sorted(set(seen_ports)))
|
|
214
|
+
raise BoardDetectionError(
|
|
215
|
+
f"No {profile.name} was identified. Ports seen: {', '.join(ports)}.",
|
|
216
|
+
reason="not-found", ports=ports,
|
|
217
|
+
)
|
|
218
|
+
raise BoardDetectionError(
|
|
219
|
+
f"No {profile.name} was identified. Connect the board and make sure its USB serial driver is available.",
|
|
220
|
+
reason="not-found", ports=(),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
_KEEP_BUILD_DIRS = 4
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _prune_old_builds(build_root: Path, keep: int = _KEEP_BUILD_DIRS) -> None:
|
|
228
|
+
"""Keep only the newest build folders; each one holds megabytes of objects."""
|
|
229
|
+
try:
|
|
230
|
+
dirs = [d for d in build_root.iterdir() if d.is_dir()]
|
|
231
|
+
dirs.sort(key=lambda d: d.stat().st_mtime, reverse=True)
|
|
232
|
+
for old in dirs[keep:]:
|
|
233
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
234
|
+
except OSError:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def create_sketch(cpp: str, work_root: str | Path | None = None) -> Path:
|
|
239
|
+
if work_root:
|
|
240
|
+
root = Path(work_root)
|
|
241
|
+
else:
|
|
242
|
+
build_root = ensure_runtime_dirs()["build"]
|
|
243
|
+
_prune_old_builds(build_root, keep=_KEEP_BUILD_DIRS - 1)
|
|
244
|
+
root = build_root / next(tempfile._get_candidate_names())
|
|
245
|
+
sketch_dir = root / "BarmajaGenerated"
|
|
246
|
+
sketch_dir.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
(sketch_dir / "BarmajaGenerated.ino").write_text(cpp, encoding="utf-8")
|
|
248
|
+
return sketch_dir
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _memory_from_output(text: str, profile: BoardProfile) -> BuildMemory:
|
|
252
|
+
flash_used = sram_used = None
|
|
253
|
+
flash_total, sram_total = profile.flash_bytes, profile.sram_bytes
|
|
254
|
+
# Arduino CLI English output (Barmaja forces ARDUINO_LOCALE=en):
|
|
255
|
+
# "Sketch uses 924 bytes (2%) of program storage space. Maximum is 32256 bytes."
|
|
256
|
+
# "Global variables use 9 bytes (0%) of dynamic memory, ... Maximum is 2048 bytes."
|
|
257
|
+
m = re.search(r"Sketch uses\s+([\d,]+)\s+bytes", text, re.I)
|
|
258
|
+
if m:
|
|
259
|
+
flash_used = int(m.group(1).replace(",", ""))
|
|
260
|
+
mx = re.search(r"program storage space\.\s*Maximum is\s+([\d,]+)", text, re.I)
|
|
261
|
+
if mx:
|
|
262
|
+
flash_total = int(mx.group(1).replace(",", ""))
|
|
263
|
+
m = re.search(r"Global variables use\s+([\d,]+)\s+bytes", text, re.I)
|
|
264
|
+
if m:
|
|
265
|
+
sram_used = int(m.group(1).replace(",", ""))
|
|
266
|
+
mx = re.search(r"dynamic memory.*?Maximum is\s+([\d,]+)", text, re.I | re.S)
|
|
267
|
+
if mx:
|
|
268
|
+
sram_total = int(mx.group(1).replace(",", ""))
|
|
269
|
+
return BuildMemory(flash_used, flash_total if flash_used is not None else None, sram_used, sram_total if sram_used is not None else None)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
_MEMORY_OVERFLOW_MARKERS = (
|
|
273
|
+
"not within region", "region `data' overflowed", "region `text' overflowed", "region 'data' overflowed",
|
|
274
|
+
"region 'text' overflowed", "will not fit in region", "sketch too big", "not enough memory",
|
|
275
|
+
"text section exceeds available space", "data section exceeds available space",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _is_memory_overflow(text: str) -> bool:
|
|
280
|
+
low = (text or "").lower()
|
|
281
|
+
return any(marker in low for marker in _MEMORY_OVERFLOW_MARKERS)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _missing_header(text: str) -> str | None:
|
|
285
|
+
m = re.search(r"fatal error:\s*([\w./-]+\.h):\s*No such file", text or "", re.I)
|
|
286
|
+
return m.group(1) if m else None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _generated_code_errors(text: str) -> list[str]:
|
|
290
|
+
"""Compiler errors located in the Barmaja-generated sketch itself."""
|
|
291
|
+
return [line.strip() for line in (text or "").splitlines()
|
|
292
|
+
if "BarmajaGenerated.ino" in line and re.search(r"\berror:", line)]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _translate_tool_failure(text: str, default: str) -> str:
|
|
296
|
+
low = (text or "").lower()
|
|
297
|
+
if _is_memory_overflow(text):
|
|
298
|
+
return "The program is too big for the Arduino Uno memory. Use shorter lists/strings or fewer libraries."
|
|
299
|
+
header = _missing_header(text)
|
|
300
|
+
if header:
|
|
301
|
+
return f"A required Arduino library is missing ({header}). Run 'pyarduino doctor' for the missing library list."
|
|
302
|
+
if "access is denied" in low or "permission denied" in low or "resource busy" in low or "can't open device" in low or "cannot open port" in low:
|
|
303
|
+
return "The Arduino port is busy or unavailable. Close Serial Monitor/PictoBlox/other Arduino tools and try again."
|
|
304
|
+
if "programmer is not responding" in low or "stk500_recv" in low or "not in sync" in low:
|
|
305
|
+
return "Arduino Uno did not respond during upload. Check the selected board, USB cable, and COM port."
|
|
306
|
+
if "no such file or directory" in low and ("avr-g" in low or "avrdude" in low or "ctags" in low):
|
|
307
|
+
return "The Arduino compiler environment is incomplete. Run 'pyarduino doctor' or use the Barmaja Offline Pack."
|
|
308
|
+
if "library" in low and ("not found" in low or "no such file" in low):
|
|
309
|
+
return "A required Arduino library is missing. Run 'pyarduino doctor' for the missing library list."
|
|
310
|
+
if "timed out" in low:
|
|
311
|
+
return "The Arduino tool timed out. Reconnect the board and try again."
|
|
312
|
+
compact = " ".join((text or "").strip().splitlines()[-3:])
|
|
313
|
+
return compact[:700] if compact else default
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def pinned_library_dirs() -> list[Path]:
|
|
317
|
+
"""Pinned BARMAJA libraries from both root spellings (installed first)."""
|
|
318
|
+
from .environment import private_arduino_library_roots
|
|
319
|
+
found: dict[str, Path] = {}
|
|
320
|
+
for private in private_arduino_library_roots():
|
|
321
|
+
for path in sorted(private.iterdir(), key=lambda p: p.name.lower()):
|
|
322
|
+
if path.is_dir() and (path / "library.properties").exists():
|
|
323
|
+
found.setdefault(path.name.lower(), path)
|
|
324
|
+
return [found[key] for key in sorted(found)]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _private_library_args() -> list[str]:
|
|
328
|
+
"""Pin each Barmaja library with --library (top priority in arduino-cli).
|
|
329
|
+
|
|
330
|
+
--libraries only adds a search folder; a same-named header in the user's
|
|
331
|
+
sketchbook (e.g. a random 'DHT' zip library) could still win. --library
|
|
332
|
+
makes the tested pinned copy authoritative for its headers.
|
|
333
|
+
"""
|
|
334
|
+
args: list[str] = []
|
|
335
|
+
for lib in pinned_library_dirs():
|
|
336
|
+
args += ["--library", str(lib)]
|
|
337
|
+
# Device-pack adapters ship inside the Python package (or external pack folders).
|
|
338
|
+
from .device_packs import adapter_library_dirs
|
|
339
|
+
for lib in adapter_library_dirs():
|
|
340
|
+
args += ["--library", str(lib)]
|
|
341
|
+
# PRIVATE Stage-2 test only. This opt-in path does not modify the frozen
|
|
342
|
+
# V1 runtime's arduino-libraries folder or the teacher installer.
|
|
343
|
+
dev_root = os.environ.get("BARMAJA_DEV_LIBRARIES", "").strip()
|
|
344
|
+
if dev_root:
|
|
345
|
+
candidate = Path(dev_root).expanduser()
|
|
346
|
+
if not candidate.is_dir():
|
|
347
|
+
raise ToolchainError(f"BARMAJA_DEV_LIBRARIES does not exist: {candidate}")
|
|
348
|
+
for lib in sorted(candidate.iterdir()):
|
|
349
|
+
if lib.is_dir() and (lib / "library.properties").is_file():
|
|
350
|
+
args += ["--library", str(lib)]
|
|
351
|
+
return args
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _cli_config_args(cli: str | None = None) -> list[str]:
|
|
355
|
+
cfg = os.environ.get("BARMAJA_ARDUINO_CONFIG")
|
|
356
|
+
if cfg and Path(cfg).exists():
|
|
357
|
+
return ["--config-file", cfg]
|
|
358
|
+
bundled = ensure_runtime_dirs()["root"] / "runtime" / "arduino-cli.yaml"
|
|
359
|
+
if bundled.exists():
|
|
360
|
+
# Only force the bundled config for the bundled CLI. Existing Arduino
|
|
361
|
+
# installations keep their own configuration untouched.
|
|
362
|
+
if cli is None:
|
|
363
|
+
return ["--config-file", str(bundled)]
|
|
364
|
+
try:
|
|
365
|
+
cli_path = Path(cli).resolve()
|
|
366
|
+
runtime_root = (ensure_runtime_dirs()["root"] / "runtime").resolve()
|
|
367
|
+
if cli_path.parent == runtime_root:
|
|
368
|
+
return ["--config-file", str(bundled)]
|
|
369
|
+
except OSError:
|
|
370
|
+
pass
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def compile_sketch(cpp: str, profile: BoardProfile = UNO, work_root: str | Path | None = None) -> CompileArtifact:
|
|
375
|
+
if os.environ.get("BARMAJA_SKIP_DOCTOR") != "1":
|
|
376
|
+
from .doctor import ensure_environment_ready
|
|
377
|
+
ensure_environment_ready()
|
|
378
|
+
cli = find_arduino_cli()
|
|
379
|
+
sketch = create_sketch(cpp, work_root)
|
|
380
|
+
build_dir = sketch.parent / "build"
|
|
381
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
args = [cli, *_cli_config_args(cli), "compile", "--fqbn", profile.fqbn, "--warnings", "all", *_private_library_args(), "--build-path", str(build_dir), str(sketch)]
|
|
383
|
+
try:
|
|
384
|
+
result = _run(args, timeout=env_seconds("BARMAJA_COMPILE_TIMEOUT", 180.0))
|
|
385
|
+
except ToolchainError:
|
|
386
|
+
# A timeout/start failure may mean the compiler/runtime is damaged. Never
|
|
387
|
+
# leave a READY cache behind after an external compile failure.
|
|
388
|
+
invalidate_cache()
|
|
389
|
+
raise
|
|
390
|
+
output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
|
|
391
|
+
if result.returncode != 0:
|
|
392
|
+
if _is_memory_overflow(output):
|
|
393
|
+
# A real, student-fixable limit; the environment is fine.
|
|
394
|
+
raise HardwareError(
|
|
395
|
+
"The program is too big for the Arduino Uno memory (2048 bytes of RAM, 32256 bytes of program space). "
|
|
396
|
+
"Use shorter lists and strings, or fewer libraries at the same time.",
|
|
397
|
+
details=" ".join(output.splitlines()[-3:])[:700],
|
|
398
|
+
)
|
|
399
|
+
generated = _generated_code_errors(output)
|
|
400
|
+
if generated and not _missing_header(output):
|
|
401
|
+
# An accepted Python program must always produce valid C++. Do not
|
|
402
|
+
# blame (or re-diagnose) the student's Arduino environment.
|
|
403
|
+
raise InternalCompilerError(
|
|
404
|
+
"Barmaja PyArduino generated Arduino code that did not compile. This is a Barmaja bug, not a mistake in your program.",
|
|
405
|
+
details="\n".join(generated[:5])[:1500],
|
|
406
|
+
)
|
|
407
|
+
invalidate_cache()
|
|
408
|
+
raise ToolchainError("Arduino compilation failed. " + _translate_tool_failure(output, "Arduino compilation failed."))
|
|
409
|
+
memory = _memory_from_output(output, profile)
|
|
410
|
+
_check_real_memory(memory, profile)
|
|
411
|
+
return CompileArtifact(sketch, build_dir, memory, output)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _check_real_memory(memory: BuildMemory, profile: BoardProfile) -> None:
|
|
415
|
+
"""Enforce the student SRAM safety limit on the real linker result."""
|
|
416
|
+
if memory.sram_used is None or not memory.sram_total:
|
|
417
|
+
return
|
|
418
|
+
ratio = memory.sram_used / memory.sram_total
|
|
419
|
+
if ratio >= profile.sram_error_ratio:
|
|
420
|
+
raise HardwareError(
|
|
421
|
+
f"The program uses {memory.sram_used} of {memory.sram_total} bytes of Arduino Uno RAM ({ratio:.0%}). "
|
|
422
|
+
f"Above {profile.sram_error_ratio:.0%} the board can crash or restart while running. "
|
|
423
|
+
"Use shorter lists and strings, or fewer libraries at the same time."
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def board_for_port(port: str, profile: BoardProfile = UNO) -> DetectedBoard:
|
|
428
|
+
"""Manual fallback: the student/teacher named the COM port explicitly."""
|
|
429
|
+
port = (port or "").strip()
|
|
430
|
+
if not port:
|
|
431
|
+
raise BoardDetectionError("No serial port was given.")
|
|
432
|
+
return DetectedBoard(port, profile.fqbn, profile.name, "manual", "serial")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def compile_existing_sketch(sketch_dir: str | Path, profile: BoardProfile = UNO) -> CompileArtifact:
|
|
436
|
+
"""Compile an existing Arduino sketch directory with Barmaja's pinned toolchain.
|
|
437
|
+
|
|
438
|
+
LiveRun uses this for the bundled Telemetrix4Arduino server sketch. The
|
|
439
|
+
student's standalone compiler still uses ``compile_sketch()`` above.
|
|
440
|
+
"""
|
|
441
|
+
if os.environ.get("BARMAJA_SKIP_DOCTOR") != "1":
|
|
442
|
+
from .doctor import ensure_environment_ready
|
|
443
|
+
ensure_environment_ready()
|
|
444
|
+
|
|
445
|
+
sketch = Path(sketch_dir).expanduser().resolve()
|
|
446
|
+
if not sketch.is_dir() or not any(sketch.glob("*.ino")):
|
|
447
|
+
raise ToolchainError(f"Arduino sketch folder is missing or invalid: {sketch}")
|
|
448
|
+
|
|
449
|
+
cli = find_arduino_cli()
|
|
450
|
+
build_root = ensure_runtime_dirs()["build"]
|
|
451
|
+
_prune_old_builds(build_root, keep=_KEEP_BUILD_DIRS - 1)
|
|
452
|
+
root = build_root / next(tempfile._get_candidate_names())
|
|
453
|
+
build_dir = root / "build"
|
|
454
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
455
|
+
|
|
456
|
+
args = [
|
|
457
|
+
cli, *_cli_config_args(cli), "compile", "--fqbn", profile.fqbn,
|
|
458
|
+
"--warnings", "all", *_private_library_args(),
|
|
459
|
+
"--build-path", str(build_dir), str(sketch),
|
|
460
|
+
]
|
|
461
|
+
try:
|
|
462
|
+
result = _run(args, timeout=env_seconds("BARMAJA_COMPILE_TIMEOUT", 180.0))
|
|
463
|
+
except ToolchainError:
|
|
464
|
+
invalidate_cache()
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
|
|
468
|
+
if result.returncode != 0:
|
|
469
|
+
invalidate_cache()
|
|
470
|
+
raise ToolchainError(
|
|
471
|
+
"Arduino firmware compilation failed. "
|
|
472
|
+
+ _translate_tool_failure(output, "Arduino firmware compilation failed.")
|
|
473
|
+
)
|
|
474
|
+
memory = _memory_from_output(output, profile)
|
|
475
|
+
_check_real_memory(memory, profile)
|
|
476
|
+
return CompileArtifact(sketch, build_dir, memory, output)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def upload_compiled(artifact: CompileArtifact, board: DetectedBoard, profile: BoardProfile = UNO) -> Path:
|
|
480
|
+
"""Upload an already-compiled sketch without compiling it a second time."""
|
|
481
|
+
cli = find_arduino_cli()
|
|
482
|
+
target = board
|
|
483
|
+
if target.fqbn != profile.fqbn:
|
|
484
|
+
raise ToolchainError(f"Compile/upload board mismatch: expected {profile.fqbn}, got {target.fqbn}.")
|
|
485
|
+
args = [cli, *_cli_config_args(cli), "upload", "-p", target.port, "--fqbn", profile.fqbn, "--input-dir", str(artifact.build_dir), str(artifact.sketch_dir)]
|
|
486
|
+
try:
|
|
487
|
+
result = _run(args, timeout=env_seconds("BARMAJA_UPLOAD_TIMEOUT", 90.0))
|
|
488
|
+
except ToolchainError:
|
|
489
|
+
invalidate_cache()
|
|
490
|
+
raise
|
|
491
|
+
if result.returncode != 0:
|
|
492
|
+
# Upload failures are usually hardware/port problems; board/COM state is
|
|
493
|
+
# never trusted from the environment cache, and the cache is invalidated
|
|
494
|
+
# so the next run re-verifies the toolchain as well.
|
|
495
|
+
invalidate_cache()
|
|
496
|
+
details = ((result.stderr or "") + "\n" + (result.stdout or "")).strip()
|
|
497
|
+
raise ToolchainError("Arduino upload failed. " + _translate_tool_failure(details, "Arduino upload failed."))
|
|
498
|
+
return artifact.sketch_dir
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def upload_sketch(cpp: str, board: DetectedBoard | None = None, profile: BoardProfile = UNO) -> Path:
|
|
502
|
+
if board is None:
|
|
503
|
+
port = os.environ.get("BARMAJA_PORT")
|
|
504
|
+
board = board_for_port(port, profile) if port else detect_board(profile)
|
|
505
|
+
artifact = compile_sketch(cpp, profile)
|
|
506
|
+
return upload_compiled(artifact, board, profile)
|