dsh-eda-mcp 0.7.3 → 1.0.0
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/CHANGELOG.md +66 -0
- package/docs/schematic-workflow.md +120 -0
- package/docs/tools.md +11 -3
- package/dsh-eda-bridge-extension-0.4.6.zip +0 -0
- package/jlc-extension/api-bridge.js +40 -3
- package/jlc-extension/extension.json +1 -1
- package/lib/bridge.mjs +1004 -27
- package/lib/index.js +149 -4
- package/package.json +2 -2
- package/src/bridge/design-intake.ts +133 -0
- package/src/bridge/design-rules.ts +16 -3
- package/src/bridge/index.ts +585 -23
- package/src/bridge/netlist-erc.ts +305 -0
- package/src/bridge/schematic-audit.ts +131 -0
- package/src/bridge/workflow-guide.ts +23 -3
- package/src/guidance.ts +5 -0
- package/src/skills-content.ts +120 -0
- package/test/design-intake.test.ts +40 -0
- package/test/netlist-erc.test.ts +90 -0
- package/test/schematic-audit.test.ts +71 -0
- package/test/skills.test.ts +2 -1
- package/dsh-eda-bridge-extension-0.4.2.zip +0 -0
package/lib/bridge.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { statSync } from "node:fs";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { writeFile } from "node:fs/promises";
|
|
6
|
+
import { unlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
9
|
import { createServer } from "node:http";
|
|
@@ -176,6 +176,454 @@ function buildLaunchScript(executable, project) {
|
|
|
176
176
|
return ["@echo off", 'start "" "' + executable + '"' + args, ""].join(String.fromCharCode(13, 10));
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// src/bridge/design-intake.ts
|
|
180
|
+
var INTAKE_SECTIONS = [
|
|
181
|
+
{
|
|
182
|
+
id: "power",
|
|
183
|
+
title: "\u4F9B\u7535\u4E0E\u7535\u6D41",
|
|
184
|
+
questions: [
|
|
185
|
+
{ id: "powerSource", question: "\u677F\u5B50\u600E\u4E48\u4F9B\u7535\uFF1F\uFF08USB-C / USB-A / DC \u5EA7 / \u7535\u6C60 / \u5916\u90E8\u7AEF\u5B50\uFF09", why: "\u51B3\u5B9A\u63A5\u53E3\u5668\u4EF6\u3001\u8FDE\u63A5\u5668\u671D\u5411\u548C ESD \u4FDD\u62A4", critical: true, options: ["USB-C", "USB-A", "DC \u5EA7", "\u9502\u7535\u6C60", "\u5916\u90E8\u7AEF\u5B50", "\u591A\u79CD"] },
|
|
186
|
+
{ id: "inputVoltage", question: "\u8F93\u5165\u7535\u538B\u8303\u56F4\u4E0E\u6700\u5927\u7535\u6D41\u662F\u591A\u5C11\uFF1F", why: "\u51B3\u5B9A\u7A33\u538B\u65B9\u6848\u3001\u7EBF\u5BBD\u3001\u94DC\u539A\u4E0E\u6563\u70ED", critical: true, fallback: "5 V / < 500 mA" },
|
|
187
|
+
{ id: "railNeeds", question: "\u9700\u8981\u54EA\u4E9B\u7535\u6E90\u8F68\uFF083V3 / 5V / 1V8 / \u6A21\u62DF\u8F68\uFF09\uFF1F\u5404\u81EA\u8D1F\u8F7D\u591A\u5927\uFF1F", why: "\u51B3\u5B9A LDO \u8FD8\u662F DC-DC\u3001\u53BB\u8026\u6570\u91CF", critical: true },
|
|
188
|
+
{ id: "battery", question: "\u662F\u5426\u9700\u8981\u7535\u6C60\u5145\u7535/\u5207\u6362\uFF1F", why: "\u5145\u7535 IC \u4E0E\u8DEF\u5F84\u7BA1\u7406\u4F1A\u6539\u53D8\u5E03\u5C40", critical: false, fallback: "\u4E0D\u9700\u8981" }
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "core",
|
|
193
|
+
title: "\u4E3B\u63A7\u4E0E\u56FA\u4EF6\u7EA6\u675F",
|
|
194
|
+
questions: [
|
|
195
|
+
{ id: "mcu", question: "\u4E3B\u63A7\u578B\u53F7\uFF08\u6216\u5FC5\u987B\u517C\u5BB9\u7684\u7CFB\u5217\uFF09\uFF1F", why: "\u5F15\u811A\u3001\u65F6\u949F\u3001\u5C01\u88C5\u51B3\u5B9A\u5E03\u5C40\u4E0E\u8D70\u7EBF", critical: true },
|
|
196
|
+
{ id: "clock", question: "\u65F6\u949F\u6765\u6E90\uFF1A\u5185\u90E8 RC / \u5916\u90E8\u6676\u632F\uFF08\u9891\u7387\uFF09\uFF1F", why: "\u6676\u632F\u8981\u8D34\u811A\u3001\u8D1F\u8F7D\u7535\u5BB9\u8981\u914D\u3001\u4E0B\u65B9\u7981\u94FA\u94DC", critical: true, options: ["\u5185\u90E8 RC", "8 MHz \u6676\u632F", "16 MHz \u6676\u632F", "32.768 kHz", "\u5176\u5B83"] },
|
|
197
|
+
{ id: "programming", question: "\u70E7\u5199/\u8C03\u8BD5\u63A5\u53E3\uFF08SWD / JTAG / USB DFU / \u4E32\u53E3\uFF09\uFF1F", why: "\u51B3\u5B9A\u6392\u9488\u4E0E USB \u6570\u636E\u7EBF", critical: true, options: ["SWD 4 \u7EBF", "SWD 2 \u7EBF", "JTAG", "USB DFU", "\u4E32\u53E3 ISP", "\u4E0D\u9700\u8981"] },
|
|
198
|
+
{ id: "boot", question: "\u542F\u52A8\u65B9\u5F0F\u662F\u5426\u9700\u8981\u53EF\u5207\u6362\uFF08BOOT0 \u8DF3\u7EBF/\u6309\u952E\uFF09\uFF1F", why: "\u51B3\u5B9A\u4E0B\u62C9\u7535\u963B\u4E0E\u8DF3\u7EBF\u6392\u9488", critical: false, fallback: "\u56FA\u5B9A\u4ECE Flash \u542F\u52A8\uFF0CBOOT0 \u4E0B\u62C9" }
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "io",
|
|
203
|
+
title: "\u63A5\u53E3\u4E0E\u5916\u8BBE",
|
|
204
|
+
questions: [
|
|
205
|
+
{ id: "interfaces", question: "\u9700\u8981\u54EA\u4E9B\u5BF9\u5916\u63A5\u53E3\uFF1F\uFF08USB / UART / I2C / SPI / CAN / \u4EE5\u592A\u7F51 / \u663E\u793A\u5C4F / \u4F20\u611F\u5668\uFF09", why: "\u6BCF\u4E00\u79CD\u90FD\u5E26\u8FDE\u63A5\u5668\u3001\u4FDD\u62A4\u5668\u4EF6\u548C\u5E03\u7EBF\u7EA6\u675F", critical: true },
|
|
206
|
+
{ id: "isolation", question: "\u662F\u5426\u6709\u9694\u79BB/\u9AD8\u538B/\u5F3A\u7535\u90E8\u5206\uFF1F\u5B89\u5168\u8DDD\u79BB\u8981\u6C42\uFF1F", why: "\u9700\u8981\u722C\u7535\u95F4\u8DDD\u3001\u5F00\u69FD\u3001\u9694\u79BB\u5668\u4EF6", critical: true, options: ["\u65E0", "\u6709\u4F4E\u538B\u9694\u79BB", "\u6709\u5E02\u7535/\u9AD8\u538B"] },
|
|
207
|
+
{ id: "esd", question: "\u5BF9\u5916\u63A5\u53E3\u662F\u5426\u8981 ESD/\u6D6A\u6D8C\u4FDD\u62A4\uFF1F", why: "USBLC6 \u4E4B\u7C7B\u5668\u4EF6\u7684\u5730\u5FC5\u987B\u5355\u72EC\u56DE\u6D41", critical: false, fallback: "\u5BF9\u5916\u63A5\u53E3\u90FD\u52A0 ESD" }
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "mechanical",
|
|
212
|
+
title: "\u673A\u68B0\u4E0E\u7ED3\u6784",
|
|
213
|
+
questions: [
|
|
214
|
+
{ id: "size", question: "\u677F\u6846\u5C3A\u5BF8\u9650\u5236\uFF08\u957F\xD7\u5BBD mm\uFF09\u4E0E\u5F62\u72B6\uFF08\u77E9\u5F62/\u5E26\u7F3A\u53E3/\u5706\u89D2\u534A\u5F84\uFF09\uFF1F", why: "\u51B3\u5B9A\u5E03\u5C40\u5BC6\u5EA6\u4E0E\u8FDE\u63A5\u5668\u4F4D\u7F6E", critical: true },
|
|
215
|
+
{ id: "mounting", question: "\u5B9A\u4F4D\u5B54\u6570\u91CF\u3001\u5B54\u5F84\u3001\u4F4D\u7F6E\uFF1F", why: "\u975E\u91D1\u5C5E\u5316\u5B54\u8981\u907F\u5F00\u8D70\u7EBF\u4E0E\u5668\u4EF6", critical: true, fallback: "4 \u4E2A \u03C63.2 mm\uFF0C\u56DB\u89D2\u5185\u7F29 3 mm" },
|
|
216
|
+
{ id: "connectorFacing", question: "\u5404\u8FDE\u63A5\u5668\u7684\u671D\u5411/\u4F4D\u7F6E\uFF08USB \u671D\u677F\u8FB9\u54EA\u4E00\u4FA7\uFF09\uFF1F", why: "\u63D2\u53E3\u5FC5\u987B\u671D\u677F\u5916\uFF0C\u5426\u5219\u88C5\u4E0D\u8FDB\u5916\u58F3", critical: true },
|
|
217
|
+
{ id: "enclosure", question: "\u662F\u5426\u6709\u5916\u58F3/\u88C5\u914D\u9650\u5236\uFF08\u9AD8\u5EA6\u3001\u5E72\u6D89\u533A\uFF09\uFF1F", why: "\u9650\u5236\u5668\u4EF6\u9AD8\u5EA6\u4E0E\u6446\u653E", critical: false, fallback: "\u65E0\u5916\u58F3\uFF0C\u4EC5\u88F8\u677F" }
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "process",
|
|
222
|
+
title: "\u5DE5\u827A\u4E0E\u5236\u9020",
|
|
223
|
+
questions: [
|
|
224
|
+
{ id: "layers", question: "\u5C42\u6570\uFF082 \u5C42 / 4 \u5C42\uFF09\uFF1F", why: "\u51B3\u5B9A\u5730\u5E73\u9762\u3001\u963B\u6297\u4E0E\u5E03\u7EBF\u7B56\u7565", critical: true, options: ["2 \u5C42", "4 \u5C42"] },
|
|
225
|
+
{ id: "copper", question: "\u94DC\u539A\uFF081 oz / 2 oz\uFF09\uFF1F", why: "\u5F71\u54CD\u8F7D\u6D41\u4E0E\u6700\u5C0F\u7EBF\u5BBD", critical: false, fallback: "1 oz" },
|
|
226
|
+
{ id: "assembly", question: "\u8D34\u7247\u8FD8\u662F\u624B\u710A\uFF1F\u53CC\u9762\u8D34\u7247\uFF1F", why: "\u51B3\u5B9A\u5C01\u88C5\u9009\u578B\u3001\u4E1D\u5370\u6807\u6CE8\u4E0E\u710A\u76D8\u5916\u6269", critical: true, options: ["\u5355\u9762\u8D34\u7247", "\u53CC\u9762\u8D34\u7247", "\u624B\u5DE5\u710A\u63A5"] },
|
|
227
|
+
{ id: "quantity", question: "\u6253\u6837\u6570\u91CF\u4E0E\u9884\u7B97\u533A\u95F4\uFF1F", why: "\u5F71\u54CD\u5DE5\u827A\u9009\u62E9\uFF08\u62FC\u677F\u3001\u963B\u6297\u3001\u6C89\u91D1\uFF09", critical: false, fallback: "5 \u7247\u6253\u6837" }
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "delivery",
|
|
232
|
+
title: "\u4EA4\u4ED8\u4E0E\u9A8C\u8BC1",
|
|
233
|
+
questions: [
|
|
234
|
+
{ id: "deliverable", question: "\u8981\u4EA4\u4ED8\u5230\u4EC0\u4E48\u7A0B\u5EA6\uFF1F\uFF08\u539F\u7406\u56FE / PCB / Gerber+BOM+\u5750\u6807 / \u5168\u6D41\u7A0B\u53EF\u4E0B\u5355\uFF09", why: "\u51B3\u5B9A\u9A8C\u8BC1\u6DF1\u5EA6\u4E0E\u6587\u6863\u91CF", critical: true, options: ["\u4EC5\u539F\u7406\u56FE", "\u539F\u7406\u56FE+PCB", "\u53EF\u4E0B\u5355\uFF08Gerber+BOM+\u5750\u6807\uFF09"] },
|
|
235
|
+
{ id: "validation", question: "\u8981\u54EA\u4E9B\u9A8C\u8BC1\uFF1F\uFF08DRC / \u7F51\u8868 ERC / \u8BBE\u8BA1\u5BA1\u8BA1 / \u5B9E\u673A\u70B9\u4EAE\uFF09", why: "\u51B3\u5B9A\u4EA4\u4ED8\u524D\u5FC5\u987B\u8DD1\u7684\u68C0\u67E5", critical: false, fallback: "DRC + \u7F51\u8868 ERC + \u8BBE\u8BA1\u5BA1\u8BA1" },
|
|
236
|
+
{ id: "constraints", question: "\u8FD8\u6709\u5176\u5B83\u786C\u7EA6\u675F\uFF08\u7981\u7528\u7684\u5668\u4EF6\u3001\u5FC5\u987B\u590D\u7528\u5DF2\u6709\u5DE5\u7A0B\u3001\u8BA4\u8BC1\u8981\u6C42\uFF09\uFF1F", why: "\u907F\u514D\u8FD4\u5DE5", critical: false }
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
function buildIntake(answers) {
|
|
241
|
+
const provided = answers !== null && typeof answers === "object" ? answers : {};
|
|
242
|
+
const known = /* @__PURE__ */ new Set();
|
|
243
|
+
for (const section of INTAKE_SECTIONS) for (const question of section.questions) known.add(question.id);
|
|
244
|
+
const brief = {};
|
|
245
|
+
const answered = [];
|
|
246
|
+
const unknownAnswerKeys = [];
|
|
247
|
+
for (const [key, value] of Object.entries(provided)) {
|
|
248
|
+
if (!known.has(key)) {
|
|
249
|
+
unknownAnswerKeys.push(key);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const text = String(value ?? "").trim();
|
|
253
|
+
if (text === "") continue;
|
|
254
|
+
brief[key] = text;
|
|
255
|
+
answered.push(key);
|
|
256
|
+
}
|
|
257
|
+
const missingCritical = [];
|
|
258
|
+
let criticalCount = 0;
|
|
259
|
+
let questionCount = 0;
|
|
260
|
+
for (const section of INTAKE_SECTIONS) {
|
|
261
|
+
for (const question of section.questions) {
|
|
262
|
+
questionCount += 1;
|
|
263
|
+
if (!question.critical) continue;
|
|
264
|
+
criticalCount += 1;
|
|
265
|
+
if (brief[question.id] === void 0) missingCritical.push(question.id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
sections: INTAKE_SECTIONS,
|
|
270
|
+
questionCount,
|
|
271
|
+
criticalCount,
|
|
272
|
+
answered,
|
|
273
|
+
missingCritical,
|
|
274
|
+
unknownAnswerKeys,
|
|
275
|
+
ready: missingCritical.length === 0,
|
|
276
|
+
brief
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/bridge/schematic-audit.ts
|
|
281
|
+
var EPS = 0.5;
|
|
282
|
+
function intersects(a, b) {
|
|
283
|
+
return a.left < b.right - EPS && b.left < a.right - EPS && a.bottom < b.top - EPS && b.bottom < a.top - EPS;
|
|
284
|
+
}
|
|
285
|
+
function area(b) {
|
|
286
|
+
return Math.max(0, b.right - b.left) * Math.max(0, b.top - b.bottom);
|
|
287
|
+
}
|
|
288
|
+
function inside(inner, outer) {
|
|
289
|
+
return inner.left >= outer.left - EPS && inner.right <= outer.right + EPS && inner.bottom >= outer.bottom - EPS && inner.top <= outer.top + EPS;
|
|
290
|
+
}
|
|
291
|
+
function auditSchematicGeometry(input) {
|
|
292
|
+
const grid = input.grid === void 0 ? 10 : input.grid;
|
|
293
|
+
const checks = [];
|
|
294
|
+
const add = (id, status, message, details) => {
|
|
295
|
+
checks.push(details && details.length > 0 ? { id, status, message, details } : { id, status, message });
|
|
296
|
+
};
|
|
297
|
+
const withBounds = input.components.filter((component) => component.bounds !== void 0);
|
|
298
|
+
if (input.components.length === 0) {
|
|
299
|
+
add("sheet", "skip", "no component on the active schematic page");
|
|
300
|
+
return { ok: false, counts: { pass: 0, fail: 0, warn: 0, skip: 1 }, componentCount: 0, checks };
|
|
301
|
+
}
|
|
302
|
+
add("sheet", "pass", input.components.length + " component(s) read from the active page, " + withBounds.length + " with bounds");
|
|
303
|
+
const outside = withBounds.filter((component) => !inside(component.bounds, input.frame)).map((component) => component.designator);
|
|
304
|
+
add(
|
|
305
|
+
"outside-frame",
|
|
306
|
+
outside.length === 0 ? "pass" : "fail",
|
|
307
|
+
outside.length === 0 ? "every symbol sits inside the frame" : outside.length + " symbol(s) stick out of the frame",
|
|
308
|
+
outside.slice(0, 20)
|
|
309
|
+
);
|
|
310
|
+
if (input.titleBlock === void 0) {
|
|
311
|
+
add("title-block", "skip", "no title block range supplied");
|
|
312
|
+
} else {
|
|
313
|
+
const block = input.titleBlock;
|
|
314
|
+
const intruding = withBounds.filter((component) => intersects(component.bounds, block)).map((component) => component.designator);
|
|
315
|
+
add(
|
|
316
|
+
"title-block",
|
|
317
|
+
intruding.length === 0 ? "pass" : "fail",
|
|
318
|
+
intruding.length === 0 ? "nothing overlaps the title block" : intruding.length + " symbol(s) overlap the title block",
|
|
319
|
+
intruding.slice(0, 20)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const overlaps = [];
|
|
323
|
+
for (let i = 0; i < withBounds.length; i++) {
|
|
324
|
+
for (let j = i + 1; j < withBounds.length; j++) {
|
|
325
|
+
const a = withBounds[i];
|
|
326
|
+
const b = withBounds[j];
|
|
327
|
+
if (!intersects(a.bounds, b.bounds)) continue;
|
|
328
|
+
const sharedW = Math.min(a.bounds.right, b.bounds.right) - Math.max(a.bounds.left, b.bounds.left);
|
|
329
|
+
const sharedH = Math.min(a.bounds.top, b.bounds.top) - Math.max(a.bounds.bottom, b.bounds.bottom);
|
|
330
|
+
const shared = Math.max(0, sharedW) * Math.max(0, sharedH);
|
|
331
|
+
const smaller = Math.min(area(a.bounds), area(b.bounds));
|
|
332
|
+
if (smaller > 0 && shared / smaller > 0.08) overlaps.push(a.designator + " x " + b.designator);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
add(
|
|
336
|
+
"symbol-overlap",
|
|
337
|
+
overlaps.length === 0 ? "pass" : "fail",
|
|
338
|
+
overlaps.length === 0 ? "no two symbols overlap" : overlaps.length + " overlapping symbol pair(s)",
|
|
339
|
+
overlaps.slice(0, 20)
|
|
340
|
+
);
|
|
341
|
+
const offGrid = input.components.filter((component) => Math.abs(component.x / grid - Math.round(component.x / grid)) > 0.01 || Math.abs(component.y / grid - Math.round(component.y / grid)) > 0.01).map((component) => component.designator + " @" + component.x + "," + component.y);
|
|
342
|
+
add(
|
|
343
|
+
"grid",
|
|
344
|
+
offGrid.length === 0 ? "pass" : "warn",
|
|
345
|
+
offGrid.length === 0 ? "every symbol sits on the " + grid + "-unit grid" : offGrid.length + " symbol(s) are off the " + grid + "-unit grid",
|
|
346
|
+
offGrid.slice(0, 20)
|
|
347
|
+
);
|
|
348
|
+
const labels = input.labels ?? [];
|
|
349
|
+
if (labels.length === 0) {
|
|
350
|
+
add("label-overlap", "skip", "no label anchors supplied");
|
|
351
|
+
} else {
|
|
352
|
+
const crowded = labels.filter((label) => withBounds.some((component) => label.x > component.bounds.left && label.x < component.bounds.right && label.y > component.bounds.bottom && label.y < component.bounds.top)).map((label) => label.text + " @" + label.x + "," + label.y);
|
|
353
|
+
add(
|
|
354
|
+
"label-overlap",
|
|
355
|
+
crowded.length === 0 ? "pass" : "warn",
|
|
356
|
+
crowded.length === 0 ? "no label anchor lands inside a symbol" : crowded.length + " label(s) anchored inside a symbol body",
|
|
357
|
+
crowded.slice(0, 20)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const counts = { pass: 0, fail: 0, warn: 0, skip: 0 };
|
|
361
|
+
for (const check of checks) counts[check.status] += 1;
|
|
362
|
+
return { ok: counts.fail === 0, counts, componentCount: input.components.length, checks };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/bridge/netlist-erc.ts
|
|
366
|
+
var POWER_NET = /^(?:\+?\d+V\d*|V?(?:CC|DD|BAT|IN|BUS|SYS|MOTOR)|AVDD|DVDD|VBUS|VUSB|VBAT|3V3|5V|12V)$/i;
|
|
367
|
+
var GROUND_NET = /^(?:GND|AGND|DGND|PGND|GNDA|GNDD|VSS|0V|EARTH)$/i;
|
|
368
|
+
var USD_DP = /(?:^|_)(?:USB_?D\+?P|D\+|USB_?DP|DP)$/i;
|
|
369
|
+
var USD_DM = /(?:^|_)(?:USB_?D-?M|D-|USB_?DM|DM)$/i;
|
|
370
|
+
var CRYSTAL_HINT = /crystal|xtal|osc|谐振|晶振|5032|3225/i;
|
|
371
|
+
var MCU_PART = /STM32|GD32|ATmega|ATMEGA|ESP32|ESP8266|CH32|HC32|MM32|APM32|N76|PIC\d|RP2040|MSP430/i;
|
|
372
|
+
function normNet(value) {
|
|
373
|
+
return typeof value === "string" ? value.trim() : "";
|
|
374
|
+
}
|
|
375
|
+
function parseNetlist(raw) {
|
|
376
|
+
const source = raw && typeof raw === "object" && raw.components ? raw : {};
|
|
377
|
+
const map = source.components ?? {};
|
|
378
|
+
const components = [];
|
|
379
|
+
const pins = [];
|
|
380
|
+
for (const key of Object.keys(map)) {
|
|
381
|
+
const entry = map[key] ?? {};
|
|
382
|
+
const props = entry.props ?? {};
|
|
383
|
+
const designator = String(props.Designator ?? props.designator ?? key);
|
|
384
|
+
const pinInfoMap = entry.pinInfoMap ?? {};
|
|
385
|
+
const own = [];
|
|
386
|
+
for (const pinKey of Object.keys(pinInfoMap)) {
|
|
387
|
+
const pin = pinInfoMap[pinKey] ?? {};
|
|
388
|
+
const number = String(pin.number ?? pin.props?.["Pin Number"] ?? pinKey);
|
|
389
|
+
const name = String(pin.name ?? "");
|
|
390
|
+
const net = normNet(pin.net);
|
|
391
|
+
own.push({ number, name, net });
|
|
392
|
+
pins.push({ designator, number, name, net });
|
|
393
|
+
}
|
|
394
|
+
const flag = (value, fallback) => {
|
|
395
|
+
if (value === void 0 || value === null || value === "") return fallback;
|
|
396
|
+
return !/^(?:no|false|0)$/i.test(String(value).trim());
|
|
397
|
+
};
|
|
398
|
+
components.push({
|
|
399
|
+
designator,
|
|
400
|
+
part: String(props["Manufacturer Part"] ?? props.Device ?? ""),
|
|
401
|
+
footprint: String(props.Footprint ?? ""),
|
|
402
|
+
supplierPart: String(props["Supplier Part"] ?? props["LCSC Part"] ?? ""),
|
|
403
|
+
addIntoBom: flag(props["Add into BOM"], true),
|
|
404
|
+
convertToPcb: flag(props["Convert to PCB"], true),
|
|
405
|
+
pins: own
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return { components, pins };
|
|
409
|
+
}
|
|
410
|
+
function netsOf(pins) {
|
|
411
|
+
const nets = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const pin of pins) {
|
|
413
|
+
if (pin.net === "") continue;
|
|
414
|
+
const list = nets.get(pin.net);
|
|
415
|
+
if (list) list.push(pin);
|
|
416
|
+
else nets.set(pin.net, [pin]);
|
|
417
|
+
}
|
|
418
|
+
return nets;
|
|
419
|
+
}
|
|
420
|
+
function capsBridging(net, groundNet, components) {
|
|
421
|
+
let count = 0;
|
|
422
|
+
for (const component of components) {
|
|
423
|
+
if (!/^C\d*$/i.test(component.designator)) continue;
|
|
424
|
+
const onNet = component.pins.some((pin) => pin.net === net);
|
|
425
|
+
const onGround = component.pins.some((pin) => pin.net === groundNet);
|
|
426
|
+
if (onNet && onGround) count++;
|
|
427
|
+
}
|
|
428
|
+
return count;
|
|
429
|
+
}
|
|
430
|
+
function resistorsOn(net, components, otherNet) {
|
|
431
|
+
const found = [];
|
|
432
|
+
for (const component of components) {
|
|
433
|
+
if (!/^R\d*$/i.test(component.designator)) continue;
|
|
434
|
+
const onNet = component.pins.some((pin) => pin.net === net);
|
|
435
|
+
if (!onNet) continue;
|
|
436
|
+
if (otherNet === void 0) {
|
|
437
|
+
found.push(component.designator);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const others = component.pins.filter((pin) => pin.net !== net).map((pin) => pin.net);
|
|
441
|
+
if (others.some((name) => name === otherNet)) found.push(component.designator);
|
|
442
|
+
}
|
|
443
|
+
return found;
|
|
444
|
+
}
|
|
445
|
+
function runNetlistErc(rawNetlist) {
|
|
446
|
+
const { components, pins } = parseNetlist(rawNetlist);
|
|
447
|
+
const nets = netsOf(pins);
|
|
448
|
+
const checks = [];
|
|
449
|
+
const add = (id, status, message, details) => {
|
|
450
|
+
checks.push(details && details.length > 0 ? { id, status, message, details } : { id, status, message });
|
|
451
|
+
};
|
|
452
|
+
if (components.length === 0) {
|
|
453
|
+
add("netlist", "skip", "netlist has no components: open a schematic page first (eda_open_document)");
|
|
454
|
+
return { ok: false, counts: { pass: 0, fail: 0, warn: 0, skip: 1 }, componentCount: 0, netCount: 0, checks };
|
|
455
|
+
}
|
|
456
|
+
const seen = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const component of components) seen.set(component.designator, (seen.get(component.designator) ?? 0) + 1);
|
|
458
|
+
const duplicates = [...seen.entries()].filter(([, n]) => n > 1).map(([name, n]) => name + " x" + n);
|
|
459
|
+
add(
|
|
460
|
+
"duplicate-designator",
|
|
461
|
+
duplicates.length === 0 ? "pass" : "fail",
|
|
462
|
+
duplicates.length === 0 ? "every designator is unique" : duplicates.length + " duplicated designator(s)",
|
|
463
|
+
duplicates
|
|
464
|
+
);
|
|
465
|
+
const floating = pins.filter((pin) => pin.net === "").map((pin) => pin.designator + "." + pin.number + (pin.name ? " (" + pin.name + ")" : ""));
|
|
466
|
+
add(
|
|
467
|
+
"unconnected-pin",
|
|
468
|
+
floating.length === 0 ? "pass" : "warn",
|
|
469
|
+
floating.length === 0 ? "every pin is on a net" : floating.length + " pin(s) are on no net (mark unused pins with eda_set_no_connect or wire them)",
|
|
470
|
+
floating.slice(0, 25)
|
|
471
|
+
);
|
|
472
|
+
const dangling = [...nets.entries()].filter(([, list]) => list.length < 2).map(([name, list]) => name + " <- " + list.map((p) => p.designator + "." + p.number).join(","));
|
|
473
|
+
add(
|
|
474
|
+
"dangling-net",
|
|
475
|
+
dangling.length === 0 ? "pass" : "fail",
|
|
476
|
+
dangling.length === 0 ? "every net has at least two endpoints" : dangling.length + " net(s) have a single endpoint",
|
|
477
|
+
dangling.slice(0, 25)
|
|
478
|
+
);
|
|
479
|
+
const groundNet = [...nets.keys()].find((name) => GROUND_NET.test(name)) ?? "";
|
|
480
|
+
add(
|
|
481
|
+
"ground-net",
|
|
482
|
+
groundNet === "" ? "fail" : nets.get(groundNet).length >= 3 ? "pass" : "warn",
|
|
483
|
+
groundNet === "" ? "no ground net (GND/VSS/AGND) found" : "ground net " + groundNet + " has " + nets.get(groundNet).length + " pins"
|
|
484
|
+
);
|
|
485
|
+
const shorted = components.filter((component) => {
|
|
486
|
+
const names = new Set(component.pins.map((pin) => pin.net).filter((name) => name !== ""));
|
|
487
|
+
return component.pins.length >= 2 && names.size === 1;
|
|
488
|
+
}).map((component) => component.designator);
|
|
489
|
+
add(
|
|
490
|
+
"shorted-component",
|
|
491
|
+
shorted.length === 0 ? "pass" : "fail",
|
|
492
|
+
shorted.length === 0 ? "no component has all pins on one net" : shorted.length + " component(s) have every pin on one net",
|
|
493
|
+
shorted
|
|
494
|
+
);
|
|
495
|
+
const powerNets = [...nets.keys()].filter((name) => POWER_NET.test(name) && !GROUND_NET.test(name));
|
|
496
|
+
if (powerNets.length === 0) {
|
|
497
|
+
add("power-rail", "warn", "no power net recognised (3V3/5V/VCC/VDD/VBAT)");
|
|
498
|
+
} else {
|
|
499
|
+
const icPowerPins = pins.filter((pin) => /^(?:VDD|VCC|VBAT|VDDA|AVDD|DVDD|VIN|V\+)/i.test(pin.name) && !GROUND_NET.test(pin.net));
|
|
500
|
+
const unpowered = icPowerPins.filter((pin) => pin.net === "").map((pin) => pin.designator + "." + pin.number);
|
|
501
|
+
add(
|
|
502
|
+
"power-rail",
|
|
503
|
+
unpowered.length === 0 ? "pass" : "fail",
|
|
504
|
+
unpowered.length === 0 ? powerNets.length + " power net(s) (" + powerNets.slice(0, 4).join(", ") + ") feed " + icPowerPins.length + " IC power pin(s)" : unpowered.length + " IC power pin(s) are not on any net",
|
|
505
|
+
unpowered
|
|
506
|
+
);
|
|
507
|
+
if (groundNet !== "") {
|
|
508
|
+
const thin = powerNets.filter((name) => capsBridging(name, groundNet, components) === 0);
|
|
509
|
+
add(
|
|
510
|
+
"decoupling",
|
|
511
|
+
thin.length === 0 ? "pass" : "warn",
|
|
512
|
+
thin.length === 0 ? "every power rail has a capacitor to " + groundNet : thin.length + " power rail(s) have no capacitor to " + groundNet + " (decoupling missing)",
|
|
513
|
+
thin
|
|
514
|
+
);
|
|
515
|
+
} else {
|
|
516
|
+
add("decoupling", "skip", "no ground net, decoupling cannot be judged");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const dp = [...nets.keys()].filter((name) => USD_DP.test(name));
|
|
520
|
+
const dm = [...nets.keys()].filter((name) => USD_DM.test(name));
|
|
521
|
+
if (dp.length === 0 && dm.length === 0) {
|
|
522
|
+
add("usb-data-pair", "skip", "no USB D+/D- net, skipped");
|
|
523
|
+
} else {
|
|
524
|
+
const missing = dp.length === 0 ? "D+ net missing" : dm.length === 0 ? "D- net missing" : "";
|
|
525
|
+
const halves = dp.length === 0 || dm.length === 0 ? ["D+/D- are not a pair: " + (dp.concat(dm).join(", ") || "none")] : [];
|
|
526
|
+
add(
|
|
527
|
+
"usb-data-pair",
|
|
528
|
+
missing === "" ? "pass" : "fail",
|
|
529
|
+
missing === "" ? "D+ (" + dp.join(",") + ") and D- (" + dm.join(",") + ") both exist" : missing,
|
|
530
|
+
halves
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const crystals = components.filter((component) => /^[XY]\d*$/i.test(component.designator) || CRYSTAL_HINT.test(component.part) || CRYSTAL_HINT.test(component.footprint));
|
|
534
|
+
const realCrystals = crystals.filter((component) => {
|
|
535
|
+
const names = new Set(component.pins.map((pin) => pin.net).filter((name) => name !== ""));
|
|
536
|
+
return component.pins.length === 2 || component.pins.length >= 2 && names.size === 2;
|
|
537
|
+
});
|
|
538
|
+
if (realCrystals.length === 0) {
|
|
539
|
+
add("crystal", "skip", "no crystal found, skipped (an RC oscillator design is fine)");
|
|
540
|
+
} else if (groundNet === "") {
|
|
541
|
+
add("crystal", "skip", "crystal present but no ground net, load caps cannot be judged");
|
|
542
|
+
} else {
|
|
543
|
+
const problems = [];
|
|
544
|
+
for (const crystal of realCrystals) {
|
|
545
|
+
const own = crystal.pins.map((pin) => pin.net).filter((name) => name !== "");
|
|
546
|
+
if (new Set(own).size < 2) problems.push(crystal.designator + ": both pins are on " + (own[0] ?? "no net"));
|
|
547
|
+
for (const net of new Set(own)) {
|
|
548
|
+
if (capsBridging(net, groundNet, components) === 0) problems.push(crystal.designator + ": " + net + " has no load capacitor to " + groundNet);
|
|
549
|
+
}
|
|
550
|
+
const touchesMcu = pins.some((pin) => own.includes(pin.net) && MCU_PART.test(components.find((c) => c.designator === pin.designator)?.part ?? ""));
|
|
551
|
+
if (!touchesMcu) problems.push(crystal.designator + ": neither pin reaches an MCU clock pin");
|
|
552
|
+
}
|
|
553
|
+
add(
|
|
554
|
+
"crystal",
|
|
555
|
+
problems.length === 0 ? "pass" : "warn",
|
|
556
|
+
problems.length === 0 ? realCrystals.length + " crystal(s) have two nets, load caps and an MCU connection" : problems.length + " crystal problem(s)",
|
|
557
|
+
problems
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const resetNet = [...nets.keys()].find((name) => /^(?:N?RST|RESET|RESET_N|MCLR)$/i.test(name));
|
|
561
|
+
if (resetNet === void 0) {
|
|
562
|
+
add("reset", "skip", "no reset net, skipped");
|
|
563
|
+
} else if (groundNet === "") {
|
|
564
|
+
add("reset", "skip", "reset net present but no ground net");
|
|
565
|
+
} else {
|
|
566
|
+
const cap = capsBridging(resetNet, groundNet, components);
|
|
567
|
+
const pullup = resistorsOn(resetNet, components).length;
|
|
568
|
+
const problems = [];
|
|
569
|
+
if (cap === 0) problems.push(resetNet + " has no capacitor to " + groundNet);
|
|
570
|
+
if (pullup === 0) problems.push(resetNet + " has no series/pull resistor");
|
|
571
|
+
add(
|
|
572
|
+
"reset",
|
|
573
|
+
problems.length === 0 ? "pass" : "warn",
|
|
574
|
+
problems.length === 0 ? resetNet + " has a capacitor and a resistor" : problems.length + " reset problem(s)",
|
|
575
|
+
problems
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const bootNet = [...nets.keys()].find((name) => /^BOOT\d$/i.test(name));
|
|
579
|
+
if (bootNet === void 0) {
|
|
580
|
+
add("boot", "skip", "no BOOT0 net, skipped");
|
|
581
|
+
} else {
|
|
582
|
+
const resistors = resistorsOn(bootNet, components);
|
|
583
|
+
add(
|
|
584
|
+
"boot",
|
|
585
|
+
resistors.length > 0 ? "pass" : "warn",
|
|
586
|
+
resistors.length > 0 ? bootNet + " has resistor(s): " + resistors.join(",") : bootNet + " has no pull resistor (floating boot pin)"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const ccPins = pins.filter((pin) => /^CC[12]$/i.test(pin.name));
|
|
590
|
+
if (ccPins.length === 0) {
|
|
591
|
+
add("type-c-cc", "skip", "no CC1/CC2 pins, skipped");
|
|
592
|
+
} else {
|
|
593
|
+
const missing = ccPins.filter((pin) => pin.net === "" || groundNet !== "" && resistorsOn(pin.net, components, groundNet).length === 0).map((pin) => pin.designator + "." + pin.name + " on " + (pin.net || "<no net>") + " without a resistor to GND");
|
|
594
|
+
add(
|
|
595
|
+
"type-c-cc",
|
|
596
|
+
missing.length === 0 ? "pass" : "warn",
|
|
597
|
+
missing.length === 0 ? "CC1/CC2 each have a resistor to ground" : missing.length + " CC pin(s) lack a 5.1k pull-down",
|
|
598
|
+
missing
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const missingSupplier = components.filter((component) => component.supplierPart.trim() === "").map((component) => component.designator + (component.part ? " (" + component.part + ")" : ""));
|
|
602
|
+
add(
|
|
603
|
+
"bom-part-number",
|
|
604
|
+
missingSupplier.length === 0 ? "pass" : "warn",
|
|
605
|
+
missingSupplier.length === 0 ? "every part carries a supplier part number" : missingSupplier.length + " part(s) have no supplier/LPSC part number (not orderable by number)",
|
|
606
|
+
missingSupplier.slice(0, 20)
|
|
607
|
+
);
|
|
608
|
+
const excluded = components.filter((component) => !component.convertToPcb).map((component) => component.designator);
|
|
609
|
+
add(
|
|
610
|
+
"pcb-conversion",
|
|
611
|
+
excluded.length === 0 ? "pass" : "warn",
|
|
612
|
+
excluded.length === 0 ? "every part is marked Convert to PCB" : excluded.length + " part(s) are excluded from the PCB conversion (they will be missing on the board)",
|
|
613
|
+
excluded.slice(0, 20)
|
|
614
|
+
);
|
|
615
|
+
const noFootprint = components.filter((component) => component.convertToPcb && component.footprint.trim() === "").map((component) => component.designator);
|
|
616
|
+
add(
|
|
617
|
+
"footprint",
|
|
618
|
+
noFootprint.length === 0 ? "pass" : "fail",
|
|
619
|
+
noFootprint.length === 0 ? "every PCB-bound part has a footprint" : noFootprint.length + " PCB-bound part(s) have no footprint and cannot be placed",
|
|
620
|
+
noFootprint.slice(0, 20)
|
|
621
|
+
);
|
|
622
|
+
const counts = { pass: 0, fail: 0, warn: 0, skip: 0 };
|
|
623
|
+
for (const check of checks) counts[check.status] += 1;
|
|
624
|
+
return { ok: counts.fail === 0, counts, componentCount: components.length, netCount: nets.size, checks };
|
|
625
|
+
}
|
|
626
|
+
|
|
179
627
|
// src/bridge/design-rules.ts
|
|
180
628
|
var MIL_PER_MM = 39.37007874015748;
|
|
181
629
|
function milToMm(mil) {
|
|
@@ -195,7 +643,8 @@ function checkNetName(net) {
|
|
|
195
643
|
const warnings = [];
|
|
196
644
|
if (net === "") errors.push("net name is empty");
|
|
197
645
|
if (net.length > NET_NAME_MAX_LENGTH) errors.push(`net name is longer than ${NET_NAME_MAX_LENGTH} characters`);
|
|
198
|
-
|
|
646
|
+
const autoGenerated = /^\$?\d+N\d+$/.test(net) || /^\$/.test(net);
|
|
647
|
+
if (net !== "" && !NET_NAME_ALLOWED.test(net) && !autoGenerated) {
|
|
199
648
|
errors.push(`net name "${net}" contains characters the JLCEDA backend cannot parse (allowed: letters, digits, "_", ".", "+", "-")`);
|
|
200
649
|
}
|
|
201
650
|
if (/[a-z]/.test(net)) warnings.push(`net name "${net}" is not uppercase; the official advice is uppercase letters, digits and "_" only`);
|
|
@@ -243,8 +692,14 @@ function checkTrackWidth(net, lineWidth, force = false) {
|
|
|
243
692
|
if (widthMil > 0 && widthMil < suggestedMil && Number.isFinite(widthMil) && !force) {
|
|
244
693
|
warnings.push(`track width ${widthMil} mil is below the ${suggestedMil} mil suggested for ${role} net "${net}"`);
|
|
245
694
|
}
|
|
246
|
-
const isTierValue = Object.values(TRACK_WIDTH_MIL).includes(widthMil);
|
|
247
|
-
|
|
695
|
+
const isTierValue = Object.values(TRACK_WIDTH_MIL).includes(widthMil) || widthMil === 15 || widthMil === 25 || widthMil === 12;
|
|
696
|
+
const stepRemainder = (() => {
|
|
697
|
+
const mm = milToMm(widthMil);
|
|
698
|
+
if (mm <= 0) return 0;
|
|
699
|
+
const nearest = Math.round(mm / 0.05) * 0.05;
|
|
700
|
+
return Math.abs(mm - nearest);
|
|
701
|
+
})();
|
|
702
|
+
if (widthMil > 0 && !isTierValue && stepRemainder > 0.01) {
|
|
248
703
|
warnings.push(`track width ${Math.round(widthMil * 100) / 100} mil (${milToMm(widthMil).toFixed(3)} mm) is not on a 0.05 mm step; ${Math.round(roundToFiveHundredthsMm(widthMil) * 100) / 100} mil aligns better with JLC parameters`);
|
|
249
704
|
}
|
|
250
705
|
return { ...result(errors, warnings), widthMil, role, suggestedMil };
|
|
@@ -299,9 +754,10 @@ var WORKFLOW_SUMMARY = [
|
|
|
299
754
|
"1) \u5E03\u5C40\uFF1A\u6309 10 \u5355\u4F4D\u6805\u683C\u5BF9\u9F50\uFF1B\u4FE1\u53F7\u4ECE\u5DE6\u5230\u53F3\u3001\u7535\u6E90\u5728\u4E0A\u3001\u5730\u5728\u4E0B\uFF1B\u540C\u7EA7\u5668\u4EF6\u7B49\u95F4\u8DDD\uFF1B\u4E0D\u538B\u5B57\u3001\u4E0D\u538B\u6807\u9898\u680F\uFF1B\u5148\u770B eda_get_page_info \u7684\u5B89\u5168\u533A\u518D\u6446\u4F4D\u3002",
|
|
300
755
|
"2) \u8FDE\u7EBF\uFF1A\u53EA\u7528 eda_draw_wire \u8D70\u76F4\u7EBF/45 \u5EA6\uFF1B**\u8DE8\u533A\u57DF\u540C\u540D\u7F51\u7EDC\u5FC5\u987B\u7528\u7F51\u7EDC\u7AEF\u53E3\uFF08eda_place_net_port\uFF09\u6216\u7F51\u7EDC\u6807\u7B7E**\uFF0C\u4E0D\u80FD\u9760\u5728\u4E24\u5904\u753B\u4E00\u6761\u53EB\u540C\u540D\u7684\u5BFC\u7EBF\u2014\u2014\u5BFC\u7EBF\u4E00\u65E6\u63A5\u89E6\u5F15\u811A\uFF0Cnet \u4F1A\u88AB\u8FDE\u901A\u6027\u91CD\u7B97\uFF0C\u6307\u5B9A\u540D\u4F1A\u88AB\u8986\u76D6\u3002",
|
|
301
756
|
"3) \u7F51\u7EDC\u6807\u7B7E\uFF1A\u5148\u8BD5 eda_place_net_label\uFF1B\u82E5\u8BE5\u6784\u5EFA\u7684 createNetLabel \u4E0D\u54CD\u5E94\uFF0C\u4F1A\u81EA\u52A8\u964D\u7EA7\u4E3A\u7EAF\u6587\u672C\u6807\u7B7E\uFF08\u65E0\u7535\u6C14\u8BED\u4E49\uFF09\uFF0C\u6B64\u65F6\u9760 stub \u5BFC\u7EBF\u627F\u8F7D\u7F51\u7EDC\uFF1B\u6279\u91CF\u7528 eda_place_net_labels \u4E00\u6B21\u8C03\u7528\uFF0C\u4E0D\u8981\u4E32\u884C\u4E0A\u767E\u6B21\u3002",
|
|
302
|
-
"4) \u6821\u9A8C\uFF1Aeda_export_netlist \u770B\u6BCF\u4E2A\u7F51\u7EDC\u7684\u7AEF\u70B9\u662F\u5426 \u22652\uFF1Beda_run_drc \u770B\u9010\u6761 DRC \u660E\u7EC6\
|
|
303
|
-
"5) \
|
|
304
|
-
"6) \
|
|
757
|
+
"4) \u6821\u9A8C\uFF1Aeda_export_netlist \u770B\u6BCF\u4E2A\u7F51\u7EDC\u7684\u7AEF\u70B9\u662F\u5426 \u22652\uFF1Beda_run_drc \u770B\u9010\u6761 DRC \u660E\u7EC6\u3002",
|
|
758
|
+
"5) \u770B\u56FE\uFF08eda_capture\uFF09\u53EA\u5728\u8FD9 4 \u4E2A\u65F6\u523B\uFF1A\u5E03\u5C40\u5B8C\u6210\u5F85\u5E03\u7EBF\u3001\u5E03\u7EBF\u5B8C\u6210\u5F85 DRC\u3001\u4E1D\u5370\u6587\u5B57\u653E\u5B8C\u3001\u5750\u6807\u4E0E\u76F4\u89C9\u51B2\u7A81\u65F6\uFF1B\u4E0D\u8981\u6BCF\u6539\u4E00\u4E2A\u56FE\u5143\u622A\u4E00\u6B21\uFF0C\u770B\u5B8C eda_capture({cleanup:true}) \u5220\u6389\u4E34\u65F6 PNG\u3002",
|
|
759
|
+
"6) \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u624D eda_auto_layout / eda_auto_route\uFF08\u9700 force:true \u4E14\u8BBE\u7F6E\u91CC\u653E\u884C\uFF09\u3002",
|
|
760
|
+
"7) \u5F15\u811A\u4FE1\u606F\u7528 eda_get_pins \u4E00\u6B21\u62FF\u5168\uFF08\u542B pinName/pinNumber\uFF09\uFF0C\u4E0D\u8981\u9010\u811A\u8C03 raw API\u3002"
|
|
305
761
|
].join("\n");
|
|
306
762
|
var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
307
763
|
"## \u5E03\u5C40\u7EC6\u5219\u662F\u600E\u4E48\u843D\u5730\u7684",
|
|
@@ -323,6 +779,25 @@ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
|
323
779
|
"",
|
|
324
780
|
"eda_draw_wire \u4F20\u7684 net \u53EA\u5BF9**\u60AC\u7A7A\u5BFC\u7EBF**\u6709\u6548\uFF1B\u5BFC\u7EBF\u7AEF\u70B9\u4E00\u65E6\u843D\u5728\u5F15\u811A/\u7F51\u7EDC\u4E0A\uFF0CEDA \u4F1A\u6309\u8FDE\u901A\u6027\u91CD\u7B97\u7F51\u7EDC\u540D\uFF08\u5B9E\u6D4B TESTNET \u5E76\u5165\u4E86 LED_A\uFF09\u3002\u6240\u4EE5\u201C\u540C\u540D\u5BFC\u7EBF = \u540C\u540D\u7F51\u7EDC\u201D\u4E0D\u6210\u7ACB\uFF0C\u8DE8\u533A\u57DF\u540C\u540D\u5FC5\u987B\u7528\u7F51\u7EDC\u6807\u7B7E\u6216\u7F51\u7EDC\u7AEF\u53E3\u3002",
|
|
325
781
|
"",
|
|
782
|
+
"## \u4EC0\u4E48\u65F6\u5019\u5FC5\u987B\u770B\u56FE\uFF08eda_capture\uFF09\uFF0C\u4EC0\u4E48\u65F6\u5019\u4E0D\u8981",
|
|
783
|
+
"",
|
|
784
|
+
'\u770B\u56FE\u4E0D\u662F"\u53EF\u9009\u52A0\u5206\u9879"\uFF0C\u4E5F\u4E0D\u662F"\u6BCF\u6B65\u90FD\u622A"\u3002\u5224\u636E\u6309\u98CE\u9669\u800C\u4E0D\u662F\u6309\u4E60\u60EF\uFF1A',
|
|
785
|
+
"",
|
|
786
|
+
"**\u5FC5\u987B\u770B\u56FE\uFF084 \u4E2A\u65F6\u523B\uFF09**",
|
|
787
|
+
"",
|
|
788
|
+
"1. \u5E03\u5C40\u5B8C\u6210\u3001\u5E03\u7EBF\u4E4B\u524D\uFF1A\u677F\u9762\u6700\u7A7A\uFF0C\u6700\u5BB9\u6613\u4E00\u773C\u770B\u51FA\u5668\u4EF6\u8D8A\u754C\u3001\u5668\u4EF6\u4E92\u76F8\u538B\u4F4F\u3001\u8FDE\u63A5\u5668\u671D\u5411\u53CD\u4E86\u3001\u5B89\u88C5\u5B54\u538B\u5230\u5668\u4EF6\u3002",
|
|
789
|
+
"2. \u5E03\u7EBF\u5B8C\u6210\u3001DRC \u4E4B\u524D\uFF1A\u770B\u6709\u6CA1\u6709\u8DE8\u7F51\u7EDC\u8D34\u5408\u3001\u5929\u7EBF stub\u3001\u7ED5\u8FDC\u7684\u957F\u8D70\u7EBF\u3001GND \u88AB\u6253\u6563\u6210\u5B64\u5C9B\u3002",
|
|
790
|
+
"3. \u4E1D\u5370/\u5DE5\u827A\u6587\u5B57\u653E\u5B8C\uFF1A\u770B\u6587\u5B57\u6709\u6CA1\u6709\u538B\u710A\u76D8\u3001\u88AB\u677F\u8FB9\u88C1\u5207\u3001\u65B9\u5411\u53CD\u4E86\uFF1B\u539F\u7406\u56FE\u540C\u7406\u770B\u6807\u7B7E\u91CD\u53E0\u3001\u56FE\u6846\u7EBF\u7A7F\u8FC7\u5668\u4EF6\u3002",
|
|
791
|
+
"4. \u5750\u6807\u7B97\u672F\u4E0E\u76F4\u89C9\u51B2\u7A81\u65F6\uFF08\u5DE5\u5177\u8BF4\u5728\u6846\u5185\u4F46\u4F60\u6000\u7591\u8D8A\u754C\u3001audit \u8BF4\u6CA1\u95EE\u9898\u4F46\u7528\u6237\u8BF4\u96BE\u770B\uFF09\uFF1A\u8FD9\u65F6\u56FE\u662F\u552F\u4E00\u88C1\u51B3\u8005\u3002",
|
|
792
|
+
"",
|
|
793
|
+
"**\u4E0D\u8981\u770B\u56FE**",
|
|
794
|
+
"",
|
|
795
|
+
"- \u6BCF\u653E\u4E00\u4E2A\u56FE\u5143\u5C31\u622A\u4E00\u6B21\uFF1A\u5355\u6B21\u7EA6 2.7 s\u30010.4 MB\uFF0C\u8FD8\u4F1A\u6324\u5360\u4E0A\u4E0B\u6587\u9884\u7B97\u2014\u2014\u6279\u91CF\u6539\u5B8C\u518D\u622A\u3002",
|
|
796
|
+
"- \u53EA\u662F\u786E\u8BA4\u67D0\u4E2A\u8FD4\u56DE\u503C/\u5C5E\u6027\uFF1A\u7528 eda_get_* \u8BFB\u6570\u636E\u6BD4\u56FE\u51C6\u3002",
|
|
797
|
+
"- DRC \u5DF2\u7ED9\u51FA primitiveId/\u5750\u6807\u7684\u9519\u8BEF\uFF1A\u76F4\u63A5\u5B9A\u4F4D\uFF0C\u4E0D\u7528\u56FE\u3002",
|
|
798
|
+
"",
|
|
799
|
+
"**\u770B\u5B8C\u7ACB\u523B\u5220**\uFF1Aeda_capture({cleanup:true})\u3002",
|
|
800
|
+
"",
|
|
326
801
|
"## \u6821\u9A8C\u987A\u5E8F",
|
|
327
802
|
"",
|
|
328
803
|
"1. eda_get_active_document \u786E\u8BA4\u5728\u539F\u7406\u56FE\u9875\uFF08\u4E0D\u662F HOME/PCB\uFF09\u3002",
|
|
@@ -332,7 +807,7 @@ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
|
332
807
|
].join("\n");
|
|
333
808
|
|
|
334
809
|
// src/bridge/index.ts
|
|
335
|
-
var VERSION = "0.4.
|
|
810
|
+
var VERSION = "0.4.6";
|
|
336
811
|
var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
|
|
337
812
|
var WS_HOST = "127.0.0.1";
|
|
338
813
|
var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
|
|
@@ -608,16 +1083,24 @@ var TOOLS = [
|
|
|
608
1083
|
{ name: "eda_get_active_document", description: "Return the document/tab that is currently active (documentType, uuid, tabId, parent project). Most tab-dependent APIs (sch_Net.*, netlist export, DRC) only work when the matching document is active - use eda_open_document to switch.", inputSchema: objectSchema({}) },
|
|
609
1084
|
{ name: "eda_open_document", description: "Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run.", inputSchema: objectSchema({ documentUuid: { type: "string" }, splitScreenId: { type: "string" } }, ["documentUuid"]) },
|
|
610
1085
|
{ name: "eda_get_net_labels", description: "List every label-like primitive on the active schematic page: attribute primitives (netlabel/netport/netflag) PLUS plain texts left by the createNetLabel fallback, so they can be audited and deleted. Reports kind/primitiveId/net per entry.", inputSchema: objectSchema({ parentPrimitiveId: { type: "string" } }) },
|
|
611
|
-
{ name: "eda_place_net_label", description: "Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] } }, ["x", "y", "net"]) },
|
|
612
|
-
{ name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" }, concurrency: { type: "integer", minimum: 1, maximum: 8 } }, ["items"]) },
|
|
1086
|
+
{ name: "eda_place_net_label", description: "Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, probe: { type: "boolean" }, textLabel: { type: "boolean" } }, ["x", "y", "net"]) },
|
|
1087
|
+
{ name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" }, textLabel: { type: "boolean" }, concurrency: { type: "integer", minimum: 1, maximum: 8 } }, ["items"]) },
|
|
613
1088
|
{ name: "eda_run_drc", description: 'Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details.', inputSchema: objectSchema({ strict: { type: "boolean" } }) },
|
|
614
1089
|
{ name: "eda_auto_layout", description: "Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
615
1090
|
{ name: "eda_auto_route", description: "Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
616
1091
|
{ name: "eda_export_netlist", description: "Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first.", inputSchema: objectSchema({ type: { type: "string" } }) },
|
|
617
1092
|
{ name: "eda_export_bom", description: "Export the schematic BOM. csv is decoded from UTF-16 to readable text; xlsx returns a base64 data URL.", inputSchema: objectSchema({ format: { type: "string", enum: ["csv", "xlsx"] } }) },
|
|
618
1093
|
{ name: "eda_move_component", description: "Move/rotate/mirror a schematic component (sch_PrimitiveComponent.modify) without deleting and re-placing it, so the designator and value survive.", inputSchema: objectSchema({ primitiveId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" } }, ["primitiveId"]) },
|
|
1094
|
+
{ name: "pcb_draw_text", description: "Put text on a PCB layer - silkscreen by default (3 top silk / 4 bottom silk). Use it for pin functions, board markings, version and fabrication notes; silkscreen text must not cover pads.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, text: { type: "string" }, layer: { type: "string" }, fontSize: { type: "number" }, lineWidth: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" }, alignMode: { type: "integer" }, locked: { type: "boolean" } }, ["x", "y", "text"]) },
|
|
1095
|
+
{ name: "pcb_draw_mounting_hole", description: 'Place a NON-plated mounting hole or a slot (pcb_PrimitivePad with metallization:false). shape "round" is a drill-sized hole, shape "slot" an oval cut-out. Use this for mounting holes and connector slots instead of hacking the board outline.', inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, diameter: { type: "number" }, shape: { type: "string", enum: ["round", "slot"] }, slotLength: { type: "number" }, rotation: { type: "number" }, plated: { type: "boolean" }, annulus: { type: "number" }, padNumber: { type: "string" }, layer: { type: "string" } }, ["x", "y", "diameter"]) },
|
|
1096
|
+
{ name: "eda_schematic_audit", description: "Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing.", inputSchema: objectSchema({ grid: { type: "number" }, includeOk: { type: "boolean" } }) },
|
|
1097
|
+
{ name: "eda_delivery_checklist", description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. Pass outPath to also write a Markdown report you can hand to a reviewer. Use it instead of claiming a board is finished.", inputSchema: objectSchema({ outPath: { type: "string" }, pcbDrc: { type: "boolean" } }) },
|
|
1098
|
+
{ name: "eda_design_intake", description: "Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing.", inputSchema: objectSchema({ answers: { type: "object", additionalProperties: true }, reset: { type: "boolean" } }) },
|
|
1099
|
+
{ name: "eda_netlist_erc", description: "Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too.", inputSchema: objectSchema({ includeOk: { type: "boolean" } }) },
|
|
1100
|
+
{ name: "eda_pcb_canvas_origin", description: "Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API.", inputSchema: objectSchema({ align: { type: "boolean" }, offsetX: { type: "number" }, offsetY: { type: "number" }, point: { type: "object", additionalProperties: true } }) },
|
|
1101
|
+
{ name: "eda_capture", description: "Render the active document to a PNG (dmt_EditorControl.getCurrentRenderedAreaImage) and write it to outPath or return a data URL. Use it to LOOK at the sheet/board instead of trusting coordinate math. The PNG is a scratch file: read it, then call eda_capture({cleanup:true}) to delete every capture path this session wrote.", inputSchema: objectSchema({ outPath: { type: "string" }, inline: { type: "boolean" }, cleanup: { type: "boolean" } }) },
|
|
619
1102
|
{ name: "eda_workflow_guide", description: "Return the schematic working standard: grid-aligned manual placement, signal flow, when net labels/ports are required (a wire net name is overridden by connectivity), net-label fallback behaviour, batch placement and the verification order. Read it before laying out a sheet.", inputSchema: objectSchema({}) },
|
|
620
|
-
{ name: "eda_list_api_methods", description:
|
|
1103
|
+
{ name: "eda_list_api_methods", description: "Ask the CONNECTED EXTENSION for the API methods it accepts right now (dsh.internal.listApiMethods) and report source/version/counts. Falls back to the bridge copy with stale:true only when the extension is an older zip. Use refresh:true to re-query.", inputSchema: objectSchema({ refresh: { type: "boolean" } }) },
|
|
621
1104
|
{ name: "eda_delete_primitive", description: 'Delete a schematic primitive by kind and primitive id. Net ports, net flags and net labels are attribute primitives and need kind "netport"/"netflag"/"netlabel"; the text fallback labels are kind "text".', inputSchema: objectSchema({ kind: { type: "string", enum: ["component", "wire", "rectangle", "circle", "arc", "polygon", "text", "netport", "netflag", "netlabel"] }, primitiveId: { type: "string" } }, ["kind", "primitiveId"]) },
|
|
622
1105
|
{ name: "eda_create_board", description: "Create a board linking a schematic and a PCB (dmt_Board.createBoard).", inputSchema: objectSchema({ schematicUuid: { type: "string" }, pcbUuid: { type: "string" } }) },
|
|
623
1106
|
{ name: "eda_api_call", description: "Call an allow-listed JLCEDA Pro extension API method directly.", inputSchema: objectSchema({ method: { type: "string" }, args: { type: "array" } }, ["method", "args"]) },
|
|
@@ -812,6 +1295,30 @@ async function pcbComponentIds(timeoutMs) {
|
|
|
812
1295
|
return Array.isArray(raw) ? raw.map(String) : [];
|
|
813
1296
|
}
|
|
814
1297
|
var nextCallId = 1;
|
|
1298
|
+
var extensionApiCache = null;
|
|
1299
|
+
var designBrief = null;
|
|
1300
|
+
var capturedFiles = [];
|
|
1301
|
+
async function extensionApiMethods(timeoutMs, refresh = false) {
|
|
1302
|
+
if (!refresh && extensionApiCache !== null) return extensionApiCache;
|
|
1303
|
+
try {
|
|
1304
|
+
const raw = await callExtension("dsh.internal.listApiMethods", [], Math.min(timeoutMs, 5e3));
|
|
1305
|
+
const record = asRecord(raw);
|
|
1306
|
+
const allowed = Array.isArray(record.allowed) ? record.allowed.map(String) : [];
|
|
1307
|
+
const files = Array.isArray(record.files) ? record.files.map(String) : [];
|
|
1308
|
+
if (allowed.length === 0 && files.length === 0) return void 0;
|
|
1309
|
+
extensionApiCache = {
|
|
1310
|
+
version: typeof record.version === "string" ? record.version : null,
|
|
1311
|
+
allowed,
|
|
1312
|
+
files,
|
|
1313
|
+
all: [...allowed, ...files].sort(),
|
|
1314
|
+
allowedSet: new Set(allowed),
|
|
1315
|
+
fileSet: new Set(files)
|
|
1316
|
+
};
|
|
1317
|
+
return extensionApiCache;
|
|
1318
|
+
} catch {
|
|
1319
|
+
return void 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
815
1322
|
var CLIENT_STALE_MS = 6e4;
|
|
816
1323
|
function pickClients() {
|
|
817
1324
|
const now = Date.now();
|
|
@@ -956,6 +1463,7 @@ var PCB_LAYERS = {
|
|
|
956
1463
|
topAssembly: 9,
|
|
957
1464
|
bottomAssembly: 10,
|
|
958
1465
|
boardOutline: 11,
|
|
1466
|
+
multi: 12,
|
|
959
1467
|
document: 13,
|
|
960
1468
|
mechanical: 14
|
|
961
1469
|
};
|
|
@@ -997,6 +1505,33 @@ function resolveRegionLayer(value, tool) {
|
|
|
997
1505
|
}
|
|
998
1506
|
return layer;
|
|
999
1507
|
}
|
|
1508
|
+
var BRIEF_TAG = "[dsh-eda-spec]";
|
|
1509
|
+
function briefBlock(brief) {
|
|
1510
|
+
return BRIEF_TAG + " " + JSON.stringify(brief);
|
|
1511
|
+
}
|
|
1512
|
+
function parseBriefBlock(description) {
|
|
1513
|
+
const text = typeof description === "string" ? description : "";
|
|
1514
|
+
const index = text.indexOf(BRIEF_TAG);
|
|
1515
|
+
if (index < 0) return null;
|
|
1516
|
+
const body = text.slice(index + BRIEF_TAG.length).trim();
|
|
1517
|
+
const close = body.indexOf("}");
|
|
1518
|
+
try {
|
|
1519
|
+
const parsed = JSON.parse(close < 0 ? body : body.slice(0, close + 1));
|
|
1520
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
1521
|
+
} catch {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function fetchParsedNetlist(timeoutMs) {
|
|
1526
|
+
const payload = await callExtension("sch_ManufactureData.getNetlistFile", [void 0, void 0], timeoutMs);
|
|
1527
|
+
const text = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : void 0;
|
|
1528
|
+
if (text === void 0) return null;
|
|
1529
|
+
try {
|
|
1530
|
+
return JSON.parse(text);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1000
1535
|
function buildPolygonSource(points) {
|
|
1001
1536
|
const source = [points[0], points[1], "L"];
|
|
1002
1537
|
for (let i = 2; i < points.length; i += 2) source.push(points[i], points[i + 1]);
|
|
@@ -1054,6 +1589,7 @@ async function auditPcb(args, timeoutMs) {
|
|
|
1054
1589
|
const vias = await pcbPrimitiveList("pcb_PrimitiveVia.getAll", [netFilter, void 0], timeoutMs);
|
|
1055
1590
|
const pours = await pcbPrimitiveList("pcb_PrimitivePour.getAll", [netFilter, void 0, void 0], timeoutMs);
|
|
1056
1591
|
const outline = await pcbPrimitiveList("pcb_PrimitivePolyline.getAll", [void 0, PCB_LAYERS.boardOutline, void 0], timeoutMs);
|
|
1592
|
+
const pcbComponents = await pcbPrimitiveList("pcb_PrimitiveComponent.getAll", [void 0, void 0], timeoutMs);
|
|
1057
1593
|
const violations = [];
|
|
1058
1594
|
const warnings = [];
|
|
1059
1595
|
const add = (bucket, rule, message, samples) => {
|
|
@@ -1101,6 +1637,73 @@ async function auditPcb(args, timeoutMs) {
|
|
|
1101
1637
|
}
|
|
1102
1638
|
if (outline.length === 0) violations.push({ rule: "board-outline", message: "No board outline on layer 11: auto-routing and Gerber export need a closed outline (pcb_draw_board_outline)", count: 1, samples: [] });
|
|
1103
1639
|
if (pours.length === 0) warnings.push({ rule: "copper-pour", message: "No copper pour: a GND pour on the top/bottom layer is the recommended way to carry ground (eda_pcb_create_pour)", count: 1, samples: [] });
|
|
1640
|
+
try {
|
|
1641
|
+
const silk = await callExtension("pcb_PrimitiveString.getAll", [PCB_LAYERS.topSilkscreen], timeoutMs);
|
|
1642
|
+
const silkCount = Array.isArray(silk) ? silk.length : 0;
|
|
1643
|
+
if (silkCount === 0) {
|
|
1644
|
+
warnings.push({ rule: "silkscreen-text", message: "No silkscreen text primitive: the official example board carries 19 (function labels, version, fab notes). Add board identity and pin functions with pcb_draw_text." });
|
|
1645
|
+
}
|
|
1646
|
+
} catch {
|
|
1647
|
+
}
|
|
1648
|
+
try {
|
|
1649
|
+
const pads = await callExtension("pcb_PrimitivePad.getAll", [PCB_LAYERS.multi], timeoutMs);
|
|
1650
|
+
const holes = (Array.isArray(pads) ? pads : []).filter((pad) => (pad?.metallization ?? pad?.Metallization) === false);
|
|
1651
|
+
const flatX = [];
|
|
1652
|
+
for (const entry of outline) {
|
|
1653
|
+
const source = entry?.polygon?.polygon ?? entry?.Polygon?.polygon ?? [];
|
|
1654
|
+
for (let i = 0; i + 1 < source.length; i += 2) {
|
|
1655
|
+
if (typeof source[i] === "number") flatX.push(source[i]);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
const wide = flatX.length > 0 ? Math.max(...flatX) - Math.min(...flatX) : 0;
|
|
1659
|
+
if (holes.length === 0 && wide > 1200) {
|
|
1660
|
+
warnings.push({ rule: "mounting-hole", message: "No non-plated mounting hole on a board wider than 1200 mil: use pcb_draw_mounting_hole (3.2 mm) in the corners." });
|
|
1661
|
+
}
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
const boundsOfComponent = (entry) => {
|
|
1665
|
+
const x = Number(entry?.x ?? entry?.X);
|
|
1666
|
+
const y = Number(entry?.y ?? entry?.Y);
|
|
1667
|
+
const w = Number(entry?.width ?? entry?.Width);
|
|
1668
|
+
const h = Number(entry?.height ?? entry?.Height);
|
|
1669
|
+
if (![x, y, w, h].every(Number.isFinite) || w <= 0 || h <= 0) return void 0;
|
|
1670
|
+
return { left: x - w / 2, right: x + w / 2, bottom: y - h / 2, top: y + h / 2 };
|
|
1671
|
+
};
|
|
1672
|
+
const bodies = pcbComponents.map((entry) => ({
|
|
1673
|
+
name: String(entry?.designator ?? entry?.Designator ?? entry?.primitiveId ?? "?"),
|
|
1674
|
+
bounds: boundsOfComponent(entry)
|
|
1675
|
+
})).filter((entry) => entry.bounds !== void 0);
|
|
1676
|
+
if (bodies.length >= 2) {
|
|
1677
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
1678
|
+
for (let j = i + 1; j < bodies.length; j++) {
|
|
1679
|
+
const a = bodies[i].bounds;
|
|
1680
|
+
const b = bodies[j].bounds;
|
|
1681
|
+
const sharedX = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
|
1682
|
+
const sharedY = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
|
|
1683
|
+
if (sharedX > 2 && sharedY > 2) add(violations, "component-overlap", "Two component bodies overlap; a placement collision here becomes a placement error at assembly.", [bodies[i].name + " x " + bodies[j].name]);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const outlineX = [];
|
|
1688
|
+
const outlineY = [];
|
|
1689
|
+
for (const entry of outline) {
|
|
1690
|
+
const source = entry?.polygon?.polygon ?? entry?.Polygon?.polygon ?? [];
|
|
1691
|
+
for (let i = 0; i + 1 < source.length; i += 2) {
|
|
1692
|
+
if (typeof source[i] === "number") {
|
|
1693
|
+
outlineX.push(source[i]);
|
|
1694
|
+
outlineY.push(source[i + 1]);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (bodies.length > 0 && outlineX.length >= 3) {
|
|
1699
|
+
const box = { left: Math.min(...outlineX), right: Math.max(...outlineX), bottom: Math.min(...outlineY), top: Math.max(...outlineY) };
|
|
1700
|
+
for (const body of bodies) {
|
|
1701
|
+
const b = body.bounds;
|
|
1702
|
+
if (b.left < box.left - 5 || b.right > box.right + 5 || b.bottom < box.bottom - 5 || b.top > box.top + 5) {
|
|
1703
|
+
add(warnings, "component-off-board", "A component body extends past the board outline; on a real assembly it either hangs in the air or collides with the enclosure.", [body.name]);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1104
1707
|
let drc = { ran: false, reason: null, violationCount: null };
|
|
1105
1708
|
if (args.drc !== false) {
|
|
1106
1709
|
try {
|
|
@@ -1249,7 +1852,7 @@ async function clearRoutingBatched(args, type, timeoutMs) {
|
|
|
1249
1852
|
hint: remaining <= 0 ? "Routing cleared." : "Call eda_pcb_clear_routing again (same arguments) until done=true."
|
|
1250
1853
|
};
|
|
1251
1854
|
}
|
|
1252
|
-
async function
|
|
1855
|
+
async function parseNetlist2(previous, timeoutMs) {
|
|
1253
1856
|
let text;
|
|
1254
1857
|
try {
|
|
1255
1858
|
text = await callExtension("sch_Netlist.getNetlist", [void 0], timeoutMs);
|
|
@@ -1289,6 +1892,9 @@ async function placeNetLabel(net, x, y, fontSize, timeoutMs, options = {}) {
|
|
|
1289
1892
|
netLabelSupport = false;
|
|
1290
1893
|
}
|
|
1291
1894
|
}
|
|
1895
|
+
if (options.textLabel !== true) {
|
|
1896
|
+
return { method: "stub", net, x, y, created: false, warning: "createNetLabel is unavailable on this EDA build (" + (failure ?? "probe already failed this session") + "); the net is carried by the stub wire, and no text label was added (pass textLabel:true if you want one)." };
|
|
1897
|
+
}
|
|
1292
1898
|
const raw = await callExtension("sch_PrimitiveText.create", [x, y, net, 0, null, null, size, false, false, false, 1], timeoutMs);
|
|
1293
1899
|
const label = summarizePrimitive(raw);
|
|
1294
1900
|
if (label.primitiveId !== void 0) {
|
|
@@ -1935,6 +2541,18 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1935
2541
|
}
|
|
1936
2542
|
}
|
|
1937
2543
|
}
|
|
2544
|
+
if (designBrief === null) {
|
|
2545
|
+
const intake = buildIntake(null);
|
|
2546
|
+
return {
|
|
2547
|
+
primitiveId,
|
|
2548
|
+
result: result2,
|
|
2549
|
+
intakeWarning: {
|
|
2550
|
+
message: "No design parameters confirmed this session: supply, MCU, clocks, connectors, board size, layer count and fabrication were not agreed. Confirm them with eda_design_intake + ask_user_question before continuing, or the board may be electrically correct and practically wrong.",
|
|
2551
|
+
criticalCount: intake.criticalCount,
|
|
2552
|
+
firstMissing: intake.missingCritical.slice(0, 8)
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
1938
2556
|
return { primitiveId, result: result2 };
|
|
1939
2557
|
}
|
|
1940
2558
|
case "eda_place_net_flag": {
|
|
@@ -1997,7 +2615,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1997
2615
|
const x = Number(args.x);
|
|
1998
2616
|
const y = Number(args.y);
|
|
1999
2617
|
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("eda_place_net_label requires numeric x/y");
|
|
2000
|
-
return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs, {
|
|
2618
|
+
return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs, {
|
|
2619
|
+
probe: args.probe === true,
|
|
2620
|
+
textLabel: args.textLabel === true
|
|
2621
|
+
});
|
|
2001
2622
|
}
|
|
2002
2623
|
case "eda_place_net_labels": {
|
|
2003
2624
|
const items = Array.isArray(args.items) ? args.items.map(asRecord) : [];
|
|
@@ -2006,7 +2627,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2006
2627
|
const results = await mapLimit(items, concurrency, async (item) => {
|
|
2007
2628
|
const net = item.net === void 0 ? "" : String(item.net);
|
|
2008
2629
|
try {
|
|
2009
|
-
const placed = item.pinPrimitiveId !== void 0 ? await placeAtPin("eda_place_net_label_at_pin", { ...item, net }, timeoutMs) : await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, {
|
|
2630
|
+
const placed = item.pinPrimitiveId !== void 0 ? await placeAtPin("eda_place_net_label_at_pin", { ...item, net }, timeoutMs) : await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, {
|
|
2631
|
+
probe: args.probe === true,
|
|
2632
|
+
textLabel: args.textLabel === true
|
|
2633
|
+
});
|
|
2010
2634
|
return { ok: true, net, ...placed };
|
|
2011
2635
|
} catch (error) {
|
|
2012
2636
|
return { ok: false, net, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -2086,13 +2710,15 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2086
2710
|
activeUuid = String(pickValue(info, "uuid", "Uuid") ?? "");
|
|
2087
2711
|
} catch {
|
|
2088
2712
|
}
|
|
2089
|
-
const
|
|
2713
|
+
const activeIsTarget = activeUuid === documentUuid;
|
|
2714
|
+
const tabOpened = activeIsTarget || tabIds.some((tab) => tab.startsWith(documentUuid + "@"));
|
|
2090
2715
|
return {
|
|
2091
2716
|
...raw !== null && typeof raw === "object" ? raw : { result: raw ?? null },
|
|
2092
2717
|
opened: tabOpened,
|
|
2718
|
+
activated: activeIsTarget,
|
|
2093
2719
|
activeDocumentUuid: activeUuid || null,
|
|
2094
2720
|
tabIds,
|
|
2095
|
-
hint: tabOpened ? null : "The app accepted openDocument but
|
|
2721
|
+
hint: tabOpened ? null : "The app accepted openDocument but the document is not active (activeDocumentUuid=" + (activeUuid || "unknown") + ", tabs: " + (tabIds.join(", ") || "none") + "). After an app restart the editor sits on the Start Page: open the project first (eda_open_project), then open the document. This build cannot open a document from the HOME screen programmatically, so never assume a page is active without checking activeDocumentUuid."
|
|
2096
2722
|
};
|
|
2097
2723
|
}
|
|
2098
2724
|
case "eda_set_no_connect": {
|
|
@@ -2275,7 +2901,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2275
2901
|
const all = await callExtension("sch_Net.getAllNets", [], timeoutMs);
|
|
2276
2902
|
if (Array.isArray(all) && all.length > 0) return { source: "sch_Net.getAllNets", count: all.length, nets: all };
|
|
2277
2903
|
if (args.parseNetlist === false) return { source: "sch_Net.*", count: 0, nets: [], hint: NET_EMPTY_HINT };
|
|
2278
|
-
const parsed = await
|
|
2904
|
+
const parsed = await parseNetlist2(Array.isArray(all) ? all : project, timeoutMs);
|
|
2279
2905
|
return parsed;
|
|
2280
2906
|
}
|
|
2281
2907
|
case "eda_run_drc": {
|
|
@@ -2342,6 +2968,9 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2342
2968
|
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
|
|
2343
2969
|
if (format === "xlsx") return payload;
|
|
2344
2970
|
const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
|
|
2971
|
+
if (raw === "") {
|
|
2972
|
+
throw new Error("eda_export_bom: no BOM data (the schematic page must be the active document; call eda_get_active_document, then eda_open_document on the schematic page and retry)");
|
|
2973
|
+
}
|
|
2345
2974
|
const encoding = payload !== null && typeof payload === "object" ? payload.encoding : void 0;
|
|
2346
2975
|
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? "unknown" };
|
|
2347
2976
|
}
|
|
@@ -2357,10 +2986,332 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2357
2986
|
rotation: args.rotation === void 0 ? void 0 : Number(args.rotation),
|
|
2358
2987
|
mirror: args.mirror === void 0 ? void 0 : Boolean(args.mirror)
|
|
2359
2988
|
}], timeoutMs);
|
|
2360
|
-
case "eda_list_api_methods":
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2989
|
+
case "eda_list_api_methods": {
|
|
2990
|
+
const live = await extensionApiMethods(timeoutMs, args.refresh === true);
|
|
2991
|
+
if (live !== void 0) {
|
|
2992
|
+
return {
|
|
2993
|
+
source: "extension",
|
|
2994
|
+
extensionVersion: live.version,
|
|
2995
|
+
count: live.all.length,
|
|
2996
|
+
allowedCount: live.allowed.length,
|
|
2997
|
+
fileCount: live.files.length,
|
|
2998
|
+
fileMethods: live.files,
|
|
2999
|
+
methods: live.all,
|
|
3000
|
+
note: "This is the list the connected extension enforces (dsh.internal.listApiMethods). sendable:true means eda_api_call will forward it."
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
return {
|
|
3004
|
+
source: "bridge-local-copy",
|
|
3005
|
+
stale: true,
|
|
3006
|
+
count: ALLOWED_API.size,
|
|
3007
|
+
methods: [...ALLOWED_API].sort(),
|
|
3008
|
+
note: "The connected extension does not expose its list (older zip), so this is the bridge copy \u2014 it may be narrower than what the extension accepts. eda_api_call forwards unknown methods and reports the extension verdict, so try the call instead of trusting this list."
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
case "pcb_draw_text": {
|
|
3012
|
+
const text = String(args.text ?? "");
|
|
3013
|
+
if (text.trim() === "") throw new Error("pcb_draw_text requires text");
|
|
3014
|
+
if (text.length > 200) throw new Error("pcb_draw_text: longer than 200 characters is probably a mistake; split it");
|
|
3015
|
+
const silkLayer = args.layer === void 0 ? PCB_LAYERS.topSilkscreen : resolvePcbLayer(args.layer);
|
|
3016
|
+
const fontSize = args.fontSize === void 0 ? 40 : Number(args.fontSize);
|
|
3017
|
+
return await callExtension("pcb_PrimitiveString.create", [
|
|
3018
|
+
silkLayer,
|
|
3019
|
+
Number(args.x),
|
|
3020
|
+
Number(args.y),
|
|
3021
|
+
text,
|
|
3022
|
+
"Arial",
|
|
3023
|
+
fontSize,
|
|
3024
|
+
args.lineWidth === void 0 ? Math.max(6, Math.round(fontSize / 6)) : Number(args.lineWidth),
|
|
3025
|
+
args.alignMode === void 0 ? 1 : Number(args.alignMode),
|
|
3026
|
+
args.rotation === void 0 ? 0 : Number(args.rotation),
|
|
3027
|
+
false,
|
|
3028
|
+
0,
|
|
3029
|
+
Boolean(args.mirror ?? false),
|
|
3030
|
+
Boolean(args.locked ?? false)
|
|
3031
|
+
], timeoutMs);
|
|
3032
|
+
}
|
|
3033
|
+
case "pcb_draw_mounting_hole": {
|
|
3034
|
+
const holeX = Number(args.x);
|
|
3035
|
+
const holeY = Number(args.y);
|
|
3036
|
+
const holeDiameter = Number(args.diameter);
|
|
3037
|
+
if (![holeX, holeY, holeDiameter].every((value) => Number.isFinite(value)) || holeDiameter <= 0) {
|
|
3038
|
+
throw new Error("pcb_draw_mounting_hole requires numeric x/y and a positive diameter (mil)");
|
|
3039
|
+
}
|
|
3040
|
+
if (holeDiameter < 20) throw new Error("pcb_draw_mounting_hole: " + holeDiameter + " mil is smaller than any drill (>= 20 mil)");
|
|
3041
|
+
const holeShape = String(args.shape ?? "round");
|
|
3042
|
+
const holeRotation = args.rotation === void 0 ? 0 : Number(args.rotation);
|
|
3043
|
+
const metallization = args.plated === true;
|
|
3044
|
+
let holeSpec;
|
|
3045
|
+
if (holeShape === "slot") {
|
|
3046
|
+
const slotLength = args.slotLength === void 0 ? holeDiameter * 2 : Number(args.slotLength);
|
|
3047
|
+
if (!Number.isFinite(slotLength) || slotLength <= holeDiameter) {
|
|
3048
|
+
throw new Error("pcb_draw_mounting_hole: slotLength must exceed the slot width (diameter)");
|
|
3049
|
+
}
|
|
3050
|
+
holeSpec = ["SLOT", holeDiameter, slotLength];
|
|
3051
|
+
} else {
|
|
3052
|
+
holeSpec = ["ROUND", holeDiameter];
|
|
3053
|
+
}
|
|
3054
|
+
const holeLayer = args.layer === void 0 ? PCB_LAYERS.multi : resolvePcbLayer(args.layer);
|
|
3055
|
+
const annulus = args.annulus === void 0 ? 0 : Number(args.annulus);
|
|
3056
|
+
if (!Number.isFinite(annulus) || annulus < 0) throw new Error("pcb_draw_mounting_hole: annulus must be >= 0 (mil)");
|
|
3057
|
+
const slotLengthValue = args.slotLength === void 0 ? holeDiameter * 2 : Number(args.slotLength);
|
|
3058
|
+
const padShape = holeShape === "slot" ? ["ELLIPSE", slotLengthValue + annulus * 2, holeDiameter + annulus * 2] : ["ELLIPSE", holeDiameter + annulus * 2, holeDiameter + annulus * 2];
|
|
3059
|
+
return await callExtension("pcb_PrimitivePad.create", [
|
|
3060
|
+
holeLayer,
|
|
3061
|
+
String(args.padNumber ?? ""),
|
|
3062
|
+
holeX,
|
|
3063
|
+
holeY,
|
|
3064
|
+
holeRotation,
|
|
3065
|
+
padShape,
|
|
3066
|
+
void 0,
|
|
3067
|
+
holeSpec,
|
|
3068
|
+
0,
|
|
3069
|
+
0,
|
|
3070
|
+
0,
|
|
3071
|
+
metallization,
|
|
3072
|
+
void 0,
|
|
3073
|
+
void 0,
|
|
3074
|
+
void 0,
|
|
3075
|
+
void 0,
|
|
3076
|
+
false
|
|
3077
|
+
], timeoutMs);
|
|
3078
|
+
}
|
|
3079
|
+
case "eda_schematic_audit": {
|
|
3080
|
+
const paper = paperOf(args.paper);
|
|
3081
|
+
const rawList = await callExtension("sch_PrimitiveComponent.getAll", [], timeoutMs);
|
|
3082
|
+
const list = Array.isArray(rawList) ? rawList.slice(0, 200) : [];
|
|
3083
|
+
const components = [];
|
|
3084
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
3085
|
+
const entry = list[index] ?? {};
|
|
3086
|
+
const designator = String(entry.designator ?? entry.Designator ?? entry.name ?? "#" + index);
|
|
3087
|
+
const id = String(entry.primitiveId ?? entry.PrimitiveId ?? "");
|
|
3088
|
+
let bounds;
|
|
3089
|
+
if (id !== "") {
|
|
3090
|
+
const box = await bboxOf([id], timeoutMs);
|
|
3091
|
+
if (box !== void 0) bounds = { left: box.minX, right: box.maxX, bottom: box.minY, top: box.maxY };
|
|
3092
|
+
}
|
|
3093
|
+
components.push({ designator, x: Number(entry.x ?? entry.X ?? 0), y: Number(entry.y ?? entry.Y ?? 0), bounds });
|
|
3094
|
+
}
|
|
3095
|
+
const report = auditSchematicGeometry({
|
|
3096
|
+
components,
|
|
3097
|
+
frame: paper.frame,
|
|
3098
|
+
titleBlock: paper.titleBlock,
|
|
3099
|
+
grid: args.grid === void 0 ? 10 : Number(args.grid)
|
|
3100
|
+
});
|
|
3101
|
+
const checks = args.includeOk === true ? report.checks : report.checks.filter((check) => check.status !== "pass");
|
|
3102
|
+
return {
|
|
3103
|
+
ok: report.ok,
|
|
3104
|
+
counts: report.counts,
|
|
3105
|
+
componentCount: report.componentCount,
|
|
3106
|
+
boxesRead: components.filter((component) => component.bounds !== void 0).length,
|
|
3107
|
+
checks
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
case "eda_delivery_checklist": {
|
|
3111
|
+
const steps = [];
|
|
3112
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3113
|
+
let netlistErc = null;
|
|
3114
|
+
try {
|
|
3115
|
+
const parsedNetlist = await fetchParsedNetlist(timeoutMs);
|
|
3116
|
+
netlistErc = runNetlistErc(parsedNetlist);
|
|
3117
|
+
steps.push({ step: "netlist-erc", ok: netlistErc.ok, detail: netlistErc.counts, failing: netlistErc.checks.filter((check) => check.status === "fail").map((check) => check.id) });
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
steps.push({ step: "netlist-erc", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3120
|
+
}
|
|
3121
|
+
let pcbAudit = null;
|
|
3122
|
+
try {
|
|
3123
|
+
pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs);
|
|
3124
|
+
steps.push({
|
|
3125
|
+
step: "pcb-audit",
|
|
3126
|
+
ok: Array.isArray(pcbAudit.violations) && pcbAudit.violations.length === 0,
|
|
3127
|
+
detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
|
|
3128
|
+
failing: (pcbAudit.violations ?? []).map((entry) => entry.rule)
|
|
3129
|
+
});
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
steps.push({ step: "pcb-audit", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3132
|
+
}
|
|
3133
|
+
let gerber = null;
|
|
3134
|
+
try {
|
|
3135
|
+
const payload = await callExtension("pcb_ManufactureData.getGerberFile", [void 0, void 0, void 0, void 0, void 0, void 0], timeoutMs);
|
|
3136
|
+
const bytes = payload && typeof payload === "object" ? Number(payload.size ?? payload.bytes ?? 0) : typeof payload === "string" ? payload.length : 0;
|
|
3137
|
+
gerber = { bytes, ok: bytes > 1e3 };
|
|
3138
|
+
steps.push({ step: "gerber", ok: gerber.ok, detail: { bytes } });
|
|
3139
|
+
} catch (error) {
|
|
3140
|
+
steps.push({ step: "gerber", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3141
|
+
}
|
|
3142
|
+
let bom = null;
|
|
3143
|
+
try {
|
|
3144
|
+
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, "csv"], timeoutMs);
|
|
3145
|
+
const text = typeof payload === "string" ? payload : typeof payload?.text === "string" ? payload.text : "";
|
|
3146
|
+
const lines = text === "" ? 0 : text.split(/\r?\n/).filter((line) => line.trim() !== "").length;
|
|
3147
|
+
bom = { rows: Math.max(0, lines - 1), ok: lines > 1 };
|
|
3148
|
+
steps.push({ step: "bom", ok: bom.ok, detail: { rows: bom.rows } });
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
steps.push({ step: "bom", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3151
|
+
}
|
|
3152
|
+
const failed = steps.filter((step) => step.ok !== true);
|
|
3153
|
+
const report = {
|
|
3154
|
+
ok: failed.length === 0,
|
|
3155
|
+
startedAt,
|
|
3156
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3157
|
+
steps,
|
|
3158
|
+
netlistErc: netlistErc === null ? null : { ok: netlistErc.ok, counts: netlistErc.counts, componentCount: netlistErc.componentCount, netCount: netlistErc.netCount },
|
|
3159
|
+
pcbAudit: pcbAudit === null ? null : { violations: pcbAudit.violations, warnings: pcbAudit.warnings },
|
|
3160
|
+
gerber,
|
|
3161
|
+
bom
|
|
3162
|
+
};
|
|
3163
|
+
const outPath = args.outPath === void 0 ? "" : String(args.outPath).trim();
|
|
3164
|
+
if (outPath !== "") {
|
|
3165
|
+
const markdown = [
|
|
3166
|
+
"# \u4EA4\u4ED8\u68C0\u67E5\u62A5\u544A",
|
|
3167
|
+
"",
|
|
3168
|
+
"- \u5F00\u59CB\uFF1A" + report.startedAt,
|
|
3169
|
+
"- \u7ED3\u675F\uFF1A" + report.finishedAt,
|
|
3170
|
+
"- \u7ED3\u8BBA\uFF1A" + (report.ok ? "PASS\uFF08\u53EF\u4EE5\u4E0B\u5355\uFF09" : "FAIL\uFF08\u6709\u672A\u901A\u8FC7\u9879\uFF09"),
|
|
3171
|
+
"",
|
|
3172
|
+
"| \u6B65\u9AA4 | \u7ED3\u679C | \u8BF4\u660E |",
|
|
3173
|
+
"|---|---|---|",
|
|
3174
|
+
...steps.map((step) => "| " + step.step + " | " + (step.ok === true ? "PASS" : "FAIL") + " | " + JSON.stringify(step.detail ?? step.error ?? {}) + " |"),
|
|
3175
|
+
"",
|
|
3176
|
+
"## \u7535\u6C14\u68C0\u67E5\uFF08eda_netlist_erc\uFF09",
|
|
3177
|
+
"",
|
|
3178
|
+
...netlistErc === null ? ["\uFF08\u672A\u6267\u884C\uFF09"] : netlistErc.checks.map((check) => "- [" + check.status + "] " + check.id + "\uFF1A" + check.message),
|
|
3179
|
+
"",
|
|
3180
|
+
"## PCB \u5BA1\u8BA1",
|
|
3181
|
+
"",
|
|
3182
|
+
...pcbAudit === null ? ["\uFF08\u672A\u6267\u884C\uFF09"] : [
|
|
3183
|
+
"- \u8FDD\u89C4\uFF1A" + (pcbAudit.violations ?? []).length,
|
|
3184
|
+
"- \u8B66\u544A\uFF1A" + (pcbAudit.warnings ?? []).length,
|
|
3185
|
+
...(pcbAudit.violations ?? []).map((entry) => " - " + entry.rule + "\uFF1A" + entry.message),
|
|
3186
|
+
...(pcbAudit.warnings ?? []).map((entry) => " - (warn) " + entry.rule + "\uFF1A" + entry.message)
|
|
3187
|
+
],
|
|
3188
|
+
"",
|
|
3189
|
+
"## \u5236\u9020\u6587\u4EF6",
|
|
3190
|
+
"",
|
|
3191
|
+
"- Gerber\uFF1A" + (gerber === null ? "\u672A\u5BFC\u51FA" : gerber.bytes + " \u5B57\u8282"),
|
|
3192
|
+
"- BOM\uFF1A" + (bom === null ? "\u672A\u5BFC\u51FA" : bom.rows + " \u884C"),
|
|
3193
|
+
""
|
|
3194
|
+
].join("\n");
|
|
3195
|
+
await writeFile(outPath, markdown);
|
|
3196
|
+
report.reportPath = outPath;
|
|
3197
|
+
}
|
|
3198
|
+
return report;
|
|
3199
|
+
}
|
|
3200
|
+
case "eda_design_intake": {
|
|
3201
|
+
if (args.reset === true) designBrief = null;
|
|
3202
|
+
if (designBrief === null) {
|
|
3203
|
+
try {
|
|
3204
|
+
const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
|
|
3205
|
+
const stored = parseBriefBlock(project?.description);
|
|
3206
|
+
if (stored !== null) designBrief = stored;
|
|
3207
|
+
} catch {
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
const answers = args.answers !== void 0 && args.answers !== null ? args.answers : null;
|
|
3211
|
+
const answersReport = buildIntake(answers);
|
|
3212
|
+
const merged = { ...designBrief ?? {}, ...answersReport.brief };
|
|
3213
|
+
const mergedReport = buildIntake(merged);
|
|
3214
|
+
let persisted = false;
|
|
3215
|
+
if (Object.keys(merged).length > 0) {
|
|
3216
|
+
designBrief = merged;
|
|
3217
|
+
if (answers !== null) {
|
|
3218
|
+
try {
|
|
3219
|
+
const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
|
|
3220
|
+
const existing = typeof project?.description === "string" ? project.description : "";
|
|
3221
|
+
const index = existing.indexOf(BRIEF_TAG);
|
|
3222
|
+
const base = index < 0 ? existing : existing.slice(0, index).trimEnd();
|
|
3223
|
+
const next = (base === "" ? "" : base + "\n") + briefBlock(merged);
|
|
3224
|
+
await callExtension("dmt_Project.modifyProjectDescription", [next], timeoutMs);
|
|
3225
|
+
persisted = true;
|
|
3226
|
+
} catch {
|
|
3227
|
+
persisted = false;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return {
|
|
3232
|
+
ready: mergedReport.ready,
|
|
3233
|
+
questionCount: mergedReport.questionCount,
|
|
3234
|
+
criticalCount: mergedReport.criticalCount,
|
|
3235
|
+
answered: mergedReport.answered,
|
|
3236
|
+
missingCritical: mergedReport.missingCritical,
|
|
3237
|
+
unknownAnswerKeys: mergedReport.unknownAnswerKeys,
|
|
3238
|
+
brief: mergedReport.brief,
|
|
3239
|
+
hydratedFromProject: answers === null && designBrief !== null,
|
|
3240
|
+
persisted,
|
|
3241
|
+
sections: mergedReport.sections.filter((section) => section.questions.some((question) => mergedReport.brief[question.id] === void 0)),
|
|
3242
|
+
next: mergedReport.ready ? "All critical parameters are known: lay out the schematic, then eda_netlist_erc before routing." : "Ask the user for the missing critical parameters with ask_user_question, grouped by section, before placing parts. Do not invent values."
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
case "eda_netlist_erc": {
|
|
3246
|
+
const netlistData = await fetchParsedNetlist(timeoutMs);
|
|
3247
|
+
const report = runNetlistErc(netlistData);
|
|
3248
|
+
const listed = args.includeOk === true ? report.checks : report.checks.filter((check) => check.status !== "pass");
|
|
3249
|
+
return {
|
|
3250
|
+
ok: report.ok,
|
|
3251
|
+
counts: report.counts,
|
|
3252
|
+
componentCount: report.componentCount,
|
|
3253
|
+
netCount: report.netCount,
|
|
3254
|
+
checks: listed,
|
|
3255
|
+
hint: report.ok ? "No electrical failure found. This is a smoke test, not a simulation: run eda_run_drc and review the schematic as well." : "Fix the failing checks before routing; eda_run_drc on this build only returns aggregated counts, so this list is the actionable signal."
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
case "eda_pcb_canvas_origin": {
|
|
3259
|
+
const setTo = args.align === true ? [0, 0] : args.offsetX === void 0 && args.offsetY === void 0 ? null : [Number(args.offsetX ?? 0), Number(args.offsetY ?? 0)];
|
|
3260
|
+
let aligned = null;
|
|
3261
|
+
if (setTo !== null) aligned = await callExtension("pcb_Document.setCanvasOrigin", setTo, timeoutMs);
|
|
3262
|
+
const origin = await callExtension("pcb_Document.getCanvasOrigin", [], timeoutMs);
|
|
3263
|
+
const out = {
|
|
3264
|
+
canvasOrigin: origin,
|
|
3265
|
+
aligned,
|
|
3266
|
+
note: "JLCEDA displays canvas coordinates; APIs use data coordinates. align:true sets the offset to zero so both agree."
|
|
3267
|
+
};
|
|
3268
|
+
const point = args.point;
|
|
3269
|
+
if (point !== null && typeof point === "object") {
|
|
3270
|
+
const px = Number(point.x);
|
|
3271
|
+
const py = Number(point.y);
|
|
3272
|
+
if (Number.isFinite(px) && Number.isFinite(py)) {
|
|
3273
|
+
out.point = {
|
|
3274
|
+
input: { x: px, y: py },
|
|
3275
|
+
canvasToData: await callExtension("pcb_Document.convertCanvasOriginToDataOrigin", [px, py], timeoutMs),
|
|
3276
|
+
dataToCanvas: await callExtension("pcb_Document.convertDataOriginToCanvasOrigin", [px, py], timeoutMs)
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return out;
|
|
3281
|
+
}
|
|
3282
|
+
case "eda_capture": {
|
|
3283
|
+
if (args.cleanup === true) {
|
|
3284
|
+
const removed = [];
|
|
3285
|
+
const failed = [];
|
|
3286
|
+
for (const file of capturedFiles.splice(0, capturedFiles.length)) {
|
|
3287
|
+
try {
|
|
3288
|
+
await unlink(file);
|
|
3289
|
+
removed.push(file);
|
|
3290
|
+
} catch {
|
|
3291
|
+
failed.push(file);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return { ok: failed.length === 0, removed, failed, hint: "Capture files are scratch: delete them as soon as you have looked at the image." };
|
|
3295
|
+
}
|
|
3296
|
+
const captured = await callExtension("dmt_EditorControl.getCurrentRenderedAreaImage", [], toolTimeout(args, timeoutMs));
|
|
3297
|
+
const dataUrl = typeof captured === "string" ? captured : captured !== null && typeof captured === "object" && typeof captured.dataUrl === "string" ? captured.dataUrl : "";
|
|
3298
|
+
if (dataUrl === "") throw new Error("eda_capture: the app returned no image (is a document tab active?)");
|
|
3299
|
+
const base64 = dataUrl.includes(",") ? dataUrl.slice(dataUrl.indexOf(",") + 1) : dataUrl;
|
|
3300
|
+
const pngBytes = Buffer.from(base64, "base64");
|
|
3301
|
+
const capturePath = args.outPath === void 0 ? "" : String(args.outPath).trim();
|
|
3302
|
+
if (capturePath !== "") {
|
|
3303
|
+
await writeFile(capturePath, pngBytes);
|
|
3304
|
+
capturedFiles.push(capturePath);
|
|
3305
|
+
return {
|
|
3306
|
+
ok: true,
|
|
3307
|
+
written: capturePath,
|
|
3308
|
+
bytes: pngBytes.length,
|
|
3309
|
+
hint: "Read the file with the image reader, then call eda_capture({cleanup:true}) so the scratch PNG does not linger."
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
if (args.inline === true) return { ok: true, bytes: pngBytes.length, dataUrl };
|
|
3313
|
+
return { ok: true, bytes: pngBytes.length, dataUrlPrefix: dataUrl.slice(0, 32), hint: "Pass outPath to save a PNG, or inline:true to receive the data URL." };
|
|
3314
|
+
}
|
|
2364
3315
|
case "eda_workflow_guide":
|
|
2365
3316
|
return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
|
|
2366
3317
|
case "eda_create_board":
|
|
@@ -2368,10 +3319,15 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2368
3319
|
case "eda_api_call": {
|
|
2369
3320
|
if (!ALLOW_RAW_API) throw new Error("eda_api_call is disabled by settings (allowRawApi=false)");
|
|
2370
3321
|
const method = String(args.method ?? "");
|
|
2371
|
-
|
|
2372
|
-
|
|
3322
|
+
try {
|
|
3323
|
+
return await callExtension(method, Array.isArray(args.args) ? args.args : [], timeoutMs);
|
|
3324
|
+
} catch (failure) {
|
|
3325
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
3326
|
+
if (!/not allowed/i.test(message)) throw failure;
|
|
3327
|
+
const live = await extensionApiMethods(timeoutMs, true);
|
|
3328
|
+
const hint = live === void 0 ? "The connected extension rejected it and does not expose its list (older zip); eda_list_api_methods then shows the bridge copy, which may be narrower than reality." : "The extension enforces " + live.all.length + ' methods right now (eda_list_api_methods source:"extension").';
|
|
3329
|
+
throw new Error("API method not allowed: " + method + ". " + hint + " Wrapped helpers usually exist: eda_get_pins (pins), eda_place_net_labels (labels), eda_pcb_* (PCB primitives).");
|
|
2373
3330
|
}
|
|
2374
|
-
return await callExtension(method, Array.isArray(args.args) ? args.args : [], timeoutMs);
|
|
2375
3331
|
}
|
|
2376
3332
|
case "pcb_get_components": {
|
|
2377
3333
|
const layer = resolvePcbLayer(args.layer);
|
|
@@ -2698,23 +3654,44 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2698
3654
|
assertNetName(String(args.net), "eda_pcb_create_pour");
|
|
2699
3655
|
const fill = args.pourFillMethod === void 0 ? void 0 : String(args.pourFillMethod);
|
|
2700
3656
|
if (fill !== void 0 && fill !== "45grid" && fill !== "90grid" && fill !== "solid") throw new Error("eda_pcb_create_pour: pourFillMethod must be 45grid, 90grid or solid");
|
|
2701
|
-
|
|
3657
|
+
let polygonOk = false;
|
|
3658
|
+
try {
|
|
3659
|
+
const probe = await callExtension("pcb_MathPolygon.createPolygon", [buildPolygonSource(points)], timeoutMs);
|
|
3660
|
+
polygonOk = probe !== void 0 && probe !== null;
|
|
3661
|
+
} catch {
|
|
3662
|
+
polygonOk = false;
|
|
3663
|
+
}
|
|
3664
|
+
const attempt = async (fillMethod, preserveSilos) => callExtension("dsh.internal.pourCreate", [
|
|
2702
3665
|
String(args.net),
|
|
2703
3666
|
layer,
|
|
2704
3667
|
buildPolygonSource(points),
|
|
2705
|
-
|
|
2706
|
-
|
|
3668
|
+
fillMethod,
|
|
3669
|
+
preserveSilos,
|
|
2707
3670
|
args.pourName === void 0 ? void 0 : String(args.pourName),
|
|
2708
3671
|
args.pourPriority === void 0 ? void 0 : Number(args.pourPriority),
|
|
2709
3672
|
args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
|
|
2710
3673
|
args.locked === void 0 ? void 0 : Boolean(args.locked)
|
|
2711
3674
|
], timeoutMs);
|
|
3675
|
+
let raw;
|
|
3676
|
+
let retried = false;
|
|
3677
|
+
try {
|
|
3678
|
+
raw = await attempt(fill, args.preserveSilos === void 0 ? void 0 : Boolean(args.preserveSilos));
|
|
3679
|
+
} catch (firstFailure) {
|
|
3680
|
+
retried = true;
|
|
3681
|
+
try {
|
|
3682
|
+
raw = await attempt("solid", false);
|
|
3683
|
+
} catch (secondFailure) {
|
|
3684
|
+
const detail = secondFailure instanceof Error ? secondFailure.message : String(secondFailure);
|
|
3685
|
+
throw new Error("eda_pcb_create_pour failed twice (first: " + (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + "; retry: " + detail + "). polygonStage=" + (polygonOk ? "ok" : "failed") + ": " + (polygonOk ? "the app accepted the outline but refused the pour itself, so this is an application limit, not a coordinate problem. Pour by hand in the PCB editor (\u8986\u94DC \u2192 select GND \u2192 top + bottom); eda_design_audit reports it as a warn until then." : "the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].") + " retried=" + retried);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
2712
3688
|
return {
|
|
2713
3689
|
net: String(args.net),
|
|
2714
3690
|
layer,
|
|
2715
3691
|
layerId: layer,
|
|
2716
3692
|
pointCount: points.length / 2,
|
|
2717
3693
|
pour: summarizePrimitive(raw),
|
|
3694
|
+
retried,
|
|
2718
3695
|
rules: {
|
|
2719
3696
|
clearanceMil: { required: POUR_CLEARANCE_MIN_MIL, recommended: POUR_CLEARANCE_RECOMMENDED_MIL },
|
|
2720
3697
|
note: "The pour/clearance value lives in the board design rules, not in this call: set it in \u8BBE\u8BA1 \u2192 \u8BBE\u8BA1\u89C4\u5219 (recommended " + POUR_CLEARANCE_RECOMMENDED_MIL + " mil) and remove islands before exporting."
|