Skip to content

feat(device): add Sony INZONE H5 (WH-G500)#524

Open
splitbrain wants to merge 1 commit into
Sapd:masterfrom
splitbrain:master
Open

feat(device): add Sony INZONE H5 (WH-G500)#524
splitbrain wants to merge 1 commit into
Sapd:masterfrom
splitbrain:master

Conversation

@splitbrain
Copy link
Copy Markdown

Note: this has been implemented using Claude. It works for me but I can not judge the code. Feel free to close this if you do not want to review AI code.

Changes made

Implements the Sony vendor HCI protocol over the 2.4 GHz USB dongle (VID 054C, PID 0EBF). Supports:

  • Battery (level + charging state) via BATTERY_INFO GET
  • Chatmix via GAME_CHAT_MIX_BALANCE GET (device 0..90 mapped to 0..128)
  • Sidetone via SIDETONE_VOLUME SET (0..128 mapped to device 0..50)
  • Mic volume via MIC_VOLUME SET (0..128 mapped to device 0..50)

The control protocol lives on the Sony-vendor HID collection (usage page 0xFF04, report ID 0x02). Each report wraps an HCI packet: host issues COMMAND (opcode 0xFC00), device replies with EVENT (event code 0xFF). The exchange() helper builds the COMMAND, computes the checksum, validates the response shell, matches event_id + TID, and skips intervening NTFY_ACTIVE pushes from the dongle.

Checklist

  • I adjusted the README (if needed)
  • For new features in HeadsetControl: I discussed it beforehand in Issues or Discussions and adhered to the wiki

Implements the Sony vendor HCI protocol over the 2.4 GHz USB dongle
(VID 054C, PID 0EBF). Supports:

- Battery (level + charging state) via BATTERY_INFO GET
- Chatmix via GAME_CHAT_MIX_BALANCE GET (device 0..90 mapped to 0..128)
- Sidetone via SIDETONE_VOLUME SET (0..128 mapped to device 0..50)
- Mic volume via MIC_VOLUME SET (0..128 mapped to device 0..50)

The control protocol lives on the Sony-vendor HID collection (usage
page 0xFF04, report ID 0x02). Each report wraps an HCI packet: host
issues COMMAND (opcode 0xFC00), device replies with EVENT (event code
0xFF). The exchange() helper builds the COMMAND, computes the
checksum, validates the response shell, matches event_id + TID, and
skips intervening NTFY_ACTIVE pushes from the dongle.
@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Code review — 2 issues found

Issue 1 (Bug): Transaction counter wraps onto TID=1 (lines 228–231)

uint16_t tid = ++transaction_counter_;
if (tid == 0) {
tid = ++transaction_counter_; // never use 0; the dongle's own NTFY_ACTIVE uses TID=1
}

After 65535 exchange() calls, transaction_counter_ wraps from 0xFFFF to 0, the guard fires, and the next pre-increment yields 1. The comment on line 230 explicitly states the dongle's own NTFY_ACTIVE uses TID=1 — yet the guard only skips 0 and lands on 1. The matching loop accepts ETYPE_NTFY_ACTIVE when both event_id and tid match, so an unsolicited NTFY_ACTIVE could be returned as the authoritative reply (stale battery data, etc.).

Relevant matching logic:

// Prefer a response that matches both event_id and our TID
// (which also implicitly filters out unsolicited NTFY_ACTIVE,
// since those carry TID=1 originated by the dongle).
if (parsed->event_id == event_id
&& parsed->transaction_id == tid
&& (parsed->event_type == want_type
|| parsed->event_type == ETYPE_NTFY_ACTIVE)) {
return *parsed;
}

Suggested fix (replace lines 228–231):

        uint16_t tid = ++transaction_counter_;
        if (tid <= 1) {
            tid = transaction_counter_ = 2; // skip 0 (overflow) and 1 (dongle NTFY_ACTIVE TID)
        }

Issue 2 (CLAUDE.md): Sequential field assignment instead of designated initializers (lines 368–379)

ParsedEvent ev {};
ev.event_id = buf[9];
ev.event_type = buf[10];
ev.address = address;
ev.transaction_id = static_cast<uint16_t>(buf[11])
| static_cast<uint16_t>(buf[12]) << 8;
// Payload runs from buf[13] up to buf[hid_length] inclusive.
if (hid_length > 12) {
ev.payload.assign(buf.begin() + 13, buf.begin() + hid_length + 1);
}
return ev;

parseEvent() initializes ParsedEvent with sequential field assignment (ev.event_id = ..., ev.event_type = ...) instead of designated initializer syntax. CLAUDE.md rule: Designated initializers for struct initialization. ParsedEvent is a plain aggregate; the conditional payload is expressible with a ternary.

Suggested fix (replace lines 368–379):

        return ParsedEvent {
            .event_id = buf[9],
            .event_type = buf[10],
            .address = address,
            .transaction_id = static_cast<uint16_t>(buf[11])
                | static_cast<uint16_t>(buf[12]) << 8,
            .payload = (hid_length > 12)
                ? std::vector<uint8_t>(buf.begin() + 13, buf.begin() + hid_length + 1)
                : std::vector<uint8_t> {},
        };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant