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