Skip to content

Releases: jzbz/gflex

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 08:44
v0.3.0
816a5f1

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, which gflex devices lists.
  • info --all fails 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. Plain info is unaffected.
  • Refused before a device is opened, when the arguments alone settle it:
    • voltage set with a negative value, a value outside 3300-48000 mV, or one that overflows 16 bits, now exits 7.
    • vlimit set with an inverted or out-of-envelope pair, when both --low and --high are given, now exits 7.
    • firmware fetch --raw without -o now 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-run and before any device is opened. v0.2.0 exited 1 after claiming the device, and under --dry-run printed the frame and exited 0. --no-ack still 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 e0 is the likeliest typo of the voltage write 04 92 2e e0. Without a TTY and without --yes it exits 7. The PDO chunk read 03 11 kk still passes silently. Set the 0x80 flag if a write was meant.
    • A 0x prefix now marks a byte boundary, so 0x4 exits 2. v0.2.0 borrowed a nibble from the next token, so raw 0x0 0x4 0x92 0x2E 0xE0 was parsed as the 12000 mV write 04 92 2e e0. Unprefixed input parses as before.
  • --dry-run of the setting commands now lists the reads each command sends around its write. For voltage set, vlimit set and calibrate adc the write is no longer frames[0] in the JSON, so select it by "write": true or cmd.
  • 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. Under flash --fetch it now reads "fetching the image for serial ...".
    • A permission failure reading the image or writing -o exits 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.
  • 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 --force would flash it. Use firmware fetch --raw -o FILE to inspect such a reply.
    • An image that gives pg_id on 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/stdin or a regular file.
  • Firmware behaviour:
    • firmware fetch --dry-run is 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 -o saves 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 --force flash 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 usb that skips the driver rebind.
  • Other exit and text changes:
    • monitor now prints the frames already decoded when --for expires or Ctrl-C lands, and exits non-zero if a transport error was already waiting.
    • An install-udev interrupted 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 adc marks each term (written) or (unchanged) in its human output, and tolerance set marks 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 adc and authlock set now warn about this and name the get command to check with. voltage set also reads once and, if that read answers, appends the voltage the device now reports. Exit codes are unchanged.
  • voltage set, current set and vlimit set add a read_back boolean to their --json output. current set and vlimit set also warn when the read-back differs from the value written.
  • calibrate adc writes only the terms given on the command line. An omitted term is filled from its own read, so --offset N no 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-ready gflex vlimit set command 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>mV and <n>mA. A bare number is read as volts or amps, so the old --low 3300 --high 48000 was refused.

Firmware

  • A Ctrl-C between the jump and CMD_BOOTLOAD_END now 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-url or --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-size no longer loses up to its first 4 KiB. --crc warns when it replaces a different CRC the image declares.
  • Vendor fetch:
    • A Ctrl-C during the handshake takes effect at once.
    • A Connection header 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] in pdo dump and scan. It appears as pps_power_limited and pps_budget_w in --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_recorded caveat.
  • devices marks ports matched only by name as yes (name only), with identified_by in ...
Read more

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 06:01
v0.2.0
2de7864

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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 19:16
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 ...
Read more