Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
Some checks failed
check / check (push) Has been cancelled

First commit of the repo. Contents beyond the v0.1 simulator and layout:

- Full Pico 2 W firmware (firmware/app, drivers, engine port, host test).
- BANK page and 404 mode A/B removed; learned pads play exactly what was learned.
- 404/MiniFreak renamed to KIT/SYNTH throughout (jacks, code, sim, docs,
  layout labels; wiring rebuilt).
- ROLL returns to page select after a re-roll; a freshly learned track turns on.
- LCD contrast 66.
- ROOT and SCALE pages: 21 scales, AUTO = the seed picks; global and sticky.

Intended music change: the scale table grew from 8 to 21 and AUTO picks from
all of them, so every seed's scale changed. Vectors regenerated with
npm run vectors, plus new key-override cases. Flash record is now version 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:39:36 +03:00
commit 2196633ed5
77 changed files with 52207 additions and 0 deletions

93
hardware/check-layout.js Normal file
View File

@@ -0,0 +1,93 @@
/* 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<L.W-1 && !cut[key([b+1,h[1]])]) b++; return 'r'+h[1]+':'+a+'-'+b; }
var par={}; function find(x){ while(par[x]!==undefined && par[x]!==x) x=par[x]; return x; }
function join(a,b){ a=find(a); b=find(b); if(a!==b) par[a]=b; }
// --- every lead and wire end, by name ---
var occ={}, ends={}; // ends: name -> 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;gi<gnds.length;gi++) join(gnds[0],gnds[gi]);
L.wires.forEach(function(w,i){ var nm='P'+(i+1)+' '+w.to; use(w.h,nm); ends['P'+(i+1)]=w.h; ends[w.to]=w.h; });
Object.keys(occ).forEach(function(k){ if(occ[k].length>1) 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]<uc1 && pin.h[1]>=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<sigs.length;i++) for(var j=i+1;j<sigs.length;j++) if(net(sigs[i])===net(sigs[j])) issues.push('shorted: '+sigs[i]+' and '+sigs[j]);
sigs.forEach(function(s){ [VBUS,V33,GND].forEach(function(r){ if(net(s)===net(r)) issues.push('shorted to a rail: '+s+' and '+r); }); });
if(process.argv.indexOf('--nets')>=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');