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

243
tools/bom.html Normal file
View File

@@ -0,0 +1,243 @@
<title>Seedbox Build Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
/* Layout: one column workbench sheet. Parts list grouped by subsystem with a tick-off column, then pin map and wiring notes. */
:root{
--bg:#E8ECEF; --surface:#F6F7F9; --ink:#18202A; --muted:#566271; --line:#C3CBD3; --accent:#1E4C9E;
--have:#1D7A4F; --check:#9A6B10; --buy:#B4441F;
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
--f-mono:'Martian Mono', ui-monospace, Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2;
--have:#5CCB92; --check:#E2B455; --buy:#F07B52; color-scheme:dark } }
:root[data-theme="dark"]{
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2;
--have:#5CCB92; --check:#E2B455; --buy:#F07B52; color-scheme:dark }
*{box-sizing:border-box}
body{background:var(--bg);color:var(--ink);font:15px/1.55 var(--f-ui);padding-block:28px 48px;padding-inline:16px}
.wrap{max-width:980px;margin:0 auto;display:grid;gap:22px}
h1{font:700 24px/1.15 var(--f-mono);margin:0;letter-spacing:-.01em}
h2{font:700 13px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0}
.sub{color:var(--muted);margin:6px 0 0;max-width:68ch}
.bar{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.tally{display:flex;flex-wrap:wrap;gap:6px 18px;font:500 12px var(--f-mono);font-variant-numeric:tabular-nums;color:var(--muted)}
.tally b{color:var(--ink)}
.ctrls{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.btn{font:600 12px var(--f-mono);border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:6px;padding:7px 12px;cursor:pointer}
.btn:hover{border-color:var(--accent)}
.btn:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.check{display:flex;gap:8px;align-items:center;font:500 13px var(--f-ui);color:var(--muted)}
section{display:grid;gap:10px}
.tbl{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:10px}
table{width:100%;border-collapse:collapse;min-width:640px}
th{font:600 10px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
td{padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top}
tr:last-child td{border-bottom:0}
td.part{font-weight:600}
td.part small{display:block;font-weight:400;color:var(--muted);font-size:12px}
td.qty{font:500 13px var(--f-mono);font-variant-numeric:tabular-nums;white-space:nowrap}
td.note{color:var(--muted);font-size:13px;max-width:44ch}
td.tick{width:44px;text-align:center}
td.tick input{width:18px;height:18px;accent-color:var(--have)}
.st{display:inline-block;font:600 10px var(--f-mono);letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:999px;border:1px solid currentColor;white-space:nowrap}
.st.have{color:var(--have)} .st.check{color:var(--check)} .st.buy{color:var(--buy)} .st.opt{color:var(--muted)}
tr.got td:not(.tick){opacity:.5}
tr.got td.part{text-decoration:line-through}
.hide-got tr.got{display:none}
.mono{font-family:var(--f-mono);font-size:12.5px}
.pins td:first-child{font:600 13px var(--f-mono);white-space:nowrap}
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px;min-width:0}
.card h3{font:700 14px var(--f-ui);margin:0 0 8px}
.card ul{margin:0;padding-left:18px;display:grid;gap:4px;font-size:14px}
.card p{margin:0 0 8px;font-size:14px}
pre{font:400 12px/1.5 var(--f-mono);margin:0;overflow-x:auto;color:var(--ink)}
.gone{color:var(--muted);font-size:14px}
</style>
<div class="wrap">
<header>
<h1>Seedbox build sheet</h1>
<p class="sub">Everything for the Pico 2W build: Nokia 5110 screen with the on-screen track map, 11 track buttons in a diode matrix, encoder, two pots, SEED, RUN, one MIDI in and two MIDI outs. "Have" means it was on your parts list; tick items off as you find them in the drawers.</p>
</header>
<div class="bar">
<div class="tally" id="tally"></div>
<div class="ctrls">
<label class="check"><input type="checkbox" id="hideGot"> Hide ticked</label>
<button class="btn" id="copyBuy" type="button">Copy shopping list</button>
<button class="btn" id="reset" type="button">Clear ticks</button>
</div>
</div>
<div id="parts"></div>
<section>
<h2>Pin map</h2>
<div class="tbl"><table class="pins">
<thead><tr><th>GPIO</th><th>Function</th><th>Connects to</th></tr></thead>
<tbody>
<tr><td>GP0</td><td>UART0 TX · MIDI OUT A</td><td>10 Ω → OUT KIT tip (pin 5)</td></tr>
<tr><td>GP1</td><td>UART0 RX · MIDI IN</td><td>6N137 pin 6 (1 kΩ pull-up to 3V3)</td></tr>
<tr><td>GP4</td><td>UART1 TX · MIDI OUT B</td><td>10 Ω → OUT SYNTH tip (pin 5)</td></tr>
<tr><td>GP2 GP3 GP5</td><td>Button matrix rows</td><td>Inputs with internal pull-ups</td></tr>
<tr><td>GP6 GP7 GP8 GP9</td><td>Button matrix columns</td><td>Driven low one at a time</td></tr>
<tr><td>GP10 GP11</td><td>Encoder A / B</td><td>Encoder module SIA / SIB</td></tr>
<tr><td>GP12</td><td>Encoder push</td><td>Encoder module SW</td></tr>
<tr><td>GP13</td><td>SEED button</td><td>Button to GND, internal pull-up</td></tr>
<tr><td>GP14</td><td>RUN toggle</td><td>Toggle to GND, internal pull-up</td></tr>
<tr><td>GP17</td><td>LCD SCE</td><td>Can be tied to GND instead (frees the pin)</td></tr>
<tr><td>GP18</td><td>SPI0 SCK</td><td>LCD SCLK</td></tr>
<tr><td>GP19</td><td>SPI0 TX</td><td>LCD DN (MOSI)</td></tr>
<tr><td>GP20</td><td>LCD DC</td><td>LCD D/C</td></tr>
<tr><td>GP21</td><td>LCD RST</td><td>LCD RST</td></tr>
<tr><td>GP22</td><td>Free</td><td>Only for backlight dimming later, through a transistor</td></tr>
<tr><td>GP26</td><td>ADC0 · Energy pot</td><td>Pot wiper (ends to 3V3 and AGND)</td></tr>
<tr><td>GP27</td><td>ADC1 · Drift pot</td><td>Pot wiper (ends to 3V3 and AGND)</td></tr>
<tr><td>GP15 GP16 GP28</td><td>Spare</td><td>GP28 is also an analog input</td></tr>
<tr><td>GP23 24 25 29</td><td>Reserved</td><td>Used by the Pico 2W's wireless chip, don't wire</td></tr>
</tbody>
</table></div>
</section>
<section>
<h2>Wiring notes</h2>
<div class="cols">
<div class="card">
<h3>MIDI OUT (×2, 3.3 V spec, TRS Type A)</h3>
<pre>3V3 ──33 Ω── ring (DIN pin 4)
TX ──10 Ω── tip (DIN pin 5)
GND ──────── sleeve (DIN pin 2)</pre>
<p style="margin-top:8px">Lug order differs between jack models: find tip, ring and sleeve with the meter and a plug inserted.</p>
</div>
<div class="card">
<h3>MIDI IN (6N137)</h3>
<pre>ring ──220 Ω── opto 2 (anode)
tip ───────── opto 3 (cathode)
1N4148 across 2–3, band to pin 2
opto 8 → VBUS (5 V) opto 5 → GND
opto 6 → GP1, 1 kΩ to 3V3
opto 7 ──────── opto 8 (enable high)
100 nF across 8–5 (required)</pre>
<p style="margin-top:8px">Pull the output up to 3V3, never 5 V. The sleeve stays unconnected on the input.</p>
</div>
<div class="card">
<h3>Button matrix (3 × 4)</h3>
<pre> C0 GP6 C1 GP7 C2 GP8 C3 GP9
R0 GP2 KICK KICK 2 SNARE (RUN toggle, not in matrix)
R1 GP3 HAT HAT 16 PERC GHOST
R2 GP5 BASS MEL A MEL B PAD</pre>
<p style="margin-top:8px">Same layout as the lid: the RUN toggle sits in row 1 / column 4, but it has its own pin (GP14), so that matrix slot is empty. Each button: row → switch → 1N4148 → column, band toward the column. The diodes let you hold several buttons at once without phantom presses.</p>
</div>
<div class="card">
<h3>Nokia 5110 module</h3>
<ul>
<li>VCC to 3V3, not 5 V. Logic connects straight to the Pico, no resistors.</li>
<li>Backlight polarity differs between red and blue boards. Test which way yours lights (BL to GND or to 3V3), then either hard-wire it or switch that side with a transistor for dimming (NPN on the ground side, PNP on the 3.3 V side).</li>
<li>Contrast is set in firmware and drifts a little with temperature. Expect to tune it once.</li>
</ul>
</div>
<div class="card">
<h3>Encoder and pots</h3>
<ul>
<li>Power the encoder module from 3V3. Its onboard pull-ups would otherwise put 5 V on the Pico pins.</li>
<li>Pots: one end to 3V3, the other to AGND (pin 33), wiper to the ADC pin. A 100 nF cap from each wiper to AGND calms jitter.</li>
</ul>
</div>
<div class="card">
<h3>Panel layout</h3>
<ul>
<li>See the panel layout page (landscape): screen with the encoder on the left, ENERGY / DRIFT / SEED in the middle, the 4 × 3 block on the right (11 track buttons, RUN toggle top-right).</li>
<li>Screen sits on the lid, wires through one hole hidden under the module. USB and the three jacks on the back wall; the Pico's USB end sits at the stripboard edge, behind the slot.</li>
</ul>
</div>
</div>
</section>
<section>
<h2>No longer needed</h2>
<p class="gone">USB panel extension, standoffs and screws (stripboard glued behind a slot instead) · 0.91" OLED (replaced by the Nokia screen) · WS2812 LED strip (the screen shows track state) · 4.7 kΩ opto resistor (6N138 only) · 470 Ω resistors (old 5 V MIDI out values) · 5-pin DIN jacks (3.5 mm TRS instead).</p>
</section>
</div>
<script>
(function(){
/* status: have = on your parts list, check = probably in the drawer, buy = likely missing, opt = optional */
var GROUPS=[
{name:'Core',rows:[
['pico','Raspberry Pi Pico 2W','RP2350, micro-USB','1','have','The whole engine runs on it with lots of headroom.'],
['lcd','Nokia 5110 LCD module','PCD8544, 84×48, SPI','1','have','The module you used with Arduino.'],
['box','Enclosure','','1','have','About 80 × 150 × 40 mm. Panel layout: tools/panel.html.']
]},
{name:'Controls',rows:[
['enc','Rotary encoder with push','KY-040 module or bare EC11','1','have','Power it from 3V3.'],
['pots','Potentiometer, linear','B10K (anything 5k–50k linear works)','2','have','Energy and Drift.'],
['knobs','Knobs','to fit the pot and encoder shafts','3','check','Shaft types differ (D, knurled, 6 mm). Match them.'],
['btn','Momentary push button, panel mount','~12 mm, non-latching','11','check','7 kit + 4 line tracks.'],
['seedbtn','Momentary push button, large','16–22 mm, non-latching','1','check','SEED. Any big momentary works.'],
['tog','Toggle switch, SPDT','','1','have','RUN / STOP. Your old bypass switch.'],
['dio','1N4148 diode','or 1N4448 / 1N914 / any small-signal diode','12','buy','11 for the button matrix + 1 for MIDI IN. 1N4448 works too. Every button needs one: a missing diode gives phantom presses with three buttons held.']
]},
{name:'MIDI',rows:[
['trs','3.5 mm stereo (TRS) jack, panel mount','3 lugs: tip, ring, sleeve. Not mono.','3','check','IN, OUT KIT, OUT SYNTH. MIDI TRS Type A. 6 mm hole each.'],
['adapt','TRS-to-DIN MIDI adapter, Type A','female DIN, 3.5 mm plug','1','buy','For a DIN synth or sampler (e.g. MiniFreak). Must say Type A (Type B swaps tip and ring). Type A TRS gear (e.g. SP-404MKII) takes a plain stereo 3.5 mm cable.'],
['opto','6N137 optocoupler','DIP-8 (6N138 also works: then drop the pin 7 link)','1','have','MIDI IN. You have three; one is used.'],
['sock','DIP-8 IC socket','','1','buy','So the opto can be swapped.'],
['r330','Resistor 330 Ω','220–470 Ω','1','check','LCD backlight (LED pin) to 3.3 V, inline at the module.'],
['r220','Resistor 220 Ω','150–220 Ω','1','have','MIDI IN, in series with the opto LED. The 6N137 wants a bit more LED current: use 150 Ω if learning misses notes.'],
['r1k','Resistor 1 kΩ','470 Ω–2.2 kΩ is fine','2','have','Opto output pull-up, NPN base.'],
['r33','Resistor 33 Ω, ½ W','27–47 Ω is fine','2','buy','MIDI OUT ring (pin 4) line, per the MIDI 3.3 V spec. ½ W because a mono plug in an OUT jack shorts it straight to ground (0.33 W); a ¼ W one would cook.'],
['r10','Resistor 10 Ω','10–22 Ω is fine','2','check','MIDI OUT tip (pin 5) line, from TX. Don\'t replace with a wire: it protects the pin.'],
['cap','Ceramic capacitor 100 nF','anything 47–220 nF','3','check','C1 on the board (6N137 supply bypass, required). Optional: one per pot, soldered at the pot from wiper to the left lug, if the readings jitter.']
]},
{name:'Screen extras',rows:[
['blt','NPN or PNP transistor','2N3904 / BC547 (NPN) or 2N3906 / BC557 (PNP)','1','opt','Only for a dimmable backlight. NPN if your BL lights when grounded, PNP if it lights from 3.3 V. Otherwise hard-wire it.']
]},
{name:'Power and assembly',rows:[
['perf','Stripboard','at least 34 × 28 holes (about 86 × 71 mm)','1','check','Pico socketed at the top edge, so its USB port meets a slot in the back wall.'],
['hdr','Female header strip, 1 × 20','2.54 mm','2','check','Socket the Pico instead of soldering it in.'],
['mhdr','Male header strip, 1 × 20','2.54 mm','2','check','Soldered to the Pico so it plugs into the female headers. Skip if yours is a Pico 2 WH (headers fitted).'],
['tape','Double-sided foam tape or hot glue','','—','check','Holds the stripboard and the screen without screws.']
]}
];
var LABEL={have:'have',check:'check',buy:'buy',opt:'optional'};
var ticks={}; try{ ticks=JSON.parse(localStorage.getItem('seedbox.bom')||'{}')||{}; }catch(e){ ticks={}; }
function saveTicks(){ try{ localStorage.setItem('seedbox.bom',JSON.stringify(ticks)); }catch(e){} }
var root=document.getElementById('parts'), rowEls={};
GROUPS.forEach(function(g){
var sec=document.createElement('section'), h=document.createElement('h2'); h.textContent=g.name; sec.appendChild(h);
var wrap=document.createElement('div'); wrap.className='tbl';
var t=document.createElement('table'); t.innerHTML='<thead><tr><th>Got</th><th>Part</th><th>Qty</th><th>Status</th><th>Notes</th></tr></thead>';
var tb=document.createElement('tbody');
g.rows.forEach(function(r){
var tr=document.createElement('tr'), id='p_'+r[0];
tr.innerHTML='<td class="tick"><input type="checkbox" id="'+id+'" aria-label="Got '+r[1].replace(/"/g,'')+'"></td>'+
'<td class="part"></td><td class="qty"></td><td><span class="st '+r[4]+'">'+LABEL[r[4]]+'</span></td><td class="note"></td>';
var p=tr.querySelector('.part'); p.textContent=r[1]; if(r[2]){ var sm=document.createElement('small'); sm.textContent=r[2]; p.appendChild(sm); }
tr.querySelector('.qty').textContent=r[3]; tr.querySelector('.note').textContent=r[5];
var cb=tr.querySelector('input'); cb.checked=!!ticks[r[0]]; tr.classList.toggle('got',cb.checked);
cb.addEventListener('change',function(){ ticks[r[0]]=cb.checked; saveTicks(); tr.classList.toggle('got',cb.checked); tally(); });
rowEls[r[0]]={tr:tr,row:r}; tb.appendChild(tr);
});
t.appendChild(tb); wrap.appendChild(t); sec.appendChild(wrap); root.appendChild(sec);
});
function tally(){
var n=0,got=0,buy=0,miss=[];
Object.keys(rowEls).forEach(function(k){ var r=rowEls[k].row; n++; if(ticks[k]) got++; else if(r[4]!=='opt'){ miss.push(r); if(r[4]==='buy') buy++; } });
document.getElementById('tally').innerHTML='<span><b>'+got+'</b> of '+n+' ticked</span><span><b>'+miss.length+'</b> still to find</span><span><b>'+buy+'</b> likely to buy</span>';
return miss;
}
tally();
var hide=document.getElementById('hideGot');
hide.addEventListener('change',function(){ root.classList.toggle('hide-got',hide.checked); });
document.getElementById('reset').addEventListener('click',function(){ ticks={}; saveTicks(); Object.keys(rowEls).forEach(function(k){ rowEls[k].tr.querySelector('input').checked=false; rowEls[k].tr.classList.remove('got'); }); tally(); });
document.getElementById('copyBuy').addEventListener('click',function(){
var b=this, txt=tally().map(function(r){ return r[3]+' × '+r[1]+(r[2]?' ('+r[2]+')':''); }).join('\n');
function ok(){ b.textContent='Copied'; setTimeout(function(){ b.textContent='Copy shopping list'; },1400); }
try{ navigator.clipboard.writeText(txt).then(ok,function(){ b.textContent='Copy failed'; }); }catch(e){ b.textContent='Copy failed'; }
});
})();
</script>