Skip to content

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 19:16
· 16 commits to master since this release
v0.1.0
fa55310

gflex v0.1.0 — first release

Program a Werewolf VFLEX from Linux, without the proprietary phone app.

The VFLEX is a programmable USB-C Power Delivery adapter: you tell it what voltage you want, it
stores that in non-volatile memory, and from then on it negotiates that voltage from any PD source
and presents it on its X-Connector — 3.3 V to 48 V, up to 5 A, for guitar pedals and other small DC
loads that want an odd voltage out of a modern USB-C charger.

The vendor ships an Android app, an iOS app, and a web app that only works in Chrome and Edge —
because it reaches the device through Web MIDI and WebUSB, which Firefox and Safari do not
implement. There is no Linux client. This is one: a single static binary, no cgo, no libusb, no
browser, no account, firmware update included.

gflex info
gflex voltage set 9V
gflex scan
gflex firmware flash --fetch

Unofficial and independent. Not affiliated with, endorsed by, or supported by Werewolf or
Tundra Labs. It contains no vendor code. The protocol was reverse-engineered from the vendor's
own shipped application and written up first as SPEC.md, with a citation for every claim; the Go
came afterwards, from that document. SPEC.md also carries the honest list of what is still
unknown, and this release does not claim more than that list allows.


Why it exists

The device speaks USB-MIDI. That is a genuinely clever choice by the vendor — every OS already
has a class-compliant MIDI driver, so no kernel module and no signed INF were ever needed — but the
protocol has to move arbitrary 8-bit bytes through a transport whose data bytes are 7 bits. So each
protocol byte is split into two nibbles and carried as a single MIDI Note-On: high nibble as the
note number, low nibble as the velocity, with a Note-Off and a Poly Aftertouch bracketing the frame.

Everything on the wire is therefore well-formed MIDI, which is what makes a Linux client possible at
all: the kernel's snd-usb-audio passes it through untouched, so gflex writes bytes straight to
/dev/snd/midiC*D* — no root, no driver detaching, no cgo.

The same encoding hides a trap. Any protocol byte whose low nibble is zero becomes a Note-On with
velocity 0, which MIDI convention treats as a Note-Off — so any MIDI layer that "helpfully"
canonicalises that would corrupt roughly one byte in sixteen, and the corruption would look exactly
like a start-of-frame marker. The kernel and alsa-lib were checked and do not do it. That is most of
why this tool talks to rawmidi directly instead of going through a MIDI library or the ALSA
sequencer.


What works

Feature parity with the device-facing half of the vendor's app, and a few things it does not do.

gflex devices, gflex info [--all] What is attached and how it was found; identity and settings, including the fields the vendor app never reads
gflex voltage, current, vlimit, tolerance, led, authlock Read and write every setting the app exposes
gflex measure, gflex calibrate The device's own measurement of its output, and the ADC calibration behind it
gflex scan, gflex pdo dump|clear Guided capture of what a charger actually offers, decoded as USB-PD PDOs, with a verdict on whether your target voltage and current are achievable. Requires device firmware 5.0.0 or newer
gflex firmware version|fetch|bootloader|flash Firmware update end to end, in the same binary
gflex raw, gflex monitor, --dry-run, -v The escape hatch, a live frame monitor, and a hex trace of both directions
gflex install-udev [--print] The udev rule for the USB and firmware paths

--json on every command, stable exit codes (0 success, 2 usage, 3 no device, 4 busy, 5 timeout,
6 permission, 7 refused by a safety interlock), and GFLEX_* environment counterparts for the
global flags — except --yes and --dry-run, deliberately, because a GFLEX_YES left in a shell
profile would pre-answer every safety confirmation for months.

Three things here the vendor's app does not do:

  • Battery and Variable PDOs are decoded and shown. The vendor app silently discards both.
  • Compatibility is judged against the scan you just took, not against a cloud record that can
    disagree with the hardware in front of you.
  • SPR AVS current is reported per band. The vendor takes max() of the two band limits, which
    reports the strong band's current while operating in the weak one — over-reporting, which is the
    direction that destroys hardware.

