From 05a33d00ec6196f0d3466db4141c85d68c5a6500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1n=20D=C3=A9nes?= Date: Sun, 12 Jul 2020 23:23:30 +0200 Subject: [PATCH] PCB progress --- README.md | 10 +- src/cli.js | 10 +- src/footprints/index.js | 4 + src/footprints/mx.js | 33 ++++++ src/footprints/promicro.js | 88 +++++++++++++++ src/pcb.js | 223 +++++++++++++++++++++++++++++++++++-- test/fixtures/absolem.yaml | 11 +- 7 files changed, 367 insertions(+), 12 deletions(-) create mode 100644 src/footprints/index.js create mode 100644 src/footprints/mx.js create mode 100644 src/footprints/promicro.js diff --git a/README.md b/README.md index c3c8900..8c7475d 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,12 @@ Everything should be ready for a handwire, but if you'd like the design to be mo To help you get started, the necessary footprints and an edge cut can be automatically positioned so that all you need to do manually is the routing. Footprints can be specified at the key-level (under the `points` section, like we discussed above), or here with manually given anchors. -The only difference between the two footprint types is that an omitted `ref` in the anchor means the current key for key-level declarations, while here it defaults to `[0, 0]`. +The differences between the two footprint types are: + +- an omitted `ref` in the anchor means the current key for key-level declarations, while here it defaults to `[0, 0]` +- a parameter starting with an exclamation point is an indirect reference to an eponymous key-level attribute -- so, for example, `from = !col_wire` would mean that the key's `col_wire` attribute is read there. + +Another alternative to `anchor` here (here being under the `pcb` declaration) is to use the `between` key and place the footprint at the average of multiple anchors -- mostly useful for anchoring to the center, by averaging a key and its mirror. Additionally, the edge cut of the PCB can be specified using a previously defined outline name under the `edge` key. ```yaml @@ -581,6 +586,9 @@ pcb: footprints: - type: anchor: + between: + - + - ... params: - ... ``` diff --git a/src/cli.js b/src/cli.js index 80af408..31459b8 100644 --- a/src/cli.js +++ b/src/cli.js @@ -36,8 +36,14 @@ const args = yargs describe: 'Debug mode', type: 'boolean' }) + .option('clean', { + default: false, + describe: 'Clean output dir before parsing', + type: 'boolean' + }) .argv +if (args.clean) fs.removeSync(args.o) fs.mkdirpSync(args.o) const config_parser = args.c.endsWith('.yaml') ? yaml.load : JSON.parse @@ -72,7 +78,9 @@ for (const [name, outline] of Object.entries(outlines)) { console.log('Scaffolding PCB...') const pcb = pcb_lib.parse(config.pcb, points, outlines) -fs.writeFileSync(path.join(args.o, `pcb/pcb.kicad_pcb`, pcb)) +const pcb_file = path.join(args.o, `pcb/pcb.kicad_pcb`) +fs.mkdirpSync(path.dirname(pcb_file)) +fs.writeFileSync(pcb_file, pcb) // goodbye diff --git a/src/footprints/index.js b/src/footprints/index.js new file mode 100644 index 0000000..0874e71 --- /dev/null +++ b/src/footprints/index.js @@ -0,0 +1,4 @@ +module.exports = { + mx: require('./mx'), + promicro: require('./promicro') +} \ No newline at end of file diff --git a/src/footprints/mx.js b/src/footprints/mx.js new file mode 100644 index 0000000..6b0a7a8 --- /dev/null +++ b/src/footprints/mx.js @@ -0,0 +1,33 @@ +module.exports = { + params: ['from', 'to'], + body: ` + + (module MX (layer F.Cu) (tedit 5DD4F656) + + ${''/* parametric position */} + __AT + + ${''/* corner marks */} + (fp_line (start -7 -6) (end -7 -7) (layer F.SilkS) (width 0.15)) + (fp_line (start -7 7) (end -6 7) (layer F.SilkS) (width 0.15)) + (fp_line (start -6 -7) (end -7 -7) (layer F.SilkS) (width 0.15)) + (fp_line (start -7 7) (end -7 6) (layer F.SilkS) (width 0.15)) + (fp_line (start 7 6) (end 7 7) (layer F.SilkS) (width 0.15)) + (fp_line (start 7 -7) (end 6 -7) (layer F.SilkS) (width 0.15)) + (fp_line (start 6 7) (end 7 7) (layer F.SilkS) (width 0.15)) + (fp_line (start 7 -7) (end 7 -6) (layer F.SilkS) (width 0.15)) + + ${''/* pins */} + (pad 1 thru_hole circle (at 2.54 -5.08) (size 2.286 2.286) (drill 1.4986) (layers *.Cu *.Mask) __PARAM_FROM) + (pad 2 thru_hole circle (at -3.81 -2.54) (size 2.286 2.286) (drill 1.4986) (layers *.Cu *.Mask) __PARAM_TO) + + ${''/* middle shaft */} + (pad "" np_thru_hole circle (at 0 0) (size 3.9878 3.9878) (drill 3.9878) (layers *.Cu *.Mask)) + + ${''/* stabilizers */} + (pad "" np_thru_hole circle (at 5.08 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) + (pad "" np_thru_hole circle (at -5.08 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) + ) + + ` +} \ No newline at end of file diff --git a/src/footprints/promicro.js b/src/footprints/promicro.js new file mode 100644 index 0000000..51f70fe --- /dev/null +++ b/src/footprints/promicro.js @@ -0,0 +1,88 @@ +module.exports = { + nets: [ + 'RAW', 'GND', 'RST', 'VCC', + 'P21', 'P20', 'P19', 'P18', + 'P15', 'P14', 'P16', 'P10', + 'P1', 'P0', 'P2', 'P3', 'P4', + 'P5', 'P6', 'P7', 'P8', 'P9' + ], + body: ` + + (module ProMicro (layer F.Cu) (tedit 5B307E4C) + + ${''/* parametric position */} + __AT + + ${''/* illustration of the USB port overhang */} + (fp_line (start -19.304 -3.556) (end -14.224 -3.556) (layer Dwgs.User) (width 0.2)) + (fp_line (start -19.304 3.81) (end -19.304 -3.556) (layer Dwgs.User) (width 0.2)) + (fp_line (start -14.224 3.81) (end -19.304 3.81) (layer Dwgs.User) (width 0.2)) + (fp_line (start -14.224 -3.556) (end -14.224 3.81) (layer Dwgs.User) (width 0.2)) + + ${''/* component outline */} + (fp_line (start -17.78 8.89) (end 15.24 8.89) (layer F.SilkS) (width 0.381)) + (fp_line (start 15.24 8.89) (end 15.24 -8.89) (layer F.SilkS) (width 0.381)) + (fp_line (start 15.24 -8.89) (end -17.78 -8.89) (layer F.SilkS) (width 0.381)) + (fp_line (start -17.78 -8.89) (end -17.78 8.89) (layer F.SilkS) (width 0.381)) + + ${''/* extra border around "RAW", in case the rectangular shape is not distinctive enough */} + (fp_line (start -15.24 6.35) (end -12.7 6.35) (layer F.SilkS) (width 0.381)) + (fp_line (start -15.24 6.35) (end -15.24 8.89) (layer F.SilkS) (width 0.381)) + (fp_line (start -12.7 6.35) (end -12.7 8.89) (layer F.SilkS) (width 0.381)) + + ${''/* pin names */} + (fp_text user RAW (at -13.97 5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user GND (at -11.43 5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user RST (at -8.89 5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user VCC (at -6.35 5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 21 (at -3.81 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 20 (at -1.27 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 19 (at 1.27 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 18 (at 3.81 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 15 (at 6.35 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 14 (at 8.89 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 16 (at 11.43 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 10 (at 13.97 5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + + (fp_text user 1 (at -13.97 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 0 (at -11.43 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user GND (at -8.89 -5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user GND (at -6.35 -5.0) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 2 (at -3.81 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 3 (at -1.27 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 4 (at 1.27 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 5 (at 3.81 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 6 (at 6.35 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 7 (at 8.89 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 8 (at 11.43 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + (fp_text user 9 (at 13.97 -5.461) (layer F.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)))) + + ${''/* and now the actual pins */} + (pad 1 thru_hole rect (at -13.97 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_RAW) + (pad 2 thru_hole circle (at -11.43 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_GND) + (pad 3 thru_hole circle (at -8.89 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_RST) + (pad 4 thru_hole circle (at -6.35 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_VCC) + (pad 5 thru_hole circle (at -3.81 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P21) + (pad 6 thru_hole circle (at -1.27 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P20) + (pad 7 thru_hole circle (at 1.27 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P19) + (pad 8 thru_hole circle (at 3.81 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P18) + (pad 9 thru_hole circle (at 6.35 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P15) + (pad 10 thru_hole circle (at 8.89 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P14) + (pad 11 thru_hole circle (at 11.43 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P16) + (pad 12 thru_hole circle (at 13.97 7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P10) + + (pad 13 thru_hole circle (at -13.97 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P1) + (pad 14 thru_hole circle (at -11.43 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P0) + (pad 15 thru_hole circle (at -8.89 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_GND) + (pad 16 thru_hole circle (at -6.35 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_GND) + (pad 17 thru_hole circle (at -3.81 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P2) + (pad 18 thru_hole circle (at -1.27 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P3) + (pad 19 thru_hole circle (at 1.27 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P4) + (pad 20 thru_hole circle (at 3.81 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P5) + (pad 21 thru_hole circle (at 6.35 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P6) + (pad 22 thru_hole circle (at 8.89 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P7) + (pad 23 thru_hole circle (at 11.43 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P8) + (pad 24 thru_hole circle (at 13.97 -7.62 0) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) __NET_P9) + ) + ` +} diff --git a/src/pcb.js b/src/pcb.js index 7e4981b..4f8da57 100644 --- a/src/pcb.js +++ b/src/pcb.js @@ -2,7 +2,119 @@ const m = require('makerjs') const u = require('./utils') const a = require('./assert') -const makerjs2kicad = exports._makerjs2kicad = model => { +const Point = require('./point') + +const kicad_prefix = ` +(kicad_pcb (version 20171130) (host pcbnew 5.1.6) + + (page A3) + (title_block + (title KEYBOARD_NAME_HERE) + (rev VERSION_HERE) + (company YOUR_NAME_HERE) + ) + + (general + (thickness 1.6) + ) + + (layers + (0 F.Cu signal) + (31 B.Cu signal) + (32 B.Adhes user) + (33 F.Adhes user) + (34 B.Paste user) + (35 F.Paste user) + (36 B.SilkS user) + (37 F.SilkS user) + (38 B.Mask user) + (39 F.Mask user) + (40 Dwgs.User user) + (41 Cmts.User user) + (42 Eco1.User user) + (43 Eco2.User user) + (44 Edge.Cuts user) + (45 Margin user) + (46 B.CrtYd user) + (47 F.CrtYd user) + (48 B.Fab user) + (49 F.Fab user) + ) + + (setup + (last_trace_width 0.25) + (trace_clearance 0.2) + (zone_clearance 0.508) + (zone_45_only no) + (trace_min 0.2) + (via_size 0.8) + (via_drill 0.4) + (via_min_size 0.4) + (via_min_drill 0.3) + (uvia_size 0.3) + (uvia_drill 0.1) + (uvias_allowed no) + (uvia_min_size 0.2) + (uvia_min_drill 0.1) + (edge_width 0.05) + (segment_width 0.2) + (pcb_text_width 0.3) + (pcb_text_size 1.5 1.5) + (mod_edge_width 0.12) + (mod_text_size 1 1) + (mod_text_width 0.15) + (pad_size 1.524 1.524) + (pad_drill 0.762) + (pad_to_mask_clearance 0.05) + (aux_axis_origin 0 0) + (visible_elements FFFFFF7F) + (pcbplotparams + (layerselection 0x010fc_ffffffff) + (usegerberextensions false) + (usegerberattributes true) + (usegerberadvancedattributes true) + (creategerberjobfile true) + (excludeedgelayer true) + (linewidth 0.100000) + (plotframeref false) + (viasonmask false) + (mode 1) + (useauxorigin false) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (psnegative false) + (psa4output false) + (plotreference true) + (plotvalue true) + (plotinvisibletext false) + (padsonsilk false) + (subtractmaskfromsilk false) + (outputformat 1) + (mirror false) + (drillshape 1) + (scaleselection 1) + (outputdirectory "")) + ) +` + +const kicad_suffix = ` +) +` + +const kicad_netclass = ` + (net_class Default "This is the default net class." + (clearance 0.2) + (trace_width 0.25) + (via_dia 0.8) + (via_drill 0.4) + (uvia_dia 0.3) + (uvia_drill 0.1) + __ADD_NET + ) +` + +const makerjs2kicad = exports._makerjs2kicad = (model, layer='Edge.Cuts') => { const grs = [] const xy = val => `${val[0]} ${val[1]}` m.model.walk(model, { @@ -10,16 +122,14 @@ const makerjs2kicad = exports._makerjs2kicad = model => { const p = wp.pathContext switch (p.type) { case 'line': - grs.push(`(gr_line (start ${xy(p.origin)}) (end ${xy(p.end)}) (angle 90) (layer Edge.Cuts) (width 0.15))`) + grs.push(`(gr_line (start ${xy(p.origin)}) (end ${xy(p.end)}) (angle 90) (layer ${layer}) (width 0.15))`) break case 'arc': - // console.log(require('util').inspect(p, false, 200)) - // throw 2 const center = p.origin - const angle_start = Math.min(p.startAngle, p.endAngle) - const angle_diff = Math.abs(p.endAngle - p.startAngle) + const angle_start = p.startAngle > p.endAngle ? p.startAngle - 360 : p.startAngle + const angle_diff = Math.abs(p.endAngle - angle_start) const end = m.point.rotate(m.point.add(center, [p.radius, 0]), angle_start, center) - grs.push(`(gr_arc (start ${xy(center)}) (end ${xy(end)}) (angle ${angle_diff}) (layer Edge.Cuts) (width 0.15))`) + grs.push(`(gr_arc (start ${xy(center)}) (end ${xy(end)}) (angle ${angle_diff}) (layer ${layer}) (width 0.15))`) break case 'circle': break @@ -31,12 +141,107 @@ const makerjs2kicad = exports._makerjs2kicad = model => { return grs } +const footprint_types = require('./footprints') +const footprint = exports._footprint = (config, name, points, net_indexer, default_anchor) => { + + // config sanitization + a.detect_unexpected(config, name, ['type', 'anchor', 'between', 'params']) + const type = a.in(config.type, `${name}.type`, Object.keys(footprint_types)) + let anchor = a.anchor(config.anchor, `${name}.anchor`, points, true, default_anchor) + const params = a.sane(config.params, `${name}.params`, 'object') + + // averaging multiple anchors, if necessary + if (config.between) { + const between = a.sane(config.between, `${name}.between`, 'array') + let x = 0, y = 0, r = 0, bi = 0 + const len = between.length + for (const b of between) { + ba = a.anchor(b, `${name}.between[${++bi}]`, points, true) + x += ba.x + y += ba.y + r += ba.r + } + anchor = new Point(x / len, y / len, r / len) + } + + // basic setup + const fp = footprint_types[type] + let result = fp.body + + // footprint positioning + const at = `(at ${anchor.x} ${anchor.y} ${anchor.r})` + result = result.replace('__AT', at) + + // connecting static nets + for (const net of (fp.nets || [])) { + const index = net_indexer(net) + result = result.replace('__NET_' + net.toUpperCase(), `(net ${index} "${net}")`) + } + + // connecting parametric nets + for (const param of (fp.params || [])) { + const net = params[param] + a.sane(net, `${name}.params.${param}`, 'string') + const index = net_indexer(net) + result = result.replace('__PARAM_' + net.toUpperCase(), `(net ${index} "${net}")`) + } + + return result +} + exports.parse = (config, points, outlines) => { + + // config sanitization a.detect_unexpected(config, 'pcb', ['edge', 'footprints']) const edge = outlines[config.edge] if (!edge) throw new Error(`Field "pcb.edge" doesn't name a valid outline!`) + + // Edge.Cuts conversion const kicad_edge = makerjs2kicad(edge) - console.log(kicad_edge.join('\n')) - throw 28 + // making a global net index registry + const nets = {} + const net_indexer = net => { + if (nets[net]) return nets[net] + const index = Object.keys(nets).length + return nets[net] = index + } + + const footprints = [] + + // key-level footprints + for (const [pname, point] of Object.entries(points)) { + let f_index = 0 + for (const f of (point.meta.footprints || [])) { + footprints.push(footprint(f, `${pname}.footprints[${++f_index}]`, points, net_indexer, point)) + } + } + + // global one-off footprints + const global_footprints = a.sane(config.footprints || [], 'pcb.footprints', 'array') + let gf_index = 0 + for (const gf of global_footprints) { + footprints.push(footprint(gf, `pcb.footprints[${++gf_index}]`, points, net_indexer)) + } + + // finalizing nets + const nets_arr = [] + const add_nets_arr = [] + for (const [net, index] of Object.entries(nets)) { + nets_arr.push(`(net ${index} "${net}")`) + add_nets_arr.push(`(add_net "${net}")`) + } + + const netclass = kicad_netclass.replace('__ADD_NET', add_nets_arr.join('\n')) + const nets_text = nets_arr.join('\n') + const footprint_text = footprints.join('\n') + return ` + + ${kicad_prefix} + ${nets_text} + ${netclass} + ${footprint_text} + ${kicad_suffix} + + ` } \ No newline at end of file diff --git a/test/fixtures/absolem.yaml b/test/fixtures/absolem.yaml index af42ec8..8282f5a 100644 --- a/test/fixtures/absolem.yaml +++ b/test/fixtures/absolem.yaml @@ -54,6 +54,10 @@ points: home: bind: [10] top: + key: + footprints: + - type: mx + thumbfan: anchor: ref: inner_bottom @@ -152,4 +156,9 @@ outline: name: middle operation: stack pcb: - edge: outline \ No newline at end of file + edge: outline + footprints: + - type: promicro + between: + - ref: inner_top + - ref: mirror_inner_top