Repository navigation
Audeze Maxwell 2 (3329:4b29): device returns zero-length responses to all status queries; battery and chatmix unavailable #573
Description
Summary
On my Maxwell 2 PS/PC dongle (3329:4b29), every info capability reports as
unavailable: battery is BATTERY_UNAVAILABLE / -1, chatmix is always 0.
Setters work — sidetone changes are plainly audible — so the device is
reachable and the write path is fine.
Probing the HID interface directly shows the device replies to every status
request with a zero-length payload. It is not that the response is
misparsed; there is no data in it to parse.
This looks like the same underlying problem as #460 (Maxwell 1), and the two
drivers send byte-identical packets, so a fix likely covers both.
Environment
| Device | Audeze Maxwell 2, PS/PC dongle 3329:4b29 |
| HeadsetControl | 4.0.0 (Arch headsetcontrol 4.0.0-1) |
| hidapi | 0.15.0 |
| OS / kernel | Arch Linux, 7.1.9-arch1-2 |
| Firmware | updated at some point via Audeze HQ on Windows (exact version unknown) |
What headsetcontrol reports
$ headsetcontrol -b -m -o json
"device": "Audeze Maxwell 2",
"id_vendor": "0x3329",
"id_product": "0x4b29",
"battery": { "status": "BATTERY_UNAVAILABLE", "level": -1 },
"chatmix": 0What the device actually returns
I replayed the driver's exact sequence — all 15 UNIQUE_REQUESTS then all 6
STATUS_REQUESTS, 62-byte writes, 60ms delays, HIDIOCGINPUT with
buf[0] = 0x07 — straight against /dev/hidraw4. Every single response, for
all 21 packets:
07 00 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
All payload bytes zero. No D6 0C 00 00 battery marker anywhere in any
response, so getDeviceStatus() takes the BATTERY_UNAVAILABLE path
correctly — there is genuinely nothing there.
Two details that suggest the device understands the framing and is declining
the command rather than failing to parse it:
- Byte 1 looks like a length field, and the device answers with zero.
Requests carry0x08/0x07/0x09there; every reply carries0x00. - Byte 2 mirrors the request. All requests with byte 2 =
0x80get
07 00 80 ...back.UNIQUE_REQUESTS[6], the only one with byte 2 =0x00,
gets07 00 00 ...back. So the dongle is echoing header state, not
returning payload.
Transports tried
| Transport | Result |
|---|---|
hid_write + hid_get_input_report (what the driver does) |
07 00 80 00..., zero payload |
Interrupt IN endpoint (plain read(), 1s window, before and after a query) |
nothing at all, 0 reports |
Feature reports (HIDIOCSFEATURE / HIDIOCGFEATURE) |
zeros |
HIDIOCGINPUT on every declared report ID (3, 4, 5, 6, 7, 10, 12) |
only ID 7 returns non-zero, and only the 07 00 80 header |
Ruled out
- Permissions — the shipped rule matches (
3329:4b29,TAG+="uaccess"),
the ACL grants my userrw, and the node opensO_RDWRfrom an unprivileged
process. - Wrong interface — interface 5 is the only HID interface on the device,
matchinggetCapabilityDetail'sinterface_id = 5. - Wrong usage page — the report descriptor declares
0xff13, as expected. - Wrong message size — the descriptor gives report 6 output = 61 payload
bytes and report 7 input = 61, i.e. 62 with the report ID.MSG_SIZE = 62
is correct. - Headset asleep or unlinked — audio streams through the dongle at
48kHz/24-bit throughout, and the same zero responses come back while it does. - Version — the
4.0.0→masterdiff foraudeze_maxwell2.hppis only the
status-reuse caching from perf(audeze): answer every info capability from one status read #550; packet sequences and parsing are identical, so
4.1.0 behaves the same.
Also tried, also no response
Following the RACE protocol notes in
kats1123/maxwell-firmware-research
(which documents this exact framing: 06 [LEN] 80 05 5A [PLEN] 00 [CMD...],
polled GetInputReport on report ID 0x07, and a battery query of
06 07 80 05 5A 03 00 D6 0C — byte-for-byte what the driver sends):
- Delay between write and read. That write-up notes a read issued
immediately after a write returns stale data, andsendGetInputReport()
sleeps 60ms before the write then reads immediately after it. I tried
delays of 0, 5, 10, 20, 40, 60, 100, 200 and 400ms between the write and the
read, and polling the input report continuously for 1s. Always
07 00 80 00 00.... - Routing byte. Byte 2 appears to select dongle-local (
0x00) versus relay
to headset (0x80). I swept all 256 values for the battery query; none
produced a non-zero-length reply.
A documented response looks like 05 5B 06 00 01 09 2F 00 00 <state> — RACE
response magic 0x5B and a real length. Nothing I send produces a frame
containing 0x5B at all.
Likely a firmware revision difference
audeze_maxwell.hpp and audeze_maxwell2.hpp contain identical
UNIQUE_REQUESTS and STATUS_REQUESTS arrays. Given #460 reports the same
BATTERY_UNAVAILABLE on Maxwell 1, I suspect a firmware generation that
changed or gated these opcodes, affecting both models rather than one.
If a maintainer can say which firmware version the original implementation was
developed against, I'm happy to compare against mine and help narrow it.
Secondary: setters report success unconditionally
sendGetInputReport() only checks that the read returned MSG_SIZE bytes:
auto read_result = getInputReport(device_handle, tempBuff);
if (!read_result || *read_result != MSG_SIZE) {
return DeviceError::hidError("Failed to get input report");
}62 zero bytes satisfy that, so on this hardware every setter returns
success regardless of what the device did:
$ headsetcontrol -s 128 -o json
"actions": [ { "capability": "CAP_SIDETONE", "status": "success", "value": 128 } ]Here the write genuinely did apply, so that particular line is honest. But the
check cannot distinguish an applied write from an ignored one, which made this
considerably harder to diagnose — I only established that writes work by
setting sidetone to maximum and listening for it. Treating a zero-length reply
as a failure, or validating the echoed header, would surface real write errors.
Reproduction script
The probe used above is a single standalone Python file with no dependencies
(opens /dev/hidraw*, replays the two sequences, dumps every response and
flags the D6 0C 00 00 marker). Happy to attach it or open a PR adding it
under a debug/ directory if that would be useful for triaging Audeze reports.
Activity
johnsideserf commentedon Sep 7, 2026
Follow-up with a
usbmoncapture, which rules out hidapi as a factor and addsthree findings.
1. The empty responses are real at the USB level, not a hidapi artifact.
Captured bus traffic while running
headsetcontrol -bthree times. All 126GET_REPORTtransfers (a1 01 0107 0005 003e— input report0x07,interface 5, 62 bytes) completed with status 0, and returned exactly two
distinct payloads:
The 6 are the responses to
UNIQUE_REQUESTS[6], the only request with byte 2 =0x00— 2 per invocation across 3 invocations. So byte 2 of the reply tracksbyte 2 of the request, and the frame is otherwise empty. The writes go out
correctly as
21 09 0206 0005 003e(output report0x06, 62 bytes).2. The interrupt endpoint is functional — it just never carries RACE replies.
I previously reported seeing nothing on interrupt IN; that was an incomplete
test. In the same capture the endpoint delivered 68 input reports from button
presses (report ID
0x0c, Consumer page — volume and mute). So the endpointworks, and the absence of RACE responses there is the device's behaviour rather
than a reading error on my part.
3. Not a console-mode issue. The Maxwell 2 dongle has a PC/PS/Xbox switch,
and Audeze note that PC mode gives full functionality. Mine is set to PC, so
that does not explain it.
Unrelated, but visible in the capture: on 4.0.0 each
headsetcontrol -binvocation sends the 21-packet sequence twice (126 transfers over 3 runs),
once for battery and once for chatmix. That is what makes a single call cost
~2.6s here rather than ~1.3s. The status reuse added in #550 should collapse
that to one sequence in 4.1.0 — noting it only to confirm the caching change is
doing something real on this hardware.
Happy to run any specific command sequence against this unit if that would help
narrow down what changed. If whoever implemented #506 can share their firmware
version, I can compare against mine.
johnsideserf commentedon Sep 7, 2026
Resolved on my end, and my diagnosis above was wrong. A power cycle of the
dongle fixed it.
I unplugged the dongle and plugged it back in. Immediately afterwards:
Replaying the same 21-packet sequence against
/dev/hidraw4now returns realRACE frames instead of empty ones:
So the packets, the framing, the timing and the parsing in
audeze_maxwell2.hppare all correct. Nothing in the implementation needschanging, and my suggestion of a firmware revision difference was unfounded.
Sorry for the noise.
What the bug actually looks like
The dongle can enter a state where it:
and stays there until physically replugged. Everything in my original report
was accurately observed; I just drew the wrong conclusion from it. The empty
07 00 80 00 00...replies, the absent0x5b, the silence across all threetransports, the failed routing-byte sweep — all real, all symptoms of the stuck
state rather than of an unsupported protocol.
I do not know what triggers it. The dongle had been plugged in for a long
uptime before I started. If other Audeze reports share this shape — audio fine,
writes fine, battery permanently unavailable — asking whether a replug clears it
would be a cheap first triage step, and may well apply to #460 too.
Still worth considering: the write-success check
The secondary point in my original report stands on its own, and this episode is
the argument for it.
sendGetInputReport()accepts 62 zero bytes as success, sothroughout the stuck state every setter reported
"status": "success"— whichhappened to be true, since writes really were applying, but the check could not
have told the difference. Rejecting a zero-length reply, or validating that the
response carries
0x5b, would letheadsetcontroldistinguish "device answered"from "device is stuck", and could surface this state directly rather than leaving
it looking like an unsupported capability.
Happy for this to be closed, or kept open scoped to that check — your call.
fix(headsetcontrol): distinguish a detected device from a reporting one