Everything else in the app is its cloud half — accounts, inventory, photos, sharing — and none of it
is required to program the hardware. Selecting a saved device in the app results in exactly two wire
writes.


Install

This release attaches static Linux binaries for amd64 and arm64, built and checked on the
architecture they target, alongside a SHA256SUMS listing both and a SHA256SUMS.asc signing that
list. uname -m says which one you want — x86_64 is amd64, aarch64 is arm64:

gpg --verify SHA256SUMS.asc SHA256SUMS && sha256sum --ignore-missing -c SHA256SUMS
install -m 0755 gflex-v0.1.0-linux-amd64 ~/.local/bin/gflex
gflex version

gpg --verify has to report a Good signature from
252B 901C 8885 3CF9 F939 2559 2497 38C8 641C 3359, and it is worth insisting on the order: the
checksum list ships from this same release as the files it describes, so unsigned it catches a
truncated download and nothing else. The signature is what turns it into a check, and it is made by
hand — the key never goes near CI, so a compromised workflow can publish a binary but cannot sign
for one. Where the key comes from and what that warning about an uncertified key means is in the
README's Verifying a download.

Or build it — the same static, cgo-free binary with no runtime dependencies. Go 1.26 or newer:

go install github.com/jzbz/gflex/cmd/gflex@v0.1.0

or from a clone:

CGO_ENABLED=0 go build -o ~/.local/bin/gflex ./cmd/gflex

gflex version reports the version, the commit and the build date; installed with go install at a
tag, those come from the module build info with nothing to pass by hand.

Permissions. On a systemd desktop the default ALSA path usually needs no setup at all:
/dev/snd/midiC2D0 was measured carrying a uaccess ACL with no rule installed. The USB path
(--transport usb, and every firmware operation) does need one, because /dev/bus/usb/... carries
no ACL:

sudo gflex install-udev      # or: gflex install-udev --print | sudo tee /etc/udev/rules.d/70-gflex.rules

Then replug the device. The write is atomic, and an existing file you have edited is shown to you
before it is overwritten rather than discarded silently.

The udev rule printed in the vendor's own manual does not work: it matches SUBSYSTEM=="hidraw",
and the VFLEX is not an HID device. Its product ID, on the other hand, turned out to be right —
see below.


What is verified on hardware, and what is not

Two real units have been driven by this tool, on 2026-08-21: serials 81a0bcc3 and 58b4f621, both
running firmware APP.05.00.00, both carrying manufacturing date 004apr26, both measured on the
same host.

The protocol worked on the first attempt. On the first unit, every frame the specification
describes round-tripped without correction — the nibble MIDI framing, the big-endian scalars, the
inverted LED byte, the HIGH-before-LOW voltage limit order, the identity string layouts, and the
full PD capability scan including its serial-latch invariant. No decode had to be fixed after
contact with hardware. Ten of the sixteen open questions in SPEC.md §14 are now answered from
measurement rather than inference, and three of those answers corrected the documentation.

