# Phase 4: HID Data Pipeline - Research

**Researched:** 2026-03-22
**Domain:** USB HID communication, background threading, TLV parsing, CRC8
**Confidence:** HIGH

## Summary

Phase 4 extends the existing HidDevice RAII wrapper with a background reader thread, feature report send/receive methods, user signature (calibration) reading with TLV+CRC8 parsing, report rate configuration, and USB disconnect/reconnect handling. All communication with the Beyond 2 HMD follows a specific pattern: PC-to-HMD commands use HID feature reports (`hid_send_feature_report`), while HMD-to-PC responses and periodic data use standard interrupt IN reports (`hid_read_timeout`). The firmware docs confirm all reports are 64 bytes, padded with zeros.

The existing codebase uses C++17 with MSVC, HIDAPI 0.14.0 (winapi backend, static link), and DriverLog for logging. The HidDevice class currently has only Open/Close/IsOpen. This phase adds the bulk of HID communication logic while keeping the proximity algorithm and SteamVR integration for later phases.

**Primary recommendation:** Extend HidDevice with a self-contained reader thread that handles reading, reconnection, calibration, and rate configuration internally. DeviceProvider only reads cached atomic values -- no threading awareness needed at the provider level.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- Background reader thread inside HidDevice class (extends existing RAII wrapper)
- HidDevice gains StartReading()/StopReading() methods that manage an internal thread
- Thread calls hid_read() in a loop with 500ms timeout
- Thread writes latest prox_distance to an atomic uint16 -- RunFrame reads cached value (lock-free)
- Only latest value matters for proximity; no queue needed at this phase
- hid_open() called once on connect (or reconnect) -- device stays open for driver's entire lifetime
- Default report rate: 500ms (2 reports/sec)
- Rate command ('R' + uint16 big-endian milliseconds) sent immediately after successful hid_open, before starting reader thread
- Rate is sent via HID feature report (same pattern as Python: `send_feature_report(bytes([0, 'R', msb, lsb]))`)
- Reader thread detects disconnect when hid_read() returns -1
- On disconnect: close device, enter reconnect loop within the reader thread (self-contained)
- Retry hid_open every 5 seconds (periodic, not exponential backoff)
- On successful reconnect: re-read calibration parameters, re-send rate command, resume reading
- DeviceProvider just sees data stop/start -- no awareness of reconnect internals
- Pipe 'status' command exposes connection state: `hid=open/closed/reconnecting`
- Full 512-byte user signature page read via 16 sequential READ_SIG feature reports (32 bytes each)
- TLV parsing with CRC8 validation (polynomial 0x07, init 0xFF) -- reject entries with bad CRC
- Extract 4 tags: programmed_cal (0x06), proximity_threshold (0x0B), proximity_hysteresis (0x0C), user_trim (0x0E)
- Missing tag defaults: programmed_cal=0, threshold=firmware default, hysteresis=firmware default, user_trim=0
- Log which tags were found and which fell back to defaults
- Calibration values exposed in pipe 'status' response: `proximity=X hid=Y cal=Z thresh=T hyst=H trim=U`
- Calibration read performed on initial connect AND on every reconnect

