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,544 @@
|
|
|
1
|
+
"""Turn an Arduino library (ZIP or folder) into a BARMAJA device pack.
|
|
2
|
+
|
|
3
|
+
Used by the Library Adder window (library_adder.py), the ``add-library`` command
|
|
4
|
+
and tools/new_device_pack.py. It only READS the library's files; nothing from the
|
|
5
|
+
library is executed on the PC.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
import zipfile
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
CPP_TYPES = {
|
|
18
|
+
"void": "Void", "bool": "Bool", "boolean": "Bool",
|
|
19
|
+
"float": "Float", "double": "Float",
|
|
20
|
+
"int": "Int", "long": "Int", "short": "Int", "char": "Int", "byte": "Int", "word": "Int",
|
|
21
|
+
"uint8_t": "Int", "int8_t": "Int", "uint16_t": "Int", "int16_t": "Int",
|
|
22
|
+
"uint32_t": "Int", "int32_t": "Int", "size_t": "Int", "unsigned": "Int", "signed": "Int",
|
|
23
|
+
}
|
|
24
|
+
ADAPTER_CTYPE = {"Int": "long", "Float": "float", "Bool": "bool", "Void": "void"}
|
|
25
|
+
PIN_WORDS = ("pin", "dout", "sck", "clk", "sda", "scl", "rx", "tx", "trig", "echo", "rst", "din", "latch")
|
|
26
|
+
PIN_EXACT = {"cs", "ss", "dc", "rs", "rw", "en", "enable", "data", "clock", "miso", "mosi", "reset"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _looks_like_pin(name: str) -> bool:
|
|
30
|
+
n = name.lower()
|
|
31
|
+
return any(w in n for w in PIN_WORDS) or n in PIN_EXACT or bool(re.fullmatch(r"d[0-7]", n))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Param:
|
|
36
|
+
name: str
|
|
37
|
+
type: str
|
|
38
|
+
ctype: str
|
|
39
|
+
default: float | int | None = None
|
|
40
|
+
is_pin: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Method:
|
|
45
|
+
cpp_name: str
|
|
46
|
+
student_name: str
|
|
47
|
+
params: list[Param]
|
|
48
|
+
returns: str
|
|
49
|
+
selected: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ClassScan:
|
|
54
|
+
name: str
|
|
55
|
+
header: str
|
|
56
|
+
constructors: list[list[Param]] = field(default_factory=list)
|
|
57
|
+
methods: list[Method] = field(default_factory=list)
|
|
58
|
+
skipped: list[str] = field(default_factory=list)
|
|
59
|
+
printable: bool = False # derives from Print/Stream/Adafruit_GFX -> offer print()/println()
|
|
60
|
+
bases: list[str] = field(default_factory=list)
|
|
61
|
+
abstract: bool = False
|
|
62
|
+
unusable: str = "" # why students cannot get this class automatically
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class LibraryScan:
|
|
67
|
+
root: Path
|
|
68
|
+
folder_name: str
|
|
69
|
+
name: str
|
|
70
|
+
version: str
|
|
71
|
+
license: str
|
|
72
|
+
license_warning: str
|
|
73
|
+
url: str
|
|
74
|
+
classes: dict[str, ClassScan]
|
|
75
|
+
problems: list[str]
|
|
76
|
+
depends: list[str] = field(default_factory=list) # other Arduino libraries this one needs (library.properties)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------- opening ----------------
|
|
80
|
+
def open_library(path: str | Path) -> Path:
|
|
81
|
+
"""Return the folder that holds the library (extracting a ZIP to a temp folder)."""
|
|
82
|
+
p = Path(path)
|
|
83
|
+
if p.is_file() and p.suffix.lower() == ".zip":
|
|
84
|
+
dest = Path(tempfile.mkdtemp(prefix="barmaja_lib_"))
|
|
85
|
+
with zipfile.ZipFile(p) as z:
|
|
86
|
+
for info in z.infolist():
|
|
87
|
+
target = (dest / info.filename).resolve()
|
|
88
|
+
if not str(target).startswith(str(dest.resolve())):
|
|
89
|
+
raise ValueError("Unsafe path inside ZIP; refused.")
|
|
90
|
+
z.extractall(dest)
|
|
91
|
+
p = dest
|
|
92
|
+
if not p.is_dir():
|
|
93
|
+
raise FileNotFoundError(f"Not a ZIP file or folder: {path}")
|
|
94
|
+
if (p / "library.properties").is_file():
|
|
95
|
+
return p
|
|
96
|
+
candidates = sorted(x.parent for x in p.rglob("library.properties") if ".git" not in x.parts)
|
|
97
|
+
if candidates:
|
|
98
|
+
return min(candidates, key=lambda c: len(c.parts))
|
|
99
|
+
headers = [h for h in p.rglob("*.h") if "examples" not in h.parts]
|
|
100
|
+
if headers:
|
|
101
|
+
return min({h.parent for h in headers}, key=lambda c: len(c.parts))
|
|
102
|
+
raise FileNotFoundError("No Arduino library (library.properties or .h files) found.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def guess_license(root: Path) -> tuple[str, str]:
|
|
106
|
+
text = ""
|
|
107
|
+
for name in ("LICENSE", "LICENSE.txt", "LICENSE.md", "license.txt", "COPYING", "COPYING.txt", "COPYING.LESSER", "README.md", "README.adoc"):
|
|
108
|
+
f = root / name
|
|
109
|
+
if f.is_file():
|
|
110
|
+
text += f.read_text(encoding="utf-8", errors="replace")[:6000] + "\n"
|
|
111
|
+
for h in list(root.glob("*.h"))[:2] + list((root / "src").glob("*.h"))[:2]:
|
|
112
|
+
text += h.read_text(encoding="utf-8", errors="replace")[:3000]
|
|
113
|
+
t = text.lower()
|
|
114
|
+
if "lesser general public" in t or "lgpl" in t:
|
|
115
|
+
return "LGPL", "LGPL: allowed, but the installer must include the licence and the library source."
|
|
116
|
+
if "gnu general public" in t or re.search(r"\bgpl", t):
|
|
117
|
+
return "GPL", "GPL: check with BARMAJA before distributing - may require releasing combined source."
|
|
118
|
+
if "mit license" in t or "permission is hereby granted, free of charge" in t:
|
|
119
|
+
return "MIT", ""
|
|
120
|
+
if "apache license" in t:
|
|
121
|
+
return "Apache-2.0", ""
|
|
122
|
+
if "bsd" in t or "redistribution and use in source and binary forms" in t:
|
|
123
|
+
return "BSD", ""
|
|
124
|
+
return "UNKNOWN", "Licence not found: check the library's website before distributing."
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------- C++ header scanning ----------------
|
|
128
|
+
_ARCH_TRUE = {"avr": {"ARDUINO_ARCH_AVR", "__AVR__", "AVR", "__AVR_ATmega328P__", "ARDUINO_AVR_UNO", "ARDUINO_AVR_NANO", "ARDUINO"}}
|
|
129
|
+
_ARCH_FALSE = {"ARDUINO_ARCH_ESP32", "ESP32", "ESP_PLATFORM", "ESP8266", "ARDUINO_ARCH_ESP8266", "ARDUINO_ARCH_SAMD", "ARDUINO_ARCH_SAM",
|
|
130
|
+
"ARDUINO_ARCH_STM32", "STM32", "ARDUINO_ARCH_RP2040", "ARDUINO_ARCH_MBED", "ARDUINO_ARCH_RENESAS", "ARDUINO_ARCH_NRF52",
|
|
131
|
+
"ARDUINO_ARCH_MEGAAVR", "__arm__", "__ARM_ARCH", "CORE_TEENSY", "TEENSYDUINO", "PARTICLE", "SPARK", "__IMXRT1062__",
|
|
132
|
+
"ARDUINO_ARCH_ARC32", "__SAM3X8E__", "__MK20DX256__", "ARDUINO_ARCH_STM32F1", "ARDUINO_ARCH_STM32F4", "__PIC32MX__"}
|
|
133
|
+
_NUMERIC = {"ARDUINO": 10819}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _eval_condition(expr: str, arch: str) -> bool | None:
|
|
137
|
+
"""Evaluate a #if expression for the target board; None when unknown."""
|
|
138
|
+
true = _ARCH_TRUE.get(arch, set())
|
|
139
|
+
unknown = False
|
|
140
|
+
def defined(m):
|
|
141
|
+
nonlocal unknown
|
|
142
|
+
name = m.group(1)
|
|
143
|
+
if name in true: return " True "
|
|
144
|
+
if name in _ARCH_FALSE: return " False "
|
|
145
|
+
unknown = True
|
|
146
|
+
return " False "
|
|
147
|
+
e = re.sub(r"defined\s*\(\s*(\w+)\s*\)", defined, expr)
|
|
148
|
+
e = re.sub(r"defined\s+(\w+)", defined, e)
|
|
149
|
+
def ident(m):
|
|
150
|
+
nonlocal unknown
|
|
151
|
+
name = m.group(0)
|
|
152
|
+
if name in ("True", "False"): return name
|
|
153
|
+
if name in _NUMERIC: return str(_NUMERIC[name])
|
|
154
|
+
if name in true: return "1"
|
|
155
|
+
if name in _ARCH_FALSE: return "0"
|
|
156
|
+
unknown = True
|
|
157
|
+
return "0"
|
|
158
|
+
e = re.sub(r"\b[A-Za-z_]\w*\b", ident, e)
|
|
159
|
+
if unknown:
|
|
160
|
+
return None
|
|
161
|
+
e = e.replace("&&", " and ").replace("||", " or ").replace("!=", " __NE__ ").replace("!", " not ").replace(" __NE__ ", "!=")
|
|
162
|
+
e = re.sub(r"(\d+)[uUlL]+", r"\1", e)
|
|
163
|
+
try:
|
|
164
|
+
return bool(eval(e, {"__builtins__": {}}, {})) # only numbers/booleans/operators remain
|
|
165
|
+
except Exception:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _preprocess(text: str, arch: str = "avr") -> str:
|
|
170
|
+
"""Keep only the #if branches that apply to the target board (unknown conditions keep all branches)."""
|
|
171
|
+
out: list[str] = []
|
|
172
|
+
stack: list[list] = [] # [parent_active, taken(bool|None), active]
|
|
173
|
+
active = True
|
|
174
|
+
for line in text.splitlines():
|
|
175
|
+
s = line.strip()
|
|
176
|
+
m = re.match(r"#\s*(if|ifdef|ifndef|elif|else|endif)\b(.*)", s)
|
|
177
|
+
if not m:
|
|
178
|
+
if active:
|
|
179
|
+
out.append(line)
|
|
180
|
+
continue
|
|
181
|
+
kind, rest = m.group(1), m.group(2).strip()
|
|
182
|
+
if kind in ("if", "ifdef", "ifndef"):
|
|
183
|
+
cond = (_eval_condition(rest, arch) if kind == "if" else
|
|
184
|
+
_eval_condition(f"defined({rest.split()[0]})" if rest else "", arch))
|
|
185
|
+
if kind == "ifndef" and cond is not None:
|
|
186
|
+
cond = not cond
|
|
187
|
+
stack.append([active, cond if cond is not None else None, None])
|
|
188
|
+
stack[-1][2] = active and (cond is None or cond)
|
|
189
|
+
active = stack[-1][2]
|
|
190
|
+
elif kind == "elif" and stack:
|
|
191
|
+
parent, taken, _ = stack[-1]
|
|
192
|
+
cond = _eval_condition(rest, arch)
|
|
193
|
+
if taken is True:
|
|
194
|
+
now = False
|
|
195
|
+
else:
|
|
196
|
+
now = parent and (cond is None or cond)
|
|
197
|
+
if cond is True: stack[-1][1] = True
|
|
198
|
+
stack[-1][2] = now; active = now
|
|
199
|
+
elif kind == "else" and stack:
|
|
200
|
+
parent, taken, _ = stack[-1]
|
|
201
|
+
now = parent and taken is not True
|
|
202
|
+
stack[-1][2] = now; active = now
|
|
203
|
+
elif kind == "endif" and stack:
|
|
204
|
+
active = stack.pop()[0]
|
|
205
|
+
return "\n".join(out)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _strip_comments(text: str) -> str:
|
|
209
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
210
|
+
return re.sub(r"//[^\n]*", "", text)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _class_bodies(text: str):
|
|
214
|
+
for m in re.finditer(r"\bclass\s+(\w+)\s*(?:final\s*)?(:\s*[^{;]+)?\{", text):
|
|
215
|
+
if re.search(r"template\s*<[^>]*>\s*$", text[max(0, m.start() - 120):m.start()]):
|
|
216
|
+
continue
|
|
217
|
+
depth, i = 1, m.end()
|
|
218
|
+
while i < len(text) and depth:
|
|
219
|
+
depth += {"{": 1, "}": -1}.get(text[i], 0)
|
|
220
|
+
i += 1
|
|
221
|
+
yield m.group(1), (m.group(2) or ""), text[m.end(): i - 1]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _public(body: str) -> str:
|
|
225
|
+
out, public, depth = [], False, 0
|
|
226
|
+
# put every access specifier on its own line ("{ public: void a(); private: int b; }")
|
|
227
|
+
body = re.sub(r"\b(public|private|protected)\s*:(?!:)", r"\n\1:\n", body)
|
|
228
|
+
for line in body.splitlines():
|
|
229
|
+
s = line.strip()
|
|
230
|
+
if depth == 0:
|
|
231
|
+
if re.match(r"^public\s*:$", s):
|
|
232
|
+
public = True; continue
|
|
233
|
+
if re.match(r"^(private|protected)\s*:$", s):
|
|
234
|
+
public = False; continue
|
|
235
|
+
if public and depth == 0:
|
|
236
|
+
out.append(line)
|
|
237
|
+
depth += line.count("{") - line.count("}")
|
|
238
|
+
depth = max(depth, 0)
|
|
239
|
+
return "\n".join(out)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _norm_type(ctype: str) -> str | None:
|
|
243
|
+
if "&" in ctype and "const" not in ctype.split():
|
|
244
|
+
return None # output (reference) parameter: cannot come from Python
|
|
245
|
+
t = ctype.replace("&", " ").strip()
|
|
246
|
+
if "*" in t or "[" in t or "<" in t or "::" in t:
|
|
247
|
+
return None
|
|
248
|
+
words = [w for w in t.split() if w not in ("const", "volatile", "inline", "static", "virtual", "explicit", "constexpr")]
|
|
249
|
+
if not words:
|
|
250
|
+
return None
|
|
251
|
+
if words[0] == "unsigned" or words[0] == "signed":
|
|
252
|
+
return "Int"
|
|
253
|
+
return CPP_TYPES.get(words[-1])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _num(text: str):
|
|
257
|
+
text = text.strip().rstrip("fFuUlL")
|
|
258
|
+
try:
|
|
259
|
+
return float(text) if re.search(r"[.eE]", text) and not text.lower().startswith("0x") else int(text, 0)
|
|
260
|
+
except ValueError:
|
|
261
|
+
if text in ("true", "false"):
|
|
262
|
+
return 1 if text == "true" else 0
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _params(raw: str) -> tuple[list[Param] | None, str]:
|
|
267
|
+
raw = raw.strip()
|
|
268
|
+
if not raw or raw == "void":
|
|
269
|
+
return [], ""
|
|
270
|
+
out: list[Param] = []
|
|
271
|
+
parts, depth, cur = [], 0, ""
|
|
272
|
+
for ch in raw:
|
|
273
|
+
if ch in "(<": depth += 1
|
|
274
|
+
if ch in ")>": depth -= 1
|
|
275
|
+
if ch == "," and depth == 0:
|
|
276
|
+
parts.append(cur); cur = ""
|
|
277
|
+
else:
|
|
278
|
+
cur += ch
|
|
279
|
+
parts.append(cur)
|
|
280
|
+
for i, part in enumerate(x.strip() for x in parts):
|
|
281
|
+
default = None
|
|
282
|
+
has_default = "=" in part
|
|
283
|
+
if has_default:
|
|
284
|
+
part, d = part.split("=", 1)
|
|
285
|
+
default = _num(d)
|
|
286
|
+
if default is None:
|
|
287
|
+
# non-numeric default (macro/enum): stop here and let the library use its default
|
|
288
|
+
return out, f"later parameters use library defaults ({d.strip()})"
|
|
289
|
+
part = part.strip()
|
|
290
|
+
m = re.match(r"^(.*?)([A-Za-z_]\w*)$", part)
|
|
291
|
+
ctype, name = (m.group(1).strip(), m.group(2)) if m and m.group(1).strip() else (part, f"arg{i}")
|
|
292
|
+
ptype = _norm_type(ctype)
|
|
293
|
+
if ptype is None or ptype == "Void":
|
|
294
|
+
return None, f"parameter '{part}' type is not supported"
|
|
295
|
+
name = re.sub(r"_([a-z0-9])", lambda mm: mm.group(1).upper(), name.lstrip("_")) or f"arg{i}"
|
|
296
|
+
name = name.lower() if name.isupper() else name[0].lower() + name[1:]
|
|
297
|
+
is_pin = ptype == "Int" and _looks_like_pin(name)
|
|
298
|
+
out.append(Param(name, ptype, ctype, default, is_pin))
|
|
299
|
+
return out, ""
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _camel(name: str) -> str:
|
|
303
|
+
s = re.sub(r"_([a-zA-Z0-9])", lambda m: m.group(1).upper(), name.strip("_"))
|
|
304
|
+
return (s[0].lower() + s[1:]) if s else s
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def scan_library(root: Path, arch: str = "avr") -> LibraryScan:
|
|
308
|
+
props_file = root / "library.properties"
|
|
309
|
+
props = props_file.read_text(encoding="utf-8", errors="replace") if props_file.is_file() else ""
|
|
310
|
+
def prop(key: str, default: str = "") -> str:
|
|
311
|
+
m = re.search(rf"^{key}=(.*)$", props, re.M)
|
|
312
|
+
return m.group(1).strip() if m else default
|
|
313
|
+
lic, warn = guess_license(root)
|
|
314
|
+
headers = sorted(h for h in list(root.glob("*.h")) + list((root / "src").rglob("*.h")) + list((root / "src").rglob("*.hpp"))
|
|
315
|
+
if "examples" not in h.parts and "test" not in h.parts)
|
|
316
|
+
classes: dict[str, ClassScan] = {}
|
|
317
|
+
problems: list[str] = []
|
|
318
|
+
for h in headers:
|
|
319
|
+
text = _preprocess(_strip_comments(h.read_text(encoding="utf-8", errors="replace")), arch)
|
|
320
|
+
for cls, bases, body in _class_bodies(text):
|
|
321
|
+
if cls in classes or cls.startswith("_"):
|
|
322
|
+
continue
|
|
323
|
+
scan = ClassScan(cls, h.relative_to(root / "src" if (root / "src") in h.parents else root).as_posix())
|
|
324
|
+
scan.printable = bool(re.search(r"\b(Print|Stream|Adafruit_GFX)\b", bases))
|
|
325
|
+
scan.bases = re.findall(r"(?:public\s+)(\w+)", bases)
|
|
326
|
+
pub = _public(body)
|
|
327
|
+
scan.abstract = bool(re.search(r"\)\s*(?:const\s*)?=\s*0\s*;", body))
|
|
328
|
+
ctor_declared = False
|
|
329
|
+
seen: set[str] = set()
|
|
330
|
+
for m in re.finditer(r"(?:^|(?<=[;{}\n]))\s*(?:virtual\s+|static\s+|inline\s+|explicit\s+)*(?P<ret>[A-Za-z_][\w\s\*&:<>]*?\s+[\*&]?)?(?P<name>~?\w+)\s*\((?P<params>[^()]*(?:\([^()]*\)[^()]*)*)\)\s*(?:const\s*)?(?:override\s*)?(?:=\s*0\s*)?[;{]", pub):
|
|
331
|
+
name, ret = m.group("name"), (m.group("ret") or "").strip()
|
|
332
|
+
if name in ("if", "for", "while", "switch", "return", "operator"):
|
|
333
|
+
continue
|
|
334
|
+
params, note = _params(m.group("params"))
|
|
335
|
+
if name == cls:
|
|
336
|
+
ctor_declared = True
|
|
337
|
+
if params is not None:
|
|
338
|
+
cut = next((i for i, p in enumerate(params) if p.type != "Int"), None)
|
|
339
|
+
if cut is not None:
|
|
340
|
+
if params[cut].default is None:
|
|
341
|
+
scan.skipped.append(f"constructor {cls}({m.group('params').strip()}): needs a {params[cut].type} argument")
|
|
342
|
+
continue
|
|
343
|
+
params = params[:cut]
|
|
344
|
+
scan.constructors.append(params)
|
|
345
|
+
else:
|
|
346
|
+
scan.skipped.append(f"constructor {cls}({m.group('params').strip()}): {note}")
|
|
347
|
+
continue
|
|
348
|
+
if name.startswith("~") or name.startswith("_") or "operator" in ret or not ret:
|
|
349
|
+
continue
|
|
350
|
+
if "static" in m.group(0).split(name)[0]:
|
|
351
|
+
scan.skipped.append(f"{name}(): static function"); continue
|
|
352
|
+
rtype = _norm_type(ret)
|
|
353
|
+
if rtype is None:
|
|
354
|
+
scan.skipped.append(f"{name}(): returns '{ret}' (not a number/bool)"); continue
|
|
355
|
+
if params is None:
|
|
356
|
+
scan.skipped.append(f"{name}(): {note}"); continue
|
|
357
|
+
student = _camel(name)
|
|
358
|
+
if name in ("write", "print", "println") and scan.printable:
|
|
359
|
+
continue
|
|
360
|
+
if student in seen or not re.match(r"^[a-z][A-Za-z0-9]{0,30}$", student):
|
|
361
|
+
scan.skipped.append(f"{name}(): overloaded or invalid name (kept the first version)"); continue
|
|
362
|
+
seen.add(student)
|
|
363
|
+
scan.methods.append(Method(name, student, params, rtype))
|
|
364
|
+
if not scan.constructors:
|
|
365
|
+
if ctor_declared:
|
|
366
|
+
scan.unusable = "its constructor needs arrays/pointers/special types (needs a hand-written adapter)"
|
|
367
|
+
else:
|
|
368
|
+
scan.constructors.append([])
|
|
369
|
+
if scan.abstract:
|
|
370
|
+
scan.unusable = "it is an abstract base class (use one of the classes built on it)"
|
|
371
|
+
classes[cls] = scan
|
|
372
|
+
# public base classes defined in the same library: inherit their methods / Print ability
|
|
373
|
+
def inherit(c: ClassScan, depth: int = 0) -> None:
|
|
374
|
+
for b in c.bases:
|
|
375
|
+
base = classes.get(b)
|
|
376
|
+
if base is None or depth > 5:
|
|
377
|
+
continue
|
|
378
|
+
inherit(base, depth + 1)
|
|
379
|
+
have = {m.student_name for m in c.methods}
|
|
380
|
+
c.methods += [Method(m.cpp_name, m.student_name, m.params, m.returns) for m in base.methods if m.student_name not in have]
|
|
381
|
+
c.printable = c.printable or base.printable
|
|
382
|
+
for c in classes.values():
|
|
383
|
+
inherit(c)
|
|
384
|
+
unusable = {k: v.unusable for k, v in classes.items() if v.unusable and (v.methods or v.printable)}
|
|
385
|
+
classes = {k: v for k, v in classes.items() if (v.methods or v.printable) and not v.unusable}
|
|
386
|
+
for k, why in unusable.items():
|
|
387
|
+
problems.append(f"Class {k} skipped: {why}.")
|
|
388
|
+
for c in classes.values():
|
|
389
|
+
if any(s.startswith(("begin(", "init(")) for s in c.skipped) and not any(m.cpp_name in ("begin", "init") for m in c.methods):
|
|
390
|
+
problems.append(f"Class {c.name}: begin()/init() could not be converted (pointer/array parameters). "
|
|
391
|
+
"The device may need a hand-written adapter.")
|
|
392
|
+
if not classes:
|
|
393
|
+
problems.append("No usable C++ class found. Libraries that only provide functions or a ready-made global "
|
|
394
|
+
"object (for example Wire, EEPROM, IrReceiver) need a hand-written adapter.")
|
|
395
|
+
lib_name = prop("name", root.name)
|
|
396
|
+
folder = re.sub(r"[^A-Za-z0-9_.-]+", "_", lib_name).strip("_") or "ArduinoLibrary"
|
|
397
|
+
depends = [re.sub(r"\s*\(.*\)\s*$", "", d).strip() for d in prop("depends").split(",") if d.strip()]
|
|
398
|
+
return LibraryScan(root, folder, lib_name, prop("version", "SET_VERSION"), lic, warn,
|
|
399
|
+
prop("url"), classes, problems, depends)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------- pack generation ----------------
|
|
403
|
+
def main_class(lib: LibraryScan) -> str | None:
|
|
404
|
+
"""Best guess of the class students need (name like the library, has begin(), most methods)."""
|
|
405
|
+
if not lib.classes:
|
|
406
|
+
return None
|
|
407
|
+
key = re.sub(r"[^a-z0-9]", "", lib.name.lower())
|
|
408
|
+
def score(item):
|
|
409
|
+
name, c = item
|
|
410
|
+
n = re.sub(r"[^a-z0-9]", "", name.lower())
|
|
411
|
+
return ((n in key or key in n) * 3 + any(m.cpp_name in ("begin", "init") for m in c.methods) * 2
|
|
412
|
+
+ c.printable + min(len(c.methods), 30) / 30)
|
|
413
|
+
return max(lib.classes.items(), key=score)[0]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def default_constructor(scan: ClassScan) -> int:
|
|
417
|
+
"""Empty constructor when the class has begin(); else all-pin constructor; else shortest."""
|
|
418
|
+
ctors = scan.constructors
|
|
419
|
+
has_begin = any(m.cpp_name in ("begin", "init") for m in scan.methods)
|
|
420
|
+
if has_begin and [] in ctors:
|
|
421
|
+
return ctors.index([]) # I2C/bus devices: empty constructor + begin()
|
|
422
|
+
pins_only = [i for i, c in enumerate(ctors) if c and all(p.is_pin and p.default is None for p in c)]
|
|
423
|
+
if pins_only:
|
|
424
|
+
return min(pins_only, key=lambda i: len(ctors[i]))
|
|
425
|
+
empty = [i for i, c in enumerate(ctors) if not [p for p in c if p.default is None]]
|
|
426
|
+
if empty:
|
|
427
|
+
return empty[0]
|
|
428
|
+
return min(range(len(ctors)), key=lambda i: len(ctors[i]))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def default_student_name(cls: str) -> str:
|
|
432
|
+
base = re.sub(r"[^A-Za-z0-9]", "", cls)
|
|
433
|
+
base = base[0].upper() + base[1:] if base else "Device"
|
|
434
|
+
if len(base) < 3:
|
|
435
|
+
base += "Device"
|
|
436
|
+
return base + "Module" if base == cls else base
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def build_pack(lib: LibraryScan, cls: str, student: str, ctor_index: int = 0, title: str | None = None,
|
|
440
|
+
status: str = "prototype", add_print: bool | None = None) -> tuple[dict, str, str]:
|
|
441
|
+
if ctor_index < 0:
|
|
442
|
+
ctor_index = default_constructor(lib.classes[cls])
|
|
443
|
+
"""Return (pack.json dict, adapter header text, example.py text) for the selected methods."""
|
|
444
|
+
scan = lib.classes[cls]
|
|
445
|
+
if not re.match(r"^[A-Z][A-Za-z0-9]{2,40}$", student):
|
|
446
|
+
raise ValueError("Student class name must be CamelCase letters/digits, e.g. WeightSensor")
|
|
447
|
+
if student == cls:
|
|
448
|
+
raise ValueError(f"Student class name must differ from the library class '{cls}' (C++ name clash).")
|
|
449
|
+
ctor = scan.constructors[ctor_index] if scan.constructors else []
|
|
450
|
+
methods = [m for m in scan.methods if m.selected]
|
|
451
|
+
printable = scan.printable if add_print is None else add_print
|
|
452
|
+
lname = f"Barmaja_{student}"
|
|
453
|
+
hdr_name = Path(scan.header).name
|
|
454
|
+
def cparams(ps):
|
|
455
|
+
return ", ".join(f"{ADAPTER_CTYPE[p.type]} {p.name}" + (f" = {p.default}" if p.default is not None else "") for p in ps)
|
|
456
|
+
ctor_args = ", ".join(p.name for p in ctor)
|
|
457
|
+
lines = ["#pragma once", "#include <Arduino.h>", f"#include <{hdr_name}>", "",
|
|
458
|
+
f"// Generated by BARMAJA Library Adder from {lib.name} {lib.version}.",
|
|
459
|
+
f"class {student}{' : public Print' if printable else ''} {{", "public:",
|
|
460
|
+
f" {'explicit ' if len(ctor) == 1 else ''}{student}({cparams(ctor)}){(' : dev_(' + ctor_args + ')') if ctor else ''} {{}}"]
|
|
461
|
+
for m in methods:
|
|
462
|
+
call = f"dev_.{m.cpp_name}({', '.join(p.name for p in m.params)})"
|
|
463
|
+
lines.append(f" {ADAPTER_CTYPE[m.returns]} {m.student_name}({cparams(m.params)}) {{ {'return ' if m.returns != 'Void' else ''}{call}; }}")
|
|
464
|
+
if printable:
|
|
465
|
+
lines += [" size_t write(uint8_t c) override { return dev_.write(c); }", " using Print::print;", " using Print::println;"]
|
|
466
|
+
lines += ["private:", f" {cls} dev_;", "};", ""]
|
|
467
|
+
starter = next((m.student_name for m in methods if m.cpp_name in ("begin", "init", "setup")), None)
|
|
468
|
+
def pjson(p: Param) -> dict:
|
|
469
|
+
d = {"name": p.name, "type": p.type}
|
|
470
|
+
if p.is_pin: d["rule"] = "digital_pin"
|
|
471
|
+
if p.default is not None: d["default"] = p.default
|
|
472
|
+
return d
|
|
473
|
+
mjson = {}
|
|
474
|
+
for m in methods:
|
|
475
|
+
entry = {"params": [pjson(p) for p in m.params], "returns": m.returns}
|
|
476
|
+
if m.student_name == starter: entry["starts"] = True
|
|
477
|
+
elif starter is None: entry["requires_begin"] = False
|
|
478
|
+
mjson[m.student_name] = entry
|
|
479
|
+
if printable:
|
|
480
|
+
mjson["print"] = {"params": [{"name": "value", "type": "Scalar"}], "returns": "Void", **({} if starter else {"requires_begin": False})}
|
|
481
|
+
mjson["println"] = {"params": [{"name": "value", "type": "Scalar", "default": 0}], "returns": "Void", **({} if starter else {"requires_begin": False})}
|
|
482
|
+
dep_id = re.sub(r"[^a-z0-9]+", "-", lib.name.lower()).strip("-") or lib.folder_name.lower()
|
|
483
|
+
spec = {
|
|
484
|
+
"schema": 1, "class": student, "title": title or f"{lib.name} ({cls})", "status": status, "boards": ["uno", "nano"],
|
|
485
|
+
"constructor": [pjson(p) for p in ctor], "methods": mjson, "resources": [], "warnings": [],
|
|
486
|
+
"adapter": {"library": lname, "header": f"{lname}.h"},
|
|
487
|
+
"dependencies": [{"id": dep_id, "name": lib.name, "version": lib.version, "header": hdr_name,
|
|
488
|
+
"license": lib.license, "library_folder": lib.folder_name, **({"source": lib.url} if lib.url else {})}],
|
|
489
|
+
"liverun": False,
|
|
490
|
+
}
|
|
491
|
+
notes = []
|
|
492
|
+
if lib.depends:
|
|
493
|
+
notes.append("Also needs Arduino libraries: " + ", ".join(lib.depends))
|
|
494
|
+
if scan.skipped:
|
|
495
|
+
notes.append("Not converted: " + "; ".join(scan.skipped[:25]))
|
|
496
|
+
if notes:
|
|
497
|
+
spec["notes"] = " | ".join(notes)
|
|
498
|
+
# example program
|
|
499
|
+
def sample(ps):
|
|
500
|
+
vals, pin = [], 2
|
|
501
|
+
for p in ps:
|
|
502
|
+
if p.default is not None: break
|
|
503
|
+
if p.is_pin: vals.append(str(pin)); pin += 1
|
|
504
|
+
else: vals.append({"Float": "1.0", "Bool": "True"}.get(p.type, "1"))
|
|
505
|
+
return ", ".join(vals)
|
|
506
|
+
low = student.lower()
|
|
507
|
+
obj = ("screen" if any(w in low for w in ("display", "lcd", "oled", "screen", "matrix")) else
|
|
508
|
+
"motor" if any(w in low for w in ("motor", "servo", "stepper", "pwm")) else
|
|
509
|
+
"sensor" if any(w in low for w in ("sensor", "temp", "max", "bmp", "bme", "aht", "dht", "ina", "tcs", "apds", "mpu", "hx711", "vl53")) else
|
|
510
|
+
"device")
|
|
511
|
+
ex = ["from Barmaja_PyArduino import *", "upload()", "", f"{obj} = {student}({sample(ctor)})"]
|
|
512
|
+
if starter:
|
|
513
|
+
sm = next(m for m in methods if m.student_name == starter)
|
|
514
|
+
ex.append(f"{obj}.{starter}({sample(sm.params)})")
|
|
515
|
+
ex.append("while True:")
|
|
516
|
+
readers = [m for m in methods if not [p for p in m.params if p.default is None] and m.returns in ("Int", "Float", "Bool") and m.student_name != starter]
|
|
517
|
+
def useful(m):
|
|
518
|
+
n = m.student_name.lower()
|
|
519
|
+
return (("read" in n) * 4 + any(w in n for w in ("temp", "distance", "value", "unit", "weight", "pressure", "humid", "range", "position", "key", "lux", "current", "voltage")) * 3
|
|
520
|
+
+ n.startswith("get") - any(w in n for w in ("status", "id", "error", "version", "address")) * 5 + (m.returns == "Float"))
|
|
521
|
+
reader = max(readers, key=useful) if readers else None
|
|
522
|
+
if reader:
|
|
523
|
+
ex.append(f" print(\"{reader.student_name}:\", {obj}.{reader.student_name}())")
|
|
524
|
+
elif printable:
|
|
525
|
+
ex.append(f" {obj}.print(\"Hello\")")
|
|
526
|
+
ex.append(" delay(500)")
|
|
527
|
+
spec["example"] = "\n".join(ex) + "\n"
|
|
528
|
+
return spec, "\n".join(lines), spec["example"]
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def write_pack(dest_root: Path, spec: dict, adapter: str) -> Path:
|
|
532
|
+
folder = Path(dest_root) / spec["class"]
|
|
533
|
+
lname = spec["adapter"]["library"]
|
|
534
|
+
if folder.exists():
|
|
535
|
+
shutil.rmtree(folder)
|
|
536
|
+
(folder / "arduino" / lname / "src").mkdir(parents=True)
|
|
537
|
+
(folder / "arduino" / lname / "src" / spec["adapter"]["header"]).write_text(adapter, encoding="utf-8")
|
|
538
|
+
(folder / "arduino" / lname / "library.properties").write_text(
|
|
539
|
+
f"name={lname}\nversion=1.0.0\nauthor=BARMAJA Library Adder\nmaintainer=BARMAJA\n"
|
|
540
|
+
f"sentence=BARMAJA adapter for {spec['dependencies'][0]['name'] if spec['dependencies'] else spec['class']}\n"
|
|
541
|
+
"paragraph=\ncategory=Device Control\nurl=\narchitectures=*\n", encoding="utf-8")
|
|
542
|
+
(folder / "pack.json").write_text(json.dumps(spec, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
543
|
+
(folder / "example.py").write_text(spec["example"], encoding="utf-8")
|
|
544
|
+
return folder
|
|
File without changes
|