Some of what that measurement produced:

  • The USB product ID is 0x800F, and the vendor's manual had it right all along — it had been
    dismissed because it appears nowhere in the vendor's own software.
  • The device clears the write and scratchpad flag bits in its echo, on both units, so masking the
    received command byte is required rather than merely defensive.
  • The vendor-class interface is present while the application is running, which the spec had
    assumed appeared only after the jump to the bootloader. Without that discovery, firmware flash --recover would have streamed write frames at a perfectly healthy unit; it now refuses one that
    still presents a MIDI interface.
  • Every command is roughly 9× faster than the vendor's own pacing. The vendor's app puts 20 ms
    between MIDI messages. Bisecting that on hardware showed it is padding, not a firmware
    requirement: 120 gflex info invocations per unit at 1 ms — 240 in total, six command round trips
    each — produced zero failures on both units, and info end to end went from 0.38 s to 0.04 s on
    one unit and from 0.391 s to 0.045 s on the other. So 1 ms is the default. Going lower buys
    nothing measurable (100 µs came in 0.002 s ahead, inside the noise, because the wall time is
    already the device's own turnaround) and does cost something: at 1 ns the two units lost 2.5% and
    3.3% of commands. That is why --byte-delay 0 is refused outright rather than merely discouraged.
    --byte-delay 20ms puts the vendor's pacing back if a unit unlike these two proves flakier.
  • Writes were re-tested at the new default: 0 of 30 failed and 0 of 30 read back wrong, on each
    unit. The second unit's run is the stronger evidence, because it wrote the current limit
    alternating between 4900 and 5000 mA and checked each read-back against the value just written.
    A write that never reaches the device reads back as the value it already held, so a test that
    writes the same number every time reports success in precisely the case where the write was lost.
  • The device sends nothing unsolicited. Twelve seconds idle produced zero frames, on each unit.

Firmware update is verified end to end. Unit 58b4f621 was flashed with the image the vendor's
service holds for it: the jump to the bootloader, the re-enumeration, the vendor-class interface
opening inside the 8 s retry window with no manual intervention, the serial matching across the
excursion, 165 pages of 320 bytes streamed in acknowledged mode, the device's own CRC coming back
0x30, matching the value the image declared
, the jump back to the application, and every setting
replayed. A field-by-field comparison before and after found no drift.

That CRC match is also what independently validates the image parsing, since the device computes the
CRC over whatever it was actually given: an image assembled wrongly from the vendor's chunk map
would not have matched.

What that does not establish

  • Two units are a batch, not a product. Same firmware revision, same manufacturing date, same
    host. That is materially stronger than one unit and it is what §14 asked for, but it is not
    evidence about a different firmware revision, a different batch, or a different USB controller —
    and the 1 ms pacing default is exactly where that gap matters.
  • A cross-version firmware update has never been exercised. The vendor service serves a unit's
    current image, so what was flashed was the version already installed. The path is proven; the
    delivery of an actual upgrade is not.
  • Six questions in SPEC.md §14 are still open: the six commands that are dead code in the
    vendor's app (4–7, 13, 14), CMD_ENCRYPT_MSG, CMD_IOS_HOST_MODE_FLAG, the units of the
    tolerance-sag term, whether the 750 mV tolerance band is symmetric, and the firmware CRC algorithm
    — which is a device-side function, so having an image does not reveal it. Auth-lock levels beyond
    0 remain untested too: nobody knows what they gate, or whether there is a way back.
  • One known limitation, found the same day. --transport usb can cost you the ALSA MIDI port:
    after a single run, /dev/snd/midiC2D* disappeared and did not return until the device was
    physically replugged. The reattach is not silently failing — the kernel accepts the re-probe
    request, it just does not rebind snd-usb-audio to the MIDI interface on its own. Measured on one
    host and one kernel, so the mechanism is inferred; the effect on the user is not. If the rawmidi
    node is merely busy, closing whatever holds it is the cheaper answer — most often a Chrome tab
    still running the vendor's web app, which reaches the device through the ALSA sequencer and so
    will not show up among the processes holding the device node. Read /proc/asound/seq/clients
    instead.

Reports from real hardware are the most useful contribution this project can receive — above all
from a unit on a different firmware revision, a different batch, or a different USB controller,
because that is precisely where the pacing default is untested.


Safety

This device drives a power rail into someone's equipment, at up to 48 V, and the vendor's app
performs no range validation whatsoever
— it will write any 16-bit value it is handed. That is the
reason the interlocks exist, and why several of them are not optional.

  • voltage set reads the device's own limit window first and refuses anything outside it, and
    outside the documented 3.3–48 V envelope. If that window cannot be read, or the unit reports a
    pair that cannot bound anything, the write is refused rather than falling back to the hardware
    envelope. The protocol has no NACK, so a dropped frame is routine — and a fallback would let one
    lost frame silently promote a 5 V ceiling to 48 V. --ignore-device-limits overrides that
    deliberately; --yes does not, because the routine scripting flag must not be able to discard the
    owner's own guard rail.
  • Above 5 V asks for confirmation. Above 20 V also warns that an EPR source and an
    eMarker-equipped 5 A cable are required — without one the device fast-blinks red.
  • A bare number is always volts, never guessed from magnitude. voltage set 12000 is refused
    rather than read as 12000 mV, because magnitude-guessing turns a typo into a 1000× over-volt.
  • Widening the limit window asks; narrowing does not. calibrate adc asks, prints the previous
    values, and gives you the exact command that restores them — bad calibration makes every later
    reading silently wrong, which would defeat the first interlock. authlock set confirms every
    write, and a non-zero level has to be confirmed against a warning that its effect is undocumented
    and may not be reversible.
  • On a non-TTY, every one of these fails closed unless --yes is passed, so a script cannot
    quietly over-volt a load. A mistyped subcommand is a hard usage error, never a silent success.
  • gflex raw is guarded too. A read of a documented command passes straight through; a write
    frame, an undocumented command code, the scratchpad flag, or 02 14 — a plain read that drops
    the device into the bootloader — each says what it is and asks first.
  • There is no global --force. --force means one specific thing on one command: firmware flash --force flashes an image that carries no CRC. It cannot suppress verification of an image
    that does carry one, and it cannot override a mismatch.
  • A failed firmware CRC stops the flash and does not jump back. That leaves the unit in the
    bootloader with a slow-blinking white LED, which is recoverable with firmware flash --recover.
    It is not bricked. Page geometry is validated before the first byte is written, so a mis-split
    image is refused rather than half-flashed — the CRC would not catch that anyway, since the device
    computes it over whatever it was told to write.
  • --fetch requires TLS. A ws:// URL is refused, because the image and the CRC it is checked
    against arrive in the same document, so a cleartext fetch authenticates neither. ws+insecure://
    is the explicit downgrade, spelled out in full so that nobody types it out of habit.

--dry-run prints exactly what would go on the wire and sends nothing, including for the dangerous
commands. It is refused only where the frame cannot be known without first reading the device.

Every place where this implementation deliberately departs from the vendor's behaviour — mostly
because the vendor's behaviour is unsafe — is catalogued in SPEC.md §17, so that a future reader
comparing code against spec does not "correct" the code back into a bug.


What the version number promises

Every package is under internal/, so this module exposes no importable Go API. The only
compatibility surface a version number can promise here is the CLI itself
: the command tree, the
flags, the --json output shape, and the exit codes. Those are what will be treated as breaking
changes. Nothing inside internal/ is a contract, and SPEC.md documents the device, not this
tool's API.

Both remain subject to what the hardware says. Six §14 questions are open, and an answer that
contradicts a decode would change behaviour before it preserved compatibility.


Testing

CI runs build, go vet, staticcheck, race tests, a check that the binary is still static and
cgo-free, and a golden-frame smoke test — on amd64 and arm64. The arm64 job is not decoration:
internal/usbfs lays out ioctl structs by hand and passes them to the kernel through
unsafe.Pointer, and their padding and alignment differ between the two architectures. A Raspberry
Pi on a pedalboard is exactly this tool's use case.

The whole suite runs without hardware. The protocol is exercised against an in-memory fake device
that decodes the MIDI framing with a deliberately independent implementation, so a bug in the
encoder cannot hide behind a matching bug in the decoder, and testdata/golden/frames.json pins the
frame ↔ MIDI ↔ USB-MIDI encodings against silent drift.


Provenance

Built by reverse-engineering the vendor's shipped application, in two gated phases: the app was
analysed and its observable protocol written up as SPEC.md with a citation for every claim, then
the Go was written against that document rather than by transliterating the original.

Two honest qualifications, because the term gets used loosely: this is not a formal clean-room —
that requires personnel separation, and here the same author directed both phases. What the process
does establish is that no vendor code was copied. The vendor ships minified JavaScript and Hermes
bytecode; this is Go, written from a written description of behaviour. What was extracted are
interface facts — that command 18 carries a big-endian millivolt value, that the LED byte is
inverted — not creative expression.

Nothing here derives from vendor source, licensed documentation, or confidential material. The
vendor's application is not redistributed with it; SPEC.md §16 describes how to reconstruct the
same corpus from publicly obtainable inputs, if you want to check a claim against the original.

MIT licensed.


Full Changelog: https://github.com/jzbz/gflex/commits/v0.1.0