### Claude's Discretion
- Thread synchronization primitives (std::atomic vs mutex for cached values)
- HID feature report send/receive helper method signatures
- User signature TLV parser implementation details (inline vs separate class)
- Exact log message formatting and verbosity levels
- Whether to add a new `src/hid/user_signature.h` or keep TLV parsing inside HidDevice

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| HID-02 | Driver reads periodic HID reports (header '#', bytes 4-5 = uint16 big-endian prox_distance) | Firmware data packet format verified from prox_config.py lines 226-241 and firmware HID_CODE_FOR_DATA_REPLY='#'. Reader thread with hid_read_timeout in loop. |
| HID-03 | Driver reads calibration parameters from HID feature reports on startup: programmed_cal, proximity_threshold, proximity_hysteresis, user_trim | READ_SIG command ('U') sends 16 feature reports, each returns 32 bytes via interrupt IN. TLV+CRC8 parsing extracts tags 0x06, 0x0B, 0x0C, 0x0E. Firmware defaults: threshold=1500, hysteresis=100. |
| HID-04 | Driver adjusts HID report rate from default 1000ms to configured value via HID feature report command 'R' | RATE command format: feature report [0x00, 'R', msb, lsb]. Firmware validates >= MIN_REPORT_RATE (10ms). Default firmware rate = 1000ms. Target rate = 500ms. |
| HID-05 | Driver handles HID device disconnection and reconnection gracefully without crashing vrserver.exe | hid_read_timeout returns -1 on disconnect. Self-contained reconnect loop in reader thread: close, sleep 5s, retry hid_open, re-read cal, re-send rate. |
</phase_requirements>

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| HIDAPI | 0.14.0 | USB HID communication | Already vendored in extern/hidapi, static linked via hidapi::hidapi CMake target |
| C++17 std::thread | N/A | Background reader thread | MSVC ships std::thread, no additional libraries needed |
| C++17 std::atomic | N/A | Lock-free cached value sharing | std::atomic<uint16_t> for prox_distance, std::atomic<int> for connection state |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| std::chrono | N/A | Sleep durations in reconnect loop | std::this_thread::sleep_for in reconnect retry |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| std::atomic<uint16_t> | std::mutex | Mutex adds unnecessary overhead for single-value caching; atomic is lock-free and sufficient |
| Separate TLV parser class | Inline parsing in HidDevice | Separate file improves testability but adds a file; recommend separate `user_signature.h/cpp` for clarity |

**Installation:** No new dependencies. HIDAPI already vendored. std::thread/atomic are header-only on MSVC.

## Architecture Patterns

### Recommended Project Structure
```
src/
  hid/
    hid_device.h          # Extended: reader thread, feature report, calibration
    hid_device.cpp         # Extended: all HID communication logic
    user_signature.h       # NEW: TLV parser, CRC8, calibration data struct
    user_signature.cpp     # NEW: TLV parsing implementation
  driver/
    device_provider.h      # Minor: read cached prox_distance, expose in status
    device_provider.cpp    # Minor: Init calls StartReading, status extended
```

### Pattern 1: Self-Contained Reader Thread
**What:** HidDevice owns a std::thread that runs a read loop. The thread handles reading, disconnect detection, reconnection, calibration re-read, and rate re-configuration -- all internally.
**When to use:** Always -- this is the locked decision from CONTEXT.md.
**Example:**
```cpp
// HidDevice private members
std::thread m_readerThread;
std::atomic<bool> m_bStopRequested{false};
std::atomic<uint16_t> m_lastProxDistance{0};
std::atomic<int> m_connectionState{0}; // 0=closed, 1=open, 2=reconnecting

// Public API
void StartReading(uint16_t vid, uint16_t pid, uint16_t rateMs);
void StopReading();
uint16_t GetProxDistance() const { return m_lastProxDistance.load(std::memory_order_relaxed); }
int GetConnectionState() const { return m_connectionState.load(std::memory_order_relaxed); }

// Thread entry point
void ReaderThreadFunc(uint16_t vid, uint16_t pid, uint16_t rateMs);
```

### Pattern 2: Feature Report Command/Response
**What:** PC sends commands to HMD via `hid_send_feature_report`. HMD responds via standard interrupt IN endpoint (read with `hid_read_timeout`). All reports are 64 bytes, first byte is report ID (0x00 for send, command code for receive).
**When to use:** For RATE command, READ_SIG command, and any future HMD commands.
**Example:**
```cpp
// Send feature report: [0x00, command_code, ...data..., 0-padding to 64 bytes]
bool SendFeatureReport(uint8_t cmdCode, const uint8_t* data, size_t dataLen)
{
    uint8_t buf[65] = {0}; // report ID + 64 bytes
    buf[0] = 0x00;         // report ID
    buf[1] = cmdCode;
    if (data && dataLen > 0)
        memcpy(&buf[2], data, std::min(dataLen, (size_t)62));
    return hid_send_feature_report(m_pDevice, buf, sizeof(buf)) >= 0;
}

// Read response: returns via hid_read_timeout (interrupt IN)
// Response byte 0 = command echo or '#' for periodic data or '$' for success or 'E' for error
int ReadReport(uint8_t* buf, size_t bufLen, int timeoutMs)
{
    return hid_read_timeout(m_pDevice, buf, bufLen, timeoutMs);
}
```

