A bench for checking MIDI values against
the hardware: send one CC at a time and watch the encoder. Use it to confirm a
value from config.py on your own unit, or to tell a
paint bug from a wire bug.
.venv/bin/python -m mft.daemon --stop.
The focus marker, RING_CEILING and the focus pulse all depend
on this band. If this panel does nothing, none of them work. Ring
position is held at full here so there is something to dim.
Channel 6 carries the indicator tables — channel 3's, shifted up by 48: 49–56 gate · 57–64 pulse · 65 = brightness 0% · 95 = brightness 100%. An RGB brightness (18–47) sent here falls below the band and does nothing. The ceiling is the last value that still changes something, not the first that looks bright.
Slot is also the CC number on every channel: bank N is CC N×16 … N×16+15, so slot and control are the same integer. Selected: slot 0 (bank 1, enc 1)
The envelopes from mft/overlays.py and
mft/render.py, run at 30 fps on the target encoder. The
values sent are listed below.
| t | ring pos | ring lvl | ch6 | rgb lvl | ch3 |
|---|---|---|---|---|---|
| — | |||||
Every session state on its own encoder — hue, animation and ring — as
render.py paints it. Start the clock first, or the animated
states will not be in phase.
| enc | state | hue | ch3 | ring |
|---|---|---|---|---|
| — | ||||
The five-hour usage window belongs to no session, so it gets no encoder:
it gets the whole bank for a few seconds, once on the way past each
milestone. This is mft/overlays.py's
UsageOverlay at 30 fps over the target bank — the word
USE, each letter held still and then cut to the next,
and then the reading itself as rows filling from the bottom. The preview
mirrors what goes on the wire, so the panel is worth something with no
device attached.
The word no longer carries the number, so its three letters can afford to
stand still: each is held for USAGE_LETTER_SECONDS (0.85)
and cut, not faded, to the next — a fading letter is legible for half the
time it is on screen. RATE beside it is the old envelope,
strike-and-decay at the banner's pace, for the comparison.
Then the bar: one row per quarter of the window, filling from the bottom,
flashed USAGE_GAUGE_FLASHES times. A reading between rows
lights its leading row partially, which is the only thing that tells 90,
95 and 100 apart — they all reach the top row. Hue and brightness ramp
together over the whole span, amber and dim at the bottom to full red at
100. Milestone → the word's hue:
.
The other half, and the half that decides whether you ever see the word:
mft/usage.py's watermark. Move the percentage and this plays
exactly what the daemon would have played — which is usually nothing.
Readings come from Claude Code's own cache
(~/.claude.json), so they arrive minutes late and in jumps.
| reading | watermark | crossed | board |
|---|---|---|---|
| — | |||
Three silences, all deliberate: the first reading of a daemon's life is adopted, not announced; a window that rolls over resets without a word; and a percentage that wobbles back across a milestone it already spelled says nothing. A jump spells only the highest milestone it passed — try 20% → 96%.
Sixteen values at a time across bank 1. Set what you find in
config.py or an MFT_* env var.
Inbound messages below. A press is channel 2 (≥ 64 down, 0 up); a turn is channel 1, which the daemon ignores by design.
Encoders must be set to accept host LED control in the Midi Fighter Utility. Otherwise the device drives its own LEDs and ignores this page entirely, which looks the same as a wrong value table.