MustafaMulla29/stride-pedometer
This code defines schematic symbols and 3D footprints for various hardware components such as antennas, sensors, regulators, and connectors used in electronic circuit design.
- Version
- 1.0.4
- License
- unset
- Stars
- 0
scripts/export-manufacturing.ts
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { unzipSync, zipSync, strToU8, strFromU8 } from 'fflate'
const circuit = JSON.parse(readFileSync('dist/index/circuit.json', 'utf8')) as any[]
if (circuit.some(x => x.type.endsWith('_error'))) throw new Error('Resolve circuit errors before export.')
for (const via of circuit.filter(x => x.type === 'pcb_via')) {
if (via.from_layer !== 'top' || via.to_layer !== 'bottom' || via.layers.length !== 4) {
throw new Error('Run bun run build to normalize the physical through-via drill stack.')
}
}
for (const [format, name] of [
['gerbers', 'gerbers-engineering-review.zip'],
['kicad_zip', 'kicad-review.zip'],
['assembly-svg', 'assembly.svg'],
]) {
const result = spawnSync('bunx', ['tsci', 'export', 'dist/index/circuit.json',
'-f', format!, '-o', resolve('review', name!)], { stdio: 'inherit' })
if (result.status !== 0) throw new Error(`${format} export failed: ${result.error ?? result.status}`)
}
const path = 'review/gerbers-engineering-review.zip'
const files = unzipSync(readFileSync(path))
const drills = Object.keys(files).filter(x => x.endsWith('.drl'))
if (drills.length !== 1 || drills[0] !== 'drill-L1-L4.drl') {
throw new Error(`Unexpected drill stack: ${drills.join(', ')}`)
}
// Several routed branches may share one physical via. Drill it once.
const drilled = new Set<string>()
let tool = ''
files[drills[0]!] = strToU8(strFromU8(files[drills[0]!]!).split('\n').filter(line => {
if (/^T\d+$/.test(line)) tool = line
if (!/^X[-\d.]+Y[-\d.]+$/.test(line)) return true
const key = `${tool}:${line}`
if (drilled.has(key)) return false
drilled.add(key)
return true
}).join('\n'))
// The generic exporter includes every item. Substitute the reviewed population
// to exclude DNP parts, bare glass, and PCB contacts from machine assembly.
files['bom.csv'] = readFileSync('review/jlcpcb-bom.csv')
files['pick_and_place.csv'] = readFileSync('review/placement-review.csv')
files['ENGINEERING-REVIEW.txt'] = strToU8(
'Stride Rev A — engineering review, not a fabrication release.\n' +
'Four layers, 40 x 35 mm, 0.8 mm. Through vias only; filled/capped via-in-pad required.\n' +
'BOM and PnP contain 66 proposed PCBA placements. DS1 is hand-installed; C24/C25/C29/C30 are DNP.\n' +
'Coordinates are in mm about board center. Check all rotations with assembly artwork.\n' +
'Confirm stackup, RF width/tuning, lands, stencil and assembly acceptance before release.\n' +
'See docs/design.md and review/VALIDATION.md in the complete project.\n',
)
writeFileSync(path, zipSync(files))
// The KiCad converter emits both pcb_via records and trace-transition vias.
// Collapse co-located records, retain full through-via spans, and resolve nets
// from source connectivity (including manually placed vias attached to pins).
const nets = new Map(circuit.filter(x => x.type === 'source_net').map(x => [x.source_net_id, x]))
const connectivityNets = new Map([...nets.values()].map(n => [n.subcircuit_connectivity_map_key, n.name]))
const traces = new Map(circuit.filter(x => x.type === 'source_trace').map(x => [x.source_trace_id, x]))
const portNets = new Map<string, string>()
for (const trace of traces.values()) {
if (trace.connected_source_net_ids.length !== 1) continue
const name = nets.get(trace.connected_source_net_ids[0])?.name
if (name) for (const port of trace.connected_source_port_ids) portNets.set(port, name)
}
const viaKey = (x: number, y: number) => `${x.toFixed(6)},${y.toFixed(6)}`
const physicalVias = new Map<string, { net: string; size: number; drill: number }>()
for (const via of circuit.filter(x => x.type === 'pcb_via')) {
let net = connectivityNets.get(via.subcircuit_connectivity_map_key)
if (!net) {
const trace = traces.get(via.source_trace_id)
const names = new Set<string>([
...(trace?.connected_source_net_ids ?? []).map((id: string) => nets.get(id)?.name),
...(trace?.connected_source_port_ids ?? []).map((id: string) => portNets.get(id)),
].filter(Boolean))
if (names.size === 1) net = [...names][0]
}
if (!net) throw new Error(`Cannot resolve via net: ${via.pcb_via_id}`)
const key = viaKey(via.x, via.y)
const value = { net, size: via.outer_diameter, drill: via.hole_diameter }
const prior = physicalVias.get(key)
if (prior && JSON.stringify(prior) !== JSON.stringify(value)) throw new Error(`Conflicting vias at ${key}`)
physicalVias.set(key, value)
}
const kicadPath = 'review/kicad-review.zip'
const kicadFiles = unzipSync(readFileSync(kicadPath))
let pcb = strFromU8(kicadFiles['circuit.kicad_pcb']!)
const kicadNets = new Map([...pcb.matchAll(/\(net (\d+) "([^"]*)"\)/g)].map(m => [m[2], m[1]]))
const emitted = new Set<string>()
pcb = pcb.replace(/\(via\s[\s\S]*?\(uuid [^)]+\)\s*\)/g, block => {
const at = block.match(/\(at\s+([-\d.eE]+)\s+([-\d.eE]+)\)/)
if (!at) throw new Error('Cannot parse KiCad via position')
// This pinned converter translates board center to (100,100) and flips Y.
// Every converted location must match an actual via or export is rejected.
const key = viaKey(Number(at[1]) - 100, 100 - Number(at[2]))
const via = physicalVias.get(key)
if (!via) throw new Error(`Unexpected KiCad via at ${key}`)
if (emitted.has(key)) return ''
emitted.add(key)
const net = kicadNets.get(via.net)
if (!net || net === '0') throw new Error(`KiCad net missing: ${via.net}`)
return block.replace(/\(layers [^)]+\)/, '(layers F.Cu B.Cu)')
.replace(/\(net \d+\)/, `(net ${net})`)
.replace(/\(size [^)]+\)/, `(size ${via.size})`)
.replace(/\(drill [^)]+\)/, `(drill ${via.drill})`)
})
if (emitted.size !== physicalVias.size) throw new Error('KiCad export is missing physical vias')
kicadFiles['circuit.kicad_pcb'] = strToU8(pcb)
writeFileSync(kicadPath, zipSync(kicadFiles))
console.log(`Verified ${physicalVias.size} unique through vias, drill stack, and reviewed PCBA population.`)