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,595 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import dataclass, asdict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
CACHE_SCHEMA = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _home() -> Path:
|
|
19
|
+
override = os.environ.get("BARMAJA_HOME")
|
|
20
|
+
if override:
|
|
21
|
+
return Path(override).expanduser().resolve()
|
|
22
|
+
base = Path(os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or Path.home())
|
|
23
|
+
return (base / "BarmajaPyArduino").resolve()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ascii_runtime_root() -> Path:
|
|
27
|
+
override = os.environ.get("BARMAJA_RUNTIME_ROOT")
|
|
28
|
+
if override:
|
|
29
|
+
return Path(override).expanduser().resolve()
|
|
30
|
+
if os.name == "nt":
|
|
31
|
+
drive = os.environ.get("SystemDrive", "C:")
|
|
32
|
+
return Path(drive + "\\BarmajaPyArduino")
|
|
33
|
+
return Path(tempfile.gettempdir()) / "barmaja-pyarduino"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ensure_runtime_dirs() -> dict[str, Path]:
|
|
37
|
+
root = ascii_runtime_root()
|
|
38
|
+
dirs = {
|
|
39
|
+
"root": root,
|
|
40
|
+
"data": root / "arduino-data",
|
|
41
|
+
"user": root / "arduino-user",
|
|
42
|
+
"downloads": root / "downloads",
|
|
43
|
+
"build": root / "build",
|
|
44
|
+
"cache": _home() / "cache",
|
|
45
|
+
}
|
|
46
|
+
for p in dirs.values():
|
|
47
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
return dirs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def private_arduino_library_roots() -> tuple[Path, ...]:
|
|
52
|
+
"""Existing BARMAJA private Arduino library roots, in priority order.
|
|
53
|
+
|
|
54
|
+
Restored in 1.1.0.dev7: ``live.py`` (LiveRun firmware install) imports this
|
|
55
|
+
function, but it was missing from the 0.7.0.dev1 "student-errors" build and
|
|
56
|
+
every V1.1 dev build, so ``upload()`` followed by ``LiveRun()`` failed with
|
|
57
|
+
ImportError. Installed packs use ``arduino-libraries``; portable/source
|
|
58
|
+
packs use ``arduino_libraries``. Both are accepted.
|
|
59
|
+
"""
|
|
60
|
+
root = ensure_runtime_dirs()["root"]
|
|
61
|
+
found: list[Path] = []
|
|
62
|
+
seen: set[str] = set()
|
|
63
|
+
for candidate in (root / "arduino-libraries", root / "arduino_libraries"):
|
|
64
|
+
if not candidate.is_dir():
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
resolved = Path(os.path.abspath(os.fspath(candidate))) if os.name == "nt" else candidate.resolve()
|
|
68
|
+
except OSError:
|
|
69
|
+
resolved = candidate
|
|
70
|
+
key = os.path.normcase(str(resolved))
|
|
71
|
+
if key not in seen:
|
|
72
|
+
found.append(resolved)
|
|
73
|
+
seen.add(key)
|
|
74
|
+
return tuple(found)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def package_version() -> str:
|
|
78
|
+
# The package's own version is authoritative: PictoBlox copies have no
|
|
79
|
+
# dist-info, and a stale dist-info must never mask the code actually loaded.
|
|
80
|
+
from . import __version__
|
|
81
|
+
return __version__
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def arduino_cli_environment(executable: str | Path | None = None) -> dict[str, str]:
|
|
85
|
+
"""Environment for every Arduino CLI call Barmaja makes.
|
|
86
|
+
|
|
87
|
+
- English output, so memory reports and error translation are stable on
|
|
88
|
+
Arabic/French/... Windows installations;
|
|
89
|
+
- no network update notification (offline classrooms);
|
|
90
|
+
- an ASCII-only build cache (non-ASCII Windows user names break some AVR tools);
|
|
91
|
+
- when the pinned Full-Pack CLI is used, ignore inherited ``ARDUINO_*``
|
|
92
|
+
variables so another Arduino/ESP32 installation cannot redirect Barmaja
|
|
93
|
+
away from its tested offline runtime.
|
|
94
|
+
|
|
95
|
+
Only Barmaja's own subprocesses see these values; the user's Arduino IDE
|
|
96
|
+
configuration files and machine/user environment variables are never modified.
|
|
97
|
+
"""
|
|
98
|
+
env = dict(os.environ)
|
|
99
|
+
|
|
100
|
+
# The Full Offline runtime must be isolated from machine/user Arduino
|
|
101
|
+
# overrides. External/Light-Pack Arduino CLI installations keep their
|
|
102
|
+
# normal environment so Barmaja does not interfere with a user's IDE setup.
|
|
103
|
+
bundled = False
|
|
104
|
+
if executable:
|
|
105
|
+
try:
|
|
106
|
+
exe = Path(str(executable)).expanduser().resolve()
|
|
107
|
+
runtime = (ascii_runtime_root() / "runtime").resolve()
|
|
108
|
+
bundled = exe.parent == runtime and exe.name.lower() in {"arduino-cli", "arduino-cli.exe"}
|
|
109
|
+
except OSError:
|
|
110
|
+
bundled = False
|
|
111
|
+
if bundled:
|
|
112
|
+
for key in tuple(env):
|
|
113
|
+
if key.upper().startswith("ARDUINO_"):
|
|
114
|
+
env.pop(key, None)
|
|
115
|
+
|
|
116
|
+
env["ARDUINO_LOCALE"] = "en"
|
|
117
|
+
env["ARDUINO_UPDATER_ENABLE_NOTIFICATION"] = "false"
|
|
118
|
+
env["ARDUINO_METRICS_ENABLED"] = "false"
|
|
119
|
+
try:
|
|
120
|
+
cache = ascii_runtime_root() / "build-cache"
|
|
121
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
env["ARDUINO_BUILD_CACHE_PATH"] = str(cache)
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
return env
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _kill_process_tree(proc: subprocess.Popen) -> None:
|
|
129
|
+
try:
|
|
130
|
+
if os.name == "nt":
|
|
131
|
+
subprocess.run(
|
|
132
|
+
["taskkill", "/F", "/T", "/PID", str(proc.pid)],
|
|
133
|
+
capture_output=True, check=False, timeout=15,
|
|
134
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
import signal
|
|
138
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
try:
|
|
142
|
+
proc.kill()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def run_text(args: list[str], *, timeout: float = 20.0, cwd: str | Path | None = None) -> subprocess.CompletedProcess[str]:
|
|
148
|
+
"""Run an external tool with UTF-8 capture and a hard timeout.
|
|
149
|
+
|
|
150
|
+
On timeout the whole process tree (arduino-cli -> avr-gcc/avrdude) is
|
|
151
|
+
terminated so no orphaned compiler keeps build files or the COM port locked.
|
|
152
|
+
"""
|
|
153
|
+
from .errors import ToolchainError
|
|
154
|
+
kwargs: dict = dict(
|
|
155
|
+
cwd=str(cwd) if cwd else None,
|
|
156
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL,
|
|
157
|
+
text=True, encoding="utf-8", errors="replace",
|
|
158
|
+
env=arduino_cli_environment(args[0] if args else None),
|
|
159
|
+
)
|
|
160
|
+
if os.name == "nt":
|
|
161
|
+
# No console windows flashing when called from PictoBlox/IDE GUIs.
|
|
162
|
+
kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
163
|
+
else:
|
|
164
|
+
kwargs["start_new_session"] = True
|
|
165
|
+
try:
|
|
166
|
+
proc = subprocess.Popen(args, **kwargs)
|
|
167
|
+
except OSError as e:
|
|
168
|
+
raise ToolchainError(f"Could not start {Path(str(args[0])).name}: {e.strerror or e}") from e
|
|
169
|
+
try:
|
|
170
|
+
out, err = proc.communicate(timeout=timeout)
|
|
171
|
+
except subprocess.TimeoutExpired as e:
|
|
172
|
+
_kill_process_tree(proc)
|
|
173
|
+
try:
|
|
174
|
+
proc.communicate(timeout=5)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
raise ToolchainError(f"External tool timed out after {timeout:g} seconds.") from e
|
|
178
|
+
return subprocess.CompletedProcess(args, proc.returncode, out or "", err or "")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def env_seconds(name: str, default: float) -> float:
|
|
182
|
+
"""Read a timeout override without crashing on a malformed value."""
|
|
183
|
+
raw = os.environ.get(name)
|
|
184
|
+
if not raw:
|
|
185
|
+
return default
|
|
186
|
+
try:
|
|
187
|
+
value = float(raw)
|
|
188
|
+
except ValueError:
|
|
189
|
+
return default
|
|
190
|
+
return value if value > 0 else default
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True)
|
|
194
|
+
class EnvironmentFingerprint:
|
|
195
|
+
python_executable: str
|
|
196
|
+
python_version: str
|
|
197
|
+
platform: str
|
|
198
|
+
package_version: str
|
|
199
|
+
arduino_cli: str | None
|
|
200
|
+
arduino_cli_mtime_ns: int | None
|
|
201
|
+
avr_core_marker: str | None
|
|
202
|
+
library_markers: tuple[str, ...]
|
|
203
|
+
integrity_markers: tuple[str, ...] = ()
|
|
204
|
+
|
|
205
|
+
def digest(self) -> str:
|
|
206
|
+
payload = json.dumps(asdict(self), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
207
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _marker(path: Path | None) -> str | None:
|
|
211
|
+
if not path or not path.exists():
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
st = path.stat()
|
|
215
|
+
return f"{path.resolve()}|{st.st_size}|{st.st_mtime_ns}"
|
|
216
|
+
except OSError:
|
|
217
|
+
return str(path)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _sha256_file(path: Path) -> str:
|
|
221
|
+
h = hashlib.sha256()
|
|
222
|
+
with path.open("rb") as f:
|
|
223
|
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
224
|
+
h.update(chunk)
|
|
225
|
+
return h.hexdigest()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _content_marker(path: Path | None, *, source_only: bool = False) -> str | None:
|
|
229
|
+
"""Content-sensitive marker used only for cache safety.
|
|
230
|
+
|
|
231
|
+
A READY cache is allowed to skip the expensive test compile only for Barmaja's
|
|
232
|
+
pinned runtime. For its core/libraries we hash compilation-relevant source and
|
|
233
|
+
metadata, and for tool ``bin`` directories we hash every file. This deliberately
|
|
234
|
+
catches replacements that preserve file timestamps or sizes.
|
|
235
|
+
"""
|
|
236
|
+
if not path or not path.exists():
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
path = path.resolve()
|
|
240
|
+
if path.is_file():
|
|
241
|
+
return f"sha256:{path}:{_sha256_file(path)}"
|
|
242
|
+
allowed = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".s", ".S", ".txt", ".properties", ".json", ".conf", ".cfg", ".yaml", ".yml"}
|
|
243
|
+
h = hashlib.sha256()
|
|
244
|
+
count = 0
|
|
245
|
+
for child in sorted((p for p in path.rglob("*") if p.is_file()), key=lambda p: str(p.relative_to(path)).lower()):
|
|
246
|
+
if source_only and child.suffix not in allowed and child.name not in {"platform.txt", "boards.txt", "programmers.txt"}:
|
|
247
|
+
continue
|
|
248
|
+
rel = child.relative_to(path).as_posix().encode("utf-8", "surrogatepass")
|
|
249
|
+
h.update(len(rel).to_bytes(4, "big")); h.update(rel)
|
|
250
|
+
h.update(bytes.fromhex(_sha256_file(child)))
|
|
251
|
+
count += 1
|
|
252
|
+
return f"tree-sha256:{path}:{count}:{h.hexdigest()}"
|
|
253
|
+
except OSError:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _bundled_integrity_markers(cli_path: Path | None, libraries: Iterable[Path]) -> tuple[str, ...]:
|
|
258
|
+
"""Strong markers for the immutable Full-Pack runtime; empty for other CLIs."""
|
|
259
|
+
if not cli_path or not cli_path.exists():
|
|
260
|
+
return ()
|
|
261
|
+
try:
|
|
262
|
+
runtime = (ensure_runtime_dirs()["root"] / "runtime").resolve()
|
|
263
|
+
cli_resolved = cli_path.resolve()
|
|
264
|
+
if cli_resolved.parent != runtime:
|
|
265
|
+
return ()
|
|
266
|
+
except OSError:
|
|
267
|
+
return ()
|
|
268
|
+
|
|
269
|
+
paths: list[tuple[Path, bool]] = [(cli_resolved, False)]
|
|
270
|
+
cfg = runtime / "arduino-cli.yaml"
|
|
271
|
+
if cfg.exists():
|
|
272
|
+
paths.append((cfg, False))
|
|
273
|
+
|
|
274
|
+
# Pinned AVR core source/configuration.
|
|
275
|
+
core_root = runtime / "arduino-data" / "packages" / "arduino" / "hardware" / "avr"
|
|
276
|
+
if core_root.exists():
|
|
277
|
+
for version_dir in sorted(p for p in core_root.iterdir() if p.is_dir()):
|
|
278
|
+
paths.append((version_dir, True))
|
|
279
|
+
|
|
280
|
+
# Compiler and upload executables. Hash the complete bin directories so a
|
|
281
|
+
# damaged helper (cc1plus/as/ld/objcopy/size/avrdude) cannot hide behind cache.
|
|
282
|
+
tools_root = runtime / "arduino-data" / "packages" / "arduino" / "tools"
|
|
283
|
+
if tools_root.exists():
|
|
284
|
+
for family in ("avr-gcc", "avrdude"):
|
|
285
|
+
fam = tools_root / family
|
|
286
|
+
if fam.exists():
|
|
287
|
+
for version_dir in sorted(p for p in fam.iterdir() if p.is_dir()):
|
|
288
|
+
bin_dir = version_dir / "bin"
|
|
289
|
+
paths.append((bin_dir if bin_dir.exists() else version_dir, False))
|
|
290
|
+
|
|
291
|
+
# Arduino CLI builtin discovery/monitor tools are required for fully-offline
|
|
292
|
+
# board detection/monitoring. Include them in the pinned-runtime fingerprint
|
|
293
|
+
# so quarantine/corruption cannot be hidden by a stale READY cache.
|
|
294
|
+
builtin_root = runtime / "arduino-data" / "packages" / "builtin" / "tools"
|
|
295
|
+
if builtin_root.exists():
|
|
296
|
+
for family in ("serial-discovery", "mdns-discovery", "dfu-discovery", "serial-monitor"):
|
|
297
|
+
fam = builtin_root / family
|
|
298
|
+
if fam.exists():
|
|
299
|
+
for version_dir in sorted(p for p in fam.iterdir() if p.is_dir()):
|
|
300
|
+
paths.append((version_dir, False))
|
|
301
|
+
|
|
302
|
+
# Private libraries are authoritative because compile uses --library. Hash
|
|
303
|
+
# their source trees, not merely library.properties.
|
|
304
|
+
roots: set[Path] = set()
|
|
305
|
+
for marker in libraries:
|
|
306
|
+
p = Path(marker)
|
|
307
|
+
root = p.parent if p.name == "library.properties" else p
|
|
308
|
+
try:
|
|
309
|
+
roots.add(root.resolve())
|
|
310
|
+
except OSError:
|
|
311
|
+
roots.add(root)
|
|
312
|
+
for root in sorted(roots, key=lambda p: str(p).lower()):
|
|
313
|
+
paths.append((root, True))
|
|
314
|
+
|
|
315
|
+
result=[]
|
|
316
|
+
for p, source_only in paths:
|
|
317
|
+
marker = _content_marker(p, source_only=source_only)
|
|
318
|
+
if marker:
|
|
319
|
+
result.append(marker)
|
|
320
|
+
return tuple(result)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def make_fingerprint(arduino_cli: str | None, core_marker: Path | None = None, libraries: Iterable[Path] = ()) -> EnvironmentFingerprint:
|
|
324
|
+
cli_path = Path(arduino_cli) if arduino_cli else None
|
|
325
|
+
mtime = None
|
|
326
|
+
if cli_path and cli_path.exists():
|
|
327
|
+
try:
|
|
328
|
+
mtime = cli_path.stat().st_mtime_ns
|
|
329
|
+
except OSError:
|
|
330
|
+
pass
|
|
331
|
+
libs = tuple(libraries)
|
|
332
|
+
return EnvironmentFingerprint(
|
|
333
|
+
python_executable=str(Path(sys.executable).resolve()),
|
|
334
|
+
python_version=".".join(map(str, sys.version_info[:3])),
|
|
335
|
+
platform=f"{platform.system()}-{platform.machine()}",
|
|
336
|
+
package_version=package_version(),
|
|
337
|
+
arduino_cli=str(cli_path.resolve()) if cli_path and cli_path.exists() else arduino_cli,
|
|
338
|
+
arduino_cli_mtime_ns=mtime,
|
|
339
|
+
avr_core_marker=_marker(core_marker),
|
|
340
|
+
library_markers=tuple(sorted(filter(None, (_marker(p) for p in libs)))),
|
|
341
|
+
integrity_markers=_bundled_integrity_markers(cli_path, libs),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cache_file() -> Path:
|
|
346
|
+
p = ensure_runtime_dirs()["cache"] / "environment.json"
|
|
347
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
return p
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def load_cache() -> dict | None:
|
|
352
|
+
p = cache_file()
|
|
353
|
+
if not p.exists():
|
|
354
|
+
return None
|
|
355
|
+
try:
|
|
356
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
357
|
+
if data.get("schema") != CACHE_SCHEMA:
|
|
358
|
+
return None
|
|
359
|
+
return data
|
|
360
|
+
except Exception:
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def save_cache(fingerprint: EnvironmentFingerprint, *, quick_fingerprint: EnvironmentFingerprint | None = None, status: str = "READY", source: str | None = None) -> None:
|
|
365
|
+
payload = {
|
|
366
|
+
"schema": CACHE_SCHEMA,
|
|
367
|
+
"status": status,
|
|
368
|
+
"fingerprint": fingerprint.digest(),
|
|
369
|
+
"quick_fingerprint": (quick_fingerprint or fingerprint).digest(),
|
|
370
|
+
"source": source,
|
|
371
|
+
}
|
|
372
|
+
p = cache_file()
|
|
373
|
+
tmp = p.with_suffix(".tmp")
|
|
374
|
+
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
375
|
+
tmp.replace(p)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def invalidate_cache() -> None:
|
|
379
|
+
try:
|
|
380
|
+
cache_file().unlink(missing_ok=True)
|
|
381
|
+
except OSError:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def find_normal_arduino_cli() -> str | None:
|
|
386
|
+
override = os.environ.get("BARMAJA_ARDUINO_CLI")
|
|
387
|
+
if override and Path(override).exists():
|
|
388
|
+
return str(Path(override).resolve())
|
|
389
|
+
found = shutil.which("arduino-cli")
|
|
390
|
+
if found:
|
|
391
|
+
return found
|
|
392
|
+
candidates: list[Path] = []
|
|
393
|
+
if os.name == "nt":
|
|
394
|
+
ide_cli = Path("Arduino IDE") / "resources" / "app" / "lib" / "backend" / "resources" / "arduino-cli.exe"
|
|
395
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
396
|
+
if local:
|
|
397
|
+
# Arduino IDE 2 "install for me only" (default) location.
|
|
398
|
+
candidates.append(Path(local) / "Programs" / ide_cli)
|
|
399
|
+
for env_name in ("LOCALAPPDATA", "PROGRAMFILES", "PROGRAMFILES(X86)"):
|
|
400
|
+
base = os.environ.get(env_name)
|
|
401
|
+
if base:
|
|
402
|
+
candidates.extend([
|
|
403
|
+
Path(base) / ide_cli,
|
|
404
|
+
Path(base) / "Arduino15" / "arduino-cli.exe",
|
|
405
|
+
])
|
|
406
|
+
else:
|
|
407
|
+
candidates.extend([Path("/usr/bin/arduino-cli"), Path("/usr/local/bin/arduino-cli")])
|
|
408
|
+
for p in candidates:
|
|
409
|
+
if p.exists():
|
|
410
|
+
return str(p.resolve())
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def pictoblox_root_candidates() -> tuple[Path, ...]:
|
|
415
|
+
roots: list[Path] = []
|
|
416
|
+
explicit = os.environ.get("BARMAJA_PICTOBLOX_ROOT")
|
|
417
|
+
if explicit:
|
|
418
|
+
p = Path(explicit).expanduser()
|
|
419
|
+
if p.exists(): roots.append(p.resolve())
|
|
420
|
+
if os.name == "nt":
|
|
421
|
+
for env_name in ("PROGRAMFILES", "PROGRAMFILES(X86)"):
|
|
422
|
+
base = os.environ.get(env_name)
|
|
423
|
+
if base:
|
|
424
|
+
p = Path(base) / "PictoBlox"
|
|
425
|
+
if p.exists(): roots.append(p.resolve())
|
|
426
|
+
unique=[]; seen=set()
|
|
427
|
+
for p in roots:
|
|
428
|
+
k=os.path.normcase(str(p))
|
|
429
|
+
if k not in seen: unique.append(p); seen.add(k)
|
|
430
|
+
return tuple(unique)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def find_pictoblox_arduino_cli() -> str | None:
|
|
434
|
+
"""Return a PictoBlox-bundled Arduino CLI only when an actual executable is found.
|
|
435
|
+
|
|
436
|
+
We intentionally do not infer compatibility from PictoBlox being installed. The
|
|
437
|
+
caller must still run version/core/library/capability checks before reuse.
|
|
438
|
+
"""
|
|
439
|
+
explicit = os.environ.get("BARMAJA_PICTOBLOX_ARDUINO_CLI")
|
|
440
|
+
if explicit and Path(explicit).exists():
|
|
441
|
+
return str(Path(explicit).resolve())
|
|
442
|
+
rels = (
|
|
443
|
+
Path("resources") / "arduino-cli.exe",
|
|
444
|
+
Path("resources") / "app" / "arduino-cli.exe",
|
|
445
|
+
Path("resources") / "backend" / "arduino-cli.exe",
|
|
446
|
+
Path("resources") / "arduino" / "arduino-cli.exe",
|
|
447
|
+
)
|
|
448
|
+
for root in pictoblox_root_candidates():
|
|
449
|
+
for rel in rels:
|
|
450
|
+
p = root / rel
|
|
451
|
+
if p.exists(): return str(p.resolve())
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def windows_documents_dir() -> Path | None:
|
|
456
|
+
"""The real Documents folder (Known Folder API), including OneDrive/GPO redirection."""
|
|
457
|
+
if os.name != "nt":
|
|
458
|
+
return None
|
|
459
|
+
try:
|
|
460
|
+
import ctypes
|
|
461
|
+
import uuid
|
|
462
|
+
from ctypes import wintypes
|
|
463
|
+
|
|
464
|
+
class _GUID(ctypes.Structure):
|
|
465
|
+
_fields_ = [("Data1", wintypes.DWORD), ("Data2", wintypes.WORD), ("Data3", wintypes.WORD), ("Data4", ctypes.c_ubyte * 8)]
|
|
466
|
+
|
|
467
|
+
guid = _GUID()
|
|
468
|
+
ctypes.memmove(ctypes.byref(guid), uuid.UUID("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}").bytes_le, 16)
|
|
469
|
+
out = ctypes.c_wchar_p()
|
|
470
|
+
hr = ctypes.windll.shell32.SHGetKnownFolderPath(ctypes.byref(guid), 0, None, ctypes.byref(out))
|
|
471
|
+
try:
|
|
472
|
+
if hr != 0 or not out.value:
|
|
473
|
+
return None
|
|
474
|
+
return Path(out.value)
|
|
475
|
+
finally:
|
|
476
|
+
ctypes.windll.ole32.CoTaskMemFree(out)
|
|
477
|
+
except Exception:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def other_windows_documents_candidates(users_root: str | Path | None = None) -> tuple[Path, ...]:
|
|
482
|
+
"""Best-effort Documents locations for other local Windows user profiles.
|
|
483
|
+
|
|
484
|
+
This supplements the Known Folder API (current user only) for school PCs where
|
|
485
|
+
an administrator installs PictoBlox support for several student accounts. We
|
|
486
|
+
only return directories that already exist; the caller still decides whether a
|
|
487
|
+
PictoBlox module target exists beneath them.
|
|
488
|
+
"""
|
|
489
|
+
if users_root is None:
|
|
490
|
+
override = os.environ.get("BARMAJA_WINDOWS_USERS_ROOT")
|
|
491
|
+
if override:
|
|
492
|
+
users = Path(override)
|
|
493
|
+
else:
|
|
494
|
+
drive = os.environ.get("SystemDrive", "C:")
|
|
495
|
+
users = Path(drive + "\\Users")
|
|
496
|
+
else:
|
|
497
|
+
users = Path(users_root)
|
|
498
|
+
found: list[Path] = []
|
|
499
|
+
try:
|
|
500
|
+
profiles = [p for p in users.iterdir() if p.is_dir()]
|
|
501
|
+
except OSError:
|
|
502
|
+
return ()
|
|
503
|
+
for profile in profiles:
|
|
504
|
+
direct = profile / "Documents"
|
|
505
|
+
if direct.is_dir():
|
|
506
|
+
try: found.append(direct.resolve())
|
|
507
|
+
except OSError: found.append(direct)
|
|
508
|
+
try:
|
|
509
|
+
children = [p for p in profile.iterdir() if p.is_dir() and p.name.lower().startswith("onedrive")]
|
|
510
|
+
except OSError:
|
|
511
|
+
children = []
|
|
512
|
+
for cloud in children:
|
|
513
|
+
docs = cloud / "Documents"
|
|
514
|
+
if docs.is_dir():
|
|
515
|
+
try: found.append(docs.resolve())
|
|
516
|
+
except OSError: found.append(docs)
|
|
517
|
+
unique=[]; seen=set()
|
|
518
|
+
for p in found:
|
|
519
|
+
key=os.path.normcase(str(p))
|
|
520
|
+
if key not in seen:
|
|
521
|
+
unique.append(p); seen.add(key)
|
|
522
|
+
return tuple(unique)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def documents_dir_candidates() -> tuple[Path, ...]:
|
|
526
|
+
candidates: list[Path] = []
|
|
527
|
+
known = windows_documents_dir()
|
|
528
|
+
if known:
|
|
529
|
+
candidates.append(known)
|
|
530
|
+
for env_name in ("OneDrive", "OneDriveConsumer", "OneDriveCommercial"):
|
|
531
|
+
base = os.environ.get(env_name)
|
|
532
|
+
if base:
|
|
533
|
+
candidates.append(Path(base) / "Documents")
|
|
534
|
+
userprofile = os.environ.get("USERPROFILE")
|
|
535
|
+
if userprofile:
|
|
536
|
+
candidates.append(Path(userprofile) / "Documents")
|
|
537
|
+
candidates.append(Path.home() / "Documents")
|
|
538
|
+
if os.name == "nt":
|
|
539
|
+
candidates.extend(other_windows_documents_candidates())
|
|
540
|
+
unique=[]; seen=set()
|
|
541
|
+
for p in candidates:
|
|
542
|
+
try: p=p.resolve()
|
|
543
|
+
except OSError: pass
|
|
544
|
+
key=os.path.normcase(str(p))
|
|
545
|
+
if key not in seen:
|
|
546
|
+
unique.append(p); seen.add(key)
|
|
547
|
+
return tuple(unique)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _running_pictoblox_modules_dir() -> Path | None:
|
|
551
|
+
"""When imported from PictoBlox, the folder this package was loaded from."""
|
|
552
|
+
here = Path(__file__).resolve().parent.parent
|
|
553
|
+
return here if here.name.lower() == "python_modules" else None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def pictoblox_module_dirs() -> tuple[Path, ...]:
|
|
557
|
+
found: list[Path] = []
|
|
558
|
+
override = os.environ.get("BARMAJA_PICTOBLOX_MODULES")
|
|
559
|
+
if override:
|
|
560
|
+
p = Path(override).expanduser()
|
|
561
|
+
# An explicit target is authoritative even before python_modules exists;
|
|
562
|
+
# the offline installer is responsible for creating it.
|
|
563
|
+
try: found.append(p.resolve())
|
|
564
|
+
except OSError: found.append(p)
|
|
565
|
+
running = _running_pictoblox_modules_dir()
|
|
566
|
+
if running is not None:
|
|
567
|
+
found.append(running)
|
|
568
|
+
for docs in documents_dir_candidates():
|
|
569
|
+
for name in ("Pictoblox", "PictoBlox"):
|
|
570
|
+
p = docs / name / "python_modules"
|
|
571
|
+
try:
|
|
572
|
+
# Existing python_modules is ideal; an existing PictoBlox folder
|
|
573
|
+
# is also enough for the offline installer to create python_modules.
|
|
574
|
+
if p.exists() or p.parent.exists():
|
|
575
|
+
found.append(p.resolve())
|
|
576
|
+
except OSError:
|
|
577
|
+
continue
|
|
578
|
+
unique = []
|
|
579
|
+
seen = set()
|
|
580
|
+
for p in found:
|
|
581
|
+
key = os.path.normcase(str(p))
|
|
582
|
+
if key not in seen:
|
|
583
|
+
unique.append(p); seen.add(key)
|
|
584
|
+
return tuple(unique)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def installed_package_version(package_dir: Path) -> str | None:
|
|
588
|
+
"""Read __version__ from an installed Barmaja_PyArduino copy without importing it."""
|
|
589
|
+
import re
|
|
590
|
+
try:
|
|
591
|
+
text = (package_dir / "__init__.py").read_text(encoding="utf-8", errors="replace")
|
|
592
|
+
except OSError:
|
|
593
|
+
return None
|
|
594
|
+
m = re.search(r"^__version__\s*=\s*[\"']([^\"']+)[\"']", text, re.M)
|
|
595
|
+
return m.group(1) if m else None
|