"""Probe: wake a standby Tundra into full-rate IMU streaming.

Field finding (enlyzeam, post-SteamVR): the Tundra streams 0x20 reports at
~250 Hz unique with the gyro apparently dead after a SteamVR exit, vs
~994 Hz cold-boot (S1). Hypothesis: vrserver drops the Watchman into a
standby mode on exit; libsurvive's wired-device "magics" restore full mode.

Candidates (libsurvive driver_vive.c, wired PID 2300 path):
  - VIVE_REPORT_CHANGE_MODE       = feature report {0x04, mode}
  - VIVE_REPORT_LIGHTCAP_VERBOSITY = feature report {0x07, 0x03}
  - HMD power-on magic              {0x04, 0x78, 0x29, 0x38, 0x01, ...}

Sends each candidate to each Tundra interface in turn, measuring the unique
sample rate + gyro spread before/after. Stops at the first candidate that
lifts the rate above 600 Hz.

Usage: python tundra_wake_probe.py [--measure-only]
"""

import struct
import sys
import time

import hid

VID, PID = 0x28DE, 0x2300
MEASURE_S = 2.0

MAGICS = [
    ("change_mode_0", bytes([0x04, 0x00])),
    ("change_mode_1", bytes([0x04, 0x01])),
    ("lightcap_verbosity", bytes([0x07, 0x03])),
    ("hmd_power_on", bytes([0x04, 0x78, 0x29, 0x38, 0x01, 0x00, 0x00, 0x00,
                            0x00, 0x00, 0x02, 0x00, 0x01])),
]


def imu_path():
    for d in hid.enumerate(VID, PID):
        if d.get("interface_number") == 0:
            return d["path"]
    raise SystemExit("FAIL: no MI_00")


def all_ifaces():
    seen = {}
    for d in hid.enumerate(VID, PID):
        n = d.get("interface_number")
        if n not in seen:
            seen[n] = d["path"]
    return sorted(seen.items())


def measure(label):
    dev = hid.device()
    dev.open_path(imu_path())
    dev.set_nonblocking(False)
    t0 = time.perf_counter()
    uniq = 0
    last = None
    gmin = [99999] * 3
    gmax = [-99999] * 3
    reports = 0
    while time.perf_counter() - t0 < MEASURE_S:
        data = dev.read(64, timeout_ms=500)
        if not data:
            continue
        b = bytes(data)
        if b[0] != 0x20 or len(b) < 52:
            continue
        reports += 1
        for i in range(3):
            off = 1 + 17 * i
            g = struct.unpack_from("<3h", b, off + 6)
            tc = struct.unpack_from("<I", b, off + 12)[0]
            seq = b[off + 16]
            if last is not None:
                d_ = (seq - last[1]) & 0xFF
                if (d_ == 0 and tc == last[0]) or d_ >= 128:
                    continue
            last = (tc, seq)
            uniq += 1
            for k in range(3):
                gmin[k] = min(gmin[k], g[k])
                gmax[k] = max(gmax[k], g[k])
    dev.close()
    rate = uniq / MEASURE_S
    spread = [gmax[k] - gmin[k] for k in range(3)]
    print(f"  [{label}] rate={rate:.0f} Hz uniq ({reports} reports), "
          f"gyro spread={spread}")
    return rate, spread


def send_magic(path, payload):
    dev = hid.device()
    try:
        dev.open_path(path)
        buf = payload + bytes(64 - len(payload))
        n = dev.send_feature_report(buf)
        return n
    except Exception as e:
        return f"exc: {e}"
    finally:
        try:
            dev.close()
        except Exception:
            pass


def main():
    print("interfaces:", [(n, p.decode()) for n, p in all_ifaces()])
    base_rate, _ = measure("baseline")
    if "--measure-only" in sys.argv:
        return
    if base_rate > 600:
        print("already at full rate; nothing to wake")
        return

    for name, payload in MAGICS:
        for n, path in all_ifaces():
            r = send_magic(path, payload)
            print(f"send {name} -> MI_{n:02d}: {r}")
            time.sleep(0.3)
            rate, spread = measure(f"after {name} on MI_{n:02d}")
            if rate > 600:
                print(f"SUCCESS: {name} on MI_{n:02d} lifted rate to "
                      f"{rate:.0f} Hz")
                return
    print("no candidate lifted the rate — standby mode needs more digging")


if __name__ == "__main__":
    main()
