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');

84
hardware/layout.js Normal file
View File

@@ -0,0 +1,84 @@
/* Stripboard layout for Seedbox v0.1. Holes are [col,row], 0-based, seen from the COMPONENT side.
Copper strips run left to right along each row.
The Pico's pin 1 and 40 sit on the first strip (A), so its USB end is flush with the board edge. */
var LAYOUT=(function(){
var W=34, H=28;
var cuts=[];
function cut(c,r0,r1){ for(var r=r0;r<=r1;r++) cuts.push([c,r]); }
cut(15,0,19); // under the Pico: separates its left and right pin rows
cut(5,20,24); // opto: left pins / right pins, IN4 strip
cut(13,20,23); // opto area / MIDI OUT area (strip Y stays whole: it carries GP4)
cut(24,20,22); // MIDI OUT A / MIDI OUT B
cut(22,23,23); // strip X: GP0 side / KIT tip side, R4 lies across this cut
var LP=['GP0','GP1','GND','GP2','GP3','GP4','GP5','GND','GP6','GP7','GP8','GP9','GND','GP10','GP11','GP12','GP13','GND','GP14','GP15'];
var RP=['VBUS','VSYS','GND','3V3_EN','3V3','ADC_VREF','GP28','AGND','GP27','GP26','RUN','GP22','GND','GP21','GP20','GP19','GP18','GND','GP17','GP16'];
var pico={id:'pico',kind:'pico',pins:[]};
for(var i=0;i<20;i++){ pico.pins.push({h:[12,i],n:LP[i],num:i+1}); pico.pins.push({h:[19,i],n:RP[i],num:40-i}); }
var parts=[pico,
{id:'U1',kind:'dip8',label:'6N137',pins:[{h:[4,20],n:'1 NC'},{h:[4,21],n:'2 A'},{h:[4,22],n:'3 K'},{h:[4,23],n:'4 NC'},{h:[7,23],n:'5 GND'},{h:[7,22],n:'6 VO'},{h:[7,21],n:'7 VE'},{h:[7,20],n:'8 VCC'}]},
{id:'R1',kind:'res',label:'220 Ω',pins:[{h:[1,21]},{h:[1,24]}]},
{id:'D1',kind:'diode',label:'1N4148',pins:[{h:[3,21],n:'K'},{h:[3,22],n:'A'}]},
{id:'R2',kind:'res',label:'1 kΩ',pins:[{h:[9,22]},{h:[9,27]}]},
{id:'C1',kind:'cap',label:'100 nF',pins:[{h:[10,20]},{h:[10,23]}]},
{id:'R4',kind:'res',label:'10 Ω',pins:[{h:[20,23]},{h:[24,23]}]},
{id:'R5',kind:'res',label:'33 Ω ½ W',pins:[{h:[18,21]},{h:[18,27]}]},
{id:'R6',kind:'res',label:'10 Ω',pins:[{h:[27,20]},{h:[27,24]}]},
{id:'R7',kind:'res',label:'33 Ω ½ W',pins:[{h:[29,21]},{h:[29,27]}]}
];
var jumpers=[
{a:[33,0],b:[33,25],n:'5 V (VBUS) to the 5 V strip'},
{a:[31,4],b:[31,27],n:'3V3 to the 3V3 strip'},
{a:[32,17],b:[32,26],n:'GND to the GND strip'},
{a:[12,20],b:[12,25],n:'opto VCC to 5 V'},
{a:[9,20],b:[9,21],n:'opto enable (pin 7) to VCC (pin 8): the 6N137 output only works with pin 7 high'},
{a:[11,23],b:[11,26],n:'opto GND to GND'},
{a:[8,22],b:[8,1],n:'opto out to GP1 (MIDI RX), strip B'},
{a:[14,0],b:[14,23],n:'GP0 (TX KIT) to strip X (R4), straight down under the Pico between its headers'},
{a:[13,5],b:[13,24],n:'GP4 (TX SYNTH) to strip Y (R6), straight down under the Pico between its headers'}
];
/* Off-board wires. col = spool colour role. to = where the other end goes. */
var wires=[
// MIDI jacks: 3.5 mm TRS, Type A (tip = DIN pin 5, ring = pin 4, sleeve = pin 2)
{h:[0,24],col:'blue',to:'MIDI IN jack · ring (pin 4)'},
{h:[0,22],col:'blue',to:'MIDI IN jack · tip (pin 5)'},
{h:[25,23],col:'blue',to:'OUT KIT jack · tip (pin 5)'},
{h:[21,21],col:'red',to:'OUT KIT jack · ring (pin 4)'},
{h:[21,26],col:'black',to:'OUT KIT jack · sleeve (pin 2)'},
{h:[30,20],col:'blue',to:'OUT SYNTH jack · tip (pin 5)'},
{h:[30,21],col:'red',to:'OUT SYNTH jack · ring (pin 4)'},
{h:[30,26],col:'black',to:'OUT SYNTH jack · sleeve (pin 2)'},
// screen
{h:[26,13],col:'blue',to:'LCD · RST (pin 4)'},
{h:[26,18],col:'blue',to:'LCD · SCE (pin 3)'},
{h:[26,14],col:'blue',to:'LCD · D/C (pin 5)'},
{h:[26,15],col:'blue',to:'LCD · DN/MOSI (pin 6)'},
{h:[26,16],col:'blue',to:'LCD · SCLK (pin 7)'},
{h:[22,27],col:'red',to:'LCD · VCC (pin 1)'},
{h:[22,26],col:'black',to:'LCD · GND (pin 2)'},
{h:[21,27],col:'red',to:'LCD · LED (pin 8) through 330 Ω'},
// encoder
{h:[1,13],col:'green',to:'Encoder · SIA'},
{h:[1,14],col:'green',to:'Encoder · SIB'},
{h:[1,15],col:'green',to:'Encoder · SW'},
{h:[3,27],col:'red',to:'Encoder · VCC'},
{h:[3,26],col:'black',to:'Encoder · GND'},
// pots
{h:[26,9],col:'blue',to:'ENERGY pot · middle lug'},
{h:[26,8],col:'blue',to:'DRIFT pot · middle lug'},
{h:[24,27],col:'red',to:'Pots · right lugs (chain both)'},
{h:[26,7],col:'black',to:'Pots · left lugs (chain both)'},
// buttons
{h:[1,16],col:'yellow',to:'SEED · one lug'},
{h:[1,18],col:'yellow',to:'RUN toggle · middle lug'},
{h:[5,26],col:'black',to:'Panel ground bus · SEED other lug, RUN one outer lug'},
{h:[1,3],col:'yellow',to:'Matrix row 1 (KICK, KICK 2, SNARE)'},
{h:[1,4],col:'yellow',to:'Matrix row 2 (HAT, HAT 16, PERC, GHOST)'},
{h:[1,6],col:'yellow',to:'Matrix row 3 (BASS, MEL A, MEL B, PAD)'},
{h:[1,8],col:'green',to:'Matrix column 1 (KICK, HAT, BASS)'},
{h:[1,9],col:'green',to:'Matrix column 2 (KICK 2, HAT 16, MEL A)'},
{h:[1,10],col:'green',to:'Matrix column 3 (SNARE, PERC, MEL B)'},
{h:[1,11],col:'green',to:'Matrix column 4 (GHOST, PAD)'}
];
return {W:W,H:H,cuts:cuts,parts:parts,jumpers:jumpers,wires:wires};
})();
if(typeof module!=='undefined') module.exports=LAYOUT;