Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Project-specific guidance for Claude Code working in this repository. Read this

## What this project is

`openvlm` is a cross-platform CLI for reading, writing, and validating the EEPROM on **OpenVLM USB audio dongles**. The hardware is a **C-Media CM108B** USB audio chip wired to a 93C46 SPI EEPROM, with a GPIO1 hardware strap that distinguishes OpenVLM-branded dongles from generic CM108-family devices.
`openvlm` is a cross-platform CLI for reading, writing, and validating the EEPROM on **OpenVLM USB audio devices**. The hardware is a **C-Media CM108B** USB audio chip wired to a 93C46 SPI EEPROM, with a GPIO1 hardware strap that distinguishes OpenVLM-branded devices from generic CM108-family devices.

The CLI talks to the chip exclusively over USB-HID class control transfers (`Get_Input_Report` / `Set_Output_Report`) — there is no kernel driver, no vendor blob, and no platform-specific cable. The CM108B datasheet §7.4 documents the HID protocol; §7.1.3 documents the EEPROM word layout.

Expand All @@ -15,7 +15,7 @@ These facts are not in the datasheet but are required to read the code correctly
- **Stock chips ship with a blank EEPROM.** The strings `C-Media Electronics Inc.` / `USB Audio Device` come from the chip's internal ROM, not from EEPROM bytes. There is no "factory image" to read off a virgin device.
- **VID/PID are write-locked in the CLI.** Both are sourced from compiled-in constants (`cm108.OpenVLMVendorID = 0x0D8C`, `cm108.OpenVLMProductID = 0x0012`). Programming a different VID/PID would prevent this CLI from finding the device, so YAML, `update`, and per-field flags all refuse to set them.
- **Product/manufacturer strings are also write-locked** to the compiled-in `OpenVLMDefaults` (`OpenVLM` / `BuildsByShane`). Same reason — these are identity, not configuration.
- **GPIO1 strap is the OpenVLM identity probe.** A generic CM108-family dongle has GPIO1 floating low; an OpenVLM dongle pulls it high. `openvlm identify` and the `requireOpenVLM` gate on every write verb check this. `--force` bypasses for bench / bootstrap work.
- **GPIO1 strap is the OpenVLM identity probe.** A generic CM108-family device has GPIO1 floating low; an OpenVLM device pulls it high. `openvlm identify` and the `requireOpenVLM` gate on every write verb check this. `--force` bypasses for bench / bootstrap work.
- **macOS IOKit returns transient `kIOReturnError` (0xE00002BC)** under host-controller pressure during back-to-back HID reports. The protocol layer absorbs this with bounded retries; do not remove the `transferRetries` / `verifyRetries` machinery without a replacement.

## Datasheet pinning issues (open)
Expand Down Expand Up @@ -102,6 +102,7 @@ Project-level rules live in [.claude/rules/](.claude/rules/) and are authoritati

Project-specific additions:

- **Terminology: never use "dongle" in user-facing text or doc comments.** Refer to the hardware as an "OpenVLM device" (or just "device"). Applies to CLI strings, help text, error messages, docs, and Go-doc comments. Internal symbol names (`OpenVLM` etc.) are unaffected.
- **VID/PID and product/manufacturer strings are write-locked.** Do not add CLI surface to set them — every input layer (YAML, per-field flags, `update`) must reject them with a fixed error message.
- **`WriteImage` enforces the VID/PID guard.** `WipeAll` is the only documented bypass and exists exclusively for the wipe verb.
- **Decimal only for numeric input.** `update`, per-field flags, and YAML reject `0x` / `0b` / `0o` prefixes with `ErrHexInput`.
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ build: fmt vet ## Build the openvlm CLI binary.
cgobuild: fmt vet ## Build the openvlm CLI binary with CGO_ENABLED=1.
CGO_ENABLED=1 GOCACHE=$(HOME)/.gocache go build -trimpath -buildvcs=false -ldflags="-s -w" -o bin/openvlm .

.PHONY: build-windows
build-windows: fmt vet ## Build the openvlm CLI binary.
GOOS=windows CGO_ENABLED=0 GOCACHE=$(HOME)/.gocache go build -trimpath -buildvcs=false -ldflags="-s -w" -o bin/openvlm.exe .

.PHONY: run
run: fmt vet ## Run the CLI from your host (forwards args via ARGS=...).
go run . $(ARGS)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# OpenVLM

Cross-platform CLI for reading, writing, and validating the EEPROM on **OpenVLM USB audio dongles**.
Cross-platform CLI for reading, writing, and validating the EEPROM on **OpenVLM USB audio devices**.

OpenVLM dongles are based on the **C-Media CM108B** USB audio chip wired to a 93C46 SPI EEPROM, with a GPIO1 hardware strap that distinguishes OpenVLM-branded hardware from generic CM108-family devices. The `openvlm` CLI talks to the chip exclusively over USB-HID class control transfers — no kernel driver, no vendor blob, no platform-specific cable. The same binary runs on Linux, macOS, and Windows.
OpenVLM devices are based on the **C-Media CM108B** USB audio chip wired to a 93C46 SPI EEPROM, with a GPIO1 hardware strap that distinguishes OpenVLM-branded hardware from generic CM108-family devices. The `openvlm` CLI talks to the chip exclusively over USB-HID class control transfers — no kernel driver, no vendor blob, no platform-specific cable. The same binary runs on Linux, macOS, and Windows.