### Pattern 3: TLV + CRC8 Parsing (User Signature)
**What:** 512-byte flash page with Tag-Length-Value entries, each terminated by a CRC8 byte.
**When to use:** Reading calibration parameters from the Beyond 2 user signature page.
**Example:**
```cpp
// CRC8 with polynomial 0x07, initial value 0xFF
uint8_t Crc8(const uint8_t* data, size_t len)
{
    uint8_t crc = 0xFF;
    for (size_t i = 0; i < len; i++)
    {
        crc ^= data[i];
        for (int bit = 0; bit < 8; bit++)
        {
            if (crc & 0x80)
                crc = (crc << 1) ^ 0x07;
            else
                crc = crc << 1;
        }
    }
    return crc;
}

// Tag IDs from firmware
enum class SigTag : uint8_t
{
    Invalid         = 0xFF,
    ProxCal         = 0x06,
    ProxThreshold   = 0x0B,
    ProxHysteresis  = 0x0C,
    UserProxTrim    = 0x0E,
};

struct CalibrationData
{
    uint16_t programmed_cal = 0;
    uint16_t proximity_threshold = 1500;  // firmware default: PROX_SENSOR_THRESHOLD
    uint16_t proximity_hysteresis = 100;  // firmware default: PROX_SENSOR_HYSTERESIS
    int16_t  user_trim = 0;
};
```

### Anti-Patterns to Avoid
- **Polling hid_read with 0 timeout in RunFrame:** RunFrame runs at SteamVR frame rate (~90Hz). Calling hid_read there wastes CPU and adds latency. Use background thread with timeout-based blocking.
- **Sharing hid_device* across threads without synchronization:** The reader thread owns exclusive access to the hid_device handle. DeviceProvider reads cached atomic values only.
- **Queuing HID reports:** Only the latest proximity value matters. A queue adds complexity and memory for no benefit. Atomic store/load is correct.
- **Calling hid_open/hid_close from DeviceProvider while reader thread runs:** All device lifecycle management must be inside the reader thread to avoid races.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| CRC8 computation | Don't look for external CRC library | Implement the 15-line function from config_lib.py | Simple enough; polynomial/init are firmware-specific (0x07/0xFF) |
| Thread management | Don't build a thread pool | std::thread + std::atomic + stop flag | Single reader thread; no pooling needed |
| USB device enumeration | Don't walk USB device tree | hid_open(vid, pid, nullptr) | HIDAPI handles enumeration internally |
| Big-endian parsing | Don't use platform-specific byte swap | Manual `(buf[0] << 8) | buf[1]` | Two bytes; macro/function overkill |

**Key insight:** The HID protocol is fully specified in the firmware code. No need to reverse-engineer anything. Translate the Python reference code to C++ line-by-line.

## Common Pitfalls

### Pitfall 1: Feature Report Size Mismatch
**What goes wrong:** hid_send_feature_report fails silently or returns error because buffer size doesn't match the HID descriptor's feature report size.
**Why it happens:** The Beyond uses 64-byte reports. HIDAPI requires report ID as byte 0, so the buffer must be 65 bytes total (1 byte report ID + 64 bytes data).
**How to avoid:** Always send exactly 65 bytes: `uint8_t buf[65] = {0}; buf[0] = 0x00; buf[1] = cmd; ... hid_send_feature_report(dev, buf, 65);`
**Warning signs:** hid_send_feature_report returns -1.

