fix(hid): three StreamDeck+ robustness gaps from #3236 review (#3248)#3250
Conversation
Three small, independent fixes addressing the concerns surfaced during PR #3236's review and tracked in #3248. ## 1. StreamDeckPlusParser: don't drop simultaneous events Both the encoder-push and LCD-key paths in `parse()` overwrote `m_prev*` with `newState` before iterating to find the first changed bit, so any second changed bit was permanently silently dropped — the next poll() saw `m_prev* == newState` and reported no change. Switch to one-bit-at-a-time commit: `m_prev* ^= (1u << i)` consumes only the bit being reported, so subsequent polls see the remaining changed bits and emit them. ## 2. HidEncoderManager: check hid_write return on image writes setKeyImage and setTouchscreenImage looped through 1024-byte feature reports ignoring the return value. On mid-stream device disconnect, the loop would spin through the rest of the image writing into a dead handle with no log signal. Add a negative-return check that logs a warning with packet context and bails out; the next poll()'s hid_read() < 0 will trigger close() + hotplug reopen, which is now correlatable with the warning in logs. ## 3. HidEncoderManager: atomic gate-check members m_device / m_openVid / m_openPid are read from the main thread (refreshStreamDeckLabels, status snapshot) and written from m_extCtrlThread (open / close / hotplugCheck). Wrap in std::atomic so the cross-thread reads are well-defined. Relaxed memory order is sufficient because callers treat the result as a hint and the real gate is re-checked inside the queued slot. The implicit conversion operators on `std::atomic<T*>` / `<uint16_t>` mean no .cpp call-sites need explicit `.load()` / `.store()`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@AetherClaude review this PR |
ReviewTraced through all three fixes against 1. Parser — simultaneous-event fix ✅The pre-fix bug is real and exactly as described: Hand-trace for "press dial 0 + dial 1 in the same HID report" (starting from
Matches your trace — symmetric on press and release, no state drift if the user lifts before the queue drains. 2.
|
Closes #3248.
Implements the three small robustness fixes flagged during PR #3236's review and tracked in #3248. Each is independent and scoped to one file.
1. `StreamDeckPlusParser::parse` — don't drop simultaneous events
Both the encoder-push and LCD-key paths overwrote `m_prevEncBtns` / `m_prevKeys` with `newState` before the loop that finds the first changed bit, so any second-changed-bit got silently dropped (the next `poll()` saw no remaining change). Switch to one-bit-at-a-time commit:
```cpp
m_prevKeys ^= (1u << i); // consume just this bit
return {.button = i + 1, ...};
```
Now if two keys change in the same HID report, the first poll iteration emits one and the next poll iteration emits the other. Verified by hand-trace: press both → emit press 1 → emit press 2 → release both → emit release 1 → emit release 2. ✅
2. `HidEncoderManager` — check `hid_write` return on image writes
`setKeyImage` and `setTouchscreenImage` looped through 1024-byte feature reports ignoring the return. If the device unplugs mid-stream the loop spins through the rest writing into a dead handle with no log signal. Now:
```cpp
const int written = hid_write(m_device, pkt, PACKET_SIZE);
if (written < 0) {
qCWarning(lcDevices) << "HidEncoderManager::setKeyImage: hid_write failed"
<< "key=" << key << "page=" << pageNumber
<< "— device disconnected? Will retry on hotplug.";
return;
}
```
The next `poll()`'s `hid_read() < 0` triggers `close()` + hotplug reopen, which is now correlatable in logs with the user-visible "deck went blank".
3. `HidEncoderManager` — atomic gate-check members
`m_device` / `m_openVid` / `m_openPid` are read from the main thread (refreshStreamDeckLabels at MainWindow.cpp:6778, status snapshot at :7061+) and written from `m_extCtrlThread` (open / close / hotplugCheck). Wrap in `std::atomic` so cross-thread reads are well-defined. Relaxed memory order is sufficient because callers treat the result as a hint and the real gate is re-checked inside the queued slot.
`std::atomic<hid_device*>` and `std::atomic<uint16_t>` have implicit conversion / assignment operators, so no .cpp call-site needs explicit `.load()` / `.store()` — the source diff is purely the header change plus the existing reads continuing to compile under the new types.
Test plan
Diff stats: +61 / -25 across 3 files.
🤖 Generated with Claude Code