## Install

Expand All @@ -17,7 +17,7 @@ make build # produces bin/openvlm
## Quick start

```bash
openvlm list # find attached dongles
openvlm list # find attached devices
openvlm identify # confirm GPIO1 strap
openvlm provision --serial "00001234" # write OpenVLM defaults
openvlm dump --format yaml # inspect live EEPROM
Expand Down
4 changes: 2 additions & 2 deletions cmd/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ func requireOpenVLM(d cm108.Descriptor, force bool) error {

name := displayName(d.SerialNumber, d.Path)

reason := "this doesn't look like an OpenVLM dongle (its identity bit isn't set)"
reason := "this doesn't look like an OpenVLM device (its identity bit isn't set)"
if d.ProbeError != nil {
reason = "couldn't check the dongle's identity bit: " + d.ProbeError.Error()
reason = "couldn't check the device's identity bit: " + d.ProbeError.Error()
}

return &notIdentifiedError{
Expand Down
7 changes: 6 additions & 1 deletion cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,13 @@ func printDumpText(cmd *cobra.Command, d cm108.Descriptor, img *eeprom.Image) er

out := cmd.OutOrStdout()

device := displayName(d.SerialNumber, d.Path)
if flagVerbose {
device = d.Path
}

fmt.Fprintf(out, "DEVICE %s (serial=%s, openvlm=%s)\n",
d.Path, displaySerial(d.SerialNumber), openvlm)
device, displaySerial(d.SerialNumber), openvlm)
fmt.Fprintf(out, "VID:PID 0x%04X:0x%04X (read-only)\n", img.VID(), img.PID())
fmt.Fprintf(out, "PRODUCT-STRING %s\n", view.ProductString)
fmt.Fprintf(out, "MANUFACTURER %s\n", view.ManufacturerString)
Expand Down
2 changes: 1 addition & 1 deletion cmd/identify.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func runIdentify(cmd *cobra.Command, _ []string) error {

if !d.IsOpenVLM {
return &notIdentifiedError{
err: fmt.Errorf("%s: this doesn't look like an OpenVLM dongle (its identity bit isn't set)",
err: fmt.Errorf("%s: this doesn't look like an OpenVLM device (its identity bit isn't set)",
name),
}
}
Expand Down
10 changes: 5 additions & 5 deletions cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func TestIdentify_FriendlySuccess(t *testing.T) {

stdout, _, err := runRoot(t, "identify")
require.NoError(t, err)
assert.Contains(t, stdout, "this is an OpenVLM dongle")
assert.Contains(t, stdout, "this is an OpenVLM device")
}

// TestUpdate_FriendlySuccess pins the updated-field line.
Expand Down Expand Up @@ -396,9 +396,9 @@ func TestProvision_DryRunFriendlyMessage(t *testing.T) {
assert.Contains(t, stderr, "No changes made.")
}

// TestNoDongles_FriendlyError verifies the cm108.ErrNoDevice translation
// TestNoDevices_FriendlyError verifies the cm108.ErrNoDevice translation
// fires end-to-end, with the action hint appended.
func TestNoDongles_FriendlyError(t *testing.T) {
func TestNoDevices_FriendlyError(t *testing.T) {
// Empty fake backend — no devices registered.
prev := SetBackend(hidx.NewFakeBackend())

Expand All @@ -411,7 +411,7 @@ func TestNoDongles_FriendlyError(t *testing.T) {
require.Error(t, err)

got := friendlyError(err, false)
assert.Contains(t, got, "No OpenVLM dongles are plugged in")
assert.Contains(t, got, "No OpenVLM devices are plugged in")
assert.Contains(t, got, "Plug one in")
}

Expand All @@ -427,7 +427,7 @@ func TestStrapLow_FriendlyError(t *testing.T) {
require.Error(t, err)

got := friendlyError(err, false)
assert.Contains(t, got, "this doesn't look like an OpenVLM dongle")
assert.Contains(t, got, "this doesn't look like an OpenVLM device")
}

// TestVerbose_PreservesInternalDetail confirms --verbose surfaces the
Expand Down
16 changes: 14 additions & 2 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ func runList(cmd *cobra.Command, _ []string) error {

w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)

fmt.Fprintln(w, "Serial\tDevice\tOpenVLM?\tNotes")
// Default mode keeps the table compact and free of platform-specific
// device-path noise. The OS path is only useful for disambiguation when
// multiple devices have no serial number — surface it via --verbose so
// it doesn't pollute the common single-device case.
if flagVerbose {
fmt.Fprintln(w, "Serial\tOpenVLM?\tPath\tNotes")
} else {
fmt.Fprintln(w, "Serial\tOpenVLM?\tNotes")
}

for _, d := range descs {
note := ""
Expand All @@ -50,7 +58,11 @@ func runList(cmd *cobra.Command, _ []string) error {
serial = "-"
}

fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", serial, d.Path, strap, note)
if flagVerbose {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", serial, strap, d.Path, note)
} else {
fmt.Fprintf(w, "%s\t%s\t%s\n", serial, strap, note)
}
}

return w.Flush() //nolint:wrapcheck // tabwriter.Flush errors are passthrough
Expand Down
Loading
Loading