### Pitfall 2: READ_SIG Response Comes via hid_read, Not hid_get_feature_report
**What goes wrong:** Developer assumes feature report command gets a feature report response and calls hid_get_feature_report, which blocks forever or returns wrong data.
**Why it happens:** The firmware docs explain: "PC->HMD uses feature reports (endpoint 0), HMD->PC uses standard reports (interrupt IN endpoint)." Responses to ALL commands come via `hid_read`/`hid_read_timeout`.
**How to avoid:** Always use `hid_read_timeout` to receive responses, regardless of whether the command was sent via feature report.
**Warning signs:** hid_get_feature_report returns immediately with stale data or times out.

### Pitfall 3: Interleaved Periodic Data During Calibration Read
**What goes wrong:** While reading 16 READ_SIG chunks, periodic '#' data reports arrive and get mixed in with the expected 'U' responses.
**Why it happens:** The firmware sends periodic reports on a timer (default 1000ms). If calibration read takes > 1 second, periodic data arrives mid-sequence.
**How to avoid:** When reading calibration during startup/reconnect, filter responses by first byte. If byte[0]=='#', it's periodic data -- discard and retry the read. Or configure rate AFTER calibration read to minimize interference. Better: read calibration BEFORE starting periodic reporting (rate command).
**Warning signs:** Calibration parsing fails intermittently, especially with fast report rates.

### Pitfall 4: hid_read Returns 0 vs -1
**What goes wrong:** Code treats return value 0 as error and triggers reconnect.
**Why it happens:** hid_read_timeout returns 0 on timeout (no data within timeout period), -1 on error (device disconnected). These are different conditions.
**How to avoid:** `if (ret == -1) { /* disconnect */ } else if (ret == 0) { /* timeout, normal */ } else { /* data available */ }`
**Warning signs:** False disconnect detections, unnecessary reconnect cycles.

### Pitfall 5: Thread Not Joinable on Cleanup
**What goes wrong:** Destructor calls StopReading() but thread is stuck in hid_read with a long timeout, or in Sleep during reconnect.
**Why it happens:** 500ms hid_read timeout + 5s reconnect sleep = up to 5.5s before thread checks stop flag.
**How to avoid:** Use shorter sleep intervals in reconnect (e.g., 50ms x 100 instead of 5000ms x 1), checking stop flag each iteration. The 500ms hid_read timeout is already reasonable.
**Warning signs:** vrserver.exe hangs on shutdown for several seconds.

### Pitfall 6: User Trim is Signed (int16_t)
**What goes wrong:** User trim stored as uint16_t, causing negative adjustments to wrap to large positive values.
**Why it happens:** The firmware header explicitly notes: "NOTE THIS IS A SIGNED VALUE!" for SigTag_Prox_User_Trim (0x0E).
**How to avoid:** Parse user_trim as int16_t, not uint16_t. The TLV value bytes are 2 bytes representing a signed 16-bit integer.
**Warning signs:** Proximity detection threshold is unexpectedly high (65000+) instead of slightly below the programmed threshold.

## Code Examples

### Reading Periodic Data (Translate from Python)
```cpp
// Source: prox_config.py lines 226-241, firmware usbhid_interface.h line 74
// Periodic data packet: byte 0='#' (0x23), byte 1=length, bytes 4-5=prox_distance (big-endian)
void ProcessPeriodicReport(const uint8_t* data, int len)
{
    if (len < 6 || data[0] != '#')
        return;

    uint16_t prox_distance = (static_cast<uint16_t>(data[4]) << 8) | data[5];
    m_lastProxDistance.store(prox_distance, std::memory_order_relaxed);
}
```

