The keys have no velocity and no aftertouch. The BMI270 does.
Every mode in this firmware falls out of that one trade: the keyboard gives you
pitch, and your wrist gives you everything a keyboard can't.
01The instrument
Six sensors, one radio, and a 56-key grid. What makes this playable rather
than fiddly is that each sensor does exactly one job, and none of them
duplicates another.
FIG. 1Sensor assignments. Each input has exactly one musical job; nothing is doubled up,
which is why modes can share the same fingering without fighting.
02Signal path
Three physical inputs converge on one MIDI stream. Pitch is discrete and comes
from the keys; everything continuous — pressure, timbre, dynamics — comes from
the body of the device.
FIG. 2The rate limiter is load-bearing, not an optimisation. Tilt jitters constantly, and a
BLE link busy carrying redundant CC updates is exactly what makes cheap wireless
controllers feel late.
03The grid
Four rows of fourteen is a terrible piano and an excellent harmonic grid. All
three layouts are isomorphic: the interval between two keys
depends only on how far apart they are, never on where they sit. A chord is one
shape, and you transpose it by moving your hand.
The explorer below runs the same arithmetic as the firmware —
gridToNote() and chordName() ported verbatim from
scales.cpp. Click keys to build a chord and it names it the way the
device will. Learn the shapes here before the hardware arrives.
Chord
—
Held
click keys above
Range
—
Which layout, and when
Layout
Right
Up
Reach for it when
4ths
+1 semitone
+5
Default. Same tuning logic as a bass or a Linnstrument — if your hands know a fretboard, they already know this.
Wicki
+2 semitones
+5
Up-right is a perfect fifth and rows run in whole tones. Melodies sit under one hand; wide voicings are easy.
Degree
+1 degree
+3 degrees
The grid becomes the scale itself. Vertical neighbours are thirds, so stacking a triad is a straight line up. You cannot play a wrong note.
Scale lock snaps chromatic results down to the nearest scale tone, so it changes
how 4ths and Wicki behave but does nothing
in Degree — that layout is already built from scale steps.
04The six modes
Switch with Fn + a number. Pressing Fn also silences
everything sounding, so nothing survives a switch and it doubles as a panic key.
01
NOTES
Fn + 1
The core playing mode. Isomorphic grid, scale-locked, with the chord under
your fingers named on screen in real time.
While notes are held, tilt is live: leaning forward and back sends channel
pressure, rolling left and right sends CC1. That is the whole thesis in one
gesture — you fret a chord and lean into it, and the sound moves.
ENTER-less
every playable key is a note; no dead space to hunt for
Fn+L
cycle layout
Fn+K
scale lock on / off
Fn+A
tilt aftertouch on / off
Fn+V
velocity source: strike / tilt / fixed
Use case
Writing on the couch with the laptop closed. Scale lock on, Degree layout,
and you can noodle at a chord progression without once looking down —
the screen names what you just found so you can write it back later.
02
XY
Fn + 2
The whole card becomes an XY pad. Roll sends CC1, pitch sends CC74 — the
two most commonly mapped destinations in any synth, mod depth and filter
cutoff. Any key latches the current position so you can set the device down
without the values sliding away with gravity.
any key
latch / unlatch
Fn+C
re-zero tilt to how you're holding it
Fn+X / Fn+Y
invert an axis
Use case
Recording a filter sweep that a mouse can't do convincingly. Two hands on the
card, arm the track, and perform the automation as a physical gesture. Latch
it at the top of the sweep and it stays there.
03
BREATH
Fn + 3
Fingers on the grid pick pitch; the mic decides whether and how loudly it
sounds. This is the EWI workflow — you articulate by blowing, not by
pressing. Hold a note and it stays silent until you breathe into the device.
Envelope is fast-attack and slow-release so the front of a phrase lands on
time and the tail doesn't chatter the CC, with a noise gate underneath so
room tone doesn't open it.
CC11
expression, continuously
velocity
taken from breath at the moment the note opens
caveat
mic and speaker share one I2S bus, so this mode takes it exclusively
Use case
Making a sampled string or brass patch phrase like a player instead of a
keyboard. Swells, crescendos into a downbeat, and dynamics that follow your
actual lungs. A dedicated breath controller costs a few hundred dollars.
04
STRUM
Fn + 4
Hold a chord — the notes stay silent. Then shake the device and it strums
them. Harder shake gives a tighter spread and a louder stroke; the direction
you rotate picks an up- or down-stroke, exactly like hitting strings.
Notes are scheduled with a real inter-note delay rather than fired together,
which is what makes it sound strummed rather than blocked.
ENTER
manual downstroke
SPACE
manual upstroke
spread
8–46 ms, from shake strength
Use case
Guitar and harp parts that a grid-drawn chord never sells. Also the most fun
mode to hand to somebody who doesn't play — the gesture is obvious and the
chord is already correct because it's scale-locked.
05
GRAVITY
Fn + 5
A 16-step sequencer where tilt is the transport. Hold it level
and it stops dead. Tilt right and it runs forward; tilt left and it runs
backwards. How far you tilt is the tempo, up to twelve steps a second.
You scrub a riff the way you'd tip a music box, and reversing mid-phrase is a
wrist movement rather than a parameter.
tap top row
toggle that step on / off
hold step + note
set that step's pitch
level
stop, holding position
Use case
Generating source material rather than performing a part. Record a few minutes
of tilting a sequence around, then cut the good bars out. The rate is
deliberately free-running and untethered from your DAW clock — that's what
makes the timing feel human instead of quantised.
06
MACRO
Fn + 6
The whole keyboard as a 56-pad control surface, no notes involved. Two rows of
latching CC toggles, a row of momentary notes for clip launching, and a
transport row.
rows 1–2
26 latching toggles, CC20 upward, 0 / 127
row 3
momentary notes from 36 up — clip launch in most DAWs
row 4
MMC play / stop / record, panic, program changes
Use case
The pocket remote for a session — arm tracks, punch in, toggle plugin bypasses
from across the room. This is the mode that ties into the studio HUD work:
a physical surface for things that currently need the mouse.
05Tilt & expression
Tilt is measured against gravity, which means it has no absolute zero — it only
knows where "level" is because you told it. Fn+C sets the current
position as centre, and you'll reach for it constantly: sitting down, standing up,
or handing the device to somebody else all move where neutral is.
CC1 roll64
CC74 pitch64
Range±45° = full scale
Drag the pad. This is the mapping XY mode uses; NOTES mode
uses the same roll axis for CC1 and the pitch axis for channel pressure.
Three velocity sources
Source
Derived from
Character
strike
chassis shock
Experimental. A key bottoming out sends a transient through the case and the IMU sees it. Real velocity sensing on a keyboard that has none — if it survives contact with hardware.
tilt
pitch angle
Reliable and genuinely playable. How far you're leaning sets how hard the next notes land, so dynamics become a posture rather than an attack.
fixed
nothing
Constant 100. Correct choice for organ, clav, and anything where you want the DAW's own velocity curve out of the picture.
06Command layer
Everything global lives under Fn. Holding it mutes the instrument,
so you can change key or octave mid-take without a stuck note.
FIG. 3The command layer. Orange keys change what you're playing; blue keys change how
the sensors behave; the red key panics.
Combination
Action
Fn + 1…6
Select mode
Fn + , .
Octave down / up
Fn + [ ]
Root note down / up
Fn + ; '
Previous / next scale
Fn + - =
MIDI channel down / up
Fn + L
Cycle grid layout
Fn + K
Scale lock on / off
Fn + A
Tilt aftertouch on / off
Fn + V
Velocity source: strike / tilt / fixed
Fn + C
Zero the tilt to your current grip
Fn + X Y
Invert a tilt axis
Fn + Z \
Panic — all notes off
07Flashing
Pair over Bluetooth from Audio MIDI Setup → MIDI Studio →
Bluetooth. It appears as an ordinary MIDI source; no IP, no session, no dongle.
cd /Users/zsahs/uvr-env-311/cardputer-midi && pio run -t upload
Run the musical logic tests
cd /Users/zsahs/uvr-env-311/cardputer-midi && \
c++ -std=c++17 -I test/shim -I src \
test/test_scales.cpp src/scales.cpp -o /tmp/t && /tmp/t
Use the app-only image
Stage firmware.bin. Do not merge it with bootloader.bin
and partitions.bin using esptool — the launcher writes your image into
an app partition and the bootloader already on the device stays put. A merged image
on the SD card simply won't boot, and it's the most common way this goes wrong.
08Multi-boot layout
M5Launcher installs rather than boots — it writes the selected image into
an app partition and reboots into it. With the stock table that means one resident
app and a reflash every time you switch. Carving custom partitions with PMan is what
turns it into real multi-boot.
FIG. 48 MB of flash, drawn to scale. UniGeek alone is larger than Launcher and both
of my firmwares combined, and its own layout claims 5 MB of filesystem —
which is the real constraint, not the code size.
Firmware
Binary
Note
UniGeek
2,866 K
Wants a 3072 K slot plus 5000 K of SPIFFS
Launcher 2.8.0
1,368 K
No ADV-specific asset in the release — check which build you run
HA Remote
1,039 K
Will grow as you add menu items
Cardputer MIDI
729 K
Lots of headroom
The three-partition plan is the better trade: UniGeek stays resident with a workable
filesystem, and the two small firmwares share a swap slot you reinstall from SD in a
few seconds. Cramming all four leaves every slot at 93–97% with nowhere to grow.
09Tuning strike velocity
The experimental one. Three constants in config.h govern it, and they
interact — change one at a time.
Constant
Default
Symptom it fixes
STRIKE_THRESH_G
0.18
Everything reads loud → raise it. Soft presses land on the fallback velocity of 64 → lower it.
STRIKE_FULL_G
1.10
Can't reach 127 without hurting your hand → lower it. Everything pins at 127 → raise it.
STRIKE_WINDOW
25 ms
Neighbouring keys stealing each other's transients → shorten it. Hits reading as 64 at random → lengthen it.
If it stays noisy, Fn+V to tilt is not a
consolation prize — leaning to set dynamics is expressive in its own right and
perfectly stable.
10Known unknowns
Everything here is compile-verified, and the musical logic is covered by host-side
tests that already caught one real bug — Csus4 being named
Fsus2/C, because the bass note has to win over table order. None of it
has run on hardware yet. Three things to check first:
Tilt axis direction. I couldn't confirm which physical direction
the BMI270's roll and pitch map to in the hand. Fn+X and
Fn+Y invert either axis if they feel backwards.
Strike velocity. Adjacent-key crosstalk could sink it entirely.
Section 09 is the tuning path; tilt velocity is the fallback.
Chord rollover depth. The TCA8418 reports every held key, but
how many simultaneous notes actually make it through is worth measuring before
writing parts that assume six-note voicings.
Latency is a known quantity rather than an unknown: BLE MIDI jitters 7–10 ms.
Inaudible for tilt, breath and expression; marginal for tight drum triggering. The fix
if it bites is RTP-MIDI over WiFi, which is a transport swap — everything in
midi_out.cpp stays as it is.