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.
Files changed (87) hide show
  1. Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
  2. Barmaja_PyArduino/__init__.py +528 -0
  3. Barmaja_PyArduino/__init__.pyi +331 -0
  4. Barmaja_PyArduino/__main__.py +2 -0
  5. Barmaja_PyArduino/adapters/__init__.py +35 -0
  6. Barmaja_PyArduino/api_registry.py +459 -0
  7. Barmaja_PyArduino/backend/__init__.py +3 -0
  8. Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
  9. Barmaja_PyArduino/board.py +80 -0
  10. Barmaja_PyArduino/cli.py +223 -0
  11. Barmaja_PyArduino/compiler.py +1721 -0
  12. Barmaja_PyArduino/console.py +217 -0
  13. Barmaja_PyArduino/device_packs.py +367 -0
  14. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
  15. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
  16. Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
  17. Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
  18. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
  19. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
  20. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
  21. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
  22. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
  23. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
  24. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
  25. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
  26. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
  27. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
  28. Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
  29. Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
  30. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
  31. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
  32. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
  33. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
  34. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
  35. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
  36. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
  37. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
  38. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
  39. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
  40. Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
  41. Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
  42. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
  43. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
  44. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
  45. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
  46. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
  47. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
  48. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
  49. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
  50. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
  51. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
  52. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
  53. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
  54. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
  55. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
  56. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
  57. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
  58. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
  59. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
  60. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
  61. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
  62. Barmaja_PyArduino/doctor.py +406 -0
  63. Barmaja_PyArduino/environment.py +595 -0
  64. Barmaja_PyArduino/errors.py +166 -0
  65. Barmaja_PyArduino/flow.py +754 -0
  66. Barmaja_PyArduino/frontend.py +611 -0
  67. Barmaja_PyArduino/installer.py +151 -0
  68. Barmaja_PyArduino/ir.py +660 -0
  69. Barmaja_PyArduino/library_adder.py +440 -0
  70. Barmaja_PyArduino/live.py +859 -0
  71. Barmaja_PyArduino/lowering.py +596 -0
  72. Barmaja_PyArduino/numeric.py +748 -0
  73. Barmaja_PyArduino/pack_builder.py +544 -0
  74. Barmaja_PyArduino/py.typed +0 -0
  75. Barmaja_PyArduino/serial_monitor.py +172 -0
  76. Barmaja_PyArduino/source.py +184 -0
  77. Barmaja_PyArduino/stubgen.py +148 -0
  78. Barmaja_PyArduino/symbols.py +646 -0
  79. Barmaja_PyArduino/target.py +1631 -0
  80. Barmaja_PyArduino/toolchain.py +506 -0
  81. Barmaja_PyArduino/typesys.py +917 -0
  82. Barmaja_PyArduino/workflow.py +213 -0
  83. barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
  84. barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
  85. barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
  86. barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
  87. 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)