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,217 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+
6
+
7
+ # ANSI control sequences are used only for real ANSI-capable terminals.
8
+ # PictoBlox is intentionally excluded: its Python terminal/log owns rendering.
9
+ _ANSI = {
10
+ "success": "\x1b[32m", # green
11
+ "error": "\x1b[31m", # red
12
+ }
13
+ _RESET = "\x1b[0m"
14
+
15
+
16
+ def _percent(used, total):
17
+ if used is None or not total:
18
+ return None
19
+ return int((used * 100) // total)
20
+
21
+
22
+ def compilation_summary_lines(memory, board_name="Arduino Uno"):
23
+ """Return stable, ASCII-only Arduino-IDE-inspired compilation output."""
24
+ lines = [f"{board_name} compilation succeeded."]
25
+
26
+ if memory.flash_used is not None and memory.flash_total:
27
+ pct = _percent(memory.flash_used, memory.flash_total)
28
+ lines.extend([
29
+ "",
30
+ f"Sketch uses {memory.flash_used} bytes ({pct}%) of program storage space.",
31
+ f"Maximum is {memory.flash_total} bytes.",
32
+ ])
33
+
34
+ if memory.sram_used is not None and memory.sram_total:
35
+ pct = _percent(memory.sram_used, memory.sram_total)
36
+ leaving = max(0, memory.sram_total - memory.sram_used)
37
+ lines.extend([
38
+ "",
39
+ f"Global variables use {memory.sram_used} bytes ({pct}%) of dynamic memory,",
40
+ f"leaving {leaving} bytes for local variables.",
41
+ f"Maximum is {memory.sram_total} bytes.",
42
+ ])
43
+
44
+ return tuple(lines)
45
+
46
+
47
+ def _color_mode():
48
+ """Return always / never / auto for ANSI-capable consoles."""
49
+ if os.environ.get("NO_COLOR") is not None:
50
+ return "never"
51
+ raw = os.environ.get("BARMAJA_COLOR", "auto").strip().lower()
52
+ if raw in {"1", "true", "yes", "on", "always"}:
53
+ return "always"
54
+ if raw in {"0", "false", "no", "off", "never"}:
55
+ return "never"
56
+ return "auto"
57
+
58
+
59
+ def _stream_tag(stream) -> str:
60
+ cls = type(stream)
61
+ return f"{getattr(cls, '__module__', '')}.{getattr(cls, '__name__', '')}".lower()
62
+
63
+
64
+ def _looks_like_idle(stream) -> bool:
65
+ tag = _stream_tag(stream)
66
+ return "idlelib" in tag or "pseudooutputfile" in tag
67
+
68
+
69
+ def _looks_like_pictoblox_host() -> bool:
70
+ executable = str(getattr(sys, "executable", "") or "").lower()
71
+ prefix = str(getattr(sys, "prefix", "") or "").lower()
72
+ base_prefix = str(getattr(sys, "base_prefix", "") or "").lower()
73
+ return "pictoblox" in executable or "pictoblox" in prefix or "pictoblox" in base_prefix
74
+
75
+
76
+ def _known_ansi_host() -> bool:
77
+ env = os.environ
78
+
79
+ # PictoBlox is NOT an ANSI terminal. Real physical testing in 0.6.8 proved
80
+ # that ANSI color is not rendered there, so let PictoBlox own stderr/log
81
+ # styling instead of injecting escape sequences.
82
+ if _looks_like_pictoblox_host():
83
+ return False
84
+
85
+ if env.get("PYCHARM_HOSTED") == "1":
86
+ return True
87
+ if env.get("VSCODE_PID") or env.get("TERM_PROGRAM", "").lower() == "vscode":
88
+ return True
89
+ if env.get("WT_SESSION") or env.get("ANSICON"):
90
+ return True
91
+ if env.get("ConEmuANSI", "").upper() == "ON":
92
+ return True
93
+ term = env.get("TERM", "")
94
+ if term and term.lower() != "dumb":
95
+ return True
96
+ return False
97
+
98
+
99
+ def _enable_windows_vt(stream) -> bool:
100
+ """Best-effort enable ANSI colors in a real Windows console."""
101
+ if os.name != "nt":
102
+ return True
103
+ try:
104
+ if not stream.isatty():
105
+ return True
106
+ except Exception:
107
+ return True
108
+
109
+ try:
110
+ import ctypes
111
+ import msvcrt
112
+
113
+ kernel32 = ctypes.windll.kernel32
114
+ handle = msvcrt.get_osfhandle(stream.fileno())
115
+ mode = ctypes.c_uint()
116
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
117
+ return False
118
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
119
+ if mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING:
120
+ return True
121
+ return bool(kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
122
+ except Exception:
123
+ return False
124
+
125
+
126
+ def supports_color(stream=None) -> bool:
127
+ """Whether ANSI color is safe for this output stream."""
128
+ stream = stream or sys.stdout
129
+ mode = _color_mode()
130
+
131
+ # Mandatory GUI hosts must never receive ANSI garbage in auto mode.
132
+ if mode == "auto" and (_looks_like_idle(stream) or _looks_like_pictoblox_host()):
133
+ return False
134
+
135
+ if mode == "never":
136
+ return False
137
+ if mode == "always":
138
+ return True
139
+
140
+ # Do not inject escape sequences into StringIO/captured logs.
141
+ cls = type(stream)
142
+ if getattr(cls, "__module__", "") == "_io" and getattr(cls, "__name__", "") == "StringIO":
143
+ return False
144
+
145
+ try:
146
+ tty = bool(stream.isatty())
147
+ except Exception:
148
+ tty = False
149
+
150
+ if tty:
151
+ return _enable_windows_vt(stream)
152
+ return _known_ansi_host()
153
+
154
+
155
+ def style_text(text: str, style: str | None = None, *, stream=None) -> str:
156
+ """Apply ANSI color only where the host supports it."""
157
+ if not text or style not in _ANSI or not supports_color(stream):
158
+ return text
159
+ return f"{_ANSI[style]}{text}{_RESET}"
160
+
161
+
162
+ def _safe_write(stream, text: str) -> None:
163
+ """Write without crashing on restrictive Windows console encodings."""
164
+ try:
165
+ stream.write(text)
166
+ except UnicodeEncodeError:
167
+ encoding = getattr(stream, "encoding", None) or "utf-8"
168
+ safe = text.encode(encoding, errors="replace").decode(encoding, errors="replace")
169
+ stream.write(safe)
170
+
171
+
172
+ def _idle_native_write(stream, text: str, style: str | None) -> bool:
173
+ """Best-effort IDLE-native highlighting.
174
+
175
+ IDLE exposes a PseudoOutputFile whose ``shell.write`` accepts semantic tags.
176
+ Using native tags avoids ANSI escapes in IDLE. The exact colors follow the
177
+ user's IDLE theme.
178
+ """
179
+ if not _looks_like_idle(stream):
180
+ return False
181
+
182
+ shell = getattr(stream, "shell", None)
183
+ writer = getattr(shell, "write", None)
184
+ if not callable(writer):
185
+ return False
186
+
187
+ # IDLE's own stderr tag is its error color. STRING is green in the default
188
+ # IDLE themes and serves as a readable success highlight; custom themes may
189
+ # choose a different shade.
190
+ tag = "stderr" if style == "error" else "STRING" if style == "success" else getattr(stream, "tags", None)
191
+ if isinstance(tag, (tuple, list)):
192
+ tag = tag[0] if tag else None
193
+ if not isinstance(tag, str) or not tag:
194
+ tag = "stdout"
195
+
196
+ try:
197
+ writer(text, tag)
198
+ return True
199
+ except Exception:
200
+ return False
201
+
202
+
203
+ def write_styled(stream, text: str, *, style: str | None = None, end: str = "\n") -> None:
204
+ """Write one student-facing message using the host's safest color mechanism.
205
+
206
+ - IDLE: native semantic tags (theme-controlled)
207
+ - PictoBlox: plain stdout/stderr; PictoBlox owns terminal/log rendering
208
+ - ANSI terminals / PyCharm / VS Code: ANSI green/red where supported
209
+ - Other hosts: plain text
210
+ """
211
+ payload = text + end
212
+
213
+ if _idle_native_write(stream, payload, style):
214
+ return
215
+
216
+ rendered = style_text(text, style, stream=stream) + end
217
+ _safe_write(stream, rendered)
@@ -0,0 +1,367 @@
1
+ """BARMAJA device packs: add Arduino libraries/sensors WITHOUT editing the compiler.
2
+
3
+ A device pack is a folder::
4
+
5
+ <PackName>/
6
+ pack.json declarative student API + rules (no code runs)
7
+ arduino/Barmaja_<Class>/ tiny Arduino adapter library
8
+ library.properties
9
+ src/Barmaja_<Class>.h
10
+
11
+ Built-in packs live in ``Barmaja_PyArduino/device_packs/``. Teachers/developers can
12
+ add more without rebuilding the wheel by pointing ``BARMAJA_DEVICE_PACKS`` at one or
13
+ more folders (os.pathsep separated) or by copying packs into
14
+ ``<BARMAJA runtime root>/device-packs``. Third-party Arduino libraries named in
15
+ ``dependencies`` must exist in the offline library folder (see the payload builder).
16
+
17
+ pack.json is JSON only (never executed), validated strictly; an invalid pack is
18
+ skipped with a clear message and never breaks the core compiler.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import re
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ SCHEMA_VERSION = 1
30
+ TYPES = {"Int", "Float", "Bool", "String", "Scalar", "Void"}
31
+ RETURN_TYPES = {"Int", "Float", "Bool", "String", "Void"}
32
+ PIN_RULES = {"digital_pin", "analog_pin", "pwm_pin", "none"}
33
+ RESOURCES = {"i2c", "spi", "softserial", "timer1", "timer2", "interrupts"}
34
+ STATUSES = {"prototype", "compile-verified", "hardware-verified"}
35
+ _NAME = re.compile(r"^[A-Z][A-Za-z0-9]{2,40}$")
36
+ _PARAM = re.compile(r"^[a-z][A-Za-z0-9_]{0,30}$")
37
+ _METHOD = re.compile(r"^[a-z][A-Za-z0-9]{0,30}$")
38
+ _BOUND = re.compile(r"^([a-z][A-Za-z0-9_]*)\s*([+-]\s*\d+)?$")
39
+ _RESERVED_METHODS = {"__init__", "pins", "_only_upload", "_label"}
40
+
41
+
42
+ class PackError(ValueError):
43
+ """pack.json does not follow the device-pack schema."""
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class PackParam:
48
+ name: str
49
+ types: tuple[str, ...]
50
+ rule: str = "none"
51
+ range: tuple[Any, Any] | None = None # ints or "ctorparam[+-N]"
52
+ default: Any = None # optional trailing parameter (C++ adapter must use the same default)
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class PackMethod:
57
+ name: str
58
+ params: tuple[PackParam, ...]
59
+ returns: str
60
+ starts: bool = False
61
+ requires_begin: bool = True
62
+
63
+
64
+ @dataclass
65
+ class DevicePack:
66
+ class_name: str
67
+ title: str
68
+ status: str
69
+ folder: Path
70
+ constructor: tuple[PackParam, ...]
71
+ methods: dict[str, PackMethod]
72
+ resources: tuple[str, ...]
73
+ warnings: tuple[str, ...]
74
+ forbid_pins: tuple[int, ...]
75
+ distinct_pins: bool
76
+ adapter_library: str
77
+ adapter_header: str
78
+ dependencies: tuple[dict, ...]
79
+ boards: tuple[str, ...] = ("uno",)
80
+ liverun: bool = False
81
+ example: str = ""
82
+ source: str = "built-in"
83
+ errors: list[str] = field(default_factory=list)
84
+
85
+ @property
86
+ def adapter_dir(self) -> Path:
87
+ return self.folder / "arduino" / self.adapter_library
88
+
89
+
90
+ def _types(value: Any, where: str, allowed: set[str]) -> tuple[str, ...]:
91
+ items = value if isinstance(value, list) else [value]
92
+ if not items or any(not isinstance(t, str) or t not in allowed for t in items):
93
+ raise PackError(f"{where}: type must be one of {sorted(allowed)} (or a list of them)")
94
+ return tuple(items)
95
+
96
+
97
+ def _range(value: Any, where: str, ctor_names: set[str]) -> tuple[Any, Any] | None:
98
+ if value is None:
99
+ return None
100
+ if not (isinstance(value, list) and len(value) == 2):
101
+ raise PackError(f"{where}: range must be [low, high]")
102
+ out = []
103
+ for bound in value:
104
+ if isinstance(bound, bool) or not isinstance(bound, (int, str)):
105
+ raise PackError(f"{where}: range bounds must be integers or 'constructorParam-1'")
106
+ if isinstance(bound, str):
107
+ m = _BOUND.match(bound.strip())
108
+ if not m or m.group(1) not in ctor_names:
109
+ raise PackError(f"{where}: bound '{bound}' must name a constructor parameter, e.g. 'count-1'")
110
+ out.append(bound)
111
+ return (out[0], out[1])
112
+
113
+
114
+ def _param(raw: Any, where: str, ctor_names: set[str], *, ctor: bool) -> PackParam:
115
+ if not isinstance(raw, dict):
116
+ raise PackError(f"{where}: each parameter must be an object")
117
+ name = raw.get("name")
118
+ if not isinstance(name, str) or not _PARAM.match(name):
119
+ raise PackError(f"{where}: parameter name must be lower_snake/camel, e.g. 'pin'")
120
+ types = _types(raw.get("type", "Int"), f"{where}.{name}", TYPES - {"Void"})
121
+ rule = raw.get("rule", "none")
122
+ if rule not in PIN_RULES:
123
+ raise PackError(f"{where}.{name}: rule must be one of {sorted(PIN_RULES)}")
124
+ if rule != "none" and types != ("Int",):
125
+ raise PackError(f"{where}.{name}: pin parameters must have type Int")
126
+ if ctor and types != ("Int",):
127
+ raise PackError(f"{where}.{name}: constructor parameters must be Int (compile-time numbers)")
128
+ default = raw.get("default")
129
+ if default is not None and (isinstance(default, bool) or not isinstance(default, (int, float))):
130
+ raise PackError(f"{where}.{name}: default must be a number")
131
+ return PackParam(name, types, rule, _range(raw.get("range"), f"{where}.{name}", ctor_names), default)
132
+
133
+
134
+ def _check_defaults(params: tuple[PackParam, ...], where: str) -> None:
135
+ seen_default = False
136
+ for p in params:
137
+ if p.default is not None:
138
+ seen_default = True
139
+ elif seen_default:
140
+ raise PackError(f"{where}: parameters with a default must come last")
141
+
142
+
143
+ def required_count(params: tuple[PackParam, ...]) -> int:
144
+ return sum(1 for p in params if p.default is None)
145
+
146
+
147
+ def parse_pack(folder: Path, *, source: str = "built-in") -> DevicePack:
148
+ path = folder / "pack.json"
149
+ try:
150
+ raw = json.loads(path.read_text(encoding="utf-8"))
151
+ except FileNotFoundError:
152
+ raise PackError(f"{folder.name}: pack.json missing") from None
153
+ except (OSError, json.JSONDecodeError) as exc:
154
+ raise PackError(f"{folder.name}: pack.json is not valid JSON ({exc})") from None
155
+ if not isinstance(raw, dict) or raw.get("schema") != SCHEMA_VERSION:
156
+ raise PackError(f"{folder.name}: 'schema' must be {SCHEMA_VERSION}")
157
+ allowed_keys = {"schema", "class", "title", "status", "boards", "constructor", "methods", "resources",
158
+ "warnings", "pins", "adapter", "dependencies", "liverun", "example", "notes"}
159
+ unknown = set(raw) - allowed_keys
160
+ if unknown:
161
+ raise PackError(f"{folder.name}: unknown keys {sorted(unknown)}")
162
+ cls = raw.get("class")
163
+ if not isinstance(cls, str) or not _NAME.match(cls):
164
+ raise PackError(f"{folder.name}: 'class' must be a CamelCase name like 'TemperatureDS18B20'")
165
+ status = raw.get("status", "prototype")
166
+ if status not in STATUSES:
167
+ raise PackError(f"{cls}: status must be one of {sorted(STATUSES)}")
168
+ ctor_raw = raw.get("constructor", [])
169
+ if not isinstance(ctor_raw, list) or len(ctor_raw) > 10:
170
+ raise PackError(f"{cls}: constructor must be a list (max 10 parameters)")
171
+ ctor_names = {p.get("name") for p in ctor_raw if isinstance(p, dict)}
172
+ ctor = tuple(_param(p, f"{cls}.constructor", ctor_names, ctor=True) for p in ctor_raw)
173
+ _check_defaults(ctor, f"{cls}.constructor")
174
+ if len({p.name for p in ctor}) != len(ctor):
175
+ raise PackError(f"{cls}: duplicate constructor parameter names")
176
+ methods_raw = raw.get("methods")
177
+ if not isinstance(methods_raw, dict) or not methods_raw:
178
+ raise PackError(f"{cls}: 'methods' must be a non-empty object")
179
+ methods: dict[str, PackMethod] = {}
180
+ for mname, mraw in methods_raw.items():
181
+ if not _METHOD.match(mname) or mname in _RESERVED_METHODS:
182
+ raise PackError(f"{cls}.{mname}: invalid method name")
183
+ if not isinstance(mraw, dict):
184
+ raise PackError(f"{cls}.{mname}: must be an object")
185
+ bad = set(mraw) - {"params", "returns", "starts", "requires_begin", "doc"}
186
+ if bad:
187
+ raise PackError(f"{cls}.{mname}: unknown keys {sorted(bad)}")
188
+ params = tuple(_param(p, f"{cls}.{mname}", ctor_names, ctor=False) for p in mraw.get("params", []))
189
+ _check_defaults(params, f"{cls}.{mname}")
190
+ returns = _types(mraw.get("returns", "Void"), f"{cls}.{mname}.returns", RETURN_TYPES)
191
+ if len(returns) != 1:
192
+ raise PackError(f"{cls}.{mname}: exactly one return type")
193
+ methods[mname] = PackMethod(mname, params, returns[0], bool(mraw.get("starts", False)),
194
+ bool(mraw.get("requires_begin", True)))
195
+ pins = raw.get("pins", {}) or {}
196
+ forbid = tuple(int(p) for p in pins.get("forbid", [0, 1]))
197
+ resources = tuple(raw.get("resources", []))
198
+ if any(r not in RESOURCES for r in resources):
199
+ raise PackError(f"{cls}: resources must be from {sorted(RESOURCES)}")
200
+ adapter = raw.get("adapter") or {}
201
+ lib = adapter.get("library", f"Barmaja_{cls}")
202
+ header = adapter.get("header", f"Barmaja_{cls}.h")
203
+ if not (folder / "arduino" / lib / "src" / header).is_file() and not (folder / "arduino" / lib / header).is_file():
204
+ raise PackError(f"{cls}: adapter header arduino/{lib}/src/{header} not found")
205
+ if not (folder / "arduino" / lib / "library.properties").is_file():
206
+ raise PackError(f"{cls}: arduino/{lib}/library.properties missing")
207
+ adapter_text = ""
208
+ for candidate in (folder / "arduino" / lib / "src" / header, folder / "arduino" / lib / header):
209
+ if candidate.is_file():
210
+ adapter_text = candidate.read_text(encoding="utf-8", errors="replace")
211
+ if not re.search(rf"\bclass\s+{cls}\b", adapter_text):
212
+ raise PackError(f"{cls}: adapter header must define 'class {cls}' (the generated C++ uses that name)")
213
+ deps = raw.get("dependencies", [])
214
+ if not isinstance(deps, list):
215
+ raise PackError(f"{cls}: dependencies must be a list")
216
+ for d in deps:
217
+ need = {"id", "name", "version", "header", "license"}
218
+ if not isinstance(d, dict) or not need <= set(d):
219
+ raise PackError(f"{cls}: each dependency needs {sorted(need)}")
220
+ if str(d["version"]).strip().lower() in {"", "latest", "pin_pending", "*"}:
221
+ raise PackError(f"{cls}: dependency {d['name']} must have an exact pinned version")
222
+ for m in methods.values():
223
+ try:
224
+ _signatures(m.params)
225
+ except PackError as exc:
226
+ raise PackError(f"{cls}.{m.name}: {exc}") from None
227
+ starters = [m for m in methods.values() if m.starts]
228
+ if len(starters) > 1:
229
+ raise PackError(f"{cls}: at most one method can have \"starts\": true")
230
+ return DevicePack(
231
+ class_name=cls, title=str(raw.get("title", cls)), status=status, folder=folder,
232
+ constructor=ctor, methods=methods, resources=resources,
233
+ warnings=tuple(str(w) for w in raw.get("warnings", [])), forbid_pins=forbid,
234
+ distinct_pins=bool(pins.get("distinct", True)), adapter_library=lib, adapter_header=header,
235
+ dependencies=tuple(deps), boards=tuple(raw.get("boards", ["uno"])), liverun=bool(raw.get("liverun", False)),
236
+ example=str(raw.get("example", "")), source=source,
237
+ )
238
+
239
+
240
+ def pack_search_dirs() -> list[tuple[Path, str]]:
241
+ dirs: list[tuple[Path, str]] = [(Path(__file__).resolve().parent / "device_packs_builtin", "built-in")]
242
+ try:
243
+ from .environment import ensure_runtime_dirs
244
+ dirs.append((ensure_runtime_dirs()["root"] / "device-packs", "runtime"))
245
+ except Exception:
246
+ pass
247
+ for item in os.environ.get("BARMAJA_DEVICE_PACKS", "").split(os.pathsep):
248
+ if item.strip():
249
+ dirs.append((Path(item.strip()).expanduser(), "BARMAJA_DEVICE_PACKS"))
250
+ return dirs
251
+
252
+
253
+ LOADED: dict[str, DevicePack] = {}
254
+ LOAD_ERRORS: list[str] = []
255
+
256
+
257
+ def load_packs(existing_names: set[str]) -> dict[str, DevicePack]:
258
+ LOADED.clear(); LOAD_ERRORS.clear()
259
+ for root, source in pack_search_dirs():
260
+ if not root.is_dir():
261
+ continue
262
+ for folder in sorted(p for p in root.iterdir() if p.is_dir() and not p.name.startswith((".", "_"))):
263
+ try:
264
+ pack = parse_pack(folder, source=source)
265
+ except PackError as exc:
266
+ LOAD_ERRORS.append(f"[{source}] {exc}")
267
+ continue
268
+ if source == "built-in" and pack.class_name in os.environ.get("BARMAJA_SKIP_BUILTIN_PACKS", "").split(","):
269
+ continue # check_device_pack.py is testing an edited copy of this pack
270
+ if pack.class_name in existing_names or pack.class_name in LOADED:
271
+ LOAD_ERRORS.append(f"[{source}] {pack.class_name}: name already used; pack skipped")
272
+ continue
273
+ LOADED[pack.class_name] = pack
274
+ return LOADED
275
+
276
+
277
+ # ---------------- registry integration ----------------
278
+ def _signatures(params: tuple[PackParam, ...]) -> list[tuple[str, ...]]:
279
+ """All accepted argument-type tuples, including shorter calls that use defaults."""
280
+ out: list[tuple[str, ...]] = []
281
+ for length in range(required_count(params), len(params) + 1):
282
+ combos: list[tuple[str, ...]] = [()]
283
+ for p in params[:length]:
284
+ combos = [c + (t,) for c in combos for t in p.types]
285
+ out.extend(combos)
286
+ if len(out) > 24:
287
+ raise PackError("too many type combinations (max 24)")
288
+ return out
289
+
290
+
291
+ def register_into(hardware_classes: dict, dependencies: dict, specs) -> None:
292
+ """Add LOADED packs to the compiler registry (called from api_registry)."""
293
+ SignatureSpec, MethodSpec, HardwareClassSpec, DependencySpec = specs
294
+ for pack in LOADED.values():
295
+ ctor_constraints = tuple(p.rule for p in pack.constructor if p.rule != "none")
296
+ ctor_sigs = tuple(SignatureSpec(combo, f"Hardware[{pack.class_name}]", ctor_constraints)
297
+ for combo in _signatures(pack.constructor))
298
+ methods = {}
299
+ for m in pack.methods.values():
300
+ sigs = tuple(SignatureSpec(combo, m.returns) for combo in _signatures(m.params))
301
+ methods[m.name] = MethodSpec(m.name, sigs)
302
+ dep_ids = [f"pack-adapter-{pack.class_name.lower()}"]
303
+ dependencies[dep_ids[0]] = DependencySpec(dep_ids[0], f"{pack.title} adapter", "pack", pack.adapter_header,
304
+ f"BARMAJA device pack ({pack.status})")
305
+ for d in pack.dependencies:
306
+ if d["id"] not in dependencies:
307
+ dependencies[d["id"]] = DependencySpec(d["id"], d["name"], str(d["version"]), d.get("header"),
308
+ f"{d.get('source', '')} {d['license']}".strip())
309
+ dep_ids.append(d["id"])
310
+ hardware_classes[pack.class_name] = HardwareClassSpec(
311
+ pack.class_name, ctor_sigs, methods, includes=(pack.adapter_header,),
312
+ dependency_ids=tuple(dep_ids), resources=pack.resources,
313
+ )
314
+
315
+
316
+ def adapter_library_dirs() -> list[Path]:
317
+ return [p.adapter_dir for p in LOADED.values() if p.adapter_dir.is_dir()]
318
+
319
+
320
+ def resolve_bound(bound: Any, ctor_values: dict[str, int | None]) -> int | None:
321
+ if isinstance(bound, int):
322
+ return bound
323
+ m = _BOUND.match(str(bound).strip())
324
+ if not m:
325
+ return None
326
+ base = ctor_values.get(m.group(1))
327
+ if base is None:
328
+ return None
329
+ return base + (int(m.group(2).replace(" ", "")) if m.group(2) else 0)
330
+
331
+
332
+ # ---------------- Python student classes + stubs ----------------
333
+ _PY = {"Int": "int", "Float": "float", "Bool": "bool", "String": "str", "Scalar": "int | float | bool | str", "Void": "None"}
334
+
335
+
336
+ def make_python_class(pack: DevicePack, base):
337
+ ctor_names = [p.name for p in pack.constructor]
338
+
339
+ low, high = required_count(pack.constructor), len(ctor_names)
340
+
341
+ def __init__(self, *args):
342
+ if not low <= len(args) <= high:
343
+ raise TypeError(f"{pack.class_name}() takes {low}..{high} argument(s): {', '.join(ctor_names)}")
344
+ self.pins = tuple(args)
345
+
346
+ ns = {"__init__": __init__, "_label": f"{pack.class_name} ({pack.title}) [device pack, {pack.status}]",
347
+ "__doc__": f"{pack.title}. Upload-only device pack ({pack.status})."}
348
+ for m in pack.methods.values():
349
+ def method(self, *args, _m=m):
350
+ return self._only_upload(*args)
351
+ method.__name__ = m.name
352
+ ns[m.name] = method
353
+ return type(pack.class_name, (base,), ns)
354
+
355
+
356
+ def render_stubs(packs: dict[str, DevicePack] | None = None) -> str:
357
+ lines: list[str] = []
358
+ for pack in (packs or LOADED).values():
359
+ lines.append(f"class {pack.class_name}:")
360
+ lines.append(f' """{pack.title} [device pack, {pack.status}]"""')
361
+ args = ", ".join(f"{p.name}: int" + (f" = {p.default}" if p.default is not None else "") for p in pack.constructor)
362
+ lines.append(f" def __init__(self{', ' if args else ''}{args}) -> None: ...")
363
+ for m in pack.methods.values():
364
+ ps = ", ".join(f"{p.name}: {' | '.join(_PY[t] for t in p.types)}" + (f" = {p.default}" if p.default is not None else "") for p in m.params)
365
+ lines.append(f" def {m.name}(self{', ' if ps else ''}{ps}) -> {_PY[m.returns]}: ...")
366
+ lines.append("")
367
+ return "\n".join(lines)
@@ -0,0 +1,9 @@
1
+ name=Barmaja_Button
2
+ version=1.1.0
3
+ author=BARMAJA
4
+ maintainer=BARMAJA
5
+ sentence=BARMAJA PyArduino device-pack adapter: Push button with built-in pull-up and debounce (no resistor needed)
6
+ paragraph=
7
+ category=Device Control
8
+ url=
9
+ architectures=avr
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+ #include <Arduino.h>
3
+ // Wire the button between the pin and GND. Uses INPUT_PULLUP; 30 ms debounce.
4
+ class Button {
5
+ public:
6
+ explicit Button(uint8_t pin): pin_(pin) {}
7
+ bool isPressed() { setup(); return digitalRead(pin_) == LOW; }
8
+ bool wasPressed() {
9
+ setup();
10
+ bool now = digitalRead(pin_) == LOW;
11
+ unsigned long t = millis();
12
+ if (now != last_ && t - changed_ >= 30) { changed_ = t; last_ = now; if (now) return true; }
13
+ return false;
14
+ }
15
+ private:
16
+ void setup() { if (!ready_) { pinMode(pin_, INPUT_PULLUP); ready_ = true; last_ = false; changed_ = millis(); } }
17
+ uint8_t pin_; bool ready_ = false; bool last_ = false; unsigned long changed_ = 0;
18
+ };
@@ -0,0 +1,10 @@
1
+ from Barmaja_PyArduino import *
2
+ upload()
3
+
4
+ button = Button(2)
5
+ pinMode(13, OUTPUT)
6
+ led = False
7
+ while True:
8
+ if button.wasPressed():
9
+ led = not led
10
+ digitalWrite(13, HIGH if led else LOW)
@@ -0,0 +1,40 @@
1
+ {
2
+ "schema": 1,
3
+ "class": "Button",
4
+ "title": "Push button with built-in pull-up and debounce (no resistor needed)",
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
+ "isPressed": {
19
+ "params": [],
20
+ "returns": "Bool",
21
+ "requires_begin": false
22
+ },
23
+ "wasPressed": {
24
+ "params": [],
25
+ "returns": "Bool",
26
+ "requires_begin": false,
27
+ "doc": "True once per press"
28
+ }
29
+ },
30
+ "resources": [],
31
+ "warnings": [],
32
+ "adapter": {
33
+ "library": "Barmaja_Button",
34
+ "header": "Barmaja_Button.h"
35
+ },
36
+ "dependencies": [],
37
+ "liverun": false,
38
+ "example": "from Barmaja_PyArduino import *\nupload()\n\nbutton = Button(2)\npinMode(13, OUTPUT)\nled = False\nwhile True:\n if button.wasPressed():\n led = not led\n digitalWrite(13, HIGH if led else LOW)\n",
39
+ "notes": "No library needed."
40
+ }
@@ -0,0 +1,9 @@
1
+ name=Barmaja_ClockDS3231
2
+ version=1.1.0
3
+ author=BARMAJA
4
+ maintainer=BARMAJA
5
+ sentence=BARMAJA PyArduino device-pack adapter: DS3231 real-time clock module (I2C)
6
+ paragraph=
7
+ category=Device Control
8
+ url=
9
+ architectures=avr