/* Checks hardware/layout.js: geometry (on board, not on a cut, one lead per hole), footprints (nothing parked under the Pico or the DIP socket that should not be), and the electrical rules of the circuit (MIDI 1.0 3.3 V spec CA-033, TRS Type A jacks, Pico pin map). Usage: node hardware/check-layout.js [--nets] exit code 0 = clean. */ var L=require('./layout.js'); function rowName(r){ return r<26 ? String.fromCharCode(65+r) : 'A'+String.fromCharCode(65+r-26); } function pos(h){ return rowName(h[1])+(h[0]+1); } var key=function(h){ return h[0]+','+h[1]; }; var cut={}; L.cuts.forEach(function(h){ cut[key(h)]=1; }); var issues=[]; // --- copper: each uncut run of a strip is a segment; jumpers join segments --- function seg(h){ if(cut[key(h)]) return null; var a=h[0], b=h[0]; while(a>0 && !cut[key([a-1,h[1]])]) a--; while(b hole function use(h,what){ if(h[0]<0||h[1]<0||h[0]>=L.W||h[1]>=L.H) issues.push('off the board: '+what+' at '+pos(h)); if(cut[key(h)]) issues.push('on a cut: '+what+' at '+pos(h)); (occ[key(h)]=occ[key(h)]||[]).push(what); } L.parts.forEach(function(p){ p.pins.forEach(function(pin,i){ var nm = p.id==='pico' ? 'pico.'+pin.num+'.'+pin.n : p.id+'.'+(i+1); if(p.id==='U1') nm='U1.'+pin.n.split(' ')[0]; if(p.id==='D1') nm='D1.'+pin.n; use(pin.h,nm); ends[nm]=pin.h; }); }); L.jumpers.forEach(function(j,i){ use(j.a,'J'+(i+1)+'a'); use(j.b,'J'+(i+1)+'b'); var a=seg(j.a), b=seg(j.b); if(a&&b) join(a,b); }); // the Pico's GND pins are one plane inside the module var gnds=L.parts[0].pins.filter(function(p){ return p.n==='GND'; }).map(function(p){ return seg(p.h); }).filter(Boolean); for(var gi=1;gi1) issues.push('two leads in one hole '+pos(k.split(',').map(Number))+': '+occ[k].join(' & ')); }); function net(name){ var h=ends[name]; if(!h){ issues.push('rule names an unknown end: '+name); return '?'+name; } var s=seg(h); return s?find(s):'cut'; } function picoPin(sig){ var p=L.parts[0].pins.filter(function(x){ return x.n===sig; })[0]; return p?'pico.'+p.num+'.'+p.n:'?'; } function same(a,b,why){ if(net(a)!==net(b)) issues.push('not connected: '+a+' and '+b+' ('+why+')'); } function differ(a,b,why){ if(net(a)===net(b)) issues.push('shorted: '+a+' and '+b+' ('+why+')'); } function bridge(part,a,b,why){ var n1=net(part+'.1'), n2=net(part+'.2'), A=net(a), B=net(b); if(!((n1===A&&n2===B)||(n1===B&&n2===A))) issues.push(part+' should sit between '+a+' and '+b+' ('+why+')'); } // --- footprints: keep-out zones --- var pico=L.parts[0].pins, rows=pico.map(function(p){ return p.h[1]; }); var pr0=Math.min.apply(null,rows), pr1=Math.max.apply(null,rows), pc0=12, pc1=19; L.parts.forEach(function(p){ if(p.id==='pico') return; p.pins.forEach(function(pin){ if(pin.h[1]>=pr0-1 && pin.h[1]<=pr1+1 && pin.h[0]>=pc0 && pin.h[0]<=pc1) issues.push(p.id+' lead at '+pos(pin.h)+' is inside the Pico footprint'); }); }); var u=L.parts.filter(function(p){ return p.id==='U1'; })[0].pins.map(function(p){ return p.h; }); var uc0=Math.min.apply(null,u.map(function(h){return h[0];})), uc1=Math.max.apply(null,u.map(function(h){return h[0];})); var ur0=Math.min.apply(null,u.map(function(h){return h[1];})), ur1=Math.max.apply(null,u.map(function(h){return h[1];})); L.parts.forEach(function(p){ if(p.id==='U1'||p.id==='pico') return; p.pins.forEach(function(pin){ if(pin.h[0]>uc0 && pin.h[0]=ur0 && pin.h[1]<=ur1) issues.push(p.id+' lead at '+pos(pin.h)+' is under the DIP-8 socket'); }); }); // --- the circuit --- var VBUS=picoPin('VBUS'), V33=picoPin('3V3'), GND=picoPin('GND'), AGND=picoPin('AGND'); // rails differ(VBUS,V33,'5 V vs 3.3 V'); differ(VBUS,GND,'5 V vs GND'); differ(V33,GND,'3.3 V vs GND'); // MIDI IN: pin 4 -> 220R -> opto anode; pin 5 -> opto cathode; 1N4148 reverse across the LED bridge('R1','MIDI IN jack · ring (pin 4)','U1.2','MIDI IN current limit'); same('MIDI IN jack · tip (pin 5)','U1.3','MIDI IN pin 5 to opto cathode'); same('D1.K','U1.2','diode cathode on the LED anode'); same('D1.A','U1.3','diode anode on the LED cathode'); same('U1.8',VBUS,'6N137 VCC from 5 V'); same('U1.5',GND,'6N137 GND'); same('U1.6',picoPin('GP1'),'opto out to GP1 (UART0 RX)'); bridge('R2','U1.6',V33,'1k pull-up to 3.3 V'); same('U1.7',VBUS,'6N137 enable (VE) high, or the output stays off'); bridge('C1',VBUS,GND,'100 nF supply bypass, required for the 6N137'); // MIDI OUTs: TX -> 10R -> pin 5, 3V3 -> 33R -> pin 4, pin 2 -> GND bridge('R4',picoPin('GP0'),'OUT KIT jack · tip (pin 5)','10 Ω on TX (UART0)'); bridge('R5',V33,'OUT KIT jack · ring (pin 4)','33 Ω on 3.3 V'); same('OUT KIT jack · sleeve (pin 2)',GND,'shield'); bridge('R6',picoPin('GP4'),'OUT SYNTH jack · tip (pin 5)','10 Ω on TX (UART1)'); bridge('R7',V33,'OUT SYNTH jack · ring (pin 4)','33 Ω on 3.3 V'); same('OUT SYNTH jack · sleeve (pin 2)',GND,'shield'); // panel var SIG={'LCD · RST (pin 4)':'GP21','LCD · SCE (pin 3)':'GP17','LCD · D/C (pin 5)':'GP20','LCD · DN/MOSI (pin 6)':'GP19','LCD · SCLK (pin 7)':'GP18', 'Encoder · SIA':'GP10','Encoder · SIB':'GP11','Encoder · SW':'GP12','SEED · one lug':'GP13','RUN toggle · middle lug':'GP14', 'ENERGY pot · middle lug':'GP26','DRIFT pot · middle lug':'GP27', 'Matrix row 1 (KICK, KICK 2, SNARE)':'GP2','Matrix row 2 (HAT, HAT 16, PERC, GHOST)':'GP3','Matrix row 3 (BASS, MEL A, MEL B, PAD)':'GP5', 'Matrix column 1 (KICK, HAT, BASS)':'GP6','Matrix column 2 (KICK 2, HAT 16, MEL A)':'GP7','Matrix column 3 (SNARE, PERC, MEL B)':'GP8','Matrix column 4 (GHOST, PAD)':'GP9'}; Object.keys(SIG).forEach(function(w){ same(w,picoPin(SIG[w]),'pin map'); }); ['LCD · VCC (pin 1)','LCD · LED (pin 8) through 330 Ω','Encoder · VCC','Pots · right lugs (chain both)'].forEach(function(w){ same(w,V33,'3.3 V supply'); }); ['LCD · GND (pin 2)','Encoder · GND','Panel ground bus · SEED other lug, RUN one outer lug'].forEach(function(w){ same(w,GND,'ground'); }); same('Pots · left lugs (chain both)',AGND,'pots on analog ground'); // every signal net is distinct from every other var sigs=Object.keys(SIG).map(function(w){ return picoPin(SIG[w]); }).concat([picoPin('GP0'),picoPin('GP1'),picoPin('GP4')]); for(var i=0;i=0){ var nets={}; Object.keys(ends).forEach(function(n){ if(/^P\d+$/.test(n)) return; (nets[net(n)]=nets[net(n)]||[]).push(n+' @'+pos(ends[n])); }); Object.keys(nets).sort().forEach(function(k){ console.log(k); nets[k].forEach(function(x){ console.log(' '+x); }); }); } console.log(L.parts.length+' parts, '+L.jumpers.length+' links, '+L.wires.length+' wire pads, '+L.cuts.length+' cut holes'); if(issues.length){ console.log(issues.length+' issue(s):'); issues.forEach(function(x){ console.log(' - '+x); }); process.exit(1); } console.log('layout clean');