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,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,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,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
|
+
}
|