@typecad/framework-arduino 0.1.0-alpha.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/arduino/i2c-arduino.d.ts +47 -0
  4. package/dist/arduino/i2c-arduino.js +6 -0
  5. package/dist/arduino/index.d.ts +6 -0
  6. package/dist/arduino/index.js +8 -0
  7. package/dist/arduino/spi-arduino.d.ts +42 -0
  8. package/dist/arduino/spi-arduino.js +6 -0
  9. package/dist/arduino/stubs-uno.d.ts +9 -0
  10. package/dist/arduino/stubs-uno.js +65 -0
  11. package/dist/arduino/uart-arduino.d.ts +41 -0
  12. package/dist/arduino/uart-arduino.js +6 -0
  13. package/dist/arduino-class-map.d.ts +5 -0
  14. package/dist/arduino-class-map.js +21 -0
  15. package/dist/arduino-compile.d.ts +24 -0
  16. package/dist/arduino-compile.js +316 -0
  17. package/dist/arduino-libs.d.ts +6 -0
  18. package/dist/arduino-libs.js +11 -0
  19. package/dist/cli-metadata.d.ts +12 -0
  20. package/dist/cli-metadata.js +196 -0
  21. package/dist/cpp-parser.d.ts +47 -0
  22. package/dist/cpp-parser.js +292 -0
  23. package/dist/debug-codegen.d.ts +20 -0
  24. package/dist/debug-codegen.js +115 -0
  25. package/dist/displays/ili9341-spi.d.ts +6 -0
  26. package/dist/displays/ili9341-spi.js +28 -0
  27. package/dist/displays/ssd1309-i2c.d.ts +2 -0
  28. package/dist/displays/ssd1309-i2c.js +16 -0
  29. package/dist/displays/st7796-spi.d.ts +2 -0
  30. package/dist/displays/st7796-spi.js +20 -0
  31. package/dist/graphics/ili9341.d.ts +24 -0
  32. package/dist/graphics/ili9341.js +59 -0
  33. package/dist/index.d.ts +13 -0
  34. package/dist/index.js +27 -0
  35. package/dist/lib-declaration.d.ts +35 -0
  36. package/dist/lib-declaration.js +399 -0
  37. package/dist/lib-discovery.d.ts +38 -0
  38. package/dist/lib-discovery.js +189 -0
  39. package/dist/profile.d.ts +8 -0
  40. package/dist/profile.js +584 -0
  41. package/dist/strategy.d.ts +174 -0
  42. package/dist/strategy.js +2032 -0
  43. package/package.json +73 -0
  44. package/src/arduino/i2c-arduino.ts +72 -0
  45. package/src/arduino/index.ts +18 -0
  46. package/src/arduino/spi-arduino.ts +63 -0
  47. package/src/arduino/stubs-uno.ts +80 -0
  48. package/src/arduino/uart-arduino.ts +59 -0
  49. package/src/arduino-class-map.ts +32 -0
  50. package/src/arduino-compile.ts +359 -0
  51. package/src/arduino-libs.ts +33 -0
  52. package/src/cli-metadata.ts +250 -0
  53. package/src/cpp-parser.ts +361 -0
  54. package/src/debug-codegen.ts +155 -0
  55. package/src/displays/ili9341-spi.ts +33 -0
  56. package/src/displays/ssd1309-i2c.ts +19 -0
  57. package/src/displays/st7796-spi.ts +23 -0
  58. package/src/graphics/ili9341.ts +80 -0
  59. package/src/index.ts +37 -0
  60. package/src/lib-declaration.ts +493 -0
  61. package/src/lib-discovery.ts +267 -0
  62. package/src/profile.ts +676 -0
  63. package/src/strategy.ts +2126 -0
