Gaze first: plan, and tools for the headset tests

docs/gaze-first.md is the plan agreed on 2026-09-30: with gaze on and no
game, the gaze drives the 3D pointer everywhere, either trigger clicks
where you look (tap, precision by hand movement, hold to drag), and the
controllers keep everything but their lasers.

For its four tests:
- The driver takes "role treadmill", and its compositor bindings repeat
  the right hand's under /user/treadmill. They're additive, so nothing
  changes while the device is a hand. POINTER_ROLE accepts treadmill.
- pointer/probe/lasertest follows who has the dashboard laser and the
  controllers' roles, and with --snapback takes the laser back.
- input/vrws.py reads vrserver's web socket with the standard library
  (the host's Python has no websockets module) and prints component
  changes and update rates. Checked: it connects, and lists the
  controllers' bumper, thumbstick axes, and Steam button, and the
  headset's /proximity, which flickers off for 0.3-0.5 s while worn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-30 17:00:09 -06:00
1 parent d5c2ec65d7
commit bd06207db9
7 files changed
+575 -10

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
// Watch who has SteamVR's laser mouse, for the gaze-first tests (docs/gaze-first.md, tests 1-3).
// Prints the dashboard's primary device and every controller's role whenever either changes,
// and the dashboard and role events. With --snapback, when the laser goes to any device but
// ours (the ft_pointer driver's), it presses our /input/a (switchlaserhand) to take it back,
// at most every 100 ms, and prints how long that took.
// Runs as a background OpenVR client (in the dev container).
// Usage: lasertest [--snapback] [--seconds N]
#include <openvr.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
using Clock = std::chrono::steady_clock;
static volatile std::sig_atomic_t g_stop = 0;
static void Send(const char *msg) {
static int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
sockaddr_un a{};
a.sun_family = AF_UNIX;
const char name[] = "ft_pointer"; // the driver's abstract socket
std::memcpy(a.sun_path + 1, name, sizeof name - 1);
sendto(fd, msg, std::strlen(msg), 0, reinterpret_cast<sockaddr *>(&a),
socklen_t(offsetof(sockaddr_un, sun_path) + 1 + sizeof name - 1));
}
static std::string Describe(vr::IVRSystem *sys, vr::TrackedDeviceIndex_t i) {
if (i == vr::k_unTrackedDeviceIndexInvalid) return "none";
static const char *roles[] = {"none", "left", "right", "optout", "treadmill", "stylus"};
char type[64] = "", serial[64] = "";
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
sys->GetStringTrackedDeviceProperty(i, vr::Prop_SerialNumber_String, serial, sizeof serial);
const auto role = sys->GetControllerRoleForTrackedDeviceIndex(i);
const int hint = sys->GetInt32TrackedDeviceProperty(i, vr::Prop_ControllerRoleHint_Int32);
char out[200];
std::snprintf(out, sizeof out, "%u %s %s role=%s hint=%d", i, type, serial, role < 6 ? roles[role] : "?", hint);
return out;
}
int main(int argc, char **argv) {
bool snapback = false;
double seconds = 0;
for (int i = 1; i < argc; ++i) {
if (!std::strcmp(argv[i], "--snapback")) snapback = true;
else if (!std::strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = std::atof(argv[++i]);
else {
std::fprintf(stderr, "usage: %s [--snapback] [--seconds N]\n", argv[0]);
return 2;
}
}
std::signal(SIGINT, [](int) { g_stop = 1; });
std::signal(SIGTERM, [](int) { g_stop = 1; });
vr::EVRInitError err = vr::VRInitError_None;
vr::IVRSystem *sys = vr::VR_Init(&err, vr::VRApplication_Background);
if (err != vr::VRInitError_None) {
std::printf("VR_Init failed: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
return 1;
}
const auto start = Clock::now();
auto ms = [&] { return std::chrono::duration<double, std::milli>(Clock::now() - start).count(); };
std::string lastPrimary, lastRoles;
Clock::time_point lostAt{}, pressedAt{};
bool pressed = false;
while (!g_stop && (seconds <= 0 || ms() < seconds * 1000)) {
vr::VREvent_t ev;
while (sys->PollNextEvent(&ev, sizeof ev)) {
const char *name = sys->GetEventTypeNameFromEnum(vr::EVREventType(ev.eventType));
switch (ev.eventType) {
case vr::VREvent_DashboardActivated:
case vr::VREvent_DashboardDeactivated:
case vr::VREvent_TrackedDeviceRoleChanged:
case vr::VREvent_TrackedDeviceActivated:
case vr::VREvent_TrackedDeviceDeactivated:
case vr::VREvent_ButtonPress:
case vr::VREvent_ButtonUnpress:
std::printf("%9.1f ms event %s device %u button %u\n", ms(), name, ev.trackedDeviceIndex,
ev.data.controller.button);
break;
default:
break;
}
}
vr::TrackedDeviceIndex_t ours = vr::k_unTrackedDeviceIndexInvalid;
std::string roles;
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
if (sys->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) continue;
char type[64] = "";
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
if (!std::strcmp(type, "ft_pointer")) ours = i;
roles += " [" + Describe(sys, i) + " connected=" + std::to_string(sys->IsTrackedDeviceConnected(i)) + "]";
}
if (roles != lastRoles) {
std::printf("%9.1f ms controllers:%s\n", ms(), roles.c_str());
lastRoles = roles;
}
const auto primary = vr::VROverlay()->GetPrimaryDashboardDevice();
const std::string p = Describe(sys, primary);
if (p != lastPrimary) {
std::printf("%9.1f ms laser: %s%s\n", ms(), p.c_str(), primary == ours ? " (ours)" : "");
if (primary == ours && lostAt != Clock::time_point{}) {
std::printf("%9.1f ms back on ours after %.0f ms\n", ms(),
std::chrono::duration<double, std::milli>(Clock::now() - lostAt).count());
lostAt = {};
}
if (primary != ours && ours != vr::k_unTrackedDeviceIndexInvalid && primary != vr::k_unTrackedDeviceIndexInvalid)
lostAt = Clock::now();
lastPrimary = p;
}
if (snapback) {
const auto now = Clock::now();
if (pressed && now - pressedAt > std::chrono::milliseconds(40)) {
Send("btn a 0");
pressed = false;
}
if (!pressed && lostAt != Clock::time_point{} && now - pressedAt > std::chrono::milliseconds(100)) {
Send("btn a 1");
pressed = true;
pressedAt = now;
std::printf("%9.1f ms snapback: pressed our a\n", ms());
}
}
std::fflush(stdout);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (pressed) Send("btn a 0");
vr::VR_Shutdown();
return 0;
}