@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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/arduino/i2c-arduino.d.ts +47 -0
- package/dist/arduino/i2c-arduino.js +6 -0
- package/dist/arduino/index.d.ts +6 -0
- package/dist/arduino/index.js +8 -0
- package/dist/arduino/spi-arduino.d.ts +42 -0
- package/dist/arduino/spi-arduino.js +6 -0
- package/dist/arduino/stubs-uno.d.ts +9 -0
- package/dist/arduino/stubs-uno.js +65 -0
- package/dist/arduino/uart-arduino.d.ts +41 -0
- package/dist/arduino/uart-arduino.js +6 -0
- package/dist/arduino-class-map.d.ts +5 -0
- package/dist/arduino-class-map.js +21 -0
- package/dist/arduino-compile.d.ts +24 -0
- package/dist/arduino-compile.js +316 -0
- package/dist/arduino-libs.d.ts +6 -0
- package/dist/arduino-libs.js +11 -0
- package/dist/cli-metadata.d.ts +12 -0
- package/dist/cli-metadata.js +196 -0
- package/dist/cpp-parser.d.ts +47 -0
- package/dist/cpp-parser.js +292 -0
- package/dist/debug-codegen.d.ts +20 -0
- package/dist/debug-codegen.js +115 -0
- package/dist/displays/ili9341-spi.d.ts +6 -0
- package/dist/displays/ili9341-spi.js +28 -0
- package/dist/displays/ssd1309-i2c.d.ts +2 -0
- package/dist/displays/ssd1309-i2c.js +16 -0
- package/dist/displays/st7796-spi.d.ts +2 -0
- package/dist/displays/st7796-spi.js +20 -0
- package/dist/graphics/ili9341.d.ts +24 -0
- package/dist/graphics/ili9341.js +59 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +27 -0
- package/dist/lib-declaration.d.ts +35 -0
- package/dist/lib-declaration.js +399 -0
- package/dist/lib-discovery.d.ts +38 -0
- package/dist/lib-discovery.js +189 -0
- package/dist/profile.d.ts +8 -0
- package/dist/profile.js +584 -0
- package/dist/strategy.d.ts +174 -0
- package/dist/strategy.js +2032 -0
- package/package.json +73 -0
- package/src/arduino/i2c-arduino.ts +72 -0
- package/src/arduino/index.ts +18 -0
- package/src/arduino/spi-arduino.ts +63 -0
- package/src/arduino/stubs-uno.ts +80 -0
- package/src/arduino/uart-arduino.ts +59 -0
- package/src/arduino-class-map.ts +32 -0
- package/src/arduino-compile.ts +359 -0
- package/src/arduino-libs.ts +33 -0
- package/src/cli-metadata.ts +250 -0
- package/src/cpp-parser.ts +361 -0
- package/src/debug-codegen.ts +155 -0
- package/src/displays/ili9341-spi.ts +33 -0
- package/src/displays/ssd1309-i2c.ts +19 -0
- package/src/displays/st7796-spi.ts +23 -0
- package/src/graphics/ili9341.ts +80 -0
- package/src/index.ts +37 -0
- package/src/lib-declaration.ts +493 -0
- package/src/lib-discovery.ts +267 -0
- package/src/profile.ts +676 -0
- 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 {};
|