### Sending Rate Command
```cpp
// Source: prox_monitor.py line 52, firmware usbhid_interface.c lines 635-645
// RATE command: feature report [0x00, 'R', msb, lsb]
// MIN_REPORT_RATE = 10ms (firmware enforced)
bool SetReportRate(uint16_t rateMs)
{
    uint8_t buf[65] = {0};
    buf[0] = 0x00;  // report ID
    buf[1] = 'R';   // HID_CODE_FOR_RATE
    buf[2] = static_cast<uint8_t>(rateMs >> 8);    // MSB
    buf[3] = static_cast<uint8_t>(rateMs & 0xFF);  // LSB
    int ret = hid_send_feature_report(m_pDevice, buf, sizeof(buf));
    if (ret < 0)
    {
        DriverLog("HID: Failed to set report rate: %ls\n", hid_error(m_pDevice));
        return false;
    }

    // Read the response (comes via interrupt IN)
    uint8_t resp[65];
    int rlen = hid_read_timeout(m_pDevice, resp, sizeof(resp), 1000);
    if (rlen > 0 && resp[0] == '$')  // HID_CODE_FOR_SUCCESS_REPLY
    {
        DriverLog("HID: Report rate set to %ums\n", rateMs);
        return true;
    }
    else if (rlen > 0 && resp[0] == 'E')  // HID_CODE_FOR_ERROR_REPLY
    {
        DriverLog("HID: Rate command rejected by firmware\n");
        return false;
    }
    DriverLog("HID: Rate command - unexpected response (byte0=0x%02X, len=%d)\n",
              rlen > 0 ? resp[0] : 0, rlen);
    return false;
}
```

### Reading User Signature (16 Chunks)
```cpp
// Source: config_lib.py read_sig() lines 154-173, HID_Commands.md READ_SIG section
// READ_SIG command: feature report [0x00, 'U', block_number]
// Response: [0x55/'U', length=32, ...32 bytes of data...]
bool ReadUserSignature(uint8_t* sigOut, size_t sigLen)  // sigLen must be 512
{
    for (int block = 0; block < 16; block++)
    {
        // Send READ_SIG command
        uint8_t cmd[65] = {0};
        cmd[0] = 0x00;   // report ID
        cmd[1] = 'U';    // HID_CODE_FOR_READ_SIG
        cmd[2] = static_cast<uint8_t>(block);
        if (hid_send_feature_report(m_pDevice, cmd, sizeof(cmd)) < 0)
            return false;

        // Read response -- may need to skip periodic '#' reports
        uint8_t resp[65];
        for (int attempts = 0; attempts < 10; attempts++)
        {
            int rlen = hid_read_timeout(m_pDevice, resp, sizeof(resp), 1000);
            if (rlen < 0)
                return false;  // device disconnected
            if (rlen == 0)
                continue;      // timeout, retry
            if (resp[0] == '#')
                continue;      // periodic data, skip
            if (resp[0] == 'U')
            {
                uint8_t chunkLen = resp[1];  // should be 32
                memcpy(&sigOut[block * 32], &resp[2], chunkLen);
                break;
            }
            if (resp[0] == 'E')
                return false;  // error
        }
    }
    return true;
}
```

### TLV Parsing
```cpp
// Source: config_lib.py parse_sig() lines 93-127
CalibrationData ParseCalibration(const uint8_t* sig, size_t sigLen)
{
    CalibrationData cal;  // initialized with defaults
    size_t ptr = 0;

    while (ptr < sigLen)
    {
        uint8_t tag = sig[ptr];
        if (tag == 0xFF)
            break;  // end of valid tags

        if (ptr + 1 >= sigLen)
            break;
        uint8_t length = sig[ptr + 1];

        if (ptr + 2 + length >= sigLen)
            break;  // overrun

        const uint8_t* valueData = &sig[ptr + 2];
        uint8_t crcByte = sig[ptr + 2 + length];

        // Verify CRC over tag + length + value
        uint8_t expectedCrc = Crc8(&sig[ptr], 2 + length);
        if (crcByte != expectedCrc)
        {
            DriverLog("HID: Sig tag 0x%02X CRC mismatch (got 0x%02X, expected 0x%02X)\n",
                      tag, crcByte, expectedCrc);
            ptr += 3 + length;
            continue;
        }

        if (length == 2)
        {
            uint16_t val16 = (static_cast<uint16_t>(valueData[0]) << 8) | valueData[1];
            switch (tag)
            {
                case 0x06: cal.programmed_cal = val16; break;
                case 0x0B: cal.proximity_threshold = val16; break;
                case 0x0C: cal.proximity_hysteresis = val16; break;
                case 0x0E: cal.user_trim = static_cast<int16_t>(val16); break;
            }
        }

        ptr += 3 + length;  // tag + length + value + crc
    }
    return cal;
}
```

