@intelligentelectron/universal-netlist 0.0.12
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 +121 -0
- package/LICENSE +190 -0
- package/NOTICE +4 -0
- package/README.md +246 -0
- package/dist/circuit-traversal.d.ts +73 -0
- package/dist/circuit-traversal.d.ts.map +1 -0
- package/dist/circuit-traversal.js +299 -0
- package/dist/circuit-traversal.js.map +1 -0
- package/dist/cli/commands.d.ts +23 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +140 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/prompts.d.ts +10 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +22 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/cli/shell.d.ts +15 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +66 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/updater.d.ts +46 -0
- package/dist/cli/updater.d.ts.map +1 -0
- package/dist/cli/updater.js +319 -0
- package/dist/cli/updater.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/altium/connectivity.d.ts +32 -0
- package/dist/parsers/altium/connectivity.d.ts.map +1 -0
- package/dist/parsers/altium/connectivity.js +308 -0
- package/dist/parsers/altium/connectivity.js.map +1 -0
- package/dist/parsers/altium/discovery.d.ts +30 -0
- package/dist/parsers/altium/discovery.d.ts.map +1 -0
- package/dist/parsers/altium/discovery.js +174 -0
- package/dist/parsers/altium/discovery.js.map +1 -0
- package/dist/parsers/altium/hierarchy.d.ts +29 -0
- package/dist/parsers/altium/hierarchy.d.ts.map +1 -0
- package/dist/parsers/altium/hierarchy.js +94 -0
- package/dist/parsers/altium/hierarchy.js.map +1 -0
- package/dist/parsers/altium/index.d.ts +53 -0
- package/dist/parsers/altium/index.d.ts.map +1 -0
- package/dist/parsers/altium/index.js +404 -0
- package/dist/parsers/altium/index.js.map +1 -0
- package/dist/parsers/altium/net-extractor.d.ts +24 -0
- package/dist/parsers/altium/net-extractor.d.ts.map +1 -0
- package/dist/parsers/altium/net-extractor.js +295 -0
- package/dist/parsers/altium/net-extractor.js.map +1 -0
- package/dist/parsers/altium/ole-reader.d.ts +91 -0
- package/dist/parsers/altium/ole-reader.d.ts.map +1 -0
- package/dist/parsers/altium/ole-reader.js +304 -0
- package/dist/parsers/altium/ole-reader.js.map +1 -0
- package/dist/parsers/altium/record-parser.d.ts +21 -0
- package/dist/parsers/altium/record-parser.d.ts.map +1 -0
- package/dist/parsers/altium/record-parser.js +117 -0
- package/dist/parsers/altium/record-parser.js.map +1 -0
- package/dist/parsers/altium/schemas.d.ts +277 -0
- package/dist/parsers/altium/schemas.d.ts.map +1 -0
- package/dist/parsers/altium/schemas.js +246 -0
- package/dist/parsers/altium/schemas.js.map +1 -0
- package/dist/parsers/altium/types.d.ts +213 -0
- package/dist/parsers/altium/types.d.ts.map +1 -0
- package/dist/parsers/altium/types.js +180 -0
- package/dist/parsers/altium/types.js.map +1 -0
- package/dist/parsers/cadence/discovery.d.ts +45 -0
- package/dist/parsers/cadence/discovery.d.ts.map +1 -0
- package/dist/parsers/cadence/discovery.js +277 -0
- package/dist/parsers/cadence/discovery.js.map +1 -0
- package/dist/parsers/cadence/index.d.ts +41 -0
- package/dist/parsers/cadence/index.d.ts.map +1 -0
- package/dist/parsers/cadence/index.js +139 -0
- package/dist/parsers/cadence/index.js.map +1 -0
- package/dist/parsers/cadence/pstchip-parser.d.ts +23 -0
- package/dist/parsers/cadence/pstchip-parser.d.ts.map +1 -0
- package/dist/parsers/cadence/pstchip-parser.js +82 -0
- package/dist/parsers/cadence/pstchip-parser.js.map +1 -0
- package/dist/parsers/cadence/pstxnet-parser.d.ts +15 -0
- package/dist/parsers/cadence/pstxnet-parser.d.ts.map +1 -0
- package/dist/parsers/cadence/pstxnet-parser.js +55 -0
- package/dist/parsers/cadence/pstxnet-parser.js.map +1 -0
- package/dist/parsers/cadence/pstxprt-parser.d.ts +24 -0
- package/dist/parsers/cadence/pstxprt-parser.d.ts.map +1 -0
- package/dist/parsers/cadence/pstxprt-parser.js +75 -0
- package/dist/parsers/cadence/pstxprt-parser.js.map +1 -0
- package/dist/parsers/index.d.ts +33 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +49 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +277 -0
- package/dist/server.js.map +1 -0
- package/dist/service.d.ts +129 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +759 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +242 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/package.json +74 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlist Service
|
|
3
|
+
*
|
|
4
|
+
* Query methods for Cadence and Altium netlists using absolute paths.
|
|
5
|
+
* All methods take an absolute path to the design FILE as input.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { discoverDesigns, findHandler, parseDesign } from "./parsers/index.js";
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Path Normalization
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a file path to use native separators.
|
|
17
|
+
* This ensures paths work correctly regardless of whether forward or
|
|
18
|
+
* backward slashes are provided (important for cross-platform compatibility).
|
|
19
|
+
*
|
|
20
|
+
* On Windows, path.normalize() converts / to \
|
|
21
|
+
* On Unix, we must manually convert \ to / since path.normalize() doesn't
|
|
22
|
+
* (backslash is a valid filename character on Unix, but agents often send
|
|
23
|
+
* Windows-style paths regardless of platform).
|
|
24
|
+
*
|
|
25
|
+
* Examples:
|
|
26
|
+
* Windows: "C:/Users/foo/bar" -> "C:\\Users\\foo\\bar"
|
|
27
|
+
* Unix: "\\Users\\foo\\bar" -> "/Users/foo/bar"
|
|
28
|
+
*/
|
|
29
|
+
const normalizePath = (inputPath) => {
|
|
30
|
+
if (process.platform === "win32") {
|
|
31
|
+
return path.normalize(inputPath);
|
|
32
|
+
}
|
|
33
|
+
// On Unix, convert backslashes to forward slashes before normalizing
|
|
34
|
+
return path.normalize(inputPath.replace(/\\/g, "/"));
|
|
35
|
+
};
|
|
36
|
+
import { naturalSort, traverseCircuitFromNet, computeCircuitHash, isDnsComponent, matchesRefdesType, getRefdesPrefix, isValidRefdes, isGroundNet, } from "./circuit-traversal.js";
|
|
37
|
+
import { compactArray, getPinNet, isErrorResult, } from "./types.js";
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Design Loading
|
|
40
|
+
// =============================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Normalize unconnected pins to "NC" (No Connect).
|
|
43
|
+
*/
|
|
44
|
+
const normalizeUnconnectedPins = (netlist) => {
|
|
45
|
+
for (const component of Object.values(netlist.components)) {
|
|
46
|
+
for (const [pin, net] of Object.entries(component.pins)) {
|
|
47
|
+
if (typeof net === "string") {
|
|
48
|
+
if (net === "") {
|
|
49
|
+
component.pins[pin] = "NC";
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (net?.net === "") {
|
|
54
|
+
net.net = "NC";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Load netlist from an absolute design file path.
|
|
61
|
+
* Delegates to the appropriate handler based on file extension.
|
|
62
|
+
*/
|
|
63
|
+
export const loadNetlist = async (designPath) => {
|
|
64
|
+
const normalizedPath = normalizePath(designPath);
|
|
65
|
+
const handler = findHandler(normalizedPath);
|
|
66
|
+
if (!handler) {
|
|
67
|
+
const ext = path.extname(normalizedPath);
|
|
68
|
+
return {
|
|
69
|
+
error: `Unsupported design file format '${ext}'. Supported: .dsn, .cpm (Cadence), .PrjPcb (Altium)`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parsed = await parseDesign(normalizedPath);
|
|
74
|
+
normalizeUnconnectedPins(parsed);
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
79
|
+
return { error: message };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Component Grouping
|
|
84
|
+
// =============================================================================
|
|
85
|
+
const MPN_MISSING_NOTE = "MPN not found in exported netlist data. Tell user to update symbol properties in library, or to point you to the BOM";
|
|
86
|
+
/**
|
|
87
|
+
* Group components by MPN for compact output.
|
|
88
|
+
*/
|
|
89
|
+
const groupComponentsByMpn = (entries, includeDns) => {
|
|
90
|
+
const groups = new Map();
|
|
91
|
+
for (const [refdes, component] of entries) {
|
|
92
|
+
const dns = isDnsComponent(component);
|
|
93
|
+
if (!includeDns && dns) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const mpnTrimmed = component.mpn?.trim() || null;
|
|
97
|
+
const descriptionValue = component.description?.trim() || undefined;
|
|
98
|
+
const commentValue = component.comment?.trim() || undefined;
|
|
99
|
+
const valueValue = component.value?.trim() || undefined;
|
|
100
|
+
const keyBase = mpnTrimmed ? `mpn:${mpnTrimmed}` : `refdes:${refdes}`;
|
|
101
|
+
const groupKey = `${keyBase}||dns:${dns ? "1" : "0"}`;
|
|
102
|
+
if (!groups.has(groupKey)) {
|
|
103
|
+
groups.set(groupKey, {
|
|
104
|
+
mpn: mpnTrimmed,
|
|
105
|
+
description: descriptionValue,
|
|
106
|
+
comment: commentValue,
|
|
107
|
+
value: valueValue,
|
|
108
|
+
dns: dns || undefined,
|
|
109
|
+
notes: mpnTrimmed ? undefined : [MPN_MISSING_NOTE],
|
|
110
|
+
refdes: [],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else if (valueValue && !groups.get(groupKey).value) {
|
|
114
|
+
groups.get(groupKey).value = valueValue;
|
|
115
|
+
}
|
|
116
|
+
groups.get(groupKey).refdes.push(refdes);
|
|
117
|
+
}
|
|
118
|
+
return Array.from(groups.values())
|
|
119
|
+
.map((group) => {
|
|
120
|
+
const entry = {
|
|
121
|
+
mpn: group.mpn,
|
|
122
|
+
count: group.refdes.length,
|
|
123
|
+
refdes: compactArray(group.refdes.sort(naturalSort)),
|
|
124
|
+
};
|
|
125
|
+
if (group.description !== undefined) {
|
|
126
|
+
entry.description = group.description;
|
|
127
|
+
}
|
|
128
|
+
if (group.comment !== undefined) {
|
|
129
|
+
entry.comment = group.comment;
|
|
130
|
+
}
|
|
131
|
+
if (group.value !== undefined) {
|
|
132
|
+
entry.value = group.value;
|
|
133
|
+
}
|
|
134
|
+
if (group.dns !== undefined) {
|
|
135
|
+
entry.dns = group.dns;
|
|
136
|
+
}
|
|
137
|
+
if (group.notes !== undefined) {
|
|
138
|
+
entry.notes = group.notes;
|
|
139
|
+
}
|
|
140
|
+
return entry;
|
|
141
|
+
})
|
|
142
|
+
.sort((a, b) => (a.mpn ?? "").localeCompare(b.mpn ?? ""));
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Aggregate circuit components by MPN for compact output.
|
|
146
|
+
*/
|
|
147
|
+
const aggregateCircuitByMpn = (components) => {
|
|
148
|
+
const groups = new Map();
|
|
149
|
+
const unaggregatable = [];
|
|
150
|
+
for (const comp of components) {
|
|
151
|
+
const mpn = comp.mpn?.trim() || null;
|
|
152
|
+
const description = comp.description?.trim() || "";
|
|
153
|
+
const value = comp.value?.trim() || undefined;
|
|
154
|
+
const dnsFlag = comp.dns ? true : undefined;
|
|
155
|
+
let aggregationKey;
|
|
156
|
+
if (mpn) {
|
|
157
|
+
aggregationKey = `mpn:${mpn}`;
|
|
158
|
+
}
|
|
159
|
+
else if (description) {
|
|
160
|
+
aggregationKey = `desc:${description}`;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
unaggregatable.push(comp);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const nets = comp.connections.map((p) => p.net);
|
|
167
|
+
const netPair = [...nets].sort().join("|");
|
|
168
|
+
const groupKey = `${aggregationKey}||${netPair}||dns:${dnsFlag ? "1" : "0"}`;
|
|
169
|
+
if (!groups.has(groupKey)) {
|
|
170
|
+
groups.set(groupKey, {
|
|
171
|
+
mpn,
|
|
172
|
+
description: description || undefined,
|
|
173
|
+
comment: comp.comment,
|
|
174
|
+
value,
|
|
175
|
+
dns: dnsFlag,
|
|
176
|
+
notes: mpn ? undefined : [MPN_MISSING_NOTE],
|
|
177
|
+
orientations: new Map(),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else if (value && !groups.get(groupKey).value) {
|
|
181
|
+
groups.get(groupKey).value = value;
|
|
182
|
+
}
|
|
183
|
+
const orientationKey = comp.connections
|
|
184
|
+
.map((p) => `${p.pins.join(",")}:${p.net}`)
|
|
185
|
+
.join("|");
|
|
186
|
+
const group = groups.get(groupKey);
|
|
187
|
+
if (!group.orientations.has(orientationKey)) {
|
|
188
|
+
group.orientations.set(orientationKey, {
|
|
189
|
+
count: 0,
|
|
190
|
+
refdes: [],
|
|
191
|
+
connections: comp.connections,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const orientation = group.orientations.get(orientationKey);
|
|
195
|
+
orientation.count++;
|
|
196
|
+
if (comp.refdes) {
|
|
197
|
+
orientation.refdes.push(comp.refdes);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const compactConnections = (connections) => connections.map((c) => ({ net: c.net, pins: compactArray(c.pins) }));
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const group of groups.values()) {
|
|
203
|
+
const orientationsList = Array.from(group.orientations.values()).sort((a, b) => b.count - a.count);
|
|
204
|
+
const totalCount = orientationsList.reduce((sum, o) => sum + o.count, 0);
|
|
205
|
+
const aggregated = {
|
|
206
|
+
mpn: group.mpn,
|
|
207
|
+
total_count: totalCount,
|
|
208
|
+
};
|
|
209
|
+
if (group.description !== undefined) {
|
|
210
|
+
aggregated.description = group.description;
|
|
211
|
+
}
|
|
212
|
+
if (group.comment !== undefined) {
|
|
213
|
+
aggregated.comment = group.comment;
|
|
214
|
+
}
|
|
215
|
+
if (group.value !== undefined) {
|
|
216
|
+
aggregated.value = group.value;
|
|
217
|
+
}
|
|
218
|
+
if (group.dns !== undefined) {
|
|
219
|
+
aggregated.dns = group.dns;
|
|
220
|
+
}
|
|
221
|
+
if (group.notes !== undefined) {
|
|
222
|
+
aggregated.notes = group.notes;
|
|
223
|
+
}
|
|
224
|
+
if (orientationsList.length === 1) {
|
|
225
|
+
aggregated.refdes = compactArray(orientationsList[0].refdes.sort(naturalSort));
|
|
226
|
+
aggregated.connections = compactConnections(orientationsList[0].connections);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
aggregated.orientations = orientationsList.map((o) => ({
|
|
230
|
+
count: o.count,
|
|
231
|
+
refdes: compactArray(o.refdes.sort(naturalSort)),
|
|
232
|
+
connections: compactConnections(o.connections),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
result.push(aggregated);
|
|
236
|
+
}
|
|
237
|
+
for (const comp of unaggregatable) {
|
|
238
|
+
const unagg = {
|
|
239
|
+
refdes: comp.refdes,
|
|
240
|
+
mpn: null,
|
|
241
|
+
notes: [MPN_MISSING_NOTE],
|
|
242
|
+
total_count: 1,
|
|
243
|
+
connections: compactConnections(comp.connections),
|
|
244
|
+
};
|
|
245
|
+
if (comp.description !== undefined) {
|
|
246
|
+
unagg.description = comp.description;
|
|
247
|
+
}
|
|
248
|
+
if (comp.comment !== undefined) {
|
|
249
|
+
unagg.comment = comp.comment;
|
|
250
|
+
}
|
|
251
|
+
if (comp.value !== undefined) {
|
|
252
|
+
unagg.value = comp.value;
|
|
253
|
+
}
|
|
254
|
+
if (comp.dns) {
|
|
255
|
+
unagg.dns = true;
|
|
256
|
+
}
|
|
257
|
+
result.push(unagg);
|
|
258
|
+
}
|
|
259
|
+
return result.sort((a, b) => b.total_count - a.total_count);
|
|
260
|
+
};
|
|
261
|
+
// =============================================================================
|
|
262
|
+
// Public API
|
|
263
|
+
// =============================================================================
|
|
264
|
+
/**
|
|
265
|
+
* List all designs in a directory.
|
|
266
|
+
*
|
|
267
|
+
* @param searchPath - Absolute path to search (defaults to CWD)
|
|
268
|
+
* @param pattern - Regex pattern to filter design names
|
|
269
|
+
*/
|
|
270
|
+
export const listDesigns = async (searchPath, pattern = ".*") => {
|
|
271
|
+
const resolvedPath = normalizePath(searchPath ?? process.cwd());
|
|
272
|
+
let regex;
|
|
273
|
+
try {
|
|
274
|
+
regex = new RegExp(pattern);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return { error: `Invalid regex pattern '${pattern}'` };
|
|
278
|
+
}
|
|
279
|
+
const designs = await discoverDesigns(resolvedPath);
|
|
280
|
+
return designs
|
|
281
|
+
.filter((design) => regex.test(design.name))
|
|
282
|
+
.map((design) => ({
|
|
283
|
+
name: design.name,
|
|
284
|
+
path: design.sourcePath,
|
|
285
|
+
error: design.error,
|
|
286
|
+
}));
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* List components of a specific type in a design.
|
|
290
|
+
*
|
|
291
|
+
* @param design - Absolute path to design file
|
|
292
|
+
* @param type - Component type prefix (e.g., "U", "R", "C")
|
|
293
|
+
* @param includeDns - Include DNS (Do Not Stuff) components
|
|
294
|
+
*/
|
|
295
|
+
export const listComponents = async (design, type, includeDns = false) => {
|
|
296
|
+
const netlist = await loadNetlist(design);
|
|
297
|
+
if (isErrorResult(netlist)) {
|
|
298
|
+
return netlist;
|
|
299
|
+
}
|
|
300
|
+
const prefix = type.trim().toUpperCase();
|
|
301
|
+
if (!prefix) {
|
|
302
|
+
return { error: "Missing required parameter: type" };
|
|
303
|
+
}
|
|
304
|
+
const entries = Object.entries(netlist.components).filter(([refdes]) => matchesRefdesType(refdes, prefix));
|
|
305
|
+
if (entries.length === 0) {
|
|
306
|
+
const availablePrefixes = Array.from(new Set(Object.keys(netlist.components)
|
|
307
|
+
.filter(isValidRefdes)
|
|
308
|
+
.map(getRefdesPrefix))).sort((a, b) => a.localeCompare(b));
|
|
309
|
+
const designName = path.basename(design, path.extname(design));
|
|
310
|
+
return {
|
|
311
|
+
error: `No components with prefix '${prefix}' found in design '${designName}'. Available prefixes: [${availablePrefixes.join(", ")}]`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
components: groupComponentsByMpn(entries, includeDns),
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* List all nets within a design.
|
|
320
|
+
*
|
|
321
|
+
* @param design - Absolute path to design file
|
|
322
|
+
*/
|
|
323
|
+
export const listNets = async (design) => {
|
|
324
|
+
const netlist = await loadNetlist(design);
|
|
325
|
+
if (isErrorResult(netlist)) {
|
|
326
|
+
return netlist;
|
|
327
|
+
}
|
|
328
|
+
const nets = Object.keys(netlist.nets).sort((a, b) => a.localeCompare(b));
|
|
329
|
+
return { nets };
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Search nets by regex pattern.
|
|
333
|
+
*
|
|
334
|
+
* @param pattern - Regex pattern
|
|
335
|
+
* @param design - Absolute path to design file
|
|
336
|
+
*/
|
|
337
|
+
export const searchNets = async (pattern, design) => {
|
|
338
|
+
let regex;
|
|
339
|
+
try {
|
|
340
|
+
regex = new RegExp(pattern);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return { error: `Invalid regex pattern '${pattern}'` };
|
|
344
|
+
}
|
|
345
|
+
const netlist = await loadNetlist(design);
|
|
346
|
+
if (isErrorResult(netlist)) {
|
|
347
|
+
return netlist;
|
|
348
|
+
}
|
|
349
|
+
const designName = path.basename(design, path.extname(design));
|
|
350
|
+
const nets = Object.keys(netlist.nets).filter((net) => regex.test(net));
|
|
351
|
+
const sorted = nets.sort((a, b) => a.localeCompare(b));
|
|
352
|
+
if (sorted.length === 0) {
|
|
353
|
+
return {
|
|
354
|
+
results: { [designName]: [] },
|
|
355
|
+
notes: [`No nets matched pattern '${pattern}'`],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return { results: { [designName]: sorted } };
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Search components by refdes pattern.
|
|
362
|
+
*
|
|
363
|
+
* @param pattern - Regex pattern
|
|
364
|
+
* @param design - Absolute path to design file
|
|
365
|
+
* @param includeDns - Include DNS components
|
|
366
|
+
*/
|
|
367
|
+
export const searchComponentsByRefdes = async (pattern, design, includeDns = false) => {
|
|
368
|
+
// TODO: Support (?i) inline flag for case-insensitive matching
|
|
369
|
+
let regex;
|
|
370
|
+
try {
|
|
371
|
+
regex = new RegExp(pattern, "i");
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return { error: `Invalid regex pattern '${pattern}'` };
|
|
375
|
+
}
|
|
376
|
+
const netlist = await loadNetlist(design);
|
|
377
|
+
if (isErrorResult(netlist)) {
|
|
378
|
+
return netlist;
|
|
379
|
+
}
|
|
380
|
+
const designName = path.basename(design, path.extname(design));
|
|
381
|
+
const entries = Object.entries(netlist.components).filter(([refdes]) => regex.test(refdes));
|
|
382
|
+
const grouped = groupComponentsByMpn(entries, includeDns);
|
|
383
|
+
if (grouped.length === 0) {
|
|
384
|
+
return {
|
|
385
|
+
results: { [designName]: [] },
|
|
386
|
+
notes: [`No components matched refdes pattern '${pattern}'`],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return { results: { [designName]: grouped } };
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Search components by MPN pattern.
|
|
393
|
+
*
|
|
394
|
+
* @param pattern - Regex pattern
|
|
395
|
+
* @param design - Absolute path to design file
|
|
396
|
+
* @param includeDns - Include DNS components
|
|
397
|
+
*/
|
|
398
|
+
export const searchComponentsByMpn = async (pattern, design, includeDns = false) => {
|
|
399
|
+
// TODO: Support (?i) inline flag for case-insensitive matching
|
|
400
|
+
let regex;
|
|
401
|
+
try {
|
|
402
|
+
regex = new RegExp(pattern, "i");
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return { error: `Invalid regex pattern '${pattern}'` };
|
|
406
|
+
}
|
|
407
|
+
const netlist = await loadNetlist(design);
|
|
408
|
+
if (isErrorResult(netlist)) {
|
|
409
|
+
return netlist;
|
|
410
|
+
}
|
|
411
|
+
const designName = path.basename(design, path.extname(design));
|
|
412
|
+
const allComponents = Object.entries(netlist.components);
|
|
413
|
+
const componentsWithMpn = allComponents.filter(([, c]) => c.mpn?.trim());
|
|
414
|
+
const entries = componentsWithMpn.filter(([, component]) => regex.test(component.mpn));
|
|
415
|
+
const grouped = groupComponentsByMpn(entries, includeDns);
|
|
416
|
+
// Case 1: No MPN data exists at all
|
|
417
|
+
if (componentsWithMpn.length === 0) {
|
|
418
|
+
return {
|
|
419
|
+
results: { [designName]: [] },
|
|
420
|
+
notes: [
|
|
421
|
+
"This netlist has no MPN data. Ask user for BOM or schematic PDF",
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Case 2: MPN data exists but pattern didn't match
|
|
426
|
+
if (grouped.length === 0) {
|
|
427
|
+
return {
|
|
428
|
+
results: { [designName]: [] },
|
|
429
|
+
notes: [
|
|
430
|
+
`No components matched pattern '${pattern}'. Try a broader pattern or use search_components_by_refdes instead`,
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return { results: { [designName]: grouped } };
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Search components by description pattern.
|
|
438
|
+
*
|
|
439
|
+
* @param pattern - Regex pattern
|
|
440
|
+
* @param design - Absolute path to design file
|
|
441
|
+
* @param includeDns - Include DNS components
|
|
442
|
+
*/
|
|
443
|
+
export const searchComponentsByDescription = async (pattern, design, includeDns = false) => {
|
|
444
|
+
// TODO: Support (?i) inline flag for case-insensitive matching
|
|
445
|
+
let regex;
|
|
446
|
+
try {
|
|
447
|
+
regex = new RegExp(pattern, "i");
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return { error: `Invalid regex pattern '${pattern}'` };
|
|
451
|
+
}
|
|
452
|
+
const netlist = await loadNetlist(design);
|
|
453
|
+
if (isErrorResult(netlist)) {
|
|
454
|
+
return netlist;
|
|
455
|
+
}
|
|
456
|
+
const designName = path.basename(design, path.extname(design));
|
|
457
|
+
const allComponents = Object.entries(netlist.components);
|
|
458
|
+
const componentsWithDescription = allComponents.filter(([, c]) => c.description?.trim());
|
|
459
|
+
const entries = componentsWithDescription.filter(([, component]) => regex.test(component.description));
|
|
460
|
+
const grouped = groupComponentsByMpn(entries, includeDns);
|
|
461
|
+
// Case 1: No description data exists at all
|
|
462
|
+
if (componentsWithDescription.length === 0) {
|
|
463
|
+
return {
|
|
464
|
+
results: { [designName]: [] },
|
|
465
|
+
notes: [
|
|
466
|
+
"This netlist has no description data. Ask user for BOM or schematic PDF",
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// Case 2: Description data exists but pattern didn't match
|
|
471
|
+
if (grouped.length === 0) {
|
|
472
|
+
return {
|
|
473
|
+
results: { [designName]: [] },
|
|
474
|
+
notes: [
|
|
475
|
+
`No components matched pattern '${pattern}'. Try a broader pattern or use search_components_by_refdes instead`,
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return { results: { [designName]: grouped } };
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Query component details by reference designator.
|
|
483
|
+
*
|
|
484
|
+
* @param design - Absolute path to design file
|
|
485
|
+
* @param refdes - Component reference designator
|
|
486
|
+
*/
|
|
487
|
+
export const queryComponent = async (design, refdes) => {
|
|
488
|
+
const netlist = await loadNetlist(design);
|
|
489
|
+
if (isErrorResult(netlist)) {
|
|
490
|
+
return netlist;
|
|
491
|
+
}
|
|
492
|
+
const targetRefdes = refdes.trim();
|
|
493
|
+
const componentEntry = Object.entries(netlist.components).find(([key]) => key.toLowerCase() === targetRefdes.toLowerCase());
|
|
494
|
+
if (!componentEntry) {
|
|
495
|
+
const designName = path.basename(design, path.extname(design));
|
|
496
|
+
return {
|
|
497
|
+
error: `Component '${refdes}' not found in design '${designName}'. Use list_components() or search_components_by_refdes() to find available components.`,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const [resolvedRefdes, component] = componentEntry;
|
|
501
|
+
const mpn = component.mpn?.trim() || null;
|
|
502
|
+
const dns = isDnsComponent(component);
|
|
503
|
+
const result = {
|
|
504
|
+
refdes: resolvedRefdes,
|
|
505
|
+
mpn,
|
|
506
|
+
pins: component.pins,
|
|
507
|
+
};
|
|
508
|
+
if (component.description !== undefined) {
|
|
509
|
+
result.description = component.description;
|
|
510
|
+
}
|
|
511
|
+
if (component.comment !== undefined) {
|
|
512
|
+
result.comment = component.comment;
|
|
513
|
+
}
|
|
514
|
+
if (component.value !== undefined) {
|
|
515
|
+
result.value = component.value;
|
|
516
|
+
}
|
|
517
|
+
if (dns) {
|
|
518
|
+
result.dns = true;
|
|
519
|
+
}
|
|
520
|
+
if (!mpn) {
|
|
521
|
+
result.notes = [MPN_MISSING_NOTE];
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Query circuit starting from a net name.
|
|
527
|
+
*
|
|
528
|
+
* @param design - Absolute path to design file
|
|
529
|
+
* @param netName - Net name
|
|
530
|
+
* @param skipTypes - Component types to skip
|
|
531
|
+
* @param includeDns - Include DNS components
|
|
532
|
+
*/
|
|
533
|
+
export const queryXnetByNetName = async (design, netName, skipTypes = [], includeDns = false) => {
|
|
534
|
+
const netlist = await loadNetlist(design);
|
|
535
|
+
if (isErrorResult(netlist)) {
|
|
536
|
+
return netlist;
|
|
537
|
+
}
|
|
538
|
+
const { nets, components } = netlist;
|
|
539
|
+
if (!nets[netName]) {
|
|
540
|
+
const designName = path.basename(design, path.extname(design));
|
|
541
|
+
return {
|
|
542
|
+
error: `Net '${netName}' not found in design '${designName}'. Use search_nets() to find available nets.`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (isGroundNet(netName)) {
|
|
546
|
+
return {
|
|
547
|
+
error: `${netName} is a ground net and cannot be queried.`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const traversal = traverseCircuitFromNet(netName, nets, components, {
|
|
551
|
+
skipTypes,
|
|
552
|
+
includeDns,
|
|
553
|
+
});
|
|
554
|
+
const circuitHash = computeCircuitHash(traversal.components);
|
|
555
|
+
const aggregated = aggregateCircuitByMpn(traversal.components);
|
|
556
|
+
const response = {
|
|
557
|
+
starting_point: netName,
|
|
558
|
+
total_components: traversal.components.length,
|
|
559
|
+
unique_configurations: aggregated.length,
|
|
560
|
+
components_by_mpn: aggregated,
|
|
561
|
+
visited_nets: traversal.visited_nets,
|
|
562
|
+
circuit_hash: circuitHash,
|
|
563
|
+
};
|
|
564
|
+
if (Object.keys(traversal.skipped).length > 0) {
|
|
565
|
+
response.skipped = traversal.skipped;
|
|
566
|
+
}
|
|
567
|
+
return response;
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Query circuit starting from a component pin.
|
|
571
|
+
*
|
|
572
|
+
* @param design - Absolute path to design file
|
|
573
|
+
* @param pinSpec - Pin specification in "REFDES.PIN" format
|
|
574
|
+
* @param skipTypes - Component types to skip
|
|
575
|
+
* @param includeDns - Include DNS components
|
|
576
|
+
*/
|
|
577
|
+
export const queryXnetByPinName = async (design, pinSpec, skipTypes = [], includeDns = false) => {
|
|
578
|
+
const netlist = await loadNetlist(design);
|
|
579
|
+
if (isErrorResult(netlist)) {
|
|
580
|
+
return netlist;
|
|
581
|
+
}
|
|
582
|
+
const parts = pinSpec.split(".");
|
|
583
|
+
if (parts.length !== 2) {
|
|
584
|
+
return {
|
|
585
|
+
error: `Invalid pin name '${pinSpec}'. Expected 'REFDES.PIN'.`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const [refdesInput, pinInput] = parts;
|
|
589
|
+
const refdesEntry = Object.entries(netlist.components).find(([refdes]) => refdes.toLowerCase() === refdesInput.trim().toLowerCase());
|
|
590
|
+
if (!refdesEntry) {
|
|
591
|
+
const designName = path.basename(design, path.extname(design));
|
|
592
|
+
return {
|
|
593
|
+
error: `Component '${refdesInput}' not found in design '${designName}'. Use list_components() or search_components_by_refdes() to find available components.`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const [resolvedRefdes, component] = refdesEntry;
|
|
597
|
+
const pinKey = Object.keys(component.pins).find((pin) => pin.toLowerCase() === pinInput.trim().toLowerCase());
|
|
598
|
+
if (!pinKey) {
|
|
599
|
+
const pins = Object.keys(component.pins).sort(naturalSort);
|
|
600
|
+
return {
|
|
601
|
+
error: `Pin '${pinSpec}' not found. Component ${resolvedRefdes} has pins: [${pins.join(", ")}]`,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const connectedNet = getPinNet(component.pins[pinKey]);
|
|
605
|
+
if (isGroundNet(connectedNet)) {
|
|
606
|
+
return {
|
|
607
|
+
error: `Pin ${resolvedRefdes}.${pinKey} is connected to ${connectedNet} (ground) and cannot be queried.`,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (connectedNet === "NC") {
|
|
611
|
+
return {
|
|
612
|
+
starting_point: `${resolvedRefdes}.${pinKey}`,
|
|
613
|
+
net: "NC",
|
|
614
|
+
total_components: 0,
|
|
615
|
+
unique_configurations: 0,
|
|
616
|
+
components_by_mpn: [],
|
|
617
|
+
visited_nets: ["NC"],
|
|
618
|
+
circuit_hash: `nc-${resolvedRefdes}.${pinKey}`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const { nets, components } = netlist;
|
|
622
|
+
const traversal = traverseCircuitFromNet(connectedNet, nets, components, {
|
|
623
|
+
skipTypes,
|
|
624
|
+
includeDns,
|
|
625
|
+
});
|
|
626
|
+
const circuitHash = computeCircuitHash(traversal.components);
|
|
627
|
+
const aggregated = aggregateCircuitByMpn(traversal.components);
|
|
628
|
+
const response = {
|
|
629
|
+
starting_point: `${resolvedRefdes}.${pinKey}`,
|
|
630
|
+
net: connectedNet,
|
|
631
|
+
total_components: traversal.components.length,
|
|
632
|
+
unique_configurations: aggregated.length,
|
|
633
|
+
components_by_mpn: aggregated,
|
|
634
|
+
visited_nets: traversal.visited_nets,
|
|
635
|
+
circuit_hash: circuitHash,
|
|
636
|
+
};
|
|
637
|
+
if (Object.keys(traversal.skipped).length > 0) {
|
|
638
|
+
response.skipped = traversal.skipped;
|
|
639
|
+
}
|
|
640
|
+
return response;
|
|
641
|
+
};
|
|
642
|
+
// =============================================================================
|
|
643
|
+
// Cadence Netlist Export (Windows Only)
|
|
644
|
+
// =============================================================================
|
|
645
|
+
const execAsync = promisify(exec);
|
|
646
|
+
/**
|
|
647
|
+
* Convert Windows path to bash-compatible path for GitBash/WSL compatibility.
|
|
648
|
+
* Example: C:\foo\bar -> /c/foo/bar
|
|
649
|
+
*/
|
|
650
|
+
const toBashPath = (winPath) => winPath
|
|
651
|
+
.replace(/\\/g, "/")
|
|
652
|
+
.replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`);
|
|
653
|
+
/**
|
|
654
|
+
* Detect installed Cadence SPB versions from the standard installation directory.
|
|
655
|
+
*
|
|
656
|
+
* @param cadenceBase - Base Cadence installation directory (default: C:/Cadence)
|
|
657
|
+
* @returns Array of detected Cadence installations, sorted by version descending
|
|
658
|
+
*/
|
|
659
|
+
export const detectCadenceVersions = async (cadenceBase = "C:/Cadence") => {
|
|
660
|
+
const installs = [];
|
|
661
|
+
try {
|
|
662
|
+
const entries = await fs.promises.readdir(cadenceBase);
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
const match = entry.match(/^SPB_(\d+\.\d+)$/);
|
|
665
|
+
if (!match)
|
|
666
|
+
continue;
|
|
667
|
+
const version = match[1];
|
|
668
|
+
const root = path.join(cadenceBase, entry);
|
|
669
|
+
const pstswp = path.join(root, "tools", "bin", "pstswp.exe");
|
|
670
|
+
const config = path.join(root, "tools", "capture", "allegro.cfg");
|
|
671
|
+
// Verify the executables exist
|
|
672
|
+
if (fs.existsSync(pstswp) && fs.existsSync(config)) {
|
|
673
|
+
installs.push({ version, root, pstswp, config });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Sort by version descending (latest first)
|
|
677
|
+
installs.sort((a, b) => parseFloat(b.version) - parseFloat(a.version));
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
// Cadence directory doesn't exist or isn't accessible
|
|
681
|
+
}
|
|
682
|
+
return installs;
|
|
683
|
+
};
|
|
684
|
+
/**
|
|
685
|
+
* Get the latest installed Cadence version.
|
|
686
|
+
*
|
|
687
|
+
* @returns The latest Cadence installation, or null if none found
|
|
688
|
+
*/
|
|
689
|
+
export const getLatestCadence = async () => {
|
|
690
|
+
const versions = await detectCadenceVersions();
|
|
691
|
+
return versions[0] ?? null;
|
|
692
|
+
};
|
|
693
|
+
/**
|
|
694
|
+
* Export Cadence schematic netlist to Allegro PCB format.
|
|
695
|
+
* Uses the pstswp utility from Cadence SPB installation.
|
|
696
|
+
*
|
|
697
|
+
* @param dsnPath - Absolute path to .DSN schematic file
|
|
698
|
+
* @returns Export result with output directory and generated files, or error
|
|
699
|
+
*/
|
|
700
|
+
export const exportCadenceNetlist = async (dsnPath) => {
|
|
701
|
+
// Platform check
|
|
702
|
+
if (process.platform !== "win32") {
|
|
703
|
+
return {
|
|
704
|
+
error: "Cadence export tools are only available on Windows. The pstswp utility requires a Windows environment with Cadence SPB installed. Manual export: Open Cadence, then: Tools → Create Netlist → PCB Editor format.",
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
// Find Cadence installation
|
|
708
|
+
const cadence = await getLatestCadence();
|
|
709
|
+
if (!cadence) {
|
|
710
|
+
return {
|
|
711
|
+
error: "No Cadence SPB installation found in C:/Cadence. Ensure Cadence Design Entry CIS or HDL is installed. Manual export: Open Cadence, then: Tools → Create Netlist → PCB Editor format.",
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const dsnDir = path.dirname(dsnPath);
|
|
715
|
+
const dsnFile = path.basename(dsnPath);
|
|
716
|
+
const outputDir = path.join(dsnDir, "Allegro");
|
|
717
|
+
// Convert to bash paths for command execution (GitBash compatibility)
|
|
718
|
+
const bashDsnDir = toBashPath(dsnDir);
|
|
719
|
+
const pstswp = toBashPath(cadence.pstswp);
|
|
720
|
+
const config = toBashPath(cadence.config);
|
|
721
|
+
const command = `cd "${bashDsnDir}" && "${pstswp}" -pst -d "${dsnFile}" -n "Allegro" -c "${config}" -v 3 -l 255 -j "PCB Footprint"`;
|
|
722
|
+
try {
|
|
723
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
724
|
+
shell: "bash",
|
|
725
|
+
timeout: 120000,
|
|
726
|
+
});
|
|
727
|
+
// List generated files
|
|
728
|
+
let generatedFiles;
|
|
729
|
+
try {
|
|
730
|
+
const files = await fs.promises.readdir(outputDir);
|
|
731
|
+
generatedFiles = files.sort();
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
// Output directory may not exist if export failed silently
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
success: true,
|
|
738
|
+
outputDir,
|
|
739
|
+
log: (stdout + stderr).trim() || undefined,
|
|
740
|
+
cadenceVersion: cadence.version,
|
|
741
|
+
generatedFiles,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
const execError = err;
|
|
746
|
+
return {
|
|
747
|
+
error: `Cadence pstswp failed: ${execError.message ?? "Unknown error"}`,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
// =============================================================================
|
|
752
|
+
// Test Exports
|
|
753
|
+
// =============================================================================
|
|
754
|
+
/**
|
|
755
|
+
* Internal exports for testing purposes only.
|
|
756
|
+
* @internal
|
|
757
|
+
*/
|
|
758
|
+
export { MPN_MISSING_NOTE, groupComponentsByMpn, aggregateCircuitByMpn };
|
|
759
|
+
//# sourceMappingURL=service.js.map
|