Add PM_K-1 "Kit" — buildable Pico touchscreen unit + MicroPython firmware
A new, first-actually-buildable form factor for the 52Pi EP-0172 "Pico Breadboard
Kit Plus" (Raspberry Pi Pico; 3.5" ST7796 320x480 cap-touch via GT911, PSP
joystick on ADC0/1, WS2812 RGB on GP12, buzzer GP13, buttons GP14/15):
- pico/main.py — one self-contained MicroPython file: ST7796 direct-draw driver,
GT911 touch (16-bit register addressing), WS2812 RGB (neopixel), PWM buzzer,
ADC joystick, buttons. It parses the project's own program-string language
(verified against the web engine's semantics) and runs a non-blocking
ticks_us scheduler with an on-screen touch UI. CONFIG flags cover panel /
colour / touch / joystick calibration. pico/README.md has flashing +
calibration steps.
- kit.html — lean widget that mirrors the firmware's on-screen UI (portrait
320x480 canvas) plus a joystick / RGB / buzzer / A-B buttons; plays via the
shared engine. info-kit.html — the real EP-0172 pinout, a parts list
(~$45 incl. Pico) and the firmware to flash (downloads /pico-main.py, links
the README + source).
- Landing + embed page list the Kit; build.sh/deploy.sh build the two pages and
serve pico/main.py as /pico-main.py for download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ee412bad04
commit
eecea625d3
9 changed files with 1025 additions and 4 deletions
6
build.sh
6
build.sh
|
|
@ -29,10 +29,12 @@ def build(name):
|
|||
out.write_text(src)
|
||||
return out.stat().st_size
|
||||
|
||||
for name in ("index.html","editor.html","player.html","teacher.html","stage.html","micro.html","showcase.html",
|
||||
for name in ("index.html","editor.html","player.html","teacher.html","stage.html","micro.html","showcase.html","kit.html",
|
||||
"embed.html",
|
||||
"info-editor.html","info-player.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html"):
|
||||
"info-editor.html","info-player.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html","info-kit.html"):
|
||||
print("built %s (%dKB)" % (name, build(name) // 1024))
|
||||
pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) # loader, served as-is
|
||||
print("copied embed.js")
|
||||
pathlib.Path("dist/pico-main.py").write_text(pathlib.Path("pico/main.py").read_text()) # PM_K-1 firmware, downloadable
|
||||
print("copied pico-main.py")
|
||||
PY
|
||||
|
|
|
|||
|
|
@ -40,13 +40,14 @@ fi
|
|||
|
||||
# stamp the version into the built copy only (source stays clean)
|
||||
echo "deployed v$BUILD -> $DEST_DIR"
|
||||
for f in index.html editor.html player.html teacher.html stage.html micro.html showcase.html \
|
||||
for f in index.html editor.html player.html teacher.html stage.html micro.html showcase.html kit.html \
|
||||
embed.html \
|
||||
info-editor.html info-player.html info-teacher.html info-stage.html info-micro.html info-showcase.html; do
|
||||
info-editor.html info-player.html info-teacher.html info-stage.html info-micro.html info-showcase.html info-kit.html; do
|
||||
sed "s|const APP_VERSION = \"[^\"]*\";|const APP_VERSION = \"$BUILD\";|" "$DIST_DIR/$f" > "$DEST_DIR/$f"
|
||||
echo " $f ($(stat -c '%s' "$DEST_DIR/$f") bytes)"
|
||||
done
|
||||
cp "$DIST_DIR/embed.js" "$DEST_DIR/embed.js"; echo " embed.js ($(stat -c '%s' "$DEST_DIR/embed.js") bytes)"
|
||||
cp "$DIST_DIR/pico-main.py" "$DEST_DIR/pico-main.py"; echo " pico-main.py ($(stat -c '%s' "$DEST_DIR/pico-main.py") bytes)" # PM_K-1 firmware download
|
||||
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
|
||||
rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/)
|
||||
# info-*.html are first-class pages again: each form factor has a lean widget page
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ const ORIGIN = "https://metronome.varasys.io";
|
|||
const DEMO_PATCH = "v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2";
|
||||
const FF = [
|
||||
{ k:"editor", name:"PM_E‑1 Editor", file:"editor.html", h:560 },
|
||||
{ k:"kit", name:"PM_K‑1 Kit", file:"kit.html", h:560 },
|
||||
{ k:"teacher", name:"PM_T‑1 Teacher", file:"teacher.html", h:440 },
|
||||
{ k:"stage", name:"PM_S‑1 Stage", file:"stage.html", h:430 },
|
||||
{ k:"micro", name:"PM_P‑1 Practice", file:"micro.html", h:240 },
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ const SAMPLES = {}; let state = { bpm:120, volume:0.85 }, meters = [], muteWindo
|
|||
|
||||
const VERSIONS = [
|
||||
{ key:"editor", file:"/editor.html", name:"PM_E‑1 Editor", chip:"app", h:620, sum:"Design grooves: stack meter lanes, per‑step accents/ghosts/mutes, swing & polyrhythm, set lists, per‑lane dB gain." },
|
||||
{ key:"kit", file:"/kit.html", name:"PM_K‑1 Kit", chip:"hw", h:560, sum:"Build it today — a Raspberry Pi Pico on the 52Pi touchscreen kit; tap the 3.5″ screen, joystick tempo, RGB beat light, buzzer. MicroPython firmware included." },
|
||||
{ key:"teacher", file:"/teacher.html", name:"PM_T‑1 Teacher", chip:"hw", h:440, sum:"Studio / lesson desk console — colour TFT of every lane, arcade buttons, instrument pass‑through." },
|
||||
{ key:"stage", file:"/stage.html", name:"PM_S‑1 Stage", chip:"hw", h:430, sum:"Live foot pedal — two footswitches, expression‑pedal tempo, a big floor‑readable RGB beat light." },
|
||||
{ key:"micro", file:"/micro.html", name:"PM_P‑1 Practice", chip:"hw", h:240, sum:"Inline practice bar — clickable thumb‑roller, amber 14‑segment, instrument in/out pass‑through." },
|
||||
|
|
|
|||
140
info-kit.html
Normal file
140
info-kit.html
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VARASYS PM_K‑1 Kit — wiring, parts & firmware (Raspberry Pi Pico build)</title>
|
||||
<meta name="description" content="PM_K‑1 Kit — build a touchscreen polymeter metronome from a Raspberry Pi Pico on the 52Pi EP‑0172 breadboard kit (3.5in ST7796 cap‑touch, joystick, RGB, buzzer). Pinout, parts list, and the MicroPython firmware to flash." />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||
<script>
|
||||
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
||||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
||||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
||||
</script>
|
||||
<style>
|
||||
/*@BUILD:include:src/base.css@*/
|
||||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
||||
--panel-bg:#161b22; --panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#2a313c; --silk:#aab2bc; }
|
||||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4;
|
||||
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#d2dae4; }
|
||||
body{ margin:0; min-height:100vh; padding:22px 16px 56px; color:var(--txt);
|
||||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); }
|
||||
a{ color:var(--link); }
|
||||
main{ width:100%; max-width:980px; margin:0 auto; }
|
||||
.info-hero{ text-align:center; padding:16px 8px 2px; }
|
||||
.info-hero h1{ font-size:clamp(24px,5vw,36px); margin:0; letter-spacing:-.01em; }
|
||||
.info-hero .sub{ margin:9px auto 0; max-width:64ch; font-size:14.5px; }
|
||||
.steps{ width:100%; max-width:760px; margin:8px auto 0; color:var(--muted); font-size:14px; line-height:1.6; }
|
||||
.steps li{ margin:5px 0; }
|
||||
.steps code, .about code, .sub code { background:var(--field-bg); border:1px solid var(--field-bd); border-radius:5px; padding:1px 5px; font-size:12.5px; }
|
||||
.dl{ display:inline-flex; align-items:center; gap:7px; margin:4px 10px 4px 0; padding:9px 14px; border-radius:10px;
|
||||
background:linear-gradient(180deg,#34c6ff,var(--cyan)); color:#04121b; font-weight:700; text-decoration:none; font-size:13.5px; }
|
||||
.dl.alt{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); font-weight:600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
/*@BUILD:include:src/header.html@*/
|
||||
|
||||
<main>
|
||||
<section class="info-hero">
|
||||
<h1>PM_K‑1 Kit</h1>
|
||||
<p class="sub">Build it yourself: a Raspberry Pi Pico on the 52Pi breadboard kit becomes a touchscreen polymeter metronome — same engine, same program strings, with MicroPython firmware you flash in two minutes.</p>
|
||||
</section>
|
||||
|
||||
/*@BUILD:include:src/infoembed.html@*/
|
||||
|
||||
<section class="about">
|
||||
<h2>What it is</h2>
|
||||
<div class="ff-tags"><span class="hw">Buildable now</span><span>Raspberry Pi Pico</span><span>52Pi EP‑0172 kit</span><span>~$45 incl. Pico</span></div>
|
||||
<p>This is the first member of the family you can actually build today from off‑the‑shelf parts: a
|
||||
<b>Raspberry Pi Pico</b> seated on the <b>52Pi EP‑0172 "Pico Breadboard Kit Plus"</b>, which carries a
|
||||
3.5″ <b>ST7796</b> 320×480 capacitive‑touch screen (<b>GT911</b>), a PSP <b>joystick</b>, a <b>WS2812 RGB</b>
|
||||
LED, a <b>buzzer</b> and two buttons — all pre‑wired, so you don't solder anything; you just seat the Pico
|
||||
and copy one file onto it.</p>
|
||||
<p>It runs the same <b>polymeter engine</b> and the same <b>program strings</b> as the web editor: design a
|
||||
groove on the site, copy its program string into the firmware's <code>PROGRAMS</code> list, and it plays on
|
||||
the device. Tap the screen, nudge tempo with the joystick; the RGB flashes each beat (amber accent / cyan
|
||||
normal / violet ghost) and the buzzer clicks. Powered over the Pico's USB.</p>
|
||||
</section>
|
||||
|
||||
<details class="spec" open>
|
||||
<summary>Wiring — the EP‑0172 fixed pinout (Raspberry Pi Pico)</summary>
|
||||
<div class="spec-body">
|
||||
<p class="sub">Everything is wired on the board; this is just what the firmware drives. No breadboarding required.</p>
|
||||
<table class="bom">
|
||||
<thead><tr><th>Component</th><th>Raspberry Pi Pico pins</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="grp"><td colspan="2">Display — 3.5″ ST7796, 320×480 (SPI0)</td></tr>
|
||||
<tr><td class="part">SCK / MOSI</td><td>GP2 / GP3</td></tr>
|
||||
<tr><td class="part">CS / DC / RST</td><td>GP5 / GP6 / GP7</td></tr>
|
||||
<tr class="grp"><td colspan="2">Touch — GT911 capacitive (I2C0)</td></tr>
|
||||
<tr><td class="part">SDA / SCL <span class="spec">— addr 0x5D</span></td><td>GP8 / GP9</td></tr>
|
||||
<tr class="grp"><td colspan="2">Controls & feedback</td></tr>
|
||||
<tr><td class="part">PSP joystick X / Y</td><td>ADC0 (GP26) / ADC1 (GP27)</td></tr>
|
||||
<tr><td class="part">Button A (play/stop) / Button B (tap)</td><td>GP15 / GP14</td></tr>
|
||||
<tr><td class="part">WS2812 RGB LED</td><td>GP12</td></tr>
|
||||
<tr><td class="part">Buzzer</td><td>GP13</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="spec" open>
|
||||
<summary>Parts</summary>
|
||||
<div class="spec-body">
|
||||
<p class="sub">An off‑the‑shelf kit, not a custom board — ballpark one‑off prices (USD).</p>
|
||||
<table class="bom">
|
||||
<thead><tr><th>Part</th><th class="q">Qty</th><th class="c">~$</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="part">Raspberry Pi Pico (or Pico W / Pico 2) <span class="spec">— the brain</span></td><td class="q">1</td><td class="c">5</td></tr>
|
||||
<tr><td class="part">52Pi EP‑0172 "Pico Breadboard Kit Plus" <span class="spec">— 3.5″ ST7796 cap‑touch, GT911, PSP joystick, WS2812 RGB, buzzer, 2 buttons, breadboard, acrylic panel</span></td><td class="q">1</td><td class="c">38</td></tr>
|
||||
<tr><td class="part">USB cable <span class="spec">— power + flashing</span></td><td class="q">1</td><td class="c">2</td></tr>
|
||||
<tr class="total"><td>Total (one‑off)</td><td class="q"></td><td class="c">≈ $45</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="sub" style="margin-top:10px">Reference: <a href="https://wiki.52pi.com/index.php?title=EP-0172" target="_blank" rel="noopener">52Pi EP‑0172 wiki</a>
|
||||
· <a href="https://github.com/geeekpi/pico_breakboard_kit" target="_blank" rel="noopener">vendor code</a>. Lots may ship the screen as ST7796 (320×480) — this build targets that.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="spec" open>
|
||||
<summary>Firmware — flash it in two minutes</summary>
|
||||
<div class="spec-body">
|
||||
<p>
|
||||
<a class="dl" href="/pico-main.py" download="main.py">Download main.py ↓</a>
|
||||
<a class="dl alt" href="https://codeberg.org/VARASYS/metronome/src/branch/main/pico" target="_blank" rel="noopener">Source + README ↗</a>
|
||||
</p>
|
||||
<ol class="steps">
|
||||
<li>Install <b>MicroPython</b>: hold <b>BOOTSEL</b>, plug the Pico into USB, and drop the MicroPython
|
||||
<code>.uf2</code> onto the <code>RPI‑RP2</code> drive
|
||||
(<a href="https://micropython.org/download/RPI_PICO/" target="_blank" rel="noopener">Pico</a> /
|
||||
<a href="https://micropython.org/download/RPI_PICO2/" target="_blank" rel="noopener">Pico 2</a>).</li>
|
||||
<li>Copy <code>main.py</code> onto the Pico as <code>main.py</code> (in
|
||||
<a href="https://thonny.org" target="_blank" rel="noopener">Thonny</a>: <i>File ▸ Save as ▸ Raspberry Pi Pico</i>,
|
||||
or <code>mpremote cp main.py :main.py</code>).</li>
|
||||
<li>Reset — it boots straight into the metronome.</li>
|
||||
<li>Add your own grooves by pasting program strings from the editor into the <code>PROGRAMS</code> list at the
|
||||
top of <code>main.py</code>. If colours, touch, or the joystick look off, flip a flag in the
|
||||
<code>CONFIG</code> block (see the README's calibration notes).</li>
|
||||
</ol>
|
||||
<p class="sub">It's one self‑contained file — the ST7796 driver, GT911 touch, WS2812 RGB, buzzer and the
|
||||
polymeter engine, no external libraries.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code><div></code> + a script —
|
||||
see <a href="/embed.html">the embed docs</a>.</p>
|
||||
</main>
|
||||
|
||||
/*@BUILD:include:src/footer.html@*/
|
||||
|
||||
<script>
|
||||
const APP_VERSION = "v0.0.1-dev";
|
||||
window.INFO_DEVICE = { file:"/kit.html", name:"PM_K‑1 Kit" };
|
||||
/*@BUILD:include:src/infoembed.js@*/
|
||||
/*@BUILD:include:src/chrome.js@*/
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
308
kit.html
Normal file
308
kit.html
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>VARASYS PM_K‑1 — Kit (Raspberry Pi Pico touchscreen build)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||
<script>
|
||||
/* ?embed=1 → strip site chrome + auto-size to the host */
|
||||
(function(){ if(!/[?&]embed=1/.test(location.search)) return;
|
||||
document.documentElement.dataset.embed="1";
|
||||
function ph(){ try{ parent.postMessage({type:"varasys-h",h:Math.ceil(document.documentElement.getBoundingClientRect().height)},"*"); }catch(e){} }
|
||||
addEventListener("load",ph); addEventListener("resize",ph); setTimeout(ph,300); setTimeout(ph,1000);
|
||||
})();
|
||||
</script>
|
||||
<!--
|
||||
PM_K-1 "Kit" — the buildable touchscreen unit: a Raspberry Pi Pico on the 52Pi EP-0172
|
||||
breadboard kit (3.5" ST7796 320x480 cap-touch, GT911 touch, PSP joystick on ADC0/1,
|
||||
WS2812 RGB on GP12, buzzer GP13, buttons GP14/15). This page mirrors the MicroPython
|
||||
firmware's on-screen UI so the web simulator looks like the real device. Shares src/engine.js.
|
||||
-->
|
||||
<script>
|
||||
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
||||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
||||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
||||
</script>
|
||||
<style>
|
||||
/*@BUILD:include:src/base.css@*/
|
||||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
||||
--panel-bd:#2a313c; --device-bd:#33363c; --silk:#aab2bc; --cyan:#0AB3F7;
|
||||
--panel-bg:#161b22; --field-bg:#0e1116; --field-bd:#2a313c; }
|
||||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4;
|
||||
--panel-bd:#d2dae4; --panel-bg:#ffffff; --field-bg:#f1f4f8; --field-bd:#d2dae4 }
|
||||
body{ margin:0; min-height:100vh; padding:26px 14px 46px;
|
||||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); color:var(--txt);
|
||||
display:flex; flex-direction:column; align-items:center; gap:16px }
|
||||
a{ color:var(--link) }
|
||||
|
||||
/* the kit: a Pico carrier board with the screen + joystick + RGB + buzzer + buttons */
|
||||
.device{ width:100%; max-width:330px; position:relative; border-radius:16px; padding:14px 14px 18px;
|
||||
background:
|
||||
radial-gradient(rgba(255,255,255,.022) .5px, transparent .6px) 0 0/3px 3px,
|
||||
linear-gradient(180deg, #1f3a2e, #0c2a20); /* 52Pi green PCB vibe */
|
||||
border:1px solid #2d5c47;
|
||||
box-shadow:0 26px 52px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05) }
|
||||
.pcbrow{ display:flex; align-items:center; justify-content:space-between; margin:0 2px 10px }
|
||||
.dev-logo{ height:18px }
|
||||
.silk{ display:flex; align-items:center; gap:7px; color:#bfe6d4 }
|
||||
.silk .model{ font-size:8.5px; text-transform:uppercase; letter-spacing:.16em; opacity:.9 }
|
||||
.pin{ font-size:7px; color:#9fd2bd; letter-spacing:.1em; text-transform:uppercase; opacity:.8 }
|
||||
|
||||
.screen-wrap{ padding:8px; border-radius:10px; background:linear-gradient(180deg,#05070a,#020406);
|
||||
border:1px solid #04060a; box-shadow:inset 0 2px 10px rgba(0,0,0,.8) }
|
||||
#screen{ display:block; width:100%; height:auto; border-radius:5px; background:#06080c; touch-action:manipulation; cursor:pointer }
|
||||
|
||||
.hw{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin:14px 4px 0 }
|
||||
/* PSP joystick */
|
||||
.joy{ width:74px; height:74px; border-radius:50%; position:relative; flex:0 0 auto; touch-action:none; cursor:grab;
|
||||
background:radial-gradient(circle at 40% 34%, #2a2f37, #0c0f13 72%); border:2px solid #0a3a2a;
|
||||
box-shadow:inset 0 2px 6px rgba(0,0,0,.6) }
|
||||
.joy .nub{ position:absolute; left:50%; top:50%; width:34px; height:34px; margin:-17px 0 0 -17px; border-radius:50%;
|
||||
background:radial-gradient(circle at 38% 32%, #e9eef3, #aab2bc 46%, #6c7480 72%, #3b424c);
|
||||
box-shadow:0 3px 6px rgba(0,0,0,.5); transition:transform .04s }
|
||||
.joy .cap{ position:absolute; left:0; right:0; bottom:-15px; text-align:center; font-size:7px; color:#9fd2bd; letter-spacing:.08em; text-transform:uppercase; opacity:.85 }
|
||||
.mids{ display:flex; flex-direction:column; align-items:center; gap:8px; flex:1 }
|
||||
.led{ width:30px; height:30px; border-radius:50%; background:#0c0f14;
|
||||
box-shadow:0 0 4px #000 inset; transition:none }
|
||||
.led-cap, .buz-cap{ font-size:7px; color:#9fd2bd; letter-spacing:.1em; text-transform:uppercase; opacity:.85 }
|
||||
.buz{ width:26px; height:26px; border-radius:50%; background:radial-gradient(circle at 50% 40%, #2a2f37, #0c0f13);
|
||||
border:2px solid #0a3a2a; position:relative }
|
||||
.buz::after{ content:""; position:absolute; left:50%; top:50%; width:6px; height:6px; margin:-3px 0 0 -3px; border-radius:50%; background:#05070a }
|
||||
.btns{ display:flex; flex-direction:column; gap:9px; flex:0 0 auto }
|
||||
.pbtn{ width:60px; padding:9px 0; border-radius:9px; border:1px solid #0a3a2a; cursor:pointer;
|
||||
background:radial-gradient(circle at 40% 30%, #d7dde3, #8b939e 70%, #5a626c); color:#0c1116;
|
||||
font-size:9px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
|
||||
box-shadow:0 3px 5px rgba(0,0,0,.4) }
|
||||
.pbtn:active{ transform:translateY(2px) }
|
||||
.pbtn small{ display:block; font-size:6.5px; font-weight:600; opacity:.7 }
|
||||
|
||||
.hint{ max-width:330px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
|
||||
[data-embed] .hint{ display:none !important }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
/*@BUILD:include:src/header.html@*/
|
||||
|
||||
<h1 class="ff-title">PM_K‑1 Kit</h1>
|
||||
<p class="ff-sum">The build‑it‑yourself touchscreen unit — a Raspberry Pi Pico on the 52Pi breadboard kit (3.5″ cap‑touch, joystick, RGB, buzzer). Tap the screen, nudge tempo with the stick; runs the same program strings, with MicroPython firmware you flash yourself.</p>
|
||||
|
||||
<div class="device">
|
||||
<div class="pcbrow">
|
||||
<div class="silk"><span class="dev-lock"><img class="dev-logo" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" /></span><span class="model">PM_K‑1 Kit</span></div>
|
||||
<span class="pin">Pico · USB‑C</span>
|
||||
</div>
|
||||
|
||||
<div class="screen-wrap"><canvas id="screen" width="320" height="480" aria-label="touchscreen metronome"></canvas></div>
|
||||
|
||||
<div class="hw">
|
||||
<div class="joy" id="joy" title="Joystick — up/down tempo · left/right groove"><div class="nub" id="nub"></div><span class="cap">Joystick</span></div>
|
||||
<div class="mids">
|
||||
<div class="led" id="led"></div><span class="led-cap">RGB</span>
|
||||
<div class="buz"></div><span class="buz-cap">Buzzer</span>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<button class="pbtn" id="btnA">A<small>play</small></button>
|
||||
<button class="pbtn" id="btnB">B<small>tap</small></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">Tap the on‑screen buttons (the real unit is capacitive touch). The <b>joystick</b> sets tempo (up/down)
|
||||
and switches grooves (left/right); <b>A</b> = play/stop, <b>B</b> = tap. The RGB LED flashes each beat and the buzzer clicks.</div>
|
||||
|
||||
/*@BUILD:include:src/progbox.html@*/
|
||||
|
||||
<p class="ff-link pageonly"><a href="/info-kit.html">Wiring, parts & firmware to flash →</a></p>
|
||||
|
||||
<script>
|
||||
const APP_VERSION = "v0.0.1-dev";
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
/* ========================= ENGINE (shared; synth voices only) ================= */
|
||||
const SAMPLES = {};
|
||||
/*@BUILD:include:src/engine.js@*/
|
||||
/*@BUILD:include:src/setlists.js@*/
|
||||
const state = { bpm:120, volume:0.85, running:false };
|
||||
let meters = [], muteWindows = [];
|
||||
|
||||
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
|
||||
function scheduler(){
|
||||
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
||||
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
||||
}
|
||||
function buildMeters(lanes){
|
||||
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
|
||||
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
|
||||
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0,
|
||||
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
|
||||
}
|
||||
function startAudio(){
|
||||
ensureAudio(); audioCtx.resume(); state.running=true;
|
||||
const t0=audioCtx.currentTime+0.08;
|
||||
for(const m of meters){ m.tick=0; m.nextTime=t0; m.vq=[]; m.vqPtr=0; m.currentStep=-1; }
|
||||
muteWindows=[];
|
||||
schedulerTimer=setInterval(scheduler,LOOKAHEAD_MS); scheduler();
|
||||
}
|
||||
function stopAudio(){ state.running=false; clearInterval(schedulerTimer); schedulerTimer=null; for(const m of meters) m.currentStep=-1; }
|
||||
function toggle(){ state.running ? stopAudio() : startAudio(); }
|
||||
|
||||
/* ========================= TRACKS (seed grooves, flattened) =================== */
|
||||
let tracks = SEED_SETLISTS.flatMap(sl => sl.items.map(([n,p]) => ({ name:n, ...patchToSetup(p) })));
|
||||
let trackIdx = 0;
|
||||
function tracksFromHash(){
|
||||
const m=(location.hash||"").match(/[#&](p|sl)=([^&]+)/); if(!m) return null;
|
||||
let payload=m[2]; try{ payload=decodeURIComponent(payload); }catch(e){}
|
||||
try{
|
||||
if(m[1]==="sl"){ const sl=codeToSetlist(payload); return sl.items.length ? sl.items.map(it=>({...it})) : null; }
|
||||
const s=patchToSetup(payload); return s.lanes.length ? [{name:"Patch",...s}] : null;
|
||||
}catch(e){ return null; }
|
||||
}
|
||||
function loadTrack(i){
|
||||
const n=tracks.length; if(!n) return; trackIdx=((i%n)+n)%n;
|
||||
const t=tracks[trackIdx]; setBpm(t.bpm||120); meters=buildMeters(t.lanes);
|
||||
const was=state.running; if(was){ clearInterval(schedulerTimer); schedulerTimer=null; state.running=false; }
|
||||
if(was) startAudio();
|
||||
}
|
||||
|
||||
/* ========================= SCREEN (canvas mirrors the firmware UI) ============ */
|
||||
const cv=$("screen"), g=cv.getContext("2d"), SW=320, SH=480;
|
||||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); cv.width=SW*dpr; cv.height=SH*dpr; g.scale(dpr,dpr); })();
|
||||
const COL={ bg:"#06090e", txt:"#c7d0db", mute:"#6e7a8a", cyan:"#0AB3F7", amber:"#ff9b2e",
|
||||
violet:"#967bff", green:"#2fe07a", dim:"#243240", btn:"#1c222c", panel:"#12161e" };
|
||||
const PRIO={2:3,1:2,3:1};
|
||||
const LEDCOL={2:[255,110,0],1:[0,150,255],3:[130,70,255]};
|
||||
let flash=0, flashLevel=1, beatIdx=-1, btnRects=[];
|
||||
|
||||
function rrect(x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
|
||||
|
||||
function layoutButtons(){
|
||||
btnRects=[]; const bw=96, bh=54, gap=(SW-3*bw)/4, xs=[gap, gap*2+bw, gap*3+bw*2];
|
||||
[["prev",300],["play",300],["next",300]].forEach((b,i)=>btnRects.push({x:xs[i],y:300,w:bw,h:bh,key:["prev","play","next"][i]}));
|
||||
["minus","tap","plus"].forEach((k,i)=>btnRects.push({x:xs[i],y:370,w:bw,h:bh,key:k}));
|
||||
}
|
||||
layoutButtons();
|
||||
|
||||
function drawScreen(){
|
||||
g.fillStyle=COL.bg; g.fillRect(0,0,SW,SH);
|
||||
// header
|
||||
g.textBaseline="alphabetic"; g.textAlign="left";
|
||||
g.fillStyle=COL.cyan; g.font="700 18px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("VARASYS",12,26);
|
||||
g.fillStyle=COL.mute; g.font="700 11px 'Segoe UI',Roboto,Arial,sans-serif"; g.textAlign="right"; g.fillText("PM_K‑1 KIT",SW-12,24);
|
||||
g.fillStyle=COL.panel; g.fillRect(0,34,SW,2);
|
||||
// BPM
|
||||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="700 20px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("BPM",12,150);
|
||||
g.textAlign="right"; g.fillStyle=COL.txt; g.font="800 92px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText(String(state.bpm),SW-12,176);
|
||||
// beat dots
|
||||
const m=meters[0]; if(m){ const bpb=m.beatsPerBar, sz=18, sp=26, x0=Math.max(12,SW-12-bpb*sp), y=196;
|
||||
for(let i=0;i<bpb;i++){ const accent=m.groupStarts.has(i)||(m.beatsOn[i*m.stepsPerBeat]|0)>=2;
|
||||
const on=state.running && i===beatIdx; g.fillStyle = on ? (accent?COL.amber:COL.cyan) : COL.dim;
|
||||
g.fillRect(x0+i*sp,y,sz,sz); } }
|
||||
// status
|
||||
g.textAlign="left"; g.fillStyle=state.running?COL.green:COL.mute; g.font="700 16px 'Segoe UI',Roboto,Arial,sans-serif";
|
||||
g.fillText(state.running?"▶ RUN":"■ STOP",12,256);
|
||||
const nm=(tracks[trackIdx]&&tracks[trackIdx].name||"—").slice(0,18);
|
||||
g.textAlign="right"; g.fillStyle=COL.txt; g.fillText(nm,SW-12,256);
|
||||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="600 11px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText((trackIdx+1)+"/"+tracks.length,12,276);
|
||||
// touch buttons
|
||||
const lbl={prev:"‹‹",play:state.running?"▮▮":"▶",next:"››",minus:"–",tap:"TAP",plus:"+"};
|
||||
for(const b of btnRects){ g.fillStyle=COL.btn; rrect(b.x,b.y,b.w,b.h,9); g.fill();
|
||||
g.fillStyle = b.key==="play" ? COL.green : COL.txt;
|
||||
g.font = (b.key==="minus"||b.key==="plus") ? "800 30px 'Segoe UI',Roboto,Arial,sans-serif" : "800 22px 'Segoe UI',Roboto,Arial,sans-serif";
|
||||
g.textAlign="center"; g.textBaseline="middle"; g.fillText(lbl[b.key], b.x+b.w/2, b.y+b.h/2+2); g.textBaseline="alphabetic"; }
|
||||
g.textAlign="left"; g.fillStyle=COL.mute; g.font="600 10px 'Segoe UI',Roboto,Arial,sans-serif";
|
||||
g.fillText("joystick: tempo / groove · A play B tap", 12, SH-14);
|
||||
}
|
||||
|
||||
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
|
||||
function frame(){
|
||||
const now = audioCtx ? audioCtx.currentTime - audioLatency() : 0;
|
||||
if(audioCtx && state.running){
|
||||
let fired=[];
|
||||
for(const m of meters){
|
||||
while(m.vqPtr<m.vq.length && m.vq[m.vqPtr].time<=now){
|
||||
const e=m.vq[m.vqPtr]; m.currentStep=e.step; m.currentBar=e.bar;
|
||||
const lvl=m.beatsOn[e.step]|0; if(lvl>0) fired.push(lvl);
|
||||
if(m===meters[0] && e.step % m.stepsPerBeat===0) beatIdx = e.step/m.stepsPerBeat;
|
||||
m.vqPtr++;
|
||||
}
|
||||
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; }
|
||||
}
|
||||
if(fired.length){ flashLevel = fired.sort((a,b)=>PRIO[b]-PRIO[a])[0]; flash=1; }
|
||||
}
|
||||
flash=Math.max(0,flash-0.08);
|
||||
// RGB LED element
|
||||
const c=LEDCOL[flashLevel]||LEDCOL[1], lit=flash;
|
||||
const led=$("led");
|
||||
if(lit>0.02){ const rgb="rgb("+c.map(v=>Math.round(v*lit)).join(",")+")";
|
||||
led.style.background=rgb; led.style.boxShadow="0 0 "+(8+lit*22)+"px rgb("+c.join(",")+")"; }
|
||||
else { led.style.background="#0c0f14"; led.style.boxShadow="0 0 4px #000 inset"; }
|
||||
drawScreen();
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
/* ========================= INPUTS ============================================ */
|
||||
function dispatch(key){
|
||||
if(key==="play") toggle();
|
||||
else if(key==="prev") loadTrack(trackIdx-1);
|
||||
else if(key==="next") loadTrack(trackIdx+1);
|
||||
else if(key==="minus") setBpm(state.bpm-1);
|
||||
else if(key==="plus") setBpm(state.bpm+1);
|
||||
else if(key==="tap") tapTempo();
|
||||
}
|
||||
cv.addEventListener("pointerdown",(e)=>{
|
||||
const r=cv.getBoundingClientRect(); const x=(e.clientX-r.left)*SW/r.width, y=(e.clientY-r.top)*SH/r.height;
|
||||
for(const b of btnRects){ if(x>=b.x&&x<=b.x+b.w&&y>=b.y&&y<=b.y+b.h){ dispatch(b.key); return; } }
|
||||
});
|
||||
|
||||
let taps=[];
|
||||
function tapTempo(){ const now=performance.now(); taps.push(now); taps=taps.filter(x=>now-x<2400);
|
||||
if(taps.length>=2){ let s=0; for(let i=1;i<taps.length;i++) s+=taps[i]-taps[i-1];
|
||||
const bpm=Math.round(60000/(s/(taps.length-1))); if(bpm>=30&&bpm<=300) setBpm(bpm); } }
|
||||
$("btnA").addEventListener("click",()=>toggle());
|
||||
$("btnB").addEventListener("click",()=>tapTempo());
|
||||
|
||||
/* joystick: drag up/down = tempo, left/right = groove */
|
||||
(function(){
|
||||
const joy=$("joy"), nub=$("nub"); let dragging=false, R=26, last=0, itemLatch=0;
|
||||
function at(e){ const r=joy.getBoundingClientRect(); return {x:e.clientX-r.left-r.width/2, y:e.clientY-r.top-r.height/2}; }
|
||||
function move(e){ if(!dragging) return; e.preventDefault();
|
||||
let {x,y}=at(e); const d=Math.hypot(x,y)||1, k=Math.min(1,R/d); let nx=x*k, ny=y*k;
|
||||
nub.style.transform="translate("+nx+"px,"+ny+"px)";
|
||||
const fy=-ny/R, fx=nx/R, now=performance.now();
|
||||
if(Math.abs(fy)>0.45 && now-last>80){ setBpm(state.bpm+(fy>0?1:-1)*(Math.abs(fy)>0.8?5:1)); last=now; }
|
||||
if(Math.abs(fx)>0.6){ if(now-itemLatch>320){ loadTrack(trackIdx+(fx>0?1:-1)); itemLatch=now; } }
|
||||
}
|
||||
function up(){ dragging=false; nub.style.transform="translate(0,0)"; }
|
||||
joy.addEventListener("pointerdown",(e)=>{ dragging=true; try{joy.setPointerCapture(e.pointerId);}catch(_){ } move(e); });
|
||||
joy.addEventListener("pointermove",move);
|
||||
joy.addEventListener("pointerup",up); joy.addEventListener("pointercancel",up);
|
||||
})();
|
||||
|
||||
/* theme toggle + version */
|
||||
/*@BUILD:include:src/chrome.js@*/
|
||||
|
||||
addEventListener("keydown",(e)=>{
|
||||
const tag=e.target?e.target.tagName:""; if(tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT") return;
|
||||
if(e.key===" "){ e.preventDefault(); toggle(); }
|
||||
else if(e.key==="t"||e.key==="T"){ tapTempo(); }
|
||||
else if(e.key==="ArrowUp"){ e.preventDefault(); setBpm(state.bpm+1); }
|
||||
else if(e.key==="ArrowDown"){ e.preventDefault(); setBpm(state.bpm-1); }
|
||||
else if(e.key==="ArrowRight"){ loadTrack(trackIdx+1); }
|
||||
else if(e.key==="ArrowLeft"){ loadTrack(trackIdx-1); }
|
||||
});
|
||||
|
||||
/* ========================= INIT ============================================== */
|
||||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||||
loadTrack(0);
|
||||
requestAnimationFrame(frame);
|
||||
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||||
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
|
||||
/*@BUILD:include:src/progbox.js@*/
|
||||
</script>
|
||||
|
||||
/*@BUILD:include:src/footer.html@*/
|
||||
</body>
|
||||
</html>
|
||||
76
pico/README.md
Normal file
76
pico/README.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# PM_K‑1 "Kit" — VARASYS PolyMeter firmware for the Raspberry Pi Pico
|
||||
|
||||
MicroPython firmware that turns a **Raspberry Pi Pico** on the **52Pi EP‑0172 "Pico
|
||||
Breadboard Kit Plus"** into a touchscreen polymeter metronome. It runs the *same program
|
||||
strings* as <https://metronome.varasys.io> — design a groove in the web editor, copy its
|
||||
program string, paste it into `PROGRAMS` in `main.py`, and it plays here.
|
||||
|
||||
Everything is in one file: `main.py` (ST7796 display driver, GT911 touch, WS2812 RGB,
|
||||
buzzer, joystick, the polymeter engine — no external libraries).
|
||||
|
||||
## The board (EP‑0172) — fixed pinout
|
||||
|
||||
| Component | Pico pins |
|
||||
|---|---|
|
||||
| 3.5″ ST7796 320×480 display | SPI0 — SCK `GP2`, MOSI `GP3`, CS `GP5`, DC `GP6`, RST `GP7` |
|
||||
| GT911 capacitive touch | I2C0 — SDA `GP8`, SCL `GP9` (addr 0x5D) |
|
||||
| WS2812 RGB LED | `GP12` |
|
||||
| Buzzer | `GP13` |
|
||||
| Button A / Button B | `GP15` / `GP14` |
|
||||
| PSP joystick | X = `ADC0`/`GP26`, Y = `ADC1`/`GP27` |
|
||||
|
||||
The components are wired on the board — you don't breadboard anything; just seat the Pico.
|
||||
|
||||
## Flash it
|
||||
|
||||
1. **MicroPython:** hold **BOOTSEL**, plug the Pico into USB, and copy the MicroPython UF2
|
||||
onto the `RPI-RP2` drive that appears:
|
||||
- Pico / Pico W → <https://micropython.org/download/RPI_PICO/>
|
||||
- Pico 2 / Pico 2 W → <https://micropython.org/download/RPI_PICO2/>
|
||||
2. **The firmware:** open `main.py` in [Thonny](https://thonny.org) (or `rshell`/`mpremote`)
|
||||
and save it to the Pico **as `main.py`**.
|
||||
- Thonny: open the file → **File ▸ Save as… ▸ Raspberry Pi Pico** → name it `main.py`.
|
||||
- mpremote: `mpremote cp main.py :main.py`
|
||||
3. Reset (replug). It boots straight into the metronome.
|
||||
|
||||
## Controls
|
||||
|
||||
- **Touch:** on‑screen `<<` / `>||` / `>>` (prev · play/stop · next) and `−` / `TAP` / `+`.
|
||||
- **Joystick:** up/down = tempo (push far for ±5), left/right = previous/next groove.
|
||||
- **Button A (GP15):** play / stop. **Button B (GP14):** tap tempo.
|
||||
- **RGB LED** flashes each beat (amber = accent, cyan = normal, violet = ghost); the
|
||||
**buzzer** clicks with matching pitch.
|
||||
|
||||
## Add your own grooves
|
||||
|
||||
Edit the `PROGRAMS` list near the top of `main.py` — each entry is `("Name", "program string")`.
|
||||
Get program strings from the web editor's program box (e.g. `v1;t120;kick:4;snare:4=.X.X;hat:4/2`).
|
||||
Supported: tempo `t<bpm>`, lanes `sound:grouping[/sub][=pattern][~][!]`, pattern chars
|
||||
`X` accent · `x` normal · `g` ghost · `.` `-` `_` rest, grouped meters like `3+3+2`, polymeter `~`.
|
||||
(Per‑lane dB gain `@n` is parsed but ignored — the buzzer is mono.)
|
||||
|
||||
## If something looks off — calibration
|
||||
|
||||
All the knobs are flags in the `CONFIG` block at the top of `main.py`:
|
||||
|
||||
- **Colours look negative / washed out:** toggle `INVERT_COLORS`.
|
||||
- **Red and blue swapped:** set `SWAP_RB = True`.
|
||||
- **Taps land in the wrong place:** set `TOUCH_DEBUG = True`, reset, watch the raw
|
||||
coordinates over the USB serial (Thonny shell) as you tap the corners, then set
|
||||
`TOUCH_SWAP_XY` / `TOUCH_INVERT_X` / `TOUCH_INVERT_Y` to match.
|
||||
- **Joystick reversed:** toggle `JOY_INVERT_X` / `JOY_INVERT_Y`; widen `JOY_DEADZONE` if it drifts.
|
||||
- **Screen stays black:** the backlight is hardwired on, so this usually means the SPI init
|
||||
didn't take — drop `SPI_BAUD` to `24_000_000` and retry.
|
||||
- **Garbled / wrong size image:** your panel lot may be a 240×320 ILI9341 instead of the
|
||||
320×480 ST7796. This firmware targets the ST7796 you have (you said 320×480); if a unit
|
||||
ever ships ILI9341, set `WIDTH,HEIGHT = 240,320` and use an ILI9341 init sequence.
|
||||
|
||||
## Notes
|
||||
|
||||
- Audio is a single passive buzzer, so coincident lane hits play one click at the highest
|
||||
priority (accent > normal > ghost); the RGB + screen still show the combined activity.
|
||||
- The scheduler is non‑blocking and timed off `time.ticks_us()`, so tempo stays steady while
|
||||
the screen and inputs update.
|
||||
|
||||
Hardware reference: [52Pi EP‑0172 wiki](https://wiki.52pi.com/index.php?title=EP-0172) ·
|
||||
[vendor code](https://github.com/geeekpi/pico_breakboard_kit). VARASYS — Simplifying Complexity.
|
||||
BIN
pico/__pycache__/main.cpython-312.pyc
Normal file
BIN
pico/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
492
pico/main.py
Normal file
492
pico/main.py
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
# VARASYS PolyMeter — PM_K-1 "Kit" firmware
|
||||
# Raspberry Pi Pico (or Pico W / Pico 2) on the 52Pi EP-0172 "Pico Breadboard Kit Plus":
|
||||
# 3.5" ST7796 320x480 capacitive-touch screen (GT911), PSP joystick, WS2812 RGB, buzzer, 2 buttons.
|
||||
#
|
||||
# It runs the SAME program-string language as https://metronome.varasys.io — design a groove in
|
||||
# the web editor, copy its program string, paste it into PROGRAMS below, and it plays here.
|
||||
#
|
||||
# FLASH: 1) Hold BOOTSEL, plug in the Pico, drop the MicroPython UF2 on the RPI-RP2 drive
|
||||
# (https://micropython.org/download/RPI_PICO/ ; use RPI_PICO2 for a Pico 2).
|
||||
# 2) Copy THIS file to the Pico as main.py (Thonny: File > Save as > Raspberry Pi Pico).
|
||||
# 3) Reset. It boots straight into the metronome.
|
||||
#
|
||||
# IF SOMETHING LOOKS WRONG, flip a flag in CONFIG below (colours, inversion, touch axes) — see README.md.
|
||||
#
|
||||
# MIT-style: do whatever you like with it. VARASYS — Simplifying Complexity.
|
||||
|
||||
from machine import Pin, SPI, I2C, ADC, PWM
|
||||
import time, framebuf
|
||||
|
||||
try:
|
||||
import neopixel
|
||||
except ImportError:
|
||||
neopixel = None
|
||||
|
||||
# ============================== CONFIG (tweak if needed) ==============================
|
||||
SPI_BAUD = 40_000_000 # 40 MHz is a safe-fast default; the vendor demo uses 62.5 MHz
|
||||
WIDTH, HEIGHT = 320, 480 # ST7796 portrait
|
||||
MADCTL = 0x48 # memory access ctrl (MX | BGR) -> portrait, 320 wide x 480 tall
|
||||
INVERT_COLORS = True # most ST7796 modules need display inversion ON; set False if colours look negative
|
||||
SWAP_RB = False # set True if red and blue are swapped
|
||||
# Touch (GT911) calibration — flip these if taps land on the wrong spot:
|
||||
TOUCH_SWAP_XY = False
|
||||
TOUCH_INVERT_X = False
|
||||
TOUCH_INVERT_Y = False
|
||||
TOUCH_DEBUG = False # True -> print raw touch coords over USB serial to calibrate
|
||||
|
||||
# Joystick calibration:
|
||||
JOY_INVERT_X = False
|
||||
JOY_INVERT_Y = False
|
||||
JOY_DEADZONE = 9000 # of 0..65535 around centre
|
||||
|
||||
# ----- pins (fixed by the EP-0172 board) -----
|
||||
PIN_SCK, PIN_MOSI, PIN_CS, PIN_DC, PIN_RST = 2, 3, 5, 6, 7
|
||||
PIN_SDA, PIN_SCL = 8, 9
|
||||
PIN_RGB = 12
|
||||
PIN_BUZZER = 13
|
||||
PIN_BTN_A = 15 # play / stop
|
||||
PIN_BTN_B = 14 # tap tempo
|
||||
PIN_JOY_X = 26 # ADC0
|
||||
PIN_JOY_Y = 27 # ADC1
|
||||
|
||||
# ----- the grooves on the device (paste program strings from the web editor) -----
|
||||
PROGRAMS = [
|
||||
("Four on the floor", "v1;t120;kick:4;snare:4=.X.X;hat:4/2"),
|
||||
("Son clave 3-2", "v1;t100;clap:4=X..X..X.;kick:4"),
|
||||
("7/8 + 4 polymeter", "v1;t132;kick:7/2;hat:4/2~;snare:4=..X."),
|
||||
("Shuffle", "v1;t96;kick:4;snare:4=.X.X;hat:4/3"),
|
||||
("Straight click", "v1;t120;beep:4"),
|
||||
]
|
||||
|
||||
# ============================== COLOURS ==============================
|
||||
def rgb565(r, g, b):
|
||||
if SWAP_RB: r, b = b, r
|
||||
v = ((b & 0xF8) << 8) | ((g & 0xFC) << 3) | (r >> 3) # packed BGR for the BGR panel
|
||||
return bytes((v >> 8, v & 0xFF)) # MSB-first for ST7796
|
||||
|
||||
C_BG = rgb565(6, 9, 14)
|
||||
C_PANEL = rgb565(18, 22, 30)
|
||||
C_TXT = rgb565(199, 208, 219)
|
||||
C_MUTE = rgb565(110, 122, 138)
|
||||
C_CYAN = rgb565(10, 179, 247) # VARASYS brand cyan / normal beat
|
||||
C_AMBER = rgb565(255, 155, 46) # accent
|
||||
C_VIOLET = rgb565(150, 100, 255) # ghost
|
||||
C_GREEN = rgb565(47, 224, 122) # running
|
||||
C_DIMDOT = rgb565(36, 50, 64)
|
||||
C_BTN = rgb565(28, 34, 44)
|
||||
C_BTNHI = rgb565(40, 52, 66)
|
||||
LEVEL_COL = {2: C_AMBER, 1: C_CYAN, 3: C_VIOLET, 0: C_DIMDOT}
|
||||
LEVEL_RGB = {2: (255, 110, 0), 1: (0, 150, 255), 3: (130, 70, 255)} # WS2812 (logical r,g,b)
|
||||
|
||||
# ============================== ST7796 DISPLAY ==============================
|
||||
class ST7796:
|
||||
def __init__(self):
|
||||
self.spi = SPI(0, baudrate=SPI_BAUD, polarity=0, phase=0,
|
||||
sck=Pin(PIN_SCK), mosi=Pin(PIN_MOSI))
|
||||
self.cs = Pin(PIN_CS, Pin.OUT, value=1)
|
||||
self.dc = Pin(PIN_DC, Pin.OUT, value=0)
|
||||
self.rst = Pin(PIN_RST, Pin.OUT, value=1)
|
||||
self._chunk = bytearray(1024) # scratch for fills (512 px)
|
||||
self.reset(); self.init()
|
||||
|
||||
def _cmd(self, c, data=None):
|
||||
self.cs(0); self.dc(0); self.spi.write(bytes((c,)))
|
||||
if data is not None:
|
||||
self.dc(1); self.spi.write(bytes(data))
|
||||
self.cs(1)
|
||||
|
||||
def reset(self):
|
||||
self.rst(1); time.sleep_ms(20); self.rst(0); time.sleep_ms(40); self.rst(1); time.sleep_ms(150)
|
||||
|
||||
def init(self):
|
||||
c = self._cmd
|
||||
c(0x01); time.sleep_ms(120) # software reset
|
||||
c(0x11); time.sleep_ms(120) # sleep out
|
||||
c(0xF0, b'\xC3'); c(0xF0, b'\x96') # command set control (unlock)
|
||||
c(0x36, bytes((MADCTL,)))
|
||||
c(0x3A, b'\x55') # 16 bits/pixel (RGB565)
|
||||
c(0xB4, b'\x01') # 1-dot inversion
|
||||
c(0xB6, b'\x80\x02\x3B') # display function control
|
||||
c(0xE8, b'\x40\x8A\x00\x00\x29\x19\xA5\x33')
|
||||
c(0xC1, b'\x06') # power control 2
|
||||
c(0xC2, b'\xA7') # power control 3
|
||||
c(0xC5, b'\x18'); time.sleep_ms(120) # VCOM
|
||||
c(0xE0, b'\xF0\x09\x0B\x06\x04\x15\x2F\x54\x42\x3C\x17\x14\x18\x1B') # +gamma
|
||||
c(0xE1, b'\xE0\x09\x0B\x06\x04\x03\x2B\x43\x42\x3B\x16\x14\x17\x1B') # -gamma
|
||||
c(0xF0, b'\x3C'); c(0xF0, b'\x69'); time.sleep_ms(120) # lock command set
|
||||
c(0x21 if INVERT_COLORS else 0x20) # inversion on/off
|
||||
c(0x29) # display on
|
||||
time.sleep_ms(50)
|
||||
|
||||
def _window(self, x, y, w, h):
|
||||
x1, y1 = x + w - 1, y + h - 1
|
||||
self._cmd(0x2A, bytes((x >> 8, x & 0xFF, x1 >> 8, x1 & 0xFF)))
|
||||
self._cmd(0x2B, bytes((y >> 8, y & 0xFF, y1 >> 8, y1 & 0xFF)))
|
||||
self.cs(0); self.dc(0); self.spi.write(bytes((0x2C,))); self.dc(1) # leaves us mid-RAMWR
|
||||
|
||||
def fill_rect(self, x, y, w, h, color):
|
||||
if w <= 0 or h <= 0: return
|
||||
self._window(x, y, w, h)
|
||||
ch = self._chunk; px = len(ch) // 2
|
||||
for i in range(px): ch[i*2] = color[0]; ch[i*2+1] = color[1]
|
||||
n = w * h
|
||||
while n > 0:
|
||||
k = px if n >= px else n
|
||||
self.spi.write(ch if k == px else ch[:k*2]); n -= k
|
||||
self.cs(1)
|
||||
|
||||
def fill(self, color): self.fill_rect(0, 0, WIDTH, HEIGHT, color)
|
||||
|
||||
# text via the built-in 8x8 mono font, expanded to colour and integer-scaled
|
||||
def text(self, s, x, y, fg, bg, scale=2):
|
||||
if not s: return
|
||||
w8 = len(s) * 8
|
||||
stride = w8 // 8
|
||||
mbuf = bytearray(stride * 8)
|
||||
mfb = framebuf.FrameBuffer(mbuf, w8, 8, framebuf.MONO_HLSB)
|
||||
mfb.fill(0); mfb.text(s, 0, 0, 1)
|
||||
dw = w8 * scale
|
||||
row = bytearray(dw * 2)
|
||||
self._window(x, y, dw, 8 * scale)
|
||||
for r in range(8):
|
||||
base = r * stride
|
||||
di = 0
|
||||
for col in range(w8):
|
||||
bit = (mbuf[base + (col >> 3)] >> (7 - (col & 7))) & 1
|
||||
cpx = fg if bit else bg
|
||||
for _ in range(scale):
|
||||
row[di] = cpx[0]; row[di+1] = cpx[1]; di += 2
|
||||
for _ in range(scale): self.spi.write(row)
|
||||
self.cs(1)
|
||||
|
||||
def text_w(self, s, scale=2): return len(s) * 8 * scale
|
||||
|
||||
# seven-segment digit renderer (for the big BPM) — no font, just rectangles
|
||||
_SEG = { # a,b,c,d,e,f,g
|
||||
'0': 0b1111110, '1': 0b0110000, '2': 0b1101101, '3': 0b1111001,
|
||||
'4': 0b0110011, '5': 0b1011011, '6': 0b1011111, '7': 0b1110000,
|
||||
'8': 0b1111111, '9': 0b1111011, ' ': 0b0000000, '-': 0b0000001,
|
||||
}
|
||||
def draw_digit(d, ch, x, y, W, H, T, on, off):
|
||||
seg = _SEG.get(ch, 0); v = (H - 3 * T) // 2
|
||||
rects = [
|
||||
(x + T, y, W - 2*T, T, 6), # a top
|
||||
(x + W - T, y + T, T, v, 5), # b top-right
|
||||
(x + W - T, y + 2*T + v, T, v, 4), # c bottom-right
|
||||
(x + T, y + H - T, W - 2*T, T, 3), # d bottom
|
||||
(x, y + 2*T + v, T, v, 2), # e bottom-left
|
||||
(x, y + T, T, v, 1), # f top-left
|
||||
(x + T, y + T + v, W - 2*T, T, 0), # g middle
|
||||
]
|
||||
for rx, ry, rw, rh, bitpos in rects:
|
||||
d.fill_rect(rx, ry, rw, rh, on if (seg >> bitpos) & 1 else off)
|
||||
|
||||
# ============================== GT911 TOUCH ==============================
|
||||
class GT911:
|
||||
def __init__(self, i2c):
|
||||
self.i2c = i2c; self.addr = None
|
||||
found = i2c.scan()
|
||||
for a in (0x5D, 0x14):
|
||||
if a in found: self.addr = a; break
|
||||
if self.addr is None and found: self.addr = found[0]
|
||||
def read(self):
|
||||
if self.addr is None: return None
|
||||
try: # GT911 uses 16-bit register addresses
|
||||
st = self.i2c.readfrom_mem(self.addr, 0x814E, 1, addrsize=16)[0]
|
||||
except OSError:
|
||||
return None
|
||||
if not (st & 0x80):
|
||||
return None
|
||||
n = st & 0x0F
|
||||
pt = None
|
||||
if n >= 1:
|
||||
b = self.i2c.readfrom_mem(self.addr, 0x8150, 4, addrsize=16)
|
||||
tx = b[0] | (b[1] << 8); ty = b[2] | (b[3] << 8)
|
||||
pt = self._map(tx, ty)
|
||||
try: self.i2c.writeto_mem(self.addr, 0x814E, b'\x00', addrsize=16) # clear ready flag
|
||||
except OSError: pass
|
||||
return pt
|
||||
def _map(self, tx, ty):
|
||||
if TOUCH_DEBUG: print("touch raw", tx, ty)
|
||||
if TOUCH_SWAP_XY: tx, ty = ty, tx
|
||||
if TOUCH_INVERT_X: tx = WIDTH - 1 - tx
|
||||
if TOUCH_INVERT_Y: ty = HEIGHT - 1 - ty
|
||||
if 0 <= tx < WIDTH and 0 <= ty < HEIGHT: return (tx, ty)
|
||||
return None
|
||||
|
||||
# ============================== POLYMETER ENGINE ==============================
|
||||
# program string: v1;t<bpm>;[vol];[cd];[b];<lane>;<lane>;...
|
||||
# lane = <sound>:<grouping>[/<sub>[s]][=pattern][@db][~][!]
|
||||
# pattern chars: X=accent(2) x=normal(1) g=ghost(3) . - _ =mute(0)
|
||||
PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0}
|
||||
PRIO = {2: 3, 1: 2, 3: 1} # click priority when lanes coincide: accent > normal > ghost
|
||||
|
||||
def parse_program(s):
|
||||
bpm = 120; lanes = []
|
||||
for tok in s.strip().split(';'):
|
||||
tok = tok.strip()
|
||||
if not tok: continue
|
||||
if tok[0] == 't' and tok[1:].isdigit():
|
||||
bpm = int(tok[1:]); continue
|
||||
if ':' not in tok: # skip v1, vol, cd, b and other globals we don't need on-device
|
||||
continue
|
||||
lane = _parse_lane(tok)
|
||||
if lane: lanes.append(lane)
|
||||
if not lanes: lanes = [_parse_lane("beep:4")]
|
||||
return max(30, min(300, bpm)), lanes
|
||||
|
||||
def _parse_lane(tok):
|
||||
poly = '~' in tok
|
||||
mute = '!' in tok
|
||||
tok = tok.replace('~', '').replace('!', '')
|
||||
db = 0
|
||||
if '@' in tok:
|
||||
tok, _, rest = tok.partition('@')
|
||||
try: db = int(rest)
|
||||
except: db = 0
|
||||
sound, _, rest = tok.partition(':')
|
||||
pattern = None
|
||||
if '=' in rest:
|
||||
rest, _, pattern = rest.partition('=')
|
||||
sub = 1
|
||||
if '/' in rest:
|
||||
rest, _, sd = rest.partition('/')
|
||||
sd = sd.rstrip('s') # ignore swing flag on-device
|
||||
sub = int(sd) if sd.isdigit() else 1
|
||||
# grouping: "4" or "3+3+2"
|
||||
groups = [int(g) for g in rest.split('+') if g.isdigit()] or [4]
|
||||
beats = sum(groups)
|
||||
starts = set(); acc = 0
|
||||
for g in groups: starts.add(acc); acc += g
|
||||
steps = beats * sub
|
||||
if pattern:
|
||||
levels = [PAT.get(c, 0) for c in pattern]
|
||||
if len(levels) < steps: levels += [0] * (steps - len(levels))
|
||||
steps = len(levels)
|
||||
else:
|
||||
levels = []
|
||||
for i in range(steps):
|
||||
if i % sub == 0:
|
||||
levels.append(2 if (i // sub) in starts else 1)
|
||||
else:
|
||||
levels.append(0)
|
||||
return {'sound': sound, 'sub': sub, 'steps': steps, 'levels': levels,
|
||||
'poly': poly, 'mute': mute, 'db': db}
|
||||
|
||||
# ============================== APP ==============================
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.d = ST7796()
|
||||
self.i2c = I2C(0, sda=Pin(PIN_SDA), scl=Pin(PIN_SCL), freq=100_000)
|
||||
self.touch = GT911(self.i2c)
|
||||
self.np = neopixel.NeoPixel(Pin(PIN_RGB), 1) if neopixel else None
|
||||
self.buz = PWM(Pin(PIN_BUZZER)); self.buz.duty_u16(0)
|
||||
self.buz_off = 0
|
||||
self.btnA = Pin(PIN_BTN_A, Pin.IN, Pin.PULL_UP)
|
||||
self.btnB = Pin(PIN_BTN_B, Pin.IN, Pin.PULL_UP)
|
||||
self._aPrev = 1; self._bPrev = 1
|
||||
self.jx = ADC(PIN_JOY_X); self.jy = ADC(PIN_JOY_Y)
|
||||
self._joyNext = 0
|
||||
self._touchLock = 0; self._unpressAt = 0; self._pending = None
|
||||
self.running = False
|
||||
self.bpm = 120
|
||||
self.idx = 0
|
||||
self.lanes = []
|
||||
self.rgb = (0, 0, 0)
|
||||
self.buttons = [] # touch hit zones: (x,y,w,h,key)
|
||||
self.load(0)
|
||||
self.draw_static()
|
||||
self.draw_bpm(force=True)
|
||||
self.draw_status()
|
||||
self.draw_dots(force=True)
|
||||
|
||||
# ---------- program ----------
|
||||
def load(self, i):
|
||||
n = len(PROGRAMS); self.idx = i % n
|
||||
name, prog = PROGRAMS[self.idx]
|
||||
self.name = name
|
||||
self.bpm, self.lanes = parse_program(prog)
|
||||
self.master = self.lanes[0]
|
||||
self.beat = -1
|
||||
self._reset_clock()
|
||||
|
||||
def _reset_clock(self):
|
||||
now = time.ticks_us()
|
||||
for L in self.lanes:
|
||||
L['next'] = now
|
||||
L['step'] = -1
|
||||
L['stepdur'] = int(60_000_000 / self.bpm / L['sub'])
|
||||
|
||||
# ---------- audio + light ----------
|
||||
def click(self, level):
|
||||
f = {2: 2300, 1: 1600, 3: 1050}.get(level, 1600)
|
||||
duty = {2: 42000, 1: 30000, 3: 14000}.get(level, 30000)
|
||||
self.buz.freq(f); self.buz.duty_u16(duty)
|
||||
self.buz_off = time.ticks_add(time.ticks_us(), 22000) # 22 ms
|
||||
def flash(self, level):
|
||||
self.rgb = LEVEL_RGB.get(level, (0, 150, 255))
|
||||
if self.np: self.np[0] = self.rgb; self.np.write()
|
||||
|
||||
# ---------- transport ----------
|
||||
def toggle(self):
|
||||
self.running = not self.running
|
||||
if self.running: self._reset_clock(); self.beat = -1
|
||||
else:
|
||||
self.buz.duty_u16(0)
|
||||
if self.np: self.np[0] = (0, 0, 0); self.np.write()
|
||||
self.draw_status(); self.draw_dots(force=True)
|
||||
def set_bpm(self, v):
|
||||
v = max(30, min(300, v))
|
||||
if v != self.bpm:
|
||||
self.bpm = v
|
||||
for L in self.lanes: L['stepdur'] = int(60_000_000 / self.bpm / L['sub'])
|
||||
self.draw_bpm()
|
||||
def goto(self, i):
|
||||
was = self.running; self.load(i)
|
||||
self.draw_bpm(force=True); self.draw_status(); self.draw_dots(force=True)
|
||||
if was: self.running = True; self._reset_clock(); self.beat = -1
|
||||
def tap(self):
|
||||
now = time.ticks_ms()
|
||||
if not hasattr(self, '_taps'): self._taps = []
|
||||
self._taps = [t for t in self._taps if time.ticks_diff(now, t) < 2400]
|
||||
self._taps.append(now)
|
||||
if len(self._taps) >= 2:
|
||||
span = time.ticks_diff(self._taps[-1], self._taps[0]) / (len(self._taps) - 1)
|
||||
if span > 0: self.set_bpm(round(60000 / span))
|
||||
|
||||
# ---------- scheduler (call often) ----------
|
||||
def tick(self):
|
||||
now = time.ticks_us()
|
||||
if self.buz_off and time.ticks_diff(now, self.buz_off) >= 0:
|
||||
self.buz.duty_u16(0); self.buz_off = 0
|
||||
if self.running:
|
||||
fired = []; beat_hit = False
|
||||
for L in self.lanes:
|
||||
while time.ticks_diff(now, L['next']) >= 0:
|
||||
L['step'] = (L['step'] + 1) % L['steps']
|
||||
lvl = 0 if L['mute'] else L['levels'][L['step']]
|
||||
if lvl > 0: fired.append(lvl)
|
||||
if L is self.master and L['step'] % L['sub'] == 0:
|
||||
beat_hit = True
|
||||
L['next'] = time.ticks_add(L['next'], L['stepdur'])
|
||||
if fired:
|
||||
best = max(fired, key=lambda l: PRIO.get(l, 0)) # accent > normal > ghost
|
||||
self.click(best); self.flash(best)
|
||||
if beat_hit:
|
||||
self.beat = (self.master['step'] // self.master['sub'])
|
||||
self.draw_dots()
|
||||
# fade the RGB between beats
|
||||
if self.rgb != (0, 0, 0):
|
||||
r, g, b = self.rgb; r = r*7//10; g = g*7//10; b = b*7//10
|
||||
self.rgb = (r, g, b) if (r + g + b) > 12 else (0, 0, 0)
|
||||
if self.np: self.np[0] = self.rgb; self.np.write()
|
||||
|
||||
# ---------- inputs ----------
|
||||
def poll(self):
|
||||
a = self.btnA.value()
|
||||
if a == 0 and self._aPrev == 1: self.toggle()
|
||||
self._aPrev = a
|
||||
b = self.btnB.value()
|
||||
if b == 0 and self._bPrev == 1: self.tap()
|
||||
self._bPrev = b
|
||||
# joystick: up/down = tempo, left/right = prev/next item (with repeat)
|
||||
now = time.ticks_ms()
|
||||
if time.ticks_diff(now, self._joyNext) >= 0:
|
||||
x = self.jx.read_u16() - 32768; y = self.jy.read_u16() - 32768
|
||||
if JOY_INVERT_X: x = -x
|
||||
if JOY_INVERT_Y: y = -y
|
||||
acted = False
|
||||
if abs(y) > JOY_DEADZONE:
|
||||
self.set_bpm(self.bpm + (1 if y > 0 else -1) * (5 if abs(y) > 26000 else 1)); acted = True
|
||||
elif abs(x) > JOY_DEADZONE:
|
||||
self.goto(self.idx + (1 if x > 0 else -1)); acted = True
|
||||
self._joyNext = time.ticks_add(now, 350); return
|
||||
self._joyNext = time.ticks_add(now, 70 if acted else 20)
|
||||
# touch — non-blocking: redraw a pressed button after its hold, debounce repeats
|
||||
if self._unpressAt and time.ticks_diff(now, self._unpressAt) >= 0:
|
||||
x, y, w, h, key = self._pending; self._draw_button(x, y, w, h, key)
|
||||
self._unpressAt = 0
|
||||
if time.ticks_diff(now, self._touchLock) >= 0:
|
||||
pt = self.touch.read()
|
||||
if pt: self.hit(pt[0], pt[1])
|
||||
|
||||
def hit(self, x, y):
|
||||
for bx, by, bw, bh, key in self.buttons:
|
||||
if bx <= x <= bx+bw and by <= y <= by+bh:
|
||||
self.d.fill_rect(bx, by, bw, bh, C_BTNHI) # pressed flash
|
||||
if key == 'play': self.toggle()
|
||||
elif key == 'prev': self.goto(self.idx - 1)
|
||||
elif key == 'next': self.goto(self.idx + 1)
|
||||
elif key == 'minus': self.set_bpm(self.bpm - 1)
|
||||
elif key == 'plus': self.set_bpm(self.bpm + 1)
|
||||
elif key == 'tap': self.tap()
|
||||
self._pending = (bx, by, bw, bh, key)
|
||||
self._unpressAt = time.ticks_add(time.ticks_ms(), 120)
|
||||
self._touchLock = time.ticks_add(time.ticks_ms(), 280) # ignore held finger
|
||||
return
|
||||
|
||||
# ---------- drawing ----------
|
||||
def draw_static(self):
|
||||
d = self.d; d.fill(C_BG)
|
||||
d.text("VARASYS", 12, 12, C_CYAN, C_BG, 2)
|
||||
d.text("PM_K-1 KIT", WIDTH - d.text_w("PM_K-1 KIT", 1) - 12, 16, C_MUTE, C_BG, 1)
|
||||
d.fill_rect(0, 34, WIDTH, 2, C_PANEL)
|
||||
d.text("BPM", 12, 196, C_MUTE, C_BG, 2)
|
||||
# build + paint the touch buttons
|
||||
self.buttons = []
|
||||
row1 = 300; bw = 96; bh = 54; gap = (WIDTH - 3*bw) // 4
|
||||
xs = [gap, gap*2 + bw, gap*3 + bw*2]
|
||||
for x, key in zip(xs, ('prev', 'play', 'next')):
|
||||
self.buttons.append((x, row1, bw, bh, key)); self._draw_button(x, row1, bw, bh, key)
|
||||
row2 = row1 + bh + 16
|
||||
for x, key in zip(xs, ('minus', 'tap', 'plus')):
|
||||
self.buttons.append((x, row2, bw, bh, key)); self._draw_button(x, row2, bw, bh, key)
|
||||
d.text("joystick: tempo / item button A: play B: tap", 12, HEIGHT - 20, C_MUTE, C_BG, 1)
|
||||
|
||||
def _draw_button(self, x, y, w, h, key):
|
||||
d = self.d; d.fill_rect(x, y, w, h, C_BTN)
|
||||
d.fill_rect(x, y, w, 2, C_PANEL); d.fill_rect(x, y+h-2, w, 2, C_PANEL)
|
||||
label = {'prev':'<<','play':'>||','next':'>>','minus':'-','plus':'+','tap':'TAP'}[key]
|
||||
col = C_GREEN if key == 'play' else C_TXT
|
||||
sc = 3 if key in ('minus','plus') else 2
|
||||
tw = d.text_w(label, sc)
|
||||
d.text(label, x + (w - tw)//2, y + (h - 8*sc)//2, col, C_BTN, sc)
|
||||
|
||||
def draw_bpm(self, force=False):
|
||||
d = self.d
|
||||
s = "%3d" % self.bpm
|
||||
W = 64; H = 96; T = 12; gap = 12; x0 = WIDTH - 12 - (3*W + 2*gap); y0 = 92
|
||||
for i, ch in enumerate(s):
|
||||
draw_digit(d, ch, x0 + i*(W+gap), y0, W, H, T, C_TXT, C_BG)
|
||||
|
||||
def draw_status(self):
|
||||
d = self.d
|
||||
d.fill_rect(0, 240, WIDTH, 40, C_BG)
|
||||
st = ">RUN" if self.running else "=STOP"
|
||||
d.text(st, 12, 244, C_GREEN if self.running else C_MUTE, C_BG, 2)
|
||||
nm = self.name[:18]
|
||||
d.text(nm, WIDTH - d.text_w(nm, 2) - 12, 244, C_TXT, C_BG, 2)
|
||||
d.text("%d/%d" % (self.idx+1, len(PROGRAMS)), 12, 266, C_MUTE, C_BG, 1)
|
||||
|
||||
def draw_dots(self, force=False):
|
||||
d = self.d; m = self.master
|
||||
bpb = max(1, m['steps'] // m['sub'])
|
||||
yy = 200; sz = 18; sp = 26
|
||||
x0 = max(12, WIDTH - 12 - bpb * sp)
|
||||
d.fill_rect(0, yy, WIDTH, sz, C_BG) # clear the dot row
|
||||
for i in range(bpb):
|
||||
lvl = m['levels'][(i*m['sub']) % m['steps']] # accent (2) shows amber when lit
|
||||
on = self.running and i == self.beat
|
||||
col = (C_AMBER if lvl == 2 else C_CYAN) if on else C_DIMDOT
|
||||
d.fill_rect(x0 + i*sp, yy, sz, sz, col)
|
||||
|
||||
def run(self):
|
||||
if self.touch.addr is None:
|
||||
self.d.text("touch: not found", 12, HEIGHT - 40, C_AMBER, C_BG, 1)
|
||||
while True:
|
||||
self.tick()
|
||||
self.poll()
|
||||
time.sleep_us(200)
|
||||
|
||||
# ============================== GO ==============================
|
||||
App().run()
|
||||
Loading…
Reference in a new issue