**IMPORTANT NOTE on byte order in TLV values:** The firmware stores values in its native byte order (ARM Cortex-M4 = little-endian). The Python code does `sigv2_find_tag(SigTag_Prox_Cal, (uint8_t*)&(proxdata.programmed_cal))` which copies raw bytes directly into a uint16_t on a little-endian MCU. The Python `parse_sig()` returns raw bytes without endian conversion. When reading on a Windows x86_64 machine (also little-endian), the raw bytes can be copied directly. Do NOT apply big-endian conversion to TLV values -- only the periodic report's prox_distance field is big-endian.

### Reader Thread Main Loop
```cpp
void HidDevice::ReaderThreadFunc(uint16_t vid, uint16_t pid, uint16_t rateMs)
{
    while (!m_bStopRequested.load())
    {
        // Attempt connection
        m_connectionState.store(m_pDevice ? 1 : 2);

        if (!m_pDevice)
        {
            m_pDevice = hid_open(vid, pid, nullptr);
            if (!m_pDevice)
            {
                // Reconnect sleep: check stop flag frequently
                for (int i = 0; i < 100 && !m_bStopRequested.load(); i++)
                    std::this_thread::sleep_for(std::chrono::milliseconds(50));
                continue;
            }
            m_connectionState.store(1);
            DriverLog("HID: Device opened/reconnected\n");

            // Read calibration first (before periodic reports start)
            ReadCalibration();

            // Set report rate
            SetReportRate(rateMs);
        }

        // Read loop
        uint8_t buf[65];
        int ret = hid_read_timeout(m_pDevice, buf, sizeof(buf), 500);
        if (ret == -1)
        {
            // Disconnect
            DriverLog("HID: Device disconnected\n");
            hid_close(m_pDevice);
            m_pDevice = nullptr;
            m_connectionState.store(2);  // reconnecting
            continue;
        }
        if (ret > 0 && buf[0] == '#' && ret >= 6)
        {
            uint16_t prox = (static_cast<uint16_t>(buf[4]) << 8) | buf[5];
            m_lastProxDistance.store(prox, std::memory_order_relaxed);
        }
    }
}
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| hid_read (blocking) | hid_read_timeout (with timeout) | HIDAPI 0.10.0+ | Enables clean thread shutdown without killing the thread |
| Raw USB I/O | HIDAPI abstraction | Always | Cross-platform, handles winapi/libusb backends transparently |

**Deprecated/outdated:**
- None relevant. HIDAPI 0.14.0 is current and stable.

## Open Questions

1. **TLV value byte order**
   - What we know: ARM Cortex-M4 is little-endian. The periodic report's prox_distance is documented as big-endian in the packet format. The firmware writes TLV values using `sigv2_find_tag()` which copies raw bytes from flash into struct fields.
   - What's unclear: Whether TLV uint16 values are stored in flash as little-endian (native ARM) or big-endian. The Python parser returns raw bytes without conversion.
   - Recommendation: Assume little-endian (same as x86_64 Windows). Verify on hardware by reading a known calibration value and comparing. Add a DriverLog showing raw hex bytes of each calibration value for debugging.

2. **Periodic reports during calibration read**
   - What we know: Default firmware report rate is 1000ms. Reading 16 sig blocks could take 1-2 seconds.
   - What's unclear: Whether the firmware pauses periodic reports during feature report processing.
   - Recommendation: Read calibration immediately after hid_open, before setting rate. Filter any '#' responses during calibration read sequence. This is already handled in the code example above.

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | PowerShell verification scripts (project pattern) |
| Config file | None -- scripts are standalone |
| Quick run command | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid_pipeline.ps1` |
| Full suite command | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid_pipeline.ps1` |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| HID-02 | Reads periodic reports, extracts prox_distance | smoke (hardware) | `beyond_prox_ctl.exe status` -- check proximity value changes | No, Wave 0 |
| HID-03 | Reads calibration params on startup | smoke (hardware) | `beyond_prox_ctl.exe status` -- check cal/thresh/hyst/trim values present | No, Wave 0 |
| HID-04 | Sets report rate to 500ms | smoke (hardware) | Observe report cadence in log, verify ~2 reports/sec | No, Wave 0 |
| HID-05 | Survives USB disconnect/reconnect | manual | Unplug/replug USB, verify status returns to hid=open | Manual-only (requires physical cable manipulation) |

### Sampling Rate
- **Per task commit:** Build succeeds (`cmake --build build --config Release`)
- **Per wave merge:** Full verification script
- **Phase gate:** Hardware smoke test with real Beyond 2

### Wave 0 Gaps
- [ ] `scripts/verify_hid_pipeline.ps1` -- HID pipeline verification script covering HID-02, HID-03, HID-04
- [ ] Extended `beyond_prox_ctl.exe status` command must parse and display calibration values
- [ ] Build verification: new source files added to CMakeLists.txt

## Sources

### Primary (HIGH confidence)
- `code_samples/beyond_firmware/docs/HID_Commands.md` -- Authoritative HID protocol documentation, feature report vs standard report, READ_SIG format
- `code_samples/beyond_firmware/src/usbhid_interface.h` -- All HID command codes, reply codes, MIN_REPORT_RATE=10
- `code_samples/beyond_firmware/src/usbhid_interface.c` -- RATE command handling (lines 635-645), periodic report task (lines 463-467), report_rate default=1000ms
- `code_samples/beyond_firmware/src/Devices/prox_control.c` -- Firmware calibration init: defaults for threshold (1500), hysteresis (100), programmed_cal (0), user_trim (0)
- `code_samples/beyond_firmware/src/Devices/prox_control.h` -- MIN_PROX_VALUE=100, MAX_PROX_VALUE=16383, Proximity_T struct
- `code_samples/beyond_firmware/src/Devices/prox_tmd2635.h` -- PROX_SENSOR_THRESHOLD=1500, PROX_SENSOR_HYSTERESIS=100
- `code_samples/proximity_sensor_access/config_lib.py` -- CRC8 implementation, TLV parser, read_sig sequence, SigTag enum
- `code_samples/proximity_sensor_access/prox_config.py` -- Periodic data packet format (lines 226-241)
- `code_samples/proximity_sensor_access/prox_monitor.py` -- HID reader thread pattern, rate command usage
- `extern/hidapi/hidapi/hidapi.h` -- HIDAPI C API: hid_read_timeout, hid_send_feature_report, hid_get_feature_report

### Secondary (MEDIUM confidence)
- None needed -- all information sourced from in-repo firmware docs and reference code

### Tertiary (LOW confidence)
- TLV byte order assumption (little-endian) -- needs hardware validation

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- all libraries already vendored and proven in Phases 1-2
- Architecture: HIGH -- locked decisions from CONTEXT.md are complete and internally consistent
- Pitfalls: HIGH -- verified against firmware source code and HIDAPI header documentation
- TLV byte order: LOW -- inferred from ARM architecture, needs hardware validation

**Research date:** 2026-03-22
**Valid until:** Indefinite -- firmware protocol is stable, HIDAPI 0.14.0 is already vendored
