Repository navigation
Releases: jzbz/gflex
Release list
v0.3.0
gflex v0.3.0 fixes the defects found by two full review passes over the whole tree. They were in exit-code classification, the safety interlocks, firmware fetch and flash, the image decoder, PDO evaluation and the USB transport. Some existing failures now exit with a different code, and several commands, info --all among them, can now fail where they used to exit 0. The README promises stable exit codes, so this is 0.3.0 rather than 0.2.1.
Changes that can affect scripts
--port/GFLEX_PORT: a path that exists but is not a character device (a regular file, directory, FIFO, socket or block device) is refused with exit 2 before anything is opened. The opened descriptor is checked again, and a regular file found there is refused the same way. v0.2.0 opened a regular file or FIFO read-write and wrote MIDI frames into it, and exited 3 for a directory or socket. Point it at a/dev/snd/midiC*D*node, whichgflex deviceslists.info --allfails if all nine optional reads fail, or if the device disappears while they are being sent. The exit code follows the cause: 5 for timeouts, 3 for an unplug. v0.2.0 printed nine blank fields and exited 0, which a script could not tell apart from a unit that simply declines those reads. Plaininfois unaffected.- Refused before a device is opened, when the arguments alone settle it:
voltage setwith a negative value, a value outside 3300-48000 mV, or one that overflows 16 bits, now exits 7.vlimit setwith an inverted or out-of-envelope pair, when both--lowand--highare given, now exits 7.firmware fetch --rawwithout-onow exits 2.- With no device attached these used to exit 3. With a busy or unreadable device the two setting commands used to exit 4 or 6.
raw:- A frame over the 64-byte MIDI receive limit exits 2 before
--dry-runand before any device is opened. v0.2.0 exited 1 after claiming the device, and under--dry-runprinted the frame and exited 0.--no-ackstill sends it, with a warning that the device will drop it. - A frame with the write flag clear that still carries a payload now needs confirmation. For example,
04 12 2e e0is the likeliest typo of the voltage write04 92 2e e0. Without a TTY and without--yesit exits 7. The PDO chunk read03 11 kkstill passes silently. Set the 0x80 flag if a write was meant. - A
0xprefix now marks a byte boundary, so0x4exits 2. v0.2.0 borrowed a nibble from the next token, soraw 0x0 0x4 0x92 0x2E 0xE0was parsed as the 12000 mV write04 92 2e e0. Unprefixed input parses as before.
- A frame over the 64-byte MIDI receive limit exits 2 before
--dry-runof the setting commands now lists the reads each command sends around its write. Forvoltage set,vlimit setandcalibrate adcthe write is no longerframes[0]in the JSON, so select it by"write": trueorcmd.- Firmware exit codes:
- A failed download from the vendor service now always exits 1; one that timed out used to exit 5. The message names the endpoint and
--fetch-timeout. Underflash --fetchit now reads "fetching the image for serial ...". - A permission failure reading the image or writing
-oexits 1, not 6, and no longer prints the install-udev hint. - A failure to open the bootloader interface now exits 6 for a permission failure, 4 for an interface another process holds, and 1 for a unit still running its application. All three used to exit 3. Under
--recover, the application-mode case now fails at once instead of after the 8 s retry window.
- A failed download from the vendor service now always exits 1; one that timed out used to exit 5. The message names the endpoint and
- Firmware refusals:
- A vendor reply that is not JSON is refused with exit 1 and a sanitised excerpt. v0.2.0 treated it as a raw image with no CRC, split into 512-byte pages, and
flash --fetch --forcewould flash it. Usefirmware fetch --raw -o FILEto inspect such a reply. - An image that gives
pg_idon some pages but not others is refused. - A named FIFO with no writer at open time is refused with exit 1, where v0.2.0 hung. Start the writer first, or use
<(...),/dev/stdinor a regular file.
- A vendor reply that is not JSON is refused with exit 1 and a sanitised excerpt. v0.2.0 treated it as a raw image with no CRC, split into 512-byte pages, and
- Firmware behaviour:
firmware fetch --dry-runis now honoured: it prints the serial read and exits 0 without opening the device, contacting the vendor or writing-o. v0.2.0 ignored the flag and sent the serial to the vendor.firmware fetch -osaves pages as hex instead of base64. gflex reads both formats. Re-fetch any v0.2.0 save made at a page size divisible by 3; the vendor's current 320-byte pages and the 512-byte default are unaffected.- After a flash, the reconnected unit's serial is checked before the settings are replayed. If a different VFLEX answers, the command exits 1 with "the settings were NOT replayed". If the serial read fails, it only warns.
- A
--forceflash of an image without a CRC that fails after the final command no longer reports that the update SUCCEEDED. The message now reads "written and started unverified".
- Interrupts:
- The first Ctrl-C prints a notice that the device is being released.
- The final line is now
gflex: interrupted: <error>, so match it as a prefix. The exit code is still 1. - A second Ctrl-C now kills the process at once, with shell status 130 (outside the exit-code table). On
--transport usbthat skips the driver rebind.
- Other exit and text changes:
monitornow prints the frames already decoded when--forexpires or Ctrl-C lands, and exits non-zero if a transport error was already waiting.- An
install-udevinterrupted during udevadm exits 1 and prints the manual reload command. - On
--transport usb, exit 4 now says the USB interface is claimed by another process and no longer prints the ALSA hint. - An unacknowledged write's error text contains "write sent but not acknowledged".
calibrate adcmarks each term(written)or(unchanged)in its human output, andtolerance setmarks what it wrote as written.
Safety
- The protocol has no NACK, so a write that was fully sent but never answered may still have been applied.
voltage set,vlimit set,calibrate adcandauthlock setnow warn about this and name thegetcommand to check with.voltage setalso reads once and, if that read answers, appends the voltage the device now reports. Exit codes are unchanged. voltage set,current setandvlimit setadd aread_backboolean to their--jsonoutput.current setandvlimit setalso warn when the read-back differs from the value written.calibrate adcwrites only the terms given on the command line. An omitted term is filled from its own read, so--offset Nno longer fails just because the offset read failed; only the scale read it needs can fail it.- After a flash, an unreadable voltage window is read up to three times and then left as the flash left it, listed under
not_restored. v0.2.0 reset it to 3300-48000 mV, which could widen a 5 V ceiling to 48 V after one dropped frame. A narrow window that fails the vendor's erased-window test is still widened, but that is now announced with a paste-readygflex vlimit setcommand to restore it. The flash prompt now says settings are reset to factory defaults. - Printed repair commands now carry units. The concrete one,
--low 3300mV --high 48000mV, can be pasted as-is, and the placeholders read<n>mVand<n>mA. A bare number is read as volts or amps, so the old--low 3300 --high 48000was refused.
Firmware
- A Ctrl-C between the jump and
CMD_BOOTLOAD_ENDnow says the unit is still in the bootloader and can be re-flashed, and prints the resume command. So does a serial mismatch found after the jump, and a CRC mismatch prints it as before. Other failures in that window say the unit is still re-flashable without printing the command. - A failure to deliver
CMD_BOOTLOAD_END, other than the device leaving the bus, is now reported as a failure that leaves the unit probably still in the bootloader and re-flashable, instead of as success. - Resume and retry commands carry every flag that decides what gets flashed:
--page-size,--crc,--force,--ack-mode, and a non-default--ws-urlor--fetch-timeout. They also carry a non-default--transport. - On a rawmidi port matched only by name, or taken as the only port on the system, the bootloader jump is reported as unconfirmed, and the post-flash wait uses USB presence instead of timing out after a good update.
- A vendor-class interface that declares the MIDIStreaming subclass, next to a second vendor-class interface, is now treated as a running application, as the audio-class MIDIStreaming interface already was. The bootloader interface picker prefers a vendor interface that is not MIDIStreaming.
- Image decoding is bounded by the largest image the page geometry can address, and the bound is applied while the image is decoded rather than after. A 140 MB flat JSON array of byte values used to cost 3.39 GB of memory. A pipe used together with
--page-sizeno longer loses up to its first 4 KiB.--crcwarns when it replaces a different CRC the image declares. - Vendor fetch:
- A Ctrl-C during the handshake takes effect at once.
- A
Connectionheader split over several lines is now accepted. - Text the server controls in error messages is reduced to printable ASCII.
PDO and scan
- A power-limited SPR PPS object (USB-PD bit 27) is marked
[power limited; source budget ~N W]inpdo dumpandscan. It appears aspps_power_limitedandpps_budget_win--json. - Verdicts cap such a range at the inferred budget divided by the voltage, so an answer that relied on the advertised current can now be NO.
- Computed currents round down to the 10 mA step instead of to the nearest step.
- An SPR verdict that falls short now moves up to an EPR AVS range that covers the voltage.
- A refusal at 15 V or above now cites a recorded EPR cable failure, as the
epr_cable_fail_recordedcaveat. devicesmarks ports matched only by name asyes (name only), withidentified_byin ...
v0.2.0
Static Linux binaries. They are cgo-free and depend on nothing at runtime -- no shared
libraries, no Go toolchain, no browser.
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
install -m 0755 gflex-v0.2.0-linux-amd64 ~/.local/bin/gflex
SHA256SUMS.asc is signed by hand once this workflow has published, because the signing key
never goes near CI; the README's "Verifying a download" says where to get that key and
which fingerprint it has to be.
The USB transport and every firmware operation need a udev rule; run gflex install-udev
or see the README's Install section.
What has been verified on real hardware and what has not is set out in the README's
"Status and limitations", and the protocol itself in SPEC.md.
Full Changelog: v0.1.0...v0.2.0
v0.1.0
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 asSPEC.md, with a citation for every claim; the Go
came afterwards, from that document.SPEC.mdalso 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 versiongpg --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.0or from a clone:
CGO_ENABLED=0 go build -o ~/.local/bin/gflex ./cmd/gflexgflex 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.rulesThen 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 --recoverwould 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: 120gflex infoinvocations per unit at 1 ms — 240 in total, six command round trips
each — produced zero failures on both units, andinfoend 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 0is refused outright rather than merely discouraged.
--byte-delay 20msputs 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 ...