Repository navigation
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--json.
Robustness
- A device that disappears between commands (ENODEV, ENXIO or ESHUTDOWN on a send), or a send into a port already closed, now stops retries at once.
- A permission-denied error opening the rawmidi node is retried for up to 1 s, which covers a reconnect that races udev applying the ACL.
--transport usb:- A transfer submitted while the interface is being released is refused, which stops a MIDI read re-claiming it in the ordinary case.
- A rebind refused with EBUSY is retried once, after handing back any claim that still slipped through.
monitorwarns when the driver was not rebound.- A transfer with an out-of-range byte count returns an error instead of panicking.
install-udevtriggers only USB devices with vendor0x37bf.
Hardware verification
- Smoke test on 2026-09-11, against a third unit (serial 86383db3, APP.05.00.00, mfg date 004apr26):
info --allanswered all nine optional reads.pdo dumpdownloaded all 12 chunks.firmware fetchdownloaded the vendor image: 165 pages of 320 bytes, crc 0x30.firmware flash --dry-runon the saved file reported the same.voltage getread 5000 mV.
- Not run on hardware for this release:
- An actual flash. The last one was on 2026-08-21, on unit 58b4f621, with a pre-v0.2.0 revision.
- Every write path:
voltage,currentandvlimit set,calibrate,led,toleranceandauthlock. These changed in this release and are verified by tests only, as is everything else not listed above.
- Decoder comparison: the saved image was decoded by five decoders, and all gave byte-identical output (52,800 bytes, sha256
2f63574e…0c85, every per-page hash equal):- this release;
- v0.2.0;
1df7d03, the revision used for the 2026-08-21 flash;- two decoders written separately from SPEC §10.3.
- Raw vendor reply: a reply for the same serial was also kept in the vendor's
{pg_id, chunks}form. This release, v0.2.0,1df7d03and a decoder written separately from SPEC §10.3 all gave byte-identical output on it (52,800 bytes, sha256edcc8ecb…f8cf9). - Limits of that comparison:
- The saved file is gflex's own
fetch -ooutput, with hex pages. It exercises only the hex-page path. - The raw reply states its pages and chunk keys in order, so it does not test reordering. That was tested only on documents built from the saved file's bytes, with pages shuffled and chunk keys scrambled. All decoders agreed on the byte-array form. Negative controls confirm that a decoder ignoring
pg_idor chunk-key order would have been caught. One spec decoder refuses hex-string chunks by design; that is a difference in what it accepts, not a disagreement about content. - Four downloads for the same serial, two of them one second apart, contain entirely different bytes. No two share a page or a 40-byte chunk, all look like random data (about 8.0 bits per byte), and all carry the same version, geometry and declared CRC. The service returns a fresh payload on every request, so two correct downloads never compare equal and there is no fixed image to check one against; SPEC.md §10.3 records the details.
- gflex computes no CRC. The
crc 0x30in--dry-runonly echoes the byte the image declares, and nothing ties these bytes to a CRC reported by a device.
- The saved file is gflex's own
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.3.0-linux-amd64 ~/.local/bin/gflex
Chained on purpose: a bad signature or a failed checksum stops that paste before install
runs. 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.2.0...v0.3.0