// autopass: install, inspect and support autopassd. // // autopass install install the systemd user unit that starts autopassd // with SteamVR and stops it with SteamVR; start it // autopass uninstall stop and remove the unit, switch back to colour // autopass status autopassd's status and whether the unit is active // autopass update download and install the latest release // autopass report write ~/frame-autopass-report.txt for a bug report // autopass version print the version // autopass guard [reset] show or clear the crash guard (XRService restarts // soon after autopassd switches) // autopass state [SECONDS] passive: camera config and colour light value // from shared memory, no SteamVR calls // autopass get read the camera config (private interface) // autopass set rgb|mono [--quiet] // switch the camera source (used by autopassd). Exit // codes: 0 done or already so, 1 error, 3 SteamVR's // interface changed, 4 camera not enabled, 5 no // Arcturus colour module. // // `get` and `set` connect to SteamVR as a background client for a few // milliseconds; that never starts SteamVR and does not wake the headset. #include "app_paths.hpp" #include "passthrough_state.hpp" #include "private_camera.hpp" #include #include #include #include #include #include #include #include namespace { constexpr const char* kUnit = "frame-autopass.service"; constexpr const char* kInstallScript = "https://github.com/bod09/frame-autopass/releases/latest/download/install.sh"; int usage() { std::fprintf(stderr, "usage: autopass install | uninstall | status | update | report | version |\n" " guard [reset] | state [SECONDS] | get | set rgb|mono [--quiet]\n"); return 2; } std::string unit_path() { const char* xdg = std::getenv("XDG_CONFIG_HOME"); const char* home = std::getenv("HOME"); const std::string base = xdg && *xdg == '/' ? xdg : std::string(home ? home : "/tmp") + "/.config"; return base + "/systemd/user/" + kUnit; } // Runs `systemctl --user ARGS...`, returns its exit code. Always against // the login session's user manager: Desktop Mode on the Frame runs Konsole // in a nested session whose XDG_RUNTIME_DIR and D-Bus cannot reach it. int systemctl(std::initializer_list args, bool quiet = false) { const std::string run = "/run/user/" + std::to_string(::getuid()); std::string cmd = "XDG_RUNTIME_DIR=" + run + " DBUS_SESSION_BUS_ADDRESS=unix:path=" + run + "/bus systemctl --user"; for (const char* a : args) cmd += std::string(" ") + a; if (quiet) cmd += " >/dev/null 2>&1"; const int rc = std::system(cmd.c_str()); return WIFEXITED(rc) ? WEXITSTATUS(rc) : 1; } std::string unit_text() { return std::string( "# Installed by `autopass install`. Starts autopassd with SteamVR and stops it\n" "# with SteamVR. Remove with `autopass uninstall`.\n" "[Unit]\n" "Description=Adaptive passthrough (colour in good light, IR in the dark)\n" "After=steamvr.service\n" "BindsTo=steamvr.service\n" "\n" "[Service]\n" "Type=simple\n" "ExecStart=") + autopass::install_dir() + "/autopassd\n" "Restart=on-failure\n" "RestartSec=5\n" "Slice=session.slice\n" "Nice=10\n" "\n" "[Install]\n" "WantedBy=steamvr.service\n"; } int install() { const std::string dir = autopass::install_dir(); if (::access((dir + "/autopassd").c_str(), X_OK) != 0 || ::access((dir + "/autopass").c_str(), X_OK) != 0) { std::fprintf(stderr, "autopassd and autopass must be in %s first\n", dir.c_str()); return 1; } if (!autopass::colour_module_present()) { std::fprintf(stderr, "The Arcturus Vision colour module was not found (no arcimx616 camera in\n" "/sys/class/video4linux). frame-autopass switches between that module and the\n" "built-in IR cameras, so it needs the module attached. Not installing.\n"); return 1; } const std::string path = unit_path(); if (!autopass::make_dirs(path.substr(0, path.rfind('/'))) || !autopass::write_file_atomic(path, unit_text())) { std::fprintf(stderr, "cannot write %s\n", path.c_str()); return 1; } if (systemctl({"daemon-reload"}) != 0 || systemctl({"enable", "--now", kUnit}) != 0) { std::fprintf(stderr, "systemctl failed; see `systemctl --user status %s`\n", kUnit); return 1; } std::printf("installed %s: autopassd now starts and stops with SteamVR\n", path.c_str()); return 0; } int switch_source(bool want_rgb, bool quiet); int uninstall() { systemctl({"disable", "--now", kUnit}, true); std::remove(unit_path().c_str()); systemctl({"daemon-reload"}, true); std::printf("removed %s\n", kUnit); // Leave the headset showing colour, the stock behaviour with the module. switch_source(true, true); return 0; } int status() { std::string text; std::printf("unit: %s, %s\n", systemctl({"is-enabled", "--quiet", kUnit}, true) == 0 ? "installed" : "not installed", systemctl({"is-active", "--quiet", kUnit}, true) == 0 ? "running" : "not running"); std::printf("version: %s\n", autopass::version()); if (autopass::read_file(autopass::status_path(), &text)) { std::printf("status: %s", text.c_str()); if (text.find("\"blocked\": \"steamvr-changed\"") != std::string::npos) std::printf("\nSteamVR has changed the camera interface frame-autopass uses, so it has\n" "stopped switching (passthrough itself works normally). Run `autopass update`;\n" "if that does not help, a new release is needed.\n"); else if (text.find("\"blocked\": \"no-colour-module\"") != std::string::npos) std::printf("\nThe Arcturus Vision colour module is not attached, so there is nothing to\n" "switch to; it resumes when the module is back.\n"); else if (text.find("\"blocked\": \"crash-guard\"") != std::string::npos) std::printf("\nXRService restarted twice soon after a switch, so switching is paused as a\n" "precaution. `autopass guard reset`, then restart SteamVR, to resume.\n"); } if (autopass::read_file(autopass::crash_guard_path(), &text) && !text.empty()) std::printf("crash guard entries (switching stops at 2):\n%s", text.c_str()); return 0; } int guard(bool reset) { std::string text; const bool present = autopass::read_file(autopass::crash_guard_path(), &text) && !text.empty(); if (reset) { std::remove(autopass::crash_guard_path().c_str()); std::printf("crash guard cleared; restart autopassd (or SteamVR) to resume switching\n"); return 0; } std::printf(present ? "crash guard entries:\n%s" : "crash guard: no entries\n", text.c_str()); return 0; } int state(double seconds) { const autopass::PassthroughState st; if (st.path().empty()) { std::fprintf(stderr, "passthrough state file not found in /dev/shm\n"); return 1; } const auto end = std::chrono::steady_clock::now() + std::chrono::duration(seconds); do { const auto s = st.read(); if (!s) { std::fprintf(stderr, "cannot read %s\n", st.path().c_str()); return 1; } std::printf("config: %s", s->config.describe().c_str()); if (s->colour) std::printf(" | colour light %.3f", s->colour->light); std::printf(" | newest mono %.3f colour %.3f\n", s->mono_timestamp, s->colour_timestamp); if (seconds > 0) std::this_thread::sleep_for(std::chrono::milliseconds(500)); } while (std::chrono::steady_clock::now() < end); return 0; } int update() { std::printf("fetching %s\n", kInstallScript); const std::string cmd = std::string("curl -fsSL ") + kInstallScript + " | bash"; const int rc = std::system(cmd.c_str()); return WIFEXITED(rc) ? WEXITSTATUS(rc) : 1; } // Appends a command's output to the report. void section(std::string* out, const char* title, const std::string& cmd) { *out += std::string("\n== ") + title + " ==\n"; if (FILE* p = ::popen((cmd + " 2>&1").c_str(), "r")) { char buf[4096]; std::size_t n; while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) out->append(buf, n); ::pclose(p); } } int report() { const char* home = std::getenv("HOME"); const std::string path = std::string(home ? home : "/tmp") + "/frame-autopass-report.txt"; std::string out = std::string("frame-autopass report, version ") + autopass::version() + "\n"; out += std::string("Arcturus colour module: ") + (autopass::colour_module_present() ? "found" : "NOT found") + "\n"; section(&out, "date", "date"); section(&out, "SteamOS", "grep -E '^(VERSION_ID|BUILD_ID)=' /etc/os-release"); section(&out, "SteamVR", "cat /opt/steamvr/bin/version.txt; grep -m1 -E 'cv: version [0-9]' ~/.local/share/Steam/logs/vrserver.txt"); section(&out, "unit", "XDG_RUNTIME_DIR=/run/user/" + std::to_string(::getuid()) + " systemctl --user status --no-pager " + kUnit + " | head -5"); section(&out, "status", "cat " + autopass::status_path()); section(&out, "config", "cat " + autopass::conf_path() + " 2>/dev/null || echo '(defaults)'"); section(&out, "crash guard", "cat " + autopass::crash_guard_path() + " 2>/dev/null || echo '(empty)'"); section(&out, "autopassd log (last 300 lines)", "tail -n 300 " + autopass::log_path()); section(&out, "XRService (last 150 relevant lines)", "grep -hE 'IREmitters|Passthrough cameras|UserPresence|IMUFallback\\] (En|Dis)|Transition to IMU' " "~/.local/share/Steam/logs/xrservice.txt | cut -c1-140 | tail -n 150"); if (!autopass::write_file_atomic(path, out)) { std::fprintf(stderr, "cannot write %s\n", path.c_str()); return 1; } std::printf("wrote %s\nIt contains your SteamOS and SteamVR versions and recent logs (no personal\n" "data that I know of; have a look before sharing it). Attach it to a GitHub issue.\n", path.c_str()); return 0; } struct Session { bool ok = false; Session() { vr::EVRInitError err = vr::VRInitError_None; vr::VR_Init(&err, vr::VRApplication_Background); ok = err == vr::VRInitError_None; if (!ok) std::fprintf(stderr, "SteamVR unavailable: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err)); } ~Session() { if (ok) vr::VR_Shutdown(); } }; int get_config() { Session session; if (!session.ok) return 1; autopass::PrivateCamera camera; if (!camera.ok()) { std::fprintf(stderr, "%s\n", camera.error().c_str()); return 3; } const auto c = camera.get(); if (!c) { std::fprintf(stderr, "%s\n", camera.error().c_str()); return 1; } std::printf("%s\n", c->describe().c_str()); return 0; } int switch_source(bool want_rgb, bool quiet) { if (!autopass::colour_module_present()) { if (!quiet) std::fprintf(stderr, "Arcturus colour module not found; not changing anything\n"); return 5; } Session session; if (!session.ok) return 1; autopass::PrivateCamera camera; if (!camera.ok()) { std::fprintf(stderr, "%s\n", camera.error().c_str()); return 3; } auto c = camera.get(); if (!c) { std::fprintf(stderr, "%s\n", camera.error().c_str()); return 1; } if (!c->enabled) { if (!quiet) std::fprintf(stderr, "passthrough camera is not enabled; not changing anything\n"); return 4; } if (c->rgb == static_cast(want_rgb)) { if (!quiet) std::printf("already %s\n", want_rgb ? "colour" : "IR"); return 0; } c->rgb = want_rgb ? 1 : 0; if (!camera.set(*c)) { std::fprintf(stderr, "%s\n", camera.error().c_str()); return 1; } if (!quiet) std::printf("switched to %s\n", want_rgb ? "colour" : "IR"); return 0; } } // namespace int main(int argc, char** argv) { if (argc < 2) return usage(); const std::string cmd = argv[1]; if (cmd == "install" && argc == 2) return install(); if (cmd == "uninstall" && argc == 2) return uninstall(); if (cmd == "status" && argc == 2) return status(); if (cmd == "update" && argc == 2) return update(); if (cmd == "report" && argc == 2) return report(); if ((cmd == "version" || cmd == "--version") && argc == 2) return std::printf("%s\n", autopass::version()) < 0; if (cmd == "guard" && argc == 2) return guard(false); if (cmd == "guard" && argc == 3 && std::string(argv[2]) == "reset") return guard(true); if (cmd == "state" && argc <= 3) return state(argc == 3 ? std::atof(argv[2]) : 0); if (cmd == "get" && argc == 2) return get_config(); if (cmd == "set" && (argc == 3 || (argc == 4 && std::string(argv[3]) == "--quiet"))) { const std::string which = argv[2]; if (which != "rgb" && which != "mono") return usage(); return switch_source(which == "rgb", argc == 4); } return usage(); }