@@ -0,0 +1,316 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Arduino sketch compilation, upload, and monitoring
3
+ //
4
+ // Provides functions to flatten generated modules into an Arduino sketch,
5
+ // compile using arduino-cli, upload to a board, and monitor serial output.
6
+ // ---------------------------------------------------------------------------
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ import { spawnSync } from "node:child_process";
10
+ import { parseCompileErrors, collectCppFiles } from "@typecad/cuttlefish/api/shared";
11
+ const ARDUINO_ERROR = /^(.*?):(\d+):\d+:\s*(error|warning|note):\s*(.*)$/i;
12
+ /**
13
+ * Parse compile errors with Arduino format support.
14
+ */
15
+ function parseArduinoCompileErrors(output, sketchDir) {
16
+ const errors = [];
17
+ for (const rawLine of output.split(/\r?\n/)) {
18
+ const line = rawLine.trim();
19
+ if (!line)
20
+ continue;
21
+ let match = line.match(ARDUINO_ERROR);
22
+ if (!match) {
23
+ continue;
24
+ }
25
+ const severityRaw = match[3].toLowerCase();
26
+ const severity = severityRaw.includes("error") ? "error" : severityRaw === "warning" ? "warning" : "note";
27
+ let filePath = match[1];
28
+ if (sketchDir) {
29
+ if (!path.isAbsolute(filePath)) {
30
+ const resolved = path.resolve(sketchDir, filePath);
31
+ if (fs.existsSync(resolved)) {
32
+ filePath = resolved;
33
+ }
34
+ }
35
+ if (filePath.includes(path.basename(sketchDir))) {
36
+ filePath = path.resolve(sketchDir, path.basename(sketchDir) + ".ino");
37
+ }
38
+ }
39
+ errors.push({
40
+ filePath: path.resolve(filePath),
41
+ line: parseInt(match[2], 10),
42
+ column: 1,
43
+ severity,
44
+ message: match[4],
45
+ });
46
+ }
47
+ return errors;
48
+ }
49
+ /**
50
+ * Parse memory usage from arduino-cli output.
51
+ */
52
+ export function parseArduinoMemoryUsage(output) {
53
+ const result = {};
54
+ // Sketch uses 444 bytes (1%) of program storage space. Maximum is 32256 bytes.
55
+ const flashMatch = output.match(/Sketch uses (\d+) bytes.*Maximum is (\d+) bytes/i);
56
+ if (flashMatch) {
57
+ result.flashUsed = parseInt(flashMatch[1], 10);
58
+ result.flashTotal = parseInt(flashMatch[2], 10);
59
+ }
60
+ // Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.
61
+ const ramMatch = output.match(/Global variables use (\d+) bytes.*Maximum is (\d+) bytes/i);
62
+ if (ramMatch) {
63
+ result.ramUsed = parseInt(ramMatch[1], 10);
64
+ result.ramTotal = parseInt(ramMatch[2], 10);
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Extract architecture from FQBN string.
70
+ * FQBN format: vendor:arch:board[:config]
71
+ */
72
+ export function toArchitectureFromFqbn(buildTarget) {
73
+ if (!buildTarget)
74
+ return undefined;
75
+ return buildTarget.split(":")[1];
76
+ }
77
+ export function flattenGeneratedModulesIntoSketch(sketchDir, sketchPath) {
78
+ const normalizedSketchPath = path.resolve(sketchPath);
79
+ const cppFiles = collectCppFiles(sketchDir).filter((filePath) => path.resolve(filePath) !== normalizedSketchPath);
80
+ if (cppFiles.length === 0) {
81
+ return;
82
+ }
83
+ // Collect header file names that correspond to the merged .cpp files
84
+ // These includes should be stripped from the entry sketch since the code is merged inline
85
+ const mergedHeaderNames = new Set();
86
+ // Also track header file paths for inlining
87
+ const headerFiles = new Map(); // baseName -> headerPath
88
+ for (const cppPath of cppFiles) {
89
+ const baseName = path.basename(cppPath, path.extname(cppPath));
90
+ const headerName = `${baseName}.h`;
91
+ mergedHeaderNames.add(headerName);
92
+ // Check if corresponding .h file exists
93
+ const headerPath = path.join(path.dirname(cppPath), headerName);
94
+ if (fs.existsSync(headerPath) && fs.statSync(headerPath).isFile()) {
95
+ headerFiles.set(baseName, headerPath);
96
+ }
97
+ }
98
+ // Also collect standalone .h files in the output directory that have no .cpp.
99
+ // These come from modules that only contain `declare function` (no bodies = no .cpp output).
100
+ for (const entry of fs.readdirSync(sketchDir)) {
101
+ if (!entry.endsWith(".h"))
102
+ continue;
103
+ if (mergedHeaderNames.has(entry))
104
+ continue;
105
+ // Skip the sketch's own header and non-generated files
106
+ if (entry === path.basename(normalizedSketchPath, ".ino") + ".h")
107
+ continue;
108
+ mergedHeaderNames.add(entry);
109
+ const headerPath = path.join(sketchDir, entry);
110
+ if (fs.statSync(headerPath).isFile()) {
111
+ headerFiles.set(path.basename(entry, ".h"), headerPath);
112
+ }
113
+ }
114
+ const originalSketch = fs.readFileSync(normalizedSketchPath, "utf8");
115
+ const sanitizedSketch = originalSketch
116
+ .replace(/^\s*#include\s+<Arduino\.h>\s*$/gm, "")
117
+ .replace(/^\s*#include\s+"Arduino\.h"\s*$/gm, "")
118
+ // Strip includes for headers corresponding to merged .cpp modules
119
+ .replace(/^\s*#include\s+"([^"]+)"\s*$/gm, (line, includePath) => {
120
+ const headerName = path.basename(includePath);
121
+ if (mergedHeaderNames.has(headerName)) {
122
+ return ""; // Remove the include since the module is merged inline
123
+ }
124
+ return line;
125
+ });
126
+ // Collect module contents - native C++ modules are NOT wrapped in namespaces
127
+ // since they're already valid C++ code
128
+ const moduleContents = [];
129
+ for (const cppPath of cppFiles) {
130
+ const baseName = path.basename(cppPath, path.extname(cppPath));
131
+ const relativePath = path.relative(sketchDir, cppPath).replace(/\\/g, "/");
132
+ // First, inline the corresponding .h file if it exists
133
+ const headerPath = headerFiles.get(baseName);
134
+ if (headerPath) {
135
+ const headerRelativePath = path.relative(sketchDir, headerPath).replace(/\\/g, "/");
136
+ const headerContent = fs.readFileSync(headerPath, "utf8");
137
+ const sanitizedHeader = headerContent
138
+ .replace(/^\s*#include\s+<Arduino\.h>\s*$/gm, "")
139
+ .replace(/^\s*#include\s+"Arduino\.h"\s*$/gm, "")
140
+ .replace(/^\s*#include\s+"([^"]+)"\s*$/gm, (line, includePath) => {
141
+ const headerName = path.basename(includePath);
142
+ if (mergedHeaderNames.has(headerName)) {
143
+ return ""; // Remove the include since the module is merged inline
144
+ }
145
+ return line;
146
+ });
147
+ moduleContents.push(`\n// ---- merged from ${headerRelativePath} ----\n${sanitizedHeader}\n`);
148
+ }
149
+ const content = fs.readFileSync(cppPath, "utf8");
150
+ const rewritten = content.replace(/^\s*#include\s+"([^"]+)"\s*$/gm, (_line, includePath) => {
151
+ const headerName = path.basename(includePath);
152
+ // Strip includes for headers that we've already inlined
153
+ if (mergedHeaderNames.has(headerName)) {
154
+ return "";
155
+ }
156
+ const resolved = path.resolve(path.dirname(cppPath), includePath);
157
+ if (!resolved.startsWith(path.resolve(sketchDir))) {
158
+ return `#include "${includePath}"`;
159
+ }
160
+ let relativeToSketch = path.relative(sketchDir, resolved).replace(/\\/g, "/");
161
+ relativeToSketch = relativeToSketch.replace(/(^|\/)([^\/]+)\/\2(?=\/|$)/g, "$1$2");
162
+ if (!relativeToSketch.startsWith(".")) {
163
+ relativeToSketch = `./${relativeToSketch}`;
164
+ }
165
+ return `#include "${relativeToSketch}"`;
166
+ });
167
+ const sanitized = rewritten
168
+ .replace(/^\s*#include\s+<Arduino\.h>\s*$/gm, "")
169
+ .replace(/^\s*#include\s+"Arduino\.h"\s*$/gm, "");
170
+ // Skip modules whose body is empty after tree-shaking
171
+ const stripped = sanitized
172
+ .replace(/^\s*#include\s+.*$/gm, "")
173
+ .replace(/^\s*\/\/.*$/gm, "")
174
+ .replace(/\/\*[\s\S]*?\*\//g, "")
175
+ .trim();
176
+ if (stripped.length === 0) {
177
+ continue;
178
+ }
179
+ moduleContents.push(`\n// ---- merged from ${relativePath} ----\n${sanitized}\n`);
180
+ }
181
+ const mergedSketch = `${moduleContents.join("")}// ---- entry sketch ----\n${sanitizedSketch}\n`;
182
+ fs.writeFileSync(normalizedSketchPath, mergedSketch, "utf8");
183
+ // Clean up merged .cpp and .h files
184
+ for (const cppPath of cppFiles) {
185
+ fs.rmSync(cppPath, { force: true });
186
+ }
187
+ for (const headerPath of headerFiles.values()) {
188
+ fs.rmSync(headerPath, { force: true });
189
+ }
190
+ }
191
+ export function compileArduinoSketch(sketchFilePath, buildTarget, options) {
192
+ const resolvedSketchFilePath = path.resolve(sketchFilePath);
193
+ let sketchDir = path.dirname(resolvedSketchFilePath);
194
+ let sketchDirName = path.basename(sketchDir);
195
+ let requiredSketchPath = path.join(sketchDir, `${sketchDirName}.ino`);
196
+ if (sketchDirName.startsWith(".")) {
197
+ const stagingDir = path.join(path.dirname(sketchDir), "ts2cpp_sketch");
198
+ try {
199
+ fs.rmSync(stagingDir, { recursive: true, force: true });
200
+ fs.mkdirSync(stagingDir, { recursive: true });
201
+ fs.cpSync(sketchDir, stagingDir, { recursive: true });
202
+ sketchDir = stagingDir;
203
+ sketchDirName = path.basename(sketchDir);
204
+ requiredSketchPath = path.join(sketchDir, `${sketchDirName}.ino`);
205
+ }
206
+ catch {
207
+ // Best-effort staging; fall back to original directory behavior.
208
+ }
209
+ }
210
+ if (path.extname(resolvedSketchFilePath).toLowerCase() === ".ino" && resolvedSketchFilePath !== requiredSketchPath) {
211
+ try {
212
+ const sourceText = fs.readFileSync(resolvedSketchFilePath, "utf8");
213
+ if (!fs.existsSync(requiredSketchPath) || fs.readFileSync(requiredSketchPath, "utf8") !== sourceText) {
214
+ fs.writeFileSync(requiredSketchPath, sourceText, "utf8");
215
+ }
216
+ // Arduino sketches must have a single primary .ino matching the folder name.
217
+ // Remove other .ino files in the sketch root to avoid duplicate setup()/loop().
218
+ for (const child of fs.readdirSync(sketchDir)) {
219
+ if (!child.toLowerCase().endsWith(".ino")) {
220
+ continue;
221
+ }
222
+ const candidate = path.join(sketchDir, child);
223
+ if (path.resolve(candidate) !== path.resolve(requiredSketchPath)) {
224
+ fs.rmSync(candidate, { force: true });
225
+ }
226
+ }
227
+ }
228
+ catch {
229
+ // Best-effort sketch alias generation. If this fails, arduino-cli output will report details.
230
+ }
231
+ }
232
+ try {
233
+ if (fs.existsSync(requiredSketchPath)) {
234
+ flattenGeneratedModulesIntoSketch(sketchDir, requiredSketchPath);
235
+ }
236
+ }
237
+ catch {
238
+ // Best-effort flattening for generated modules.
239
+ }
240
+ // Build the arduino-cli argument list. User defines/extraFlags (from
241
+ // output.defines and output.extraFlags in cuttlefish.config.ts) are injected
242
+ // via a build_opt.h file in the sketch dir, which arduino-cli reads through
243
+ // the platform recipe's @"{build.opt.path}" token and feeds to the compiler
244
+ // WITHOUT touching any platform property.
245
+ //
246
+ // Why not --build-property? Both candidate properties are load-bearing on
247
+ // modern cores and `--build-property` REPLACES rather than appends:
248
+ // - compiler.cpp.extra_flags carries -MMD/-c (dependency tracking); replacing
249
+ // it desyncs the sketch from the precompiled core, producing dozens of
250
+ // "undefined reference" errors to core symbols (pinMode, Serial, main, ...).
251
+ // - build.extra_flags on the ESP32 core is a composed value that pulls in
252
+ // the USB/CDC/boot-mode defines (ARDUINO_USB_CDC_ON_BOOT, ARDUINO_USB_MODE,
253
+ // ...); replacing it silently breaks USB-serial sketches.
254
+ // build_opt.h avoids both: the platform recipe appends it verbatim, so every
255
+ // board/core property stays intact and the user defines are applied cleanly.
256
+ const compileArgs = ["compile", "--fqbn", buildTarget];
257
+ const userFlagParts = [];
258
+ if (options?.defines) {
259
+ for (const [name, value] of Object.entries(options.defines)) {
260
+ userFlagParts.push(value !== "" ? `-D${name}=${value}` : `-D${name}`);
261
+ }
262
+ }
263
+ if (options?.extraFlags) {
264
+ userFlagParts.push(...options.extraFlags);
265
+ }
266
+ if (userFlagParts.length > 0) {
267
+ const buildOptPath = path.join(sketchDir, "build_opt.h");
268
+ try {
269
+ // Preserve any user-authored build_opt.h content; append our defines
270
+ // rather than overwriting so we don't drop flags the sketch relies on.
271
+ const existing = fs.existsSync(buildOptPath)
272
+ ? fs.readFileSync(buildOptPath, "utf8")
273
+ : "";
274
+ const generated = `${userFlagParts.join("\n")}\n`;
275
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
276
+ fs.writeFileSync(buildOptPath, existing + sep + generated, "utf8");
277
+ }
278
+ catch {
279
+ // Best-effort: if we can't write build_opt.h, fall back to no defines
280
+ // rather than failing the whole compile. The compile will still run;
281
+ // the user's defines just won't be applied.
282
+ }
283
+ }
284
+ compileArgs.push(sketchDir);
285
+ const cmd = spawnSync("arduino-cli", compileArgs, {
286
+ encoding: "utf8",
287
+ timeout: 120000,
288
+ });
289
+ const output = `${cmd.stdout ?? ""}\n${cmd.stderr ?? ""}`.trim();
290
+ const gccErrors = parseCompileErrors(output, sketchDir);
291
+ const arduinoErrors = parseArduinoCompileErrors(output, sketchDir);
292
+ const errors = gccErrors.length > 0 ? gccErrors : arduinoErrors;
293
+ return {
294
+ success: cmd.status === 0,
295
+ output,
296
+ errors,
297
+ memoryUsage: cmd.status === 0 ? parseArduinoMemoryUsage(output) : undefined,
298
+ };
299
+ }
300
+ export function uploadArduinoSketch(sketchDir, buildTarget, port) {
301
+ const cmd = spawnSync("arduino-cli", ["upload", "--fqbn", buildTarget, "--port", port, sketchDir], {
302
+ encoding: "utf8",
303
+ timeout: 60000,
304
+ });
305
+ const output = `${cmd.stdout ?? ""}\n${cmd.stderr ?? ""}`.trim();
306
+ return {
307
+ success: cmd.status === 0,
308
+ output,
309
+ };
310
+ }
311
+ export function monitorArduinoSketch(port, baud) {
312
+ spawnSync("arduino-cli", ["monitor", "--port", port, "--config", `baudrate=${baud}`], {
313
+ stdio: "inherit",
314
+ timeout: 0,
315
+ });
316
+ }
@@ -0,0 +1,6 @@
1
+ export type { ArduinoLibrary } from './lib-discovery.js';
2
+ export { getInstalledLibraries, findArduinoLibrary, findLibraryHeader, isArduinoLibraryImport, clearLibraryCache, } from './lib-discovery.js';
3
+ export type { CppParseResult } from './cpp-parser.js';
4
+ export { mapCppTypeToTs, parseParameters, parseCppClass, } from './cpp-parser.js';
5
+ export type { GeneratedArduinoLib } from './lib-declaration.js';
6
+ export { tryGenerateArduinoLibDecl, getArduinoLibraryHeaderName, getArduinoLibraryClassNames, generateUsageDocumentation, } from './lib-declaration.js';
@@ -0,0 +1,11 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/framework-arduino — Arduino Library Utilities
3
+ //
4
+ // Thin re-export barrel. Implementation is split across:
5
+ // - lib-discovery.ts — finding installed libraries via arduino-cli
6
+ // - cpp-parser.ts — parsing C++ headers into an IR
7
+ // - lib-declaration.ts — generating .d.ts declarations + docs
8
+ // ---------------------------------------------------------------------------
9
+ export { getInstalledLibraries, findArduinoLibrary, findLibraryHeader, isArduinoLibraryImport, clearLibraryCache, } from './lib-discovery.js';
10
+ export { mapCppTypeToTs, parseParameters, parseCppClass, } from './cpp-parser.js';
11
+ export { tryGenerateArduinoLibDecl, getArduinoLibraryHeaderName, getArduinoLibraryClassNames, generateUsageDocumentation, } from './lib-declaration.js';
@@ -0,0 +1,12 @@
1
+ import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
2
+ import type { ArduinoPlatformContext } from "./strategy.js";
3
+ export interface ArduinoCliMetadata {
4
+ architecture?: string;
5
+ pins: Record<string, number>;
6
+ builtinFunctions: Set<string>;
7
+ builtinGlobals: Set<string>;
8
+ }
9
+ export declare function loadArduinoCliMetadata(ctx?: ArduinoPlatformContext): {
10
+ metadata?: ArduinoCliMetadata;
11
+ diagnostics: Diagnostic[];
12
+ };
@@ -0,0 +1,196 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Arduino CLI metadata loader
3
+ //
4
+ // Loads board metadata from arduino-cli when available.
5
+ // Uses disk caching to avoid repeated slow arduino-cli calls.
6
+ // ---------------------------------------------------------------------------
7
+ import { spawnSync } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ // ---------------------------------------------------------------------------
12
+ // Disk cache for arduino-cli metadata
13
+ // ---------------------------------------------------------------------------
14
+ const CACHE_DIR = path.join(os.homedir(), ".TypeCAD", "cache");
15
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
16
+ function getCachePath(fqbn) {
17
+ const safeFqbn = fqbn.replace(/[:/\\]/g, "_");
18
+ return path.join(CACHE_DIR, `arduino-cli-${safeFqbn}.json`);
19
+ }
20
+ function loadCachedMetadata(fqbn) {
21
+ try {
22
+ const cachePath = getCachePath(fqbn);
23
+ if (!fs.existsSync(cachePath))
24
+ return null;
25
+ const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
26
+ // Check TTL
27
+ if (Date.now() - cached.timestamp > CACHE_TTL_MS) {
28
+ return null; // Expired
29
+ }
30
+ // Convert arrays back to Sets
31
+ return {
32
+ architecture: cached.metadata.architecture,
33
+ pins: cached.metadata.pins,
34
+ builtinFunctions: new Set(cached.metadata.builtinFunctions),
35
+ builtinGlobals: new Set(cached.metadata.builtinGlobals),
36
+ };
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function saveCachedMetadata(fqbn, metadata) {
43
+ try {
44
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
45
+ const cached = {
46
+ fqbn,
47
+ metadata: {
48
+ architecture: metadata.architecture,
49
+ pins: metadata.pins,
50
+ builtinFunctions: [...metadata.builtinFunctions], // Convert Set to array
51
+ builtinGlobals: [...metadata.builtinGlobals], // Convert Set to array
52
+ },
53
+ timestamp: Date.now(),
54
+ };
55
+ fs.writeFileSync(getCachePath(fqbn), JSON.stringify(cached));
56
+ }
57
+ catch {
58
+ // Ignore cache write errors
59
+ }
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // Arduino CLI probing
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Extract architecture from FQBN string.
66
+ * FQBN format: vendor:arch:board[:menu=options]
67
+ * e.g., "arduino:avr:uno" -> "avr"
68
+ */
69
+ function toArchitectureFromFqbn(fqbn) {
70
+ if (!fqbn)
71
+ return undefined;
72
+ const parts = fqbn.split(":");
73
+ return parts.length >= 2 ? parts[1] : undefined;
74
+ }
75
+ function parsePinMap(value) {
76
+ const pins = {};
77
+ if (Array.isArray(value)) {
78
+ for (const item of value) {
79
+ if (!item || typeof item !== "object") {
80
+ continue;
81
+ }
82
+ const candidate = item;
83
+ const name = typeof candidate.name === "string" ? candidate.name : undefined;
84
+ const numeric = typeof candidate.value === "number"
85
+ ? candidate.value
86
+ : typeof candidate.value === "string" && !Number.isNaN(Number(candidate.value))
87
+ ? Number(candidate.value)
88
+ : undefined;
89
+ if (name && numeric !== undefined) {
90
+ pins[name] = numeric;
91
+ }
92
+ }
93
+ return pins;
94
+ }
95
+ if (value && typeof value === "object") {
96
+ for (const [name, raw] of Object.entries(value)) {
97
+ if (typeof raw === "number") {
98
+ pins[name] = raw;
99
+ }
100
+ else if (typeof raw === "string" && !Number.isNaN(Number(raw))) {
101
+ pins[name] = Number(raw);
102
+ }
103
+ }
104
+ }
105
+ return pins;
106
+ }
107
+ function parseStringSet(value) {
108
+ if (!Array.isArray(value)) {
109
+ return new Set();
110
+ }
111
+ return new Set(value
112
+ .filter((item) => typeof item === "string")
113
+ .map((item) => item.trim())
114
+ .filter((item) => item.length > 0));
115
+ }
116
+ function normalizeMetadata(raw, context) {
117
+ const object = raw && typeof raw === "object" ? raw : {};
118
+ // board details --format json puts architecture under platform.architecture
119
+ const platform = object.platform && typeof object.platform === "object" ? object.platform : {};
120
+ const architecture = (typeof platform.architecture === "string" ? platform.architecture.toLowerCase() : undefined) ??
121
+ (typeof object.architecture === "string" ? object.architecture.toLowerCase() : undefined) ??
122
+ (typeof object.arch === "string" ? object.arch.toLowerCase() : undefined) ??
123
+ toArchitectureFromFqbn(context?.buildTarget);
124
+ const pins = {
125
+ ...parsePinMap(object.pins),
126
+ ...parsePinMap(object.pinMappings),
127
+ };
128
+ const builtinFunctions = new Set([
129
+ ...parseStringSet(object.builtinFunctions),
130
+ ...parseStringSet(object.functions),
131
+ ]);
132
+ const builtinGlobals = new Set([
133
+ ...parseStringSet(object.builtinGlobals),
134
+ ...parseStringSet(object.globals),
135
+ ]);
136
+ return {
137
+ architecture,
138
+ pins,
139
+ builtinFunctions,
140
+ builtinGlobals,
141
+ };
142
+ }
143
+ function tryArduinoCliProbe(ctx) {
144
+ if (!ctx || !ctx.buildTarget) {
145
+ return {};
146
+ }
147
+ const cmd = spawnSync("arduino-cli", ["board", "details", "--show-properties", "--format", "json", "--fqbn", ctx.buildTarget], {
148
+ encoding: "utf8",
149
+ timeout: 10000,
150
+ });
151
+ if (cmd.error || cmd.status !== 0) {
152
+ return {
153
+ diagnostic: {
154
+ severity: "warning",
155
+ code: "TS2CPP_ARDUINO_CLI_PROBE_FAILED",
156
+ message: `Failed to parse arduino-cli properties for ${ctx.buildTarget}`,
157
+ },
158
+ };
159
+ }
160
+ try {
161
+ return { raw: JSON.parse(cmd.stdout) };
162
+ }
163
+ catch {
164
+ return {
165
+ diagnostic: {
166
+ severity: "warning",
167
+ code: "TS2CPP_ARDUINO_CLI_PARSE_FAILED",
168
+ message: "arduino-cli output could not be parsed as JSON; using static profile fallbacks.",
169
+ },
170
+ };
171
+ }
172
+ }
173
+ export function loadArduinoCliMetadata(ctx) {
174
+ const diagnostics = [];
175
+ const fqbn = ctx?.buildTarget;
176
+ if (!fqbn) {
177
+ return { diagnostics };
178
+ }
179
+ // Try disk cache first
180
+ const cached = loadCachedMetadata(fqbn);
181
+ if (cached) {
182
+ return { metadata: cached, diagnostics };
183
+ }
184
+ // Cache miss - probe arduino-cli
185
+ const probe = tryArduinoCliProbe(ctx);
186
+ if (probe.diagnostic) {
187
+ diagnostics.push(probe.diagnostic);
188
+ }
189
+ if (probe.raw) {
190
+ const metadata = normalizeMetadata(probe.raw, ctx);
191
+ // Save to disk cache for future sessions
192
+ saveCachedMetadata(fqbn, metadata);
193
+ return { metadata, diagnostics };
194
+ }
195
+ return { diagnostics };
196
+ }
@@ -0,0 +1,47 @@
1
+ interface CppMethod {
2
+ name: string;
3
+ returnType: string;
4
+ parameters: {
5
+ type: string;
6
+ name: string;
7
+ }[];
8
+ isPublic: boolean;
9
+ }
10
+ interface CppClass {
11
+ name: string;
12
+ /** Fully qualified name including namespace (e.g., "Microfire::SHT3x") */
13
+ fullName: string;
14
+ /** Namespace prefix (e.g., "Microfire") */
15
+ namespace?: string;
16
+ methods: CppMethod[];
17
+ constructors: {
18
+ parameters: {
19
+ type: string;
20
+ name: string;
21
+ }[];
22
+ }[];
23
+ }
24
+ export interface CppParseResult {
25
+ classes: CppClass[];
26
+ constants: {
27
+ name: string;
28
+ type: string;
29
+ value: string;
30
+ }[];
31
+ }
32
+ /**
33
+ * Maps C++ types to TypeScript types.
34
+ */
35
+ export declare function mapCppTypeToTs(cppType: string): string;
36
+ /**
37
+ * Extracts the parameter list from a function signature.
38
+ */
39
+ export declare function parseParameters(paramString: string): {
40
+ type: string;
41
+ name: string;
42
+ }[];
43
+ /**
44
+ * Parses a C++ class definition from header content.
45
+ */
46
+ export declare function parseCppClass(content: string): CppParseResult;
47
+ export {};