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,83 @@
1
+ {
2
+ "schema": 1,
3
+ "class": "ShiftRegister595",
4
+ "title": "74HC595 shift register (8 extra outputs from 3 pins)",
5
+ "status": "compile-verified",
6
+ "boards": [
7
+ "uno",
8
+ "nano"
9
+ ],
10
+ "constructor": [
11
+ {
12
+ "name": "data_pin",
13
+ "type": "Int",
14
+ "rule": "digital_pin"
15
+ },
16
+ {
17
+ "name": "clock_pin",
18
+ "type": "Int",
19
+ "rule": "digital_pin"
20
+ },
21
+ {
22
+ "name": "latch_pin",
23
+ "type": "Int",
24
+ "rule": "digital_pin"
25
+ }
26
+ ],
27
+ "methods": {
28
+ "write": {
29
+ "params": [
30
+ {
31
+ "name": "value",
32
+ "type": "Int",
33
+ "range": [
34
+ 0,
35
+ 255
36
+ ]
37
+ }
38
+ ],
39
+ "returns": "Void",
40
+ "requires_begin": false
41
+ },
42
+ "setPin": {
43
+ "params": [
44
+ {
45
+ "name": "output",
46
+ "type": "Int",
47
+ "range": [
48
+ 0,
49
+ 7
50
+ ]
51
+ },
52
+ {
53
+ "name": "on",
54
+ "type": "Bool"
55
+ }
56
+ ],
57
+ "returns": "Void",
58
+ "requires_begin": false
59
+ },
60
+ "clear": {
61
+ "params": [],
62
+ "returns": "Void",
63
+ "requires_begin": false
64
+ }
65
+ },
66
+ "resources": [],
67
+ "warnings": [],
68
+ "adapter": {
69
+ "library": "Barmaja_ShiftRegister595",
70
+ "header": "Barmaja_ShiftRegister595.h"
71
+ },
72
+ "dependencies": [],
73
+ "liverun": false,
74
+ "example": "from Barmaja_PyArduino import *\nupload()\n\nleds = ShiftRegister595(11, 12, 8)\nwhile True:\n for k in range(8):\n leds.clear()\n leds.setPin(k, True)\n delay(150)\n",
75
+ "pins": {
76
+ "forbid": [
77
+ 0,
78
+ 1
79
+ ],
80
+ "distinct": true
81
+ },
82
+ "notes": "No library needed."
83
+ }
@@ -0,0 +1,9 @@
1
+ name=Barmaja_TemperatureDS18B20
2
+ version=1.1.0
3
+ author=BARMAJA
4
+ maintainer=BARMAJA
5
+ sentence=BARMAJA PyArduino device-pack adapter: DS18B20 digital temperature sensor (one-wire)
6
+ paragraph=
7
+ category=Device Control
8
+ url=
9
+ architectures=avr
@@ -0,0 +1,16 @@
1
+ #pragma once
2
+ #include <Arduino.h>
3
+ #include <OneWire.h>
4
+ #include <DallasTemperature.h>
5
+ // Data pin needs a 4.7k pull-up resistor to 5V. Reads the first sensor on the bus.
6
+ // Returns -127.0 when no sensor answers.
7
+ class TemperatureDS18B20 {
8
+ public:
9
+ explicit TemperatureDS18B20(uint8_t pin): wire_(pin), sensors_(&wire_) {}
10
+ void begin() { sensors_.begin(); }
11
+ float readCelsius() { sensors_.requestTemperatures(); return sensors_.getTempCByIndex(0); }
12
+ float readFahrenheit() { sensors_.requestTemperatures(); return sensors_.getTempFByIndex(0); }
13
+ private:
14
+ OneWire wire_;
15
+ DallasTemperature sensors_;
16
+ };
@@ -0,0 +1,8 @@
1
+ from Barmaja_PyArduino import *
2
+ upload()
3
+
4
+ temp = TemperatureDS18B20(4)
5
+ temp.begin()
6
+ while True:
7
+ print("Temperature =", temp.readCelsius(), "C")
8
+ delay(1000)
@@ -0,0 +1,63 @@
1
+ {
2
+ "schema": 1,
3
+ "class": "TemperatureDS18B20",
4
+ "title": "DS18B20 digital temperature sensor (one-wire)",
5
+ "status": "compile-verified",
6
+ "boards": [
7
+ "uno",
8
+ "nano"
9
+ ],
10
+ "constructor": [
11
+ {
12
+ "name": "pin",
13
+ "type": "Int",
14
+ "rule": "digital_pin"
15
+ }
16
+ ],
17
+ "methods": {
18
+ "begin": {
19
+ "params": [],
20
+ "returns": "Void",
21
+ "starts": true
22
+ },
23
+ "readCelsius": {
24
+ "params": [],
25
+ "returns": "Float"
26
+ },
27
+ "readFahrenheit": {
28
+ "params": [],
29
+ "returns": "Float"
30
+ }
31
+ },
32
+ "resources": [],
33
+ "warnings": [],
34
+ "adapter": {
35
+ "library": "Barmaja_TemperatureDS18B20",
36
+ "header": "Barmaja_TemperatureDS18B20.h"
37
+ },
38
+ "dependencies": [
39
+ {
40
+ "id": "paulstoffregen-onewire",
41
+ "name": "OneWire",
42
+ "version": "2.3.8",
43
+ "header": "OneWire.h",
44
+ "license": "MIT-style (PJRC)",
45
+ "source": "https://github.com/PaulStoffregen/OneWire",
46
+ "library_folder": "OneWire",
47
+ "bundled": true
48
+ },
49
+ {
50
+ "id": "milesburton-dallastemperature",
51
+ "name": "DallasTemperature",
52
+ "version": "4.0.6",
53
+ "header": "DallasTemperature.h",
54
+ "license": "MIT",
55
+ "source": "https://github.com/milesburton/Arduino-Temperature-Control-Library",
56
+ "tag": "v4.0.6",
57
+ "library_folder": "DallasTemperature"
58
+ }
59
+ ],
60
+ "liverun": false,
61
+ "example": "from Barmaja_PyArduino import *\nupload()\n\ntemp = TemperatureDS18B20(4)\ntemp.begin()\nwhile True:\n print(\"Temperature =\", temp.readCelsius(), \"C\")\n delay(1000)\n",
62
+ "notes": "Each read waits ~750 ms for the sensor conversion."
63
+ }
@@ -0,0 +1,406 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from .environment import (
12
+ ensure_runtime_dirs, find_normal_arduino_cli, invalidate_cache, load_cache, make_fingerprint,
13
+ pictoblox_module_dirs, run_text, save_cache,
14
+ )
15
+
16
+ def _cli_cmd(cli: str, *args: str) -> list[str]:
17
+ cfg = os.environ.get("BARMAJA_ARDUINO_CONFIG")
18
+ prefix = [cli]
19
+ if cfg and Path(cfg).exists():
20
+ prefix += ["--config-file", cfg]
21
+ else:
22
+ bundled = ensure_runtime_dirs()["root"] / "runtime" / "arduino-cli.yaml"
23
+ try:
24
+ if bundled.exists() and Path(cli).resolve().parent == bundled.parent.resolve():
25
+ prefix += ["--config-file", str(bundled)]
26
+ except OSError:
27
+ pass
28
+ return [*prefix, *args]
29
+
30
+
31
+ REQUIRED_LIBRARIES = {
32
+ "Servo": "1.3.0",
33
+ "Ultrasonic": "3.0.0",
34
+ "DHT sensor library": "1.4.7",
35
+ "Adafruit Unified Sensor": "1.1.15",
36
+ "LiquidCrystal I2C": "1.1.4",
37
+ }
38
+
39
+ @dataclass(frozen=True)
40
+ class DoctorItem:
41
+ key: str
42
+ label: str
43
+ status: str # OK/WARN/ERROR
44
+ detail: str
45
+
46
+ @dataclass(frozen=True)
47
+ class DoctorReport:
48
+ items: tuple[DoctorItem, ...]
49
+ source: str | None = None
50
+
51
+ @property
52
+ def ok(self) -> bool:
53
+ return not any(i.status == "ERROR" for i in self.items)
54
+
55
+ @property
56
+ def problems(self) -> tuple[DoctorItem, ...]:
57
+ return tuple(i for i in self.items if i.status != "OK")
58
+
59
+ def concise(self) -> str:
60
+ if self.ok and not self.problems:
61
+ return "ALL OK"
62
+ if self.ok:
63
+ return "\n".join(f"Warning: {i.label}: {i.detail}" for i in self.problems)
64
+ return "\n".join(f"- {i.label}: {i.detail}" for i in self.problems)
65
+
66
+ def detailed(self) -> str:
67
+ lines = []
68
+ for i in self.items:
69
+ lines.append(f"{i.label:<28} {i.status:<5} {i.detail}")
70
+ if self.source:
71
+ lines.append(f"Environment source {self.source}")
72
+ return "\n".join(lines)
73
+
74
+
75
+ def _cli_version(cli: str) -> tuple[str | None, DoctorItem]:
76
+ result = run_text(_cli_cmd(cli, "version", "--format", "json"), timeout=8)
77
+ if result.returncode != 0:
78
+ result = run_text(_cli_cmd(cli, "version"), timeout=8)
79
+ text = (result.stdout + "\n" + result.stderr).strip()
80
+ if result.returncode != 0:
81
+ return None, DoctorItem("arduino_cli", "Arduino CLI", "ERROR", "found but could not run")
82
+ m = re.search(r"(?:Version:|version\"\s*:\s*\")?v?(\d+\.\d+\.\d+)", text, re.I)
83
+ version = m.group(1) if m else "unknown"
84
+ return version, DoctorItem("arduino_cli", "Arduino CLI", "OK", version)
85
+
86
+
87
+ def _board_core(cli: str) -> tuple[Path | None, DoctorItem]:
88
+ result = run_text(_cli_cmd(cli, "core", "list", "--format", "json"), timeout=12)
89
+ if result.returncode != 0:
90
+ return None, DoctorItem("avr_core", "Arduino Uno AVR Core", "ERROR", "not available")
91
+ try:
92
+ payload = json.loads(result.stdout or "[]")
93
+ except json.JSONDecodeError:
94
+ payload = []
95
+ rows = payload if isinstance(payload, list) else payload.get("platforms", []) if isinstance(payload, dict) else []
96
+ for row in rows:
97
+ ident = row.get("id") or row.get("ID") or row.get("platform")
98
+ ver = row.get("installed_version") or row.get("version") or row.get("Installed")
99
+ if not ver and isinstance(row.get("releases"), dict):
100
+ for rv, meta in row["releases"].items():
101
+ if isinstance(meta, dict) and meta.get("installed") is True:
102
+ ver = rv; break
103
+ if ident == "arduino:avr":
104
+ status = "OK" if str(ver) == "1.8.8" else "WARN"
105
+ return None, DoctorItem("avr_core", "Arduino Uno AVR Core", status, str(ver or "unknown"))
106
+ return None, DoctorItem("avr_core", "Arduino Uno AVR Core", "ERROR", "missing")
107
+
108
+
109
+ def _libraries(cli: str) -> tuple[list[Path], list[DoctorItem]]:
110
+ result = run_text(_cli_cmd(cli, "lib", "list", "--format", "json"), timeout=15)
111
+ items: list[DoctorItem] = []
112
+ markers: list[Path] = []
113
+ if result.returncode != 0:
114
+ return markers, [DoctorItem("libraries", "Arduino libraries", "ERROR", "could not query libraries")]
115
+ try:
116
+ payload = json.loads(result.stdout or "[]")
117
+ except json.JSONDecodeError:
118
+ payload = []
119
+ rows = payload if isinstance(payload, list) else payload.get("installed_libraries", []) if isinstance(payload, dict) else []
120
+ available = {}
121
+ aliases = {"liquidcrystal_i2c": "LiquidCrystal I2C", "liquidcrystal i2c": "LiquidCrystal I2C"}
122
+ for row in rows:
123
+ lib = row.get("library", row) if isinstance(row, dict) else {}
124
+ name = lib.get("name")
125
+ version = lib.get("version")
126
+ install_dir = lib.get("install_dir")
127
+ if name:
128
+ canonical = aliases.get(str(name).lower(), name)
129
+ available[canonical] = (str(version or "unknown"), Path(install_dir) if install_dir else None)
130
+ if install_dir:
131
+ markers.append(Path(install_dir) / "library.properties")
132
+ # Merge Barmaja-private Light/Full-pack libraries; these intentionally take precedence.
133
+ private = ensure_runtime_dirs()["root"] / "arduino-libraries"
134
+ if private.exists():
135
+ for props in private.glob("*/library.properties"):
136
+ data={}
137
+ for line in props.read_text(encoding="utf-8", errors="replace").splitlines():
138
+ if "=" in line:
139
+ k,v=line.split("=",1); data[k.strip()]=v.strip()
140
+ n=data.get("name"); v=data.get("version","unknown")
141
+ if n:
142
+ canonical=aliases.get(n.lower(), n); available[canonical]=(v, props.parent); markers.append(props)
143
+ for name, expected in REQUIRED_LIBRARIES.items():
144
+ entry = available.get(name)
145
+ got = entry[0] if entry else None
146
+ if got is None:
147
+ items.append(DoctorItem(f"lib:{name}", name, "ERROR", f"missing (need {expected})"))
148
+ elif got == expected:
149
+ items.append(DoctorItem(f"lib:{name}", name, "OK", got))
150
+ else:
151
+ items.append(DoctorItem(f"lib:{name}", name, "WARN", f"{got}; tested version is {expected}"))
152
+ return markers, items
153
+
154
+
155
+ def _compile_capability(cli: str, verbose: bool = False) -> DoctorItem:
156
+ """Real Uno test compile with the same library pinning as student builds.
157
+
158
+ The sketch lives under the ASCII-only Barmaja runtime root (not %TEMP%, which
159
+ contains the Windows user name) so non-ASCII user names do not break the test.
160
+ """
161
+ import tempfile
162
+ from .toolchain import _private_library_args
163
+ build_root = ensure_runtime_dirs()["build"]
164
+ with tempfile.TemporaryDirectory(prefix="doctor_", dir=str(build_root)) as td:
165
+ root = Path(td); sketch = root / "DoctorBlink"; sketch.mkdir()
166
+ (sketch / "DoctorBlink.ino").write_text(
167
+ "void setup(){pinMode(13,OUTPUT);}\nvoid loop(){digitalWrite(13,HIGH);delay(10);digitalWrite(13,LOW);delay(10);}\n",
168
+ encoding="utf-8",
169
+ )
170
+ args = _cli_cmd(cli, "compile", "--fqbn", "arduino:avr:uno", "--warnings", "all")
171
+ args += _private_library_args()
172
+ args += ["--build-path", str(root / "build"), str(sketch)]
173
+ result = run_text(args, timeout=180)
174
+ if result.returncode == 0:
175
+ return DoctorItem("compile_test", "Uno test compile", "OK", "PASS")
176
+ if verbose:
177
+ tail = " | ".join(line.strip() for line in ((result.stdout or "") + "\n" + (result.stderr or "")).strip().splitlines()[-6:])
178
+ return DoctorItem("compile_test", "Uno test compile", "ERROR", f"failed: {tail[:900]}")
179
+ return DoctorItem("compile_test", "Uno test compile", "ERROR", "failed; run 'pyarduino doctor --details' for the compiler output")
180
+
181
+
182
+ def _running_in_pictoblox() -> bool:
183
+ exe = (sys.executable or "").lower().replace("\\", "/")
184
+ return "pictoblox" in exe
185
+
186
+
187
+ def _pictoblox_python_support(version_info):
188
+ """Return (supported, detail) for the embedded PictoBlox interpreter."""
189
+ major, minor, micro = int(version_info[0]), int(version_info[1]), int(version_info[2])
190
+ normal_ok = (major, minor) >= (3, 10) # 3.10 and every newer Python
191
+ pictoblox_compat_ok = (major, minor) == (3, 8)
192
+ ok = normal_ok or pictoblox_compat_ok
193
+ if pictoblox_compat_ok:
194
+ detail = f"{major}.{minor}.{micro} (PictoBlox compatibility host)"
195
+ elif normal_ok:
196
+ detail = f"{major}.{minor}.{micro}"
197
+ else:
198
+ detail = f"{major}.{minor}.{micro} (supported: PictoBlox 3.8.x or normal Python 3.10 or newer)"
199
+ return ok, detail
200
+
201
+
202
+ def _pictoblox_frontend_self_test() -> DoctorItem:
203
+ """Compile a tiny list-index program through the real Python frontend.
204
+
205
+ This specifically protects the mandatory PictoBlox Python 3.8 host, whose
206
+ AST shape differs from Python 3.9+ for ``a[i]``.
207
+ """
208
+ from .compiler import Transpiler
209
+ source = """from Barmaja_PyArduino import *
210
+
211
+ leds = [2, 3, 4]
212
+ i = 0
213
+ pinMode(leds[i], OUTPUT)
214
+
215
+ while True:
216
+ digitalWrite(leds[i], HIGH)
217
+ i = (i + 1) % 3
218
+ """
219
+ try:
220
+ Transpiler().compile_text(source)
221
+ except Exception as exc:
222
+ return DoctorItem(
223
+ "pictoblox_frontend",
224
+ "PictoBlox Python frontend",
225
+ "ERROR",
226
+ f"list-index self-test failed: {exc}",
227
+ )
228
+ return DoctorItem("pictoblox_frontend", "PictoBlox Python frontend", "OK", "list indexing PASS")
229
+
230
+
231
+ def _pictoblox_items() -> tuple[DoctorItem, ...]:
232
+ from . import __version__ as current
233
+ from .environment import installed_package_version
234
+ items: list[DoctorItem] = []
235
+ inside = _running_in_pictoblox()
236
+ if inside:
237
+ py = sys.version_info
238
+ # PictoBlox V1 embeds CPython 3.8.10 on current Windows releases.
239
+ # Normal Python is a 3.10-and-newer product target, but the mandatory
240
+ # PictoBlox host is a deliberate compatibility exception.
241
+ ok, detail = _pictoblox_python_support(py)
242
+ items.append(DoctorItem("pictoblox_python", "PictoBlox Python", "OK" if ok else "ERROR", detail))
243
+ here = Path(__file__).resolve().parent
244
+ items.append(DoctorItem("pictoblox_loaded", "Barmaja_PyArduino loaded from", "OK", f"{here} (version {current})"))
245
+ items.append(_pictoblox_frontend_self_test())
246
+ copies = []
247
+ for entry in sys.path:
248
+ try:
249
+ candidate = Path(entry or ".").resolve() / "Barmaja_PyArduino"
250
+ if (candidate / "__init__.py").exists() and candidate.resolve() != here:
251
+ copies.append(f"{candidate} ({installed_package_version(candidate) or 'unknown'})")
252
+ except OSError:
253
+ continue
254
+ if copies:
255
+ items.append(DoctorItem("pictoblox_duplicates", "Other Barmaja_PyArduino copies", "WARN", "; ".join(copies)))
256
+ dirs = pictoblox_module_dirs()
257
+ if not dirs:
258
+ items.append(DoctorItem("pictoblox", "PictoBlox", "ERROR" if not inside else "WARN",
259
+ "python_modules directory not detected (Documents\\PictoBlox\\python_modules)"))
260
+ return tuple(items)
261
+ if len(dirs) > 1:
262
+ items.append(DoctorItem(
263
+ "pictoblox_multiple_dirs", "Multiple PictoBlox module directories", "WARN",
264
+ "; ".join(str(p) for p in dirs),
265
+ ))
266
+ for p in dirs:
267
+ pkg = p / "Barmaja_PyArduino"
268
+ exists = p.is_dir()
269
+ writable = os.access(p, os.W_OK) if exists else os.access(p.parent, os.W_OK)
270
+ items.append(DoctorItem("pictoblox", "PictoBlox modules", "OK" if exists else "WARN",
271
+ str(p) if exists else f"{p} (will be created by offline installer)"))
272
+ items.append(DoctorItem("pictoblox_write", "PictoBlox modules writable", "OK" if writable else "ERROR", "yes" if writable else "no"))
273
+ if not pkg.exists():
274
+ items.append(DoctorItem("pictoblox_import", "Barmaja_PyArduino in PictoBlox", "ERROR", "not installed; run the Barmaja offline installer"))
275
+ continue
276
+ installed = installed_package_version(pkg)
277
+ if installed != current:
278
+ items.append(DoctorItem("pictoblox_import", "Barmaja_PyArduino in PictoBlox", "ERROR",
279
+ f"version {installed or 'unknown'} installed, {current} expected; run the Barmaja offline installer again"))
280
+ else:
281
+ items.append(DoctorItem("pictoblox_import", "Barmaja_PyArduino in PictoBlox", "OK", f"installed ({installed})"))
282
+ return tuple(items)
283
+
284
+
285
+ def _bundled_discovery_tools_item(cli: str) -> DoctorItem | None:
286
+ """Check the four Arduino CLI builtin helpers shipped by the Full Pack."""
287
+ try:
288
+ cli_path = Path(cli).resolve()
289
+ runtime = (ensure_runtime_dirs()["root"] / "runtime").resolve()
290
+ if cli_path.parent != runtime:
291
+ return None
292
+ except OSError:
293
+ return None
294
+
295
+ required = {
296
+ "serial-discovery": "serial-discovery.exe" if os.name == "nt" else "serial-discovery",
297
+ "mdns-discovery": "mdns-discovery.exe" if os.name == "nt" else "mdns-discovery",
298
+ "dfu-discovery": "dfu-discovery.exe" if os.name == "nt" else "dfu-discovery",
299
+ "serial-monitor": "serial-monitor.exe" if os.name == "nt" else "serial-monitor",
300
+ }
301
+ tools_root = runtime / "arduino-data" / "packages" / "builtin" / "tools"
302
+ missing = []
303
+ for family, exe_name in required.items():
304
+ family_root = tools_root / family
305
+ found = any((version_dir / exe_name).is_file() for version_dir in family_root.iterdir()) if family_root.is_dir() else False
306
+ if not found:
307
+ missing.append(family)
308
+ if missing:
309
+ return DoctorItem(
310
+ "discovery_tools",
311
+ "Arduino USB discovery tools",
312
+ "ERROR",
313
+ "missing: " + ", ".join(missing) + "; reinstall the Full Offline package",
314
+ )
315
+ return DoctorItem("discovery_tools", "Arduino USB discovery tools", "OK", "present")
316
+
317
+
318
+ def run_doctor(*, details: bool = False, pictoblox: bool = False, use_cache: bool = True) -> DoctorReport:
319
+ if pictoblox:
320
+ return DoctorReport(_pictoblox_items(), source="PictoBlox")
321
+
322
+ try:
323
+ from .toolchain import find_arduino_cli
324
+ cli = find_arduino_cli()
325
+ except Exception:
326
+ cli = None
327
+ items: list[DoctorItem] = []
328
+ if not cli:
329
+ items.append(DoctorItem("arduino_cli", "Arduino environment", "ERROR", "Arduino CLI/toolchain not found"))
330
+ return DoctorReport(tuple(items), source=None)
331
+
332
+ _, cli_item = _cli_version(cli)
333
+ items.append(cli_item)
334
+ discovery_item = _bundled_discovery_tools_item(cli)
335
+ if discovery_item is not None:
336
+ items.append(discovery_item)
337
+ _, core_item = _board_core(cli)
338
+ items.append(core_item)
339
+ lib_markers, lib_items = _libraries(cli)
340
+ items.extend(lib_items)
341
+
342
+ quick_fp = make_fingerprint(cli, libraries=_pinned_library_markers())
343
+ full_fp = make_fingerprint(cli, libraries=lib_markers)
344
+ cached = load_cache() if use_cache else None
345
+ full_cache_match = bool(_cache_is_content_safe(full_fp) and cached and cached.get("fingerprint") == full_fp.digest() and cached.get("status") == "READY")
346
+
347
+ # A first/full/manual diagnosis proves that the Uno core/compiler can really compile.
348
+ # A matching deep cache may skip this expensive step; any real compile/upload failure
349
+ # invalidates the cache in toolchain.py.
350
+ if not full_cache_match or details:
351
+ if core_item.status != "ERROR":
352
+ items.append(_compile_capability(cli, verbose=details))
353
+
354
+ from .toolchain import bundled_arduino_cli
355
+ bundled = bundled_arduino_cli()
356
+ source = "Barmaja bundled runtime" if bundled and Path(bundled).resolve() == Path(cli).resolve() else f"Arduino environment ({cli})"
357
+ report = DoctorReport(tuple(items), source=source)
358
+ if report.ok:
359
+ save_cache(full_fp, quick_fingerprint=quick_fp, source=report.source)
360
+ else:
361
+ invalidate_cache()
362
+ return report
363
+
364
+ def doctor_pictoblox(*, details: bool = False) -> str:
365
+ """In-Python doctor for PictoBlox: package install + the Arduino toolchain used by upload()."""
366
+ package = run_doctor(details=details, pictoblox=True, use_cache=False)
367
+ environment = run_doctor(details=details, pictoblox=False, use_cache=not details)
368
+ report = DoctorReport(package.items + environment.items, source=environment.source)
369
+ return report.detailed() if details else report.concise()
370
+
371
+
372
+ def _pinned_library_markers() -> list[Path]:
373
+ private = ensure_runtime_dirs()["root"] / "arduino-libraries"
374
+ return sorted(private.glob("*/library.properties")) if private.exists() else []
375
+
376
+
377
+ def _cache_is_content_safe(fp) -> bool:
378
+ # Only the Full Pack has a known immutable layout that we can fingerprint
379
+ # strongly enough to skip a real compile. Existing IDE/PictoBlox environments
380
+ # are still supported, but Doctor performs the compile instead of trusting cache.
381
+ return bool(getattr(fp, "integrity_markers", ()))
382
+
383
+
384
+ def quick_environment_ok() -> bool:
385
+ """Safe fast path for an unchanged pinned Full-Pack environment."""
386
+ try:
387
+ from .toolchain import find_arduino_cli
388
+ cli = find_arduino_cli()
389
+ except Exception:
390
+ invalidate_cache(); return False
391
+ fp = make_fingerprint(cli, libraries=_pinned_library_markers())
392
+ if not _cache_is_content_safe(fp):
393
+ return False
394
+ cached = load_cache()
395
+ return bool(cached and cached.get("status") == "READY" and cached.get("quick_fingerprint", cached.get("fingerprint")) == fp.digest())
396
+
397
+
398
+ def ensure_environment_ready() -> None:
399
+ """Silent normal-run check. Cache accelerates success but never masks failures."""
400
+ if quick_environment_ok():
401
+ return
402
+ report = run_doctor(details=False, pictoblox=False, use_cache=True)
403
+ if not report.ok:
404
+ from .errors import ToolchainError
405
+ problems = "; ".join(f"{i.label}: {i.detail}" for i in report.problems)
406
+ raise ToolchainError(problems or "Arduino environment is not ready.")