mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 06:00:09 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a796b5013 | ||
|
|
67a3c02e31 | ||
|
|
662919babf | ||
|
|
98bd6ce21c | ||
|
|
e1f27958ca | ||
|
|
c969963b83 | ||
|
|
dfc8e6eb5f |
No files matched your search
@@ -26,7 +26,7 @@ A Steam Frame is someone's personal headset, and they may be wearing it while yo
|
|||||||
|
|
||||||
- Don't kill or restart `gamescope`, `steam`, `vrserver`, `vrcompositor`, the gamescope session, or the Frametop desktop without asking. Each one ends or disrupts whatever is happening in VR.
|
- Don't kill or restart `gamescope`, `steam`, `vrserver`, `vrcompositor`, the gamescope session, or the Frametop desktop without asking. Each one ends or disrupts whatever is happening in VR.
|
||||||
- Don't run host `sudo`, `steamos-readonly disable`, `steamos-devmode` changes, pacman installs, or reboots without explicit approval. Three installers need host `sudo`, and they ask for it: the Bluetooth fixes (`setup/bluetooth/install.sh`), hand tracking (`hands/run.sh install` and `caps`, which set ft-camd's file capabilities with `setcap`), and our own eye tracker's frame grabber (`gaze/tracker/install.sh`).
|
- Don't run host `sudo`, `steamos-readonly disable`, `steamos-devmode` changes, pacman installs, or reboots without explicit approval. Three installers need host `sudo`, and they ask for it: the Bluetooth fixes (`setup/bluetooth/install.sh`), hand tracking (`hands/run.sh install` and `caps`, which set ft-camd's file capabilities with `setcap`), and our own eye tracker's frame grabber (`gaze/tracker/install.sh`).
|
||||||
- Write only inside the repo, `/tmp`, and the container unless told otherwise. The installers are the exception: they write the user services, launchers, and the SteamVR driver into the home folder. The Bluetooth fixes and the eye tracker's frame grabber also install root-owned files and system services under `/etc` (`/etc/steamframe`, `/etc/frametop`, `/etc/systemd/system`).
|
- Write only inside the repo, `/tmp`, and the container unless told otherwise. The installers are the exception: they write the user services, launchers, and the SteamVR driver into the home folder. The Bluetooth fixes and the eye tracker's frame grabber also install root-owned files and system services under `/etc` (`/etc/steamframe`, `/etc/frametop`, `/etc/systemd/system`). When an installer starts writing something new outside the repo, add it to `uninstall.sh` too: users uninstall with that script, not with each installer's `uninstall`.
|
||||||
- Never copy `.netrc`, SSH keys, or Steam config off the Frame or into this repo.
|
- Never copy `.netrc`, SSH keys, or Steam config off the Frame or into this repo.
|
||||||
|
|
||||||
## SteamOS updates
|
## SteamOS updates
|
||||||
|
|||||||
@@ -171,23 +171,22 @@ Or by hand: `cd ~/frametop && git pull && ./install.sh`.
|
|||||||
|
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|
||||||
|
In a terminal on the headset, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
./desktops.sh uninstall # the launcher's Desktop entry goes back to the stock desktop
|
curl -fsSL https://deejanuz.github.io/frametop/uninstall.sh | bash
|
||||||
./desktops.sh relay uninstall
|
|
||||||
pointer/helper/run.sh uninstall
|
|
||||||
power/run.sh uninstall
|
|
||||||
pointer/driver/install.sh uninstall # then restart SteamVR
|
|
||||||
input-settings/install.sh uninstall
|
|
||||||
display-settings/install.sh uninstall
|
|
||||||
remote/install.sh uninstall
|
|
||||||
setup/bluetooth/install.sh uninstall # if you installed the Bluetooth fixes
|
|
||||||
hands/run.sh uninstall # if you installed hand tracking by hand
|
|
||||||
gaze/run.sh uninstall # if you installed the gaze service
|
|
||||||
gaze/tracker/install.sh uninstall # if you installed our own eye tracker's frame grabber
|
|
||||||
gaze/probe/install.sh uninstall # if you installed the gaze probe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Your settings stay: `~/.config/frametop.conf`, `frametop-input.json` (button maps and key combinations), `frametop-layout.json` (the layout and profiles), `frametop-float.json`, and `frametop-remote/` in `~/.config`, and the gaze calibration in `~/.local/state/frametop/gaze`. So does the desktop's own Plasma setup, in `~/.config/frametop`. Delete them too for a clean slate.
|
It works in two steps, so it never takes away the keyboard, mouse, or desktop you're using while it runs:
|
||||||
|
|
||||||
|
1. It stops Frametop from starting. Launch a program → Desktop opens the stock desktop again, and Frametop's services, its SteamVR driver, its menu entries, and the system files of our eye tracker and the Bluetooth fixes are removed (those need your `sudo` password). Everything running now keeps running until you restart the headset, and it offers to restart it for you.
|
||||||
|
2. After the restart, run the same command again. It deletes the code in `~/frametop`, and asks whether to delete your settings, any eye or hand recordings, and the build container (1–2 GB) too.
|
||||||
|
|
||||||
|
To see what it would do without changing anything, add `-s -- --dry-run` after `bash`. If the code isn't in `~/frametop`, add `-s -- --dir <folder>`. From the repo, the same script is `./uninstall.sh`.
|
||||||
|
|
||||||
|
Don't delete `~/frametop` by hand before you uninstall and restart: the desktop and the input relay run from it, and without it Launch a program → Desktop no longer opens anything. If you've already deleted it, the command above still works, since it doesn't need the repo.
|
||||||
|
|
||||||
|
Unless you ask for them to go, your settings stay: `~/.config/frametop.conf`, `frametop-input.json` (button maps and key combinations), `frametop-layout.json` (the layout and profiles), `frametop-float.json`, and `frametop-remote/` in `~/.config`, the gaze calibration in `~/.local/state/frametop`, and the desktop's own Plasma setup in `~/.config/frametop`. A later install picks them up again.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -197,6 +196,7 @@ A Plasma session runs nested inside ft-screens (`screens/`), a small Wayland com
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `get.sh` | The one-line installer: picks stable or experimental, clones or updates the repo, and runs `install.sh`. |
|
| `get.sh` | The one-line installer: picks stable or experimental, clones or updates the repo, and runs `install.sh`. |
|
||||||
| `install.sh` | The one-step installer. Safe to re-run. |
|
| `install.sh` | The one-step installer. Safe to re-run. |
|
||||||
|
| `uninstall.sh` | The uninstaller: run it, restart the headset, and run it again. It doesn't need the rest of the repo. |
|
||||||
| `desktops.sh` | Start, stop, and configure the desktop, and install the input relay. |
|
| `desktops.sh` | Start, stop, and configure the desktop, and install the input relay. |
|
||||||
| `screens/` | ft-screens, the compositor (wlroots and OpenVR). |
|
| `screens/` | ft-screens, the compositor (wlroots and OpenVR). |
|
||||||
| `session/` | The desktop session script and its config example. |
|
| `session/` | The desktop session script and its config example. |
|
||||||
|
|||||||
+5
-1
@@ -894,7 +894,11 @@ class Checks:
|
|||||||
self.back_since = None # gone again before DON_DELAY
|
self.back_since = None # gone again before DON_DELAY
|
||||||
elif self.back_since is not None and now - self.back_since >= DON_DELAY:
|
elif self.back_since is not None and now - self.back_since >= DON_DELAY:
|
||||||
self.away, self.back_since = False, None
|
self.away, self.back_since = False, None
|
||||||
self.full_armed = True # a calibration that closed unfinished opens again
|
if not self.check:
|
||||||
|
# A calibration that closed unfinished opens again. Not one still open: gaze mode
|
||||||
|
# coming on wakes our tracker, so its eyes come back just as the calibration
|
||||||
|
# opens, and re-arming then opened a second one when the first ended.
|
||||||
|
self.full_armed = True
|
||||||
self.auto_quick("the headset went on")
|
self.auto_quick("the headset went on")
|
||||||
if svc.kind == "own" and now - svc.own_at < 5:
|
if svc.kind == "own" and now - svc.own_at < 5:
|
||||||
reseat = any(e.get("reseat") for e in (svc.own.get("eyes") or {}).values())
|
reseat = any(e.get("reseat") for e in (svc.own.get("eyes") or {}).values())
|
||||||
|
|||||||
@@ -201,8 +201,14 @@ helper_state["reply"] = "ok on worn"
|
|||||||
check("gaze mode on: the calibration opens by itself", wait(lambda: check_state().get("kind") == "full", 6), True)
|
check("gaze mode on: the calibration opens by itself", wait(lambda: check_state().get("kind") == "full", 6), True)
|
||||||
check("it runs blind (no gaze from ours yet)", check_state().get("blind"), True)
|
check("it runs blind (no gaze from ours yet)", check_state().get("blind"), True)
|
||||||
check("nothing says it can't open", any("can't open" in m for m in logs), False)
|
check("nothing says it can't open", any("can't open" in m for m in logs), False)
|
||||||
|
# Live 2026-10-05: turning gaze mode on woke our tracker, and the calibration opened before
|
||||||
|
# DON_DELAY of eyes had passed, so "the headset went on" came while it was open. That re-armed
|
||||||
|
# it, and a second calibration opened as soon as the first ended.
|
||||||
|
svc.checks.away, svc.checks.back_since = True, None
|
||||||
|
|
||||||
check("dot 1: a click takes it", take_dot(0), True)
|
check("dot 1: a click takes it", take_dot(0), True)
|
||||||
|
check("the headset went on while it was open",
|
||||||
|
wait(lambda: not svc.checks.away, gazecheck.DON_DELAY + 2) and bool(svc.checks.check), True)
|
||||||
t0, t1, yaw, pitch = eyes_state["points"][0]
|
t0, t1, yaw, pitch = eyes_state["points"][0]
|
||||||
dot = gazecheck.check_dots("full", True)[0]
|
dot = gazecheck.check_dots("full", True)[0]
|
||||||
check("its window is the look up to the click (about CHECK_WINDOW)",
|
check("its window is the look up to the click (about CHECK_WINDOW)",
|
||||||
@@ -222,6 +228,8 @@ check("all dots: ours fits its calibration (calib-fit)",
|
|||||||
wait(lambda: eyes_state["cal"] is not None and not svc.checks.check, 4), True)
|
wait(lambda: eyes_state["cal"] is not None and not svc.checks.check, 4), True)
|
||||||
check("the service sees it calibrated", wait(lambda: svc.checks.calibrated() is True, 4), True)
|
check("the service sees it calibrated", wait(lambda: svc.checks.calibrated() is True, 4), True)
|
||||||
check("and gaze mode stays on", "gaze off" in helper_state["heard"], False)
|
check("and gaze mode stays on", "gaze off" in helper_state["heard"], False)
|
||||||
|
check("and no second calibration opens", wait(lambda: svc.checks.check is not None, 3), False)
|
||||||
|
check("one calibration in the log", sum(m.startswith("full check:") for m in logs), 1)
|
||||||
|
|
||||||
print("FAILED: " + ", ".join(failures) if failures else "all passed", flush=True)
|
print("FAILED: " + ", ".join(failures) if failures else "all passed", flush=True)
|
||||||
svc.running = False
|
svc.running = False
|
||||||
|
|||||||
+42
-5
@@ -56,13 +56,19 @@ ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|||||||
STAMP = re.compile(r"^\w{3} \w{3} \d{2} \d{4} (\d{2}:\d{2}:\d{2})\.\d+ (\w+): ?(.*)$")
|
STAMP = re.compile(r"^\w{3} \w{3} \d{2} \d{4} (\d{2}:\d{2}:\d{2})\.\d+ (\w+): ?(.*)$")
|
||||||
# Lines worth reading; anything else is skipped before the regexes (the log grows by MBs a day).
|
# Lines worth reading; anything else is skipped before the regexes (the log grows by MBs a day).
|
||||||
KEYS = ("FPGA", "VCINT", "Created", "TrackingCameraInit", "Closing tracking camera", "Streaming",
|
KEYS = ("FPGA", "VCINT", "Created", "TrackingCameraInit", "Closing tracking camera", "Streaming",
|
||||||
"systemd suspend", "systemd resume", "XRService logging to", "Exiting XRService")
|
"systemd suspend", "systemd resume", "XRService logging to", "Exiting XRService", "ISP ")
|
||||||
|
# XRService's numbering of its tracking cameras (the TrackingCameraInit index): ft-hands names
|
||||||
|
# them this way too (track/main.cpp, cameras_from_xrservice_log).
|
||||||
|
NAMES = ("slam_left", "slam_right", "upper_left", "upper_right")
|
||||||
RE_PASSTHRU = re.compile(r"Passthrough connected but FPGA is (\S+) - loading VCINT")
|
RE_PASSTHRU = re.compile(r"Passthrough connected but FPGA is (\S+) - loading VCINT")
|
||||||
RE_INTERLEAVE = re.compile(r"Upper cameras FPGA interleaving support: (\d)")
|
RE_INTERLEAVE = re.compile(r"Upper cameras FPGA interleaving support: (\d)")
|
||||||
RE_TASKS = re.compile(r"Created (\d+) tasks \((\d+) tracking, (\d+) passthrough\)")
|
RE_TASKS = re.compile(r"Created (\d+) tasks \((\d+) tracking, (\d+) passthrough\)")
|
||||||
RE_INIT = re.compile(r"TrackingCameraInit: index: (\d+)\. video device: /dev/video(\d+)")
|
RE_INIT = re.compile(r"TrackingCameraInit: index: (\d+)\. video device: /dev/video(\d+)")
|
||||||
RE_STREAM = re.compile(r"Streaming resumed \(FPGA: (\S+), VC interleaving: (\w+)\)")
|
RE_STREAM = re.compile(r"Streaming resumed \(FPGA: (\S+), VC interleaving: (\w+)\)")
|
||||||
RE_STATE = re.compile(r"FPGA state check: (\S+)")
|
RE_STATE = re.compile(r"FPGA state check: (\S+)")
|
||||||
|
# Without the colour module XRService runs the side cameras through the ISP, as NV12 on other
|
||||||
|
# capture pipes (vfe0 and vfe1), and the upper pair on vfe3 and vfe4.
|
||||||
|
RE_ISP = re.compile(r"ISP (enabled|disabled) for tracking cameras")
|
||||||
|
|
||||||
|
|
||||||
class LogState:
|
class LogState:
|
||||||
@@ -85,7 +91,7 @@ class LogState:
|
|||||||
def _new_episode(self, t):
|
def _new_episode(self, t):
|
||||||
self.closed = False
|
self.closed = False
|
||||||
self.episode = {"start": t, "fpga_before": "", "vcint": "", "interleave": None, "tasks": None,
|
self.episode = {"start": t, "fpga_before": "", "vcint": "", "interleave": None, "tasks": None,
|
||||||
"inits": {}, "stream": "", "failure": "", "evidence": []}
|
"inits": {}, "stream": "", "isp": None, "failure": "", "evidence": []}
|
||||||
if self.closed_at:
|
if self.closed_at:
|
||||||
self.episode["evidence"].append(self.closed_at)
|
self.episode["evidence"].append(self.closed_at)
|
||||||
|
|
||||||
@@ -146,6 +152,12 @@ class LogState:
|
|||||||
ep["interleave"] = int(m.group(1))
|
ep["interleave"] = int(m.group(1))
|
||||||
ep["evidence"].append(short)
|
ep["evidence"].append(short)
|
||||||
return
|
return
|
||||||
|
m = RE_ISP.search(text)
|
||||||
|
if m:
|
||||||
|
ep = self._ep(t)
|
||||||
|
ep["isp"] = m.group(1) == "enabled"
|
||||||
|
ep["evidence"].append(short)
|
||||||
|
return
|
||||||
m = RE_TASKS.search(text)
|
m = RE_TASKS.search(text)
|
||||||
if m:
|
if m:
|
||||||
ep = self._ep(t)
|
ep = self._ep(t)
|
||||||
@@ -184,6 +196,12 @@ class LogState:
|
|||||||
got = tuple(self.nodes[i] for i in (2, 3) if i in self.nodes)
|
got = tuple(self.nodes[i] for i in (2, 3) if i in self.nodes)
|
||||||
return got if len(got) == 2 else UPPER_NODES
|
return got if len(got) == 2 else UPPER_NODES
|
||||||
|
|
||||||
|
def camera_map(self):
|
||||||
|
"""{calibration name: /dev/videoN's N} from the latest camera start's TrackingCameraInit
|
||||||
|
lines (the whole log's when that start has none yet)."""
|
||||||
|
inits = (self.episode or {}).get("inits") or self.nodes
|
||||||
|
return {NAMES[i]: node for i, node in sorted(inits.items()) if 0 <= i < len(NAMES)}
|
||||||
|
|
||||||
def tracking_nodes(self):
|
def tracking_nodes(self):
|
||||||
got = tuple(self.nodes[i] for i in range(TRACKING) if i in self.nodes)
|
got = tuple(self.nodes[i] for i in range(TRACKING) if i in self.nodes)
|
||||||
return got if len(got) == TRACKING else SIDE_NODES + UPPER_NODES
|
return got if len(got) == TRACKING else SIDE_NODES + UPPER_NODES
|
||||||
@@ -353,7 +371,9 @@ def check(log=None, proc=True, ring=True, ring_path=None):
|
|||||||
status, reason = "unknown", "no XRService log in %s" % LOG_DIR
|
status, reason = "unknown", "no XRService log in %s" % LOG_DIR
|
||||||
out = {"log": path, "xrservice": None, "ring": None,
|
out = {"log": path, "xrservice": None, "ring": None,
|
||||||
"episode": state.snapshot()["episode"] if state else {},
|
"episode": state.snapshot()["episode"] if state else {},
|
||||||
"failure": state.snapshot()["failure"] if state else ""}
|
"failure": state.snapshot()["failure"] if state else "",
|
||||||
|
"map": {name: {"node": n, "pipe": pipe_name(n)} for name, n in state.camera_map().items()} if state else {},
|
||||||
|
"ring_missing": []}
|
||||||
|
|
||||||
if proc:
|
if proc:
|
||||||
if pid is None:
|
if pid is None:
|
||||||
@@ -389,13 +409,30 @@ def check(log=None, proc=True, ring=True, ring_path=None):
|
|||||||
evidence.append("ft-camd (pid %d, %s): %d mono cameras: %s" % (
|
evidence.append("ft-camd (pid %d, %s): %d mono cameras: %s" % (
|
||||||
r["writer_pid"], "running" if r["alive"] else "stale ring", len(r["mono"]), names))
|
r["writer_pid"], "running" if r["alive"] else "stale ring", len(r["mono"]), names))
|
||||||
if r["alive"] and len(r["mono"]) < TRACKING and status == "ok":
|
if r["alive"] and len(r["mono"]) < TRACKING and status == "ok":
|
||||||
status, reason = "degraded", ("ft-camd publishes only %d of %d mono cameras (it started while "
|
have = {c["node"] for c in r["mono"]}
|
||||||
"they were missing: restart it)" % (len(r["mono"]), TRACKING))
|
want = state.tracking_nodes() if state else SIDE_NODES + UPPER_NODES
|
||||||
|
out["ring_missing"] = [n for n in want if n not in have]
|
||||||
|
status, reason = "degraded", ("ft-camd publishes only %d of %d mono cameras, missing %s" % (
|
||||||
|
len(r["mono"]), TRACKING, " ".join("video%d" % n for n in out["ring_missing"]) or "?"))
|
||||||
out.update(status=status, reason=reason, evidence=evidence,
|
out.update(status=status, reason=reason, evidence=evidence,
|
||||||
summary="ok" if status == "ok" else "%s: %s" % (status, reason))
|
summary="ok" if status == "ok" else "%s: %s" % (status, reason))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def pipe_name(node):
|
||||||
|
"""The capture pipe behind /dev/videoN (msm_vfe3_video0 and so on), or ""."""
|
||||||
|
try:
|
||||||
|
with open("/sys/class/video4linux/video%d/name" % node) as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_ring_short(result):
|
||||||
|
"""XRService runs all the tracking cameras, but ft-camd doesn't publish them all."""
|
||||||
|
return bool(result) and result.get("status") == "degraded" and bool(result.get("ring_missing"))
|
||||||
|
|
||||||
|
|
||||||
def is_vcint_failure(result):
|
def is_vcint_failure(result):
|
||||||
return bool(result) and result.get("status") == "degraded" and result.get("reason") == VCINT_REASON
|
return bool(result) and result.get("status") == "degraded" and result.get("reason") == VCINT_REASON
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -379,9 +379,15 @@ static bool resolve_block(cam_t *c)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Through the ISP (no colour module), the near-black exposures come out all zeros,
|
||||||
|
* the same every time, so their dequeues change no buffer and get no votes. Such
|
||||||
|
* an index is left unmapped (slot -1): on_frame counts its frames as dark without
|
||||||
|
* reading them. Most of a camera's indices silent means it isn't streaming yet.
|
||||||
|
*/
|
||||||
static bool resolve_each(cam_t *c)
|
static bool resolve_each(cam_t *c)
|
||||||
{
|
{
|
||||||
int depth = c->maxindex + 1;
|
int depth = c->maxindex + 1, silent = 0;
|
||||||
bool taken[MAX_SLOTS] = { false };
|
bool taken[MAX_SLOTS] = { false };
|
||||||
|
|
||||||
for (int i = 0; i < depth; i++) {
|
for (int i = 0; i < depth; i++) {
|
||||||
@@ -396,6 +402,12 @@ static bool resolve_each(cam_t *c)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c->nobs[i] >= 3 && v1 <= 0.2 * c->nobs[i]) {
|
||||||
|
c->slot_of[i] = -1;
|
||||||
|
silent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (c->nobs[i] < 3 || best < 0 || taken[best] || v1 < 0.8 * c->nobs[i] || v2 > 0.3 * c->nobs[i])
|
if (c->nobs[i] < 3 || best < 0 || taken[best] || v1 < 0.8 * c->nobs[i] || v2 > 0.3 * c->nobs[i])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -403,9 +415,14 @@ static bool resolve_each(cam_t *c)
|
|||||||
c->slot_of[i] = best;
|
c->slot_of[i] = best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (silent * 2 > depth)
|
||||||
|
return false;
|
||||||
|
|
||||||
printf("%s: queue depth %d, buffers mapped one by one:", c->slug, depth);
|
printf("%s: queue depth %d, buffers mapped one by one:", c->slug, depth);
|
||||||
for (int i = 0; i < depth; i++)
|
for (int i = 0; i < depth; i++)
|
||||||
printf(" %d", c->slot_of[i]);
|
c->slot_of[i] < 0 ? printf(" -") : printf(" %d", c->slot_of[i]);
|
||||||
|
if (silent)
|
||||||
|
printf(" (- : %d indices whose frames are all zeros, skipped as dark)", silent);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -677,6 +694,12 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
|
|
||||||
int slot = c->slot_of[index];
|
int slot = c->slot_of[index];
|
||||||
|
|
||||||
|
if (slot < 0) { /* an index whose frames are all zeros (resolve_each): a dark one */
|
||||||
|
c->dark++;
|
||||||
|
c->rc->dropped++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* color runs at 60 fps: skip frames early enough that the asked rate holds, before any sync */
|
/* color runs at 60 fps: skip frames early enough that the asked rate holds, before any sync */
|
||||||
if (c->color && color_fps > 0 && evtime - c->last_pub_ns < (uint64_t)(1e9 / color_fps) - 3000000) {
|
if (c->color && color_fps > 0 && evtime - c->last_pub_ns < (uint64_t)(1e9 / color_fps) - 3000000) {
|
||||||
c->paced++;
|
c->paced++;
|
||||||
|
|||||||
+24
-7
@@ -544,22 +544,26 @@ static void probe_cameras(xr_state_t *st)
|
|||||||
/*
|
/*
|
||||||
* qcom-camss can report bytesperline as the visible width while the VFE
|
* qcom-camss can report bytesperline as the visible width while the VFE
|
||||||
* writes a larger aligned pitch. sizeimage is right, so derive the pitch.
|
* writes a larger aligned pitch. sizeimage is right, so derive the pitch.
|
||||||
|
* For NV12, plane 0 normally holds the chroma rows after the luma (the side
|
||||||
|
* cameras through the ISP: 1056 wide, 1152 bytes a row); if it's too small for
|
||||||
|
* that, it holds the luma alone.
|
||||||
*/
|
*/
|
||||||
unsigned xr_camera_stride(const xr_camera_t *c)
|
unsigned xr_camera_stride(const xr_camera_t *c)
|
||||||
{
|
{
|
||||||
if (!c->height || !c->planesize[0])
|
if (!c->height || !c->planesize[0])
|
||||||
return c->bytesperline ? c->bytesperline : c->width;
|
return c->bytesperline ? c->bytesperline : c->width;
|
||||||
|
|
||||||
double bpp = 1.0;
|
bool yuv = c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21;
|
||||||
|
unsigned s = (unsigned)((double)c->planesize[0] / ((double)c->height * (yuv ? 1.5 : 1.0)));
|
||||||
if (c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21)
|
|
||||||
bpp = 1.5;
|
|
||||||
|
|
||||||
unsigned s = (unsigned)((double)c->planesize[0] / ((double)c->height * bpp));
|
|
||||||
|
|
||||||
if (s >= c->width && s <= c->width * 4)
|
if (s >= c->width && s <= c->width * 4)
|
||||||
return s;
|
return s;
|
||||||
|
|
||||||
|
s = (unsigned)(c->planesize[0] / c->height);
|
||||||
|
|
||||||
|
if (yuv && s >= c->width && s <= c->width * 4)
|
||||||
|
return s;
|
||||||
|
|
||||||
return c->bytesperline ? c->bytesperline : c->width;
|
return c->bytesperline ? c->bytesperline : c->width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +595,20 @@ void xr_camera_layout(const xr_camera_t *c, xr_layout_t *l)
|
|||||||
l->pitch = xr_camera_stride(c);
|
l->pitch = xr_camera_stride(c);
|
||||||
l->width = c->width < l->pitch ? c->width : l->pitch;
|
l->width = c->width < l->pitch ? c->width : l->pitch;
|
||||||
|
|
||||||
if (c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21) {
|
/*
|
||||||
|
* Without the colour module, XRService runs the side cameras through the
|
||||||
|
* ISP ("ISP enabled for tracking cameras (main VFE available)" in its log),
|
||||||
|
* and they come out NV12. They're mono sensors, so the luma is the image.
|
||||||
|
*/
|
||||||
|
bool yuv = c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21;
|
||||||
|
|
||||||
|
if (yuv && c->role && !strcmp(c->role, "tracking")) {
|
||||||
|
l->fmt = XR_FMT_GREY8;
|
||||||
|
l->rows = c->height;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yuv) {
|
||||||
l->fmt = XR_FMT_NV12;
|
l->fmt = XR_FMT_NV12;
|
||||||
l->rows = c->height + c->height / 2;
|
l->rows = c->height + c->height / 2;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -457,11 +457,12 @@ class Backend(QObject):
|
|||||||
"""Start stays off: the check found the cameras degraded (unless --ignore-cameras)."""
|
"""Start stays off: the check found the cameras degraded (unless --ignore-cameras)."""
|
||||||
return not self._ignore_cameras and self._camera.get("status") == "degraded"
|
return not self._ignore_cameras and self._camera.get("status") == "degraded"
|
||||||
|
|
||||||
def _check_now(self):
|
def _check_now(self, repair=False):
|
||||||
|
"""repair (off this thread only: it may restart ft-camd, up to 15 s): see session.camera_check."""
|
||||||
mod = self._runner()
|
mod = self._runner()
|
||||||
if not mod or self._session_options.get("dry_run"):
|
if not mod or self._session_options.get("dry_run"):
|
||||||
return {"status": "unknown", "summary": "not checked (dry run)", "reason": "dry run", "evidence": []}
|
return {"status": "unknown", "summary": "not checked (dry run)", "reason": "dry run", "evidence": []}
|
||||||
return mod.camera_check()
|
return mod.camera_check(repair=repair)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def checkCameras(self):
|
def checkCameras(self):
|
||||||
@@ -470,7 +471,7 @@ class Backend(QObject):
|
|||||||
return
|
return
|
||||||
self._camera_busy = True
|
self._camera_busy = True
|
||||||
self.cameraChanged.emit()
|
self.cameraChanged.emit()
|
||||||
self._thread(lambda: self._cameraArrived.emit(self._check_now()))
|
self._thread(lambda: self._cameraArrived.emit(self._check_now(repair=True)))
|
||||||
|
|
||||||
def _on_camera(self, result):
|
def _on_camera(self, result):
|
||||||
self._camera = result
|
self._camera = result
|
||||||
@@ -499,7 +500,7 @@ class Backend(QObject):
|
|||||||
end = time.monotonic() + RECHECK_FOR_S
|
end = time.monotonic() + RECHECK_FOR_S
|
||||||
time.sleep(RECHECK_S * 2)
|
time.sleep(RECHECK_S * 2)
|
||||||
while time.monotonic() < end:
|
while time.monotonic() < end:
|
||||||
result = self._check_now()
|
result = self._check_now(repair=True)
|
||||||
# Done once a new XRService (a new log) has opened its cameras, ok or not.
|
# Done once a new XRService (a new log) has opened its cameras, ok or not.
|
||||||
if result.get("status") in ("ok", "degraded") and result.get("log") != before:
|
if result.get("status") in ("ok", "degraded") and result.get("log") != before:
|
||||||
break
|
break
|
||||||
|
|||||||
+34
-4
@@ -1153,11 +1153,33 @@ def _steamvr_version():
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def camera_check():
|
_camd_restarted = set() # ft-camd pids camera_check(repair=True) has restarted: once each
|
||||||
"""camcheck.check() (the XRService log, its open cameras where readable, ft-camd's ring);
|
|
||||||
never raises: a failure is "unknown"."""
|
|
||||||
|
def _unit_pid(unit):
|
||||||
|
r = subprocess.run(host_command("systemctl", "--user", "show", "-p", "MainPID", "--value", unit),
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
try:
|
try:
|
||||||
return camcheck.check()
|
return int(r.stdout.strip() or 0)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def camera_check(repair=False):
|
||||||
|
"""camcheck.check() (the XRService log, its open cameras where readable, ft-camd's ring);
|
||||||
|
never raises: a failure is "unknown". repair (the window, never during a session): when
|
||||||
|
XRService runs every tracking camera but the recorder's own ft-camd doesn't publish them all
|
||||||
|
(it started while some were missing), restart it, once per ft-camd, and check again."""
|
||||||
|
try:
|
||||||
|
r = camcheck.check()
|
||||||
|
pid = (r.get("ring") or {}).get("writer_pid")
|
||||||
|
if repair and camcheck.is_ring_short(r) and pid not in _camd_restarted and _unit_pid(CAMD_UNIT) == pid:
|
||||||
|
_camd_restarted.add(pid)
|
||||||
|
stop_unit(CAMD_UNIT)
|
||||||
|
start_camd()
|
||||||
|
r = camcheck.check()
|
||||||
|
r["evidence"].append("restarted ft-camd (pid %d) because it published only some of the cameras" % pid)
|
||||||
|
return r
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "unknown", "summary": "unknown: the camera check failed (%s)" % e,
|
return {"status": "unknown", "summary": "unknown: the camera check failed (%s)" % e,
|
||||||
"reason": str(e), "evidence": []}
|
"reason": str(e), "evidence": []}
|
||||||
@@ -1169,6 +1191,10 @@ def camera_text(result):
|
|||||||
return ""
|
return ""
|
||||||
if camcheck.is_vcint_failure(result):
|
if camcheck.is_vcint_failure(result):
|
||||||
return camcheck.USER_TEXT
|
return camcheck.USER_TEXT
|
||||||
|
if camcheck.is_ring_short(result):
|
||||||
|
return ("The headset's tracking cameras are all running, but the recorder can't read some of them (%s). "
|
||||||
|
"Close the Hand Recorder, run ~/frametop/hands/rec/install.sh again, and open it again. If that "
|
||||||
|
"doesn't help, ask in the Frametop Discord." % result.get("reason", ""))
|
||||||
return ("Not all of the headset's tracking cameras are running (%s). Restart SteamVR, or restart the "
|
return ("Not all of the headset's tracking cameras are running (%s). Restart SteamVR, or restart the "
|
||||||
"headset if that doesn't fix it." % result.get("reason", ""))
|
"headset if that doesn't fix it." % result.get("reason", ""))
|
||||||
|
|
||||||
@@ -1451,6 +1477,10 @@ class Session:
|
|||||||
cam = self._status.get("camera")
|
cam = self._status.get("camera")
|
||||||
if cam:
|
if cam:
|
||||||
self._session_json["camera"] = {"status": cam.get("status"), "reason": cam.get("reason", "")}
|
self._session_json["camera"] = {"status": cam.get("status"), "reason": cam.get("reason", "")}
|
||||||
|
# which device each calibrated camera was (XRService's numbering) and whether XRService
|
||||||
|
# ran the side cameras through the ISP (no colour module): to check the names later
|
||||||
|
self._session_json["device"]["camera_map"] = cam.get("map") or {}
|
||||||
|
self._session_json["device"]["isp"] = (cam.get("episode") or {}).get("isp")
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
self._session_json["dry_run"] = True
|
self._session_json["dry_run"] = True
|
||||||
if self.speed != 1:
|
if self.speed != 1:
|
||||||
|
|||||||
@@ -66,6 +66,22 @@ RESUME_OK = [L("12:30:00", "[SystemdInhibitor] Received systemd resume notificat
|
|||||||
for i, n in enumerate((9, 13, 6, 7))] + \
|
for i, n in enumerate((9, 13, 6, 7))] + \
|
||||||
[L("12:30:02", "[DeckardCaptureSource] Streaming resumed (FPGA: VCINT, VC interleaving: enabled)")]
|
[L("12:30:02", "[DeckardCaptureSource] Streaming resumed (FPGA: VCINT, VC interleaving: enabled)")]
|
||||||
EXIT = [L("13:00:00", "XRService - main thread exiting"), L("13:00:00", "Exiting XRService")]
|
EXIT = [L("13:00:00", "XRService - main thread exiting"), L("13:00:00", "Exiting XRService")]
|
||||||
|
# The colour module unplugged while SteamVR runs (2026-10-05 12:42): XRService reopens the
|
||||||
|
# cameras with the side pair through the ISP on vfe0 and vfe1 (NV12); VCINT stays loaded, so the
|
||||||
|
# upper pair stays on vfe2.
|
||||||
|
UNPLUG = [L("12:42:25", "Received passthrough camera connection event (connected=0)"),
|
||||||
|
L("12:42:25", "[DeckardCaptureSource] Closing tracking camera interfaces camerasToUse: 1111"),
|
||||||
|
L("12:42:26", "FPGA state check: VCINT (register value: 0x00021211)"),
|
||||||
|
L("12:42:26", "Upper cameras FPGA interleaving support: 1 (Driver features available = 1 | VCINT loaded = 1)"),
|
||||||
|
L("12:42:26", "[buildMediaCtlSetupTasks] ISP enabled for tracking cameras (main VFE available)"),
|
||||||
|
L("12:42:26", "[buildMediaCtlSetupTasks] Created 4 tasks (4 tracking, 0 passthrough)")] + \
|
||||||
|
[L("12:42:26", "TrackingCameraInit: index: %d. video device: /dev/video%d. v4l subdevice: x" % (i, n))
|
||||||
|
for i, n in enumerate((0, 3, 6, 7))]
|
||||||
|
# Started without the module (FrameEyeCameraFeed's layout): the upper pair on vfe3 and vfe4.
|
||||||
|
NO_MODULE = [L("10:00:02", "[buildMediaCtlSetupTasks] ISP enabled for tracking cameras (main VFE available)"),
|
||||||
|
L("10:00:02", "[buildMediaCtlSetupTasks] Created 4 tasks (4 tracking, 0 passthrough)")] + \
|
||||||
|
[L("10:00:03", "TrackingCameraInit: index: %d. video device: /dev/video%d. v4l subdevice: x" % (i, n))
|
||||||
|
for i, n in enumerate((0, 3, 9, 13))]
|
||||||
|
|
||||||
|
|
||||||
def state(lines):
|
def state(lines):
|
||||||
@@ -128,6 +144,23 @@ class SyntheticLogs(unittest.TestCase):
|
|||||||
self.assertEqual(status, "degraded")
|
self.assertEqual(status, "degraded")
|
||||||
self.assertEqual(reason, "only 2 of 4 tracking cameras running")
|
self.assertEqual(reason, "only 2 of 4 tracking cameras running")
|
||||||
|
|
||||||
|
def test_camera_map_with_module(self):
|
||||||
|
st = state(START + GOOD_OPEN)
|
||||||
|
self.assertEqual(st.camera_map(), {"slam_left": 9, "slam_right": 13, "upper_left": 6, "upper_right": 7})
|
||||||
|
|
||||||
|
def test_module_unplugged(self):
|
||||||
|
st = state(START + GOOD_OPEN + UNPLUG)
|
||||||
|
self.assertEqual(st.verdict()[0], "ok")
|
||||||
|
self.assertIs(st.episode["isp"], True)
|
||||||
|
self.assertEqual(st.camera_map(), {"slam_left": 0, "slam_right": 3, "upper_left": 6, "upper_right": 7})
|
||||||
|
self.assertEqual(st.tracking_nodes(), (0, 3, 6, 7))
|
||||||
|
|
||||||
|
def test_started_without_module(self):
|
||||||
|
st = state(START + NO_MODULE)
|
||||||
|
self.assertEqual(st.verdict()[0], "ok")
|
||||||
|
self.assertEqual(st.camera_map(), {"slam_left": 0, "slam_right": 3, "upper_left": 9, "upper_right": 13})
|
||||||
|
self.assertEqual(st.upper_nodes(), (9, 13))
|
||||||
|
|
||||||
def test_exited_and_empty(self):
|
def test_exited_and_empty(self):
|
||||||
self.assertEqual(state(START + GOOD_OPEN + EXIT).verdict()[0], "unknown")
|
self.assertEqual(state(START + GOOD_OPEN + EXIT).verdict()[0], "unknown")
|
||||||
self.assertEqual(state([]).verdict()[0], "unknown")
|
self.assertEqual(state([]).verdict()[0], "unknown")
|
||||||
@@ -223,9 +256,9 @@ class RealLog(unittest.TestCase):
|
|||||||
self.assertEqual(out.getvalue().split("\n")[0], "ok")
|
self.assertEqual(out.getvalue().split("\n")[0], "ok")
|
||||||
|
|
||||||
|
|
||||||
def make_ring(path, mono_names, alive=True):
|
def make_ring(path, mono_names, alive=True, nodes=None):
|
||||||
"""A ring header as ft-camd writes it (camd/fhring.h), no frames."""
|
"""A ring header as ft-camd writes it (camd/fhring.h), no frames."""
|
||||||
cams = [(b"og01a1b", n.encode(), 9 + i) for i, n in enumerate(mono_names)]
|
cams = [(b"og01a1b", n.encode(), nodes[i] if nodes else 9 + i) for i, n in enumerate(mono_names)]
|
||||||
hb = time.clock_gettime_ns(time.CLOCK_MONOTONIC) if alive else 1
|
hb = time.clock_gettime_ns(time.CLOCK_MONOTONIC) if alive else 1
|
||||||
data = bytearray(camcheck.RING_HDR.pack(b"FHRING01", 1, 0, len(cams), 0, 0, 4242, 0))
|
data = bytearray(camcheck.RING_HDR.pack(b"FHRING01", 1, 0, len(cams), 0, 0, 4242, 0))
|
||||||
struct.pack_into("<Q", data, 40, hb)
|
struct.pack_into("<Q", data, 40, hb)
|
||||||
@@ -252,6 +285,19 @@ class Ring(unittest.TestCase):
|
|||||||
self.assertEqual(r["status"], "degraded")
|
self.assertEqual(r["status"], "degraded")
|
||||||
self.assertIn("ft-camd publishes only 2 of 4", r["reason"])
|
self.assertIn("ft-camd publishes only 2 of 4", r["reason"])
|
||||||
|
|
||||||
|
def test_ring_missing_the_side_cameras(self):
|
||||||
|
# an ft-camd from before NV12 support, without the colour module: only the upper pair
|
||||||
|
with open(self.log, "w") as f:
|
||||||
|
f.write("\n".join(START + NO_MODULE) + "\n")
|
||||||
|
make_ring(self.ring, ["og0ve10_5-003e_video9", "og0ve10_5-0060_video13"], nodes=[9, 13])
|
||||||
|
r = camcheck.check(log=self.log, proc=False, ring_path=self.ring)
|
||||||
|
self.assertEqual(r["status"], "degraded")
|
||||||
|
self.assertEqual(r["ring_missing"], [0, 3])
|
||||||
|
self.assertIn("missing video0 video3", r["reason"])
|
||||||
|
self.assertTrue(camcheck.is_ring_short(r))
|
||||||
|
self.assertFalse(camcheck.is_vcint_failure(r))
|
||||||
|
self.assertEqual(r["map"]["slam_left"]["node"], 0)
|
||||||
|
|
||||||
def test_four_cameras_in_ring(self):
|
def test_four_cameras_in_ring(self):
|
||||||
make_ring(self.ring, ["a_video9", "b_video13", "c_video6", "d_video7", "a_video9_dk"])
|
make_ring(self.ring, ["a_video9", "b_video13", "c_video6", "d_video7", "a_video9_dk"])
|
||||||
r = camcheck.check(log=self.log, proc=False, ring_path=self.ring)
|
r = camcheck.check(log=self.log, proc=False, ring_path=self.ring)
|
||||||
|
|||||||
@@ -34,8 +34,28 @@ from tools.show_set import index, read_set # noqa: E402
|
|||||||
from tools import calib # noqa: E402
|
from tools import calib # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
PIPES = {'msm_vfe3_video0': 'slam_left', 'msm_vfe4_video0': 'slam_right', # as ft-hands maps them
|
PIPES = {'msm_vfe3_video0': 'slam_left', 'msm_vfe4_video0': 'slam_right', # with the colour module
|
||||||
'msm_vfe2_video0': 'upper_left', 'msm_vfe2_video1': 'upper_right'}
|
'msm_vfe2_video0': 'upper_left', 'msm_vfe2_video1': 'upper_right'}
|
||||||
|
|
||||||
|
|
||||||
|
def ring_names():
|
||||||
|
"""{/dev/videoN's N: calibration name} as ft-hands names them: by XRService's log
|
||||||
|
(camcheck.py), else by capture pipe (PIPES)."""
|
||||||
|
import camcheck
|
||||||
|
try:
|
||||||
|
by_log = {node: name for name, node in camcheck.read_log(camcheck.newest_log()).camera_map().items()}
|
||||||
|
except (OSError, ValueError):
|
||||||
|
by_log = {}
|
||||||
|
if by_log:
|
||||||
|
return by_log
|
||||||
|
out = {}
|
||||||
|
for path in os.listdir('/sys/class/video4linux'):
|
||||||
|
if path.startswith('video'):
|
||||||
|
with open('/sys/class/video4linux/%s/name' % path) as f:
|
||||||
|
name = PIPES.get(f.read().strip())
|
||||||
|
if name:
|
||||||
|
out[int(path[5:])] = name
|
||||||
|
return out
|
||||||
PAIRS = {'side': ('slam_left', 'slam_right'), 'upper': ('upper_left', 'upper_right')}
|
PAIRS = {'side': ('slam_left', 'slam_right'), 'upper': ('upper_left', 'upper_right')}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,8 +120,9 @@ def live_pairs(count, names=PAIRS['side']):
|
|||||||
if not ring.alive():
|
if not ring.alive():
|
||||||
sys.exit('ft-camd isn\'t running (no heartbeat)')
|
sys.exit('ft-camd isn\'t running (no heartbeat)')
|
||||||
cams = {}
|
cams = {}
|
||||||
|
names_of = ring_names()
|
||||||
for c in ring.cams:
|
for c in ring.cams:
|
||||||
name = PIPES.get(open('/sys/class/video4linux/video%d/name' % c.node).read().strip())
|
name = names_of.get(c.node)
|
||||||
if name and not c.name.endswith('-dark'):
|
if name and not c.name.endswith('-dark'):
|
||||||
cams[name] = c
|
cams[name] = c
|
||||||
for k in range(count):
|
for k in range(count):
|
||||||
|
|||||||
+64
-11
@@ -63,7 +63,34 @@ namespace {
|
|||||||
|
|
||||||
volatile std::sig_atomic_t g_stop = 0, g_record = 0;
|
volatile std::sig_atomic_t g_stop = 0, g_record = 0;
|
||||||
|
|
||||||
// which calibrated camera each capture pipe carries (XRService's fixed routing)
|
// Which calibrated camera each video device carries. XRService numbers its tracking cameras
|
||||||
|
// (index 0 to 3: slam_left, slam_right, upper_left, upper_right) and logs the device each one
|
||||||
|
// opened ("TrackingCameraInit: index: 0. video device: /dev/video9"). The devices depend on
|
||||||
|
// the colour module: with it, the side cameras are on vfe3 and vfe4 and the upper pair on
|
||||||
|
// vfe2; without it, XRService runs the side cameras through the ISP on vfe0 and vfe1, and the
|
||||||
|
// upper pair on vfe3 and vfe4. So the running XRService's log decides; when it can't be read,
|
||||||
|
// the capture pipes as they are with the module. {} if the log has no cameras.
|
||||||
|
std::map<int, std::string> cameras_from_xrservice_log() {
|
||||||
|
static const char *const names[] = {"slam_left", "slam_right", "upper_left", "upper_right"};
|
||||||
|
const char *home = std::getenv("HOME");
|
||||||
|
std::ifstream in(std::string(home ? home : "") + "/.local/share/Steam/logs/xrservice.txt");
|
||||||
|
const std::string key = "TrackingCameraInit: index: ";
|
||||||
|
std::map<int, int> node_of; // index -> N of /dev/videoN, from the latest camera start
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (line.find("XRService logging to") != std::string::npos) node_of.clear();
|
||||||
|
const auto at = line.find(key);
|
||||||
|
int index = -1, node = -1;
|
||||||
|
if (at != std::string::npos &&
|
||||||
|
std::sscanf(line.c_str() + at + key.size(), "%d. video device: /dev/video%d", &index, &node) == 2 &&
|
||||||
|
index >= 0 && index < 4)
|
||||||
|
node_of[index] = node;
|
||||||
|
}
|
||||||
|
std::map<int, std::string> out;
|
||||||
|
for (auto &[index, node] : node_of) out[node] = names[index];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
const char *camera_for_pipe(int node) {
|
const char *camera_for_pipe(int node) {
|
||||||
char path[64], name[64] = "";
|
char path[64], name[64] = "";
|
||||||
std::snprintf(path, sizeof path, "/sys/class/video4linux/video%d/name", node);
|
std::snprintf(path, sizeof path, "/sys/class/video4linux/video%d/name", node);
|
||||||
@@ -302,6 +329,7 @@ int main(int argc, char **argv) {
|
|||||||
double decided_after_s = -1;
|
double decided_after_s = -1;
|
||||||
if (sides_mode != "auto") truth = names_swapped, decided_by = sides_from, sides_state = "forced";
|
if (sides_mode != "auto") truth = names_swapped, decided_by = sides_from, sides_state = "forced";
|
||||||
std::string rec_dir;
|
std::string rec_dir;
|
||||||
|
std::string cams_json; // which device each calibrated camera is (set below), for the sides files
|
||||||
std::vector<std::pair<size_t, bool>> rec_names; // from which recorded set on, names_swapped was what
|
std::vector<std::pair<size_t, bool>> rec_names; // from which recorded set on, names_swapped was what
|
||||||
// DIR/sides.json beside a recording's sets.bin: how its side cameras are named. A set's
|
// DIR/sides.json beside a recording's sets.bin: how its side cameras are named. A set's
|
||||||
// names are right when its names_swapped equals swapped (null: not known when recorded).
|
// names are right when its names_swapped equals swapped (null: not known when recorded).
|
||||||
@@ -312,7 +340,8 @@ int main(int argc, char **argv) {
|
|||||||
write_file(rec_dir + "/sides.json",
|
write_file(rec_dir + "/sides.json",
|
||||||
"{\"swapped\": " + json_bool(truth) + ", \"decided_by\": " + json_str(truth ? decided_by : "") +
|
"{\"swapped\": " + json_bool(truth) + ", \"decided_by\": " + json_str(truth ? decided_by : "") +
|
||||||
", \"names_swapped\": [" + runs + "]" +
|
", \"names_swapped\": [" + runs + "]" +
|
||||||
(decision_evidence.empty() ? "" : ", \"evidence\": " + decision_evidence) + "}\n");
|
(decision_evidence.empty() ? "" : ", \"evidence\": " + decision_evidence) +
|
||||||
|
(cams_json.empty() ? "" : ", \"cameras\": " + cams_json) + "}\n");
|
||||||
};
|
};
|
||||||
auto start_recording = [&](const std::string &dir, std::string &e) {
|
auto start_recording = [&](const std::string &dir, std::string &e) {
|
||||||
rec = std::make_unique<Recorder>();
|
rec = std::make_unique<Recorder>();
|
||||||
@@ -340,19 +369,42 @@ int main(int argc, char **argv) {
|
|||||||
// (--with-color). Recorded names hold 15 characters, so "upper_right_dark" wouldn't fit.
|
// (--with-color). Recorded names hold 15 characters, so "upper_right_dark" wouldn't fit.
|
||||||
std::map<std::string, int> dark;
|
std::map<std::string, int> dark;
|
||||||
std::map<std::string, Camera> used;
|
std::map<std::string, Camera> used;
|
||||||
|
// ft-camd's cameras by XRService's numbering, else by capture pipe (see camera_for_pipe);
|
||||||
|
// ft-ringplay's (no device) by the name it gives
|
||||||
|
const std::map<int, std::string> by_log = cameras_from_xrservice_log();
|
||||||
|
const char *named_by = by_log.empty() ? "capture pipe" : "XRService's log";
|
||||||
for (int i = 0; i < ring.cameras(); ++i) {
|
for (int i = 0; i < ring.cameras(); ++i) {
|
||||||
if (ring.camera(i).flags & FH_CAM_COLOR) {
|
const fh_ring_cam_t &rc = ring.camera(i);
|
||||||
const std::string name = "color_video" + std::to_string(ring.camera(i).node);
|
if (rc.flags & FH_CAM_COLOR) {
|
||||||
|
const std::string name = "color_video" + std::to_string(rc.node);
|
||||||
dark[name] = i, color[name] = i;
|
dark[name] = i, color[name] = i;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// ft-camd's cameras by capture pipe; ft-ringplay's (no device) by the name it gives
|
const auto it = by_log.find(rc.node);
|
||||||
const char *name = camera_for_pipe(ring.camera(i).node);
|
const char *name = rc.node < 0 ? rc.name
|
||||||
if (!name && ring.camera(i).node < 0) name = ring.camera(i).name;
|
: !by_log.empty() ? (it == by_log.end() ? nullptr : it->second.c_str())
|
||||||
if (!name || !calib.count(name)) continue;
|
: camera_for_pipe(rc.node);
|
||||||
if (ring.camera(i).flags & FH_CAM_DARK) dark[std::string(name) + "_dk"] = i;
|
if (!name || !calib.count(name)) {
|
||||||
else index[name] = i, used[name] = calib[name];
|
if (!(rc.flags & FH_CAM_DARK)) std::fprintf(stderr, "video%d (%s): not one of the calibrated cameras, left out\n", rc.node, rc.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (int(rc.width) != calib[name].width || int(rc.height) != calib[name].height) {
|
||||||
|
std::fprintf(stderr, "video%d (%s) is %ux%u, but %s is calibrated at %dx%d: left out\n", rc.node, rc.name,
|
||||||
|
rc.width, rc.height, name, calib[name].width, calib[name].height);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rc.flags & FH_CAM_DARK) {
|
||||||
|
dark[std::string(name) + "_dk"] = i;
|
||||||
|
} else if (index.count(name)) {
|
||||||
|
std::fprintf(stderr, "video%d (%s) would be %s too (video%d is): left out\n", rc.node, rc.name, name,
|
||||||
|
ring.camera(index[name]).node);
|
||||||
|
} else {
|
||||||
|
index[name] = i, used[name] = calib[name];
|
||||||
|
cams_json += std::string(cams_json.empty() ? "" : ", ") + json_str(name) + ": {\"node\": " +
|
||||||
|
std::to_string(rc.node) + ", \"ring\": " + json_str(rc.name) + "}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
cams_json = "{\"named_by\": " + json_str(named_by) + ", \"cameras\": {" + cams_json + "}}";
|
||||||
// ft-camd tells the side cameras' buffers apart by XRService's allocation order, which
|
// ft-camd tells the side cameras' buffers apart by XRService's allocation order, which
|
||||||
// some XRService restarts reverse (see the top).
|
// some XRService restarts reverse (see the top).
|
||||||
const bool have_sides = index.count("slam_left") && index.count("slam_right");
|
const bool have_sides = index.count("slam_left") && index.count("slam_right");
|
||||||
@@ -397,7 +449,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
const bool switching = automatic && !color.empty();
|
const bool switching = automatic && !color.empty();
|
||||||
Cams mode = switching || color.empty() ? Cams::Mono : fixed;
|
Cams mode = switching || color.empty() ? Cams::Mono : fixed;
|
||||||
std::printf("cameras:");
|
std::printf("cameras (by %s):", named_by);
|
||||||
for (auto &[name, i] : index) std::printf(" %s=video%d", name.c_str(), ring.camera(i).node);
|
for (auto &[name, i] : index) std::printf(" %s=video%d", name.c_str(), ring.camera(i).node);
|
||||||
for (auto &[name, i] : color) std::printf(" %s", name.c_str());
|
for (auto &[name, i] : color) std::printf(" %s", name.c_str());
|
||||||
std::printf(" tracking with %s%s models: %s%s, %d threads on CPUs", cams_name(mode),
|
std::printf(" tracking with %s%s models: %s%s, %d threads on CPUs", cams_name(mode),
|
||||||
@@ -446,6 +498,7 @@ int main(int argc, char **argv) {
|
|||||||
", \"decided_after_s\": " + std::to_string(decided_after_s) +
|
", \"decided_after_s\": " + std::to_string(decided_after_s) +
|
||||||
", \"evidence\": " + (decision_evidence.empty() ? "null" : decision_evidence) +
|
", \"evidence\": " + (decision_evidence.empty() ? "null" : decision_evidence) +
|
||||||
", \"checking\": " + (checking ? side_check.json() : "null") +
|
", \"checking\": " + (checking ? side_check.json() : "null") +
|
||||||
|
", \"cameras\": " + (cams_json.empty() ? "null" : cams_json) +
|
||||||
", \"updated_ns\": " + std::to_string(mono_ns()) + "}\n");
|
", \"updated_ns\": " + std::to_string(mono_ns()) + "}\n");
|
||||||
};
|
};
|
||||||
write_sides();
|
write_sides();
|
||||||
|
|||||||
Executable
+241
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Uninstall Frametop from the Steam Frame. In a terminal on the headset (Konsole in the desktop,
|
||||||
|
# or over SSH):
|
||||||
|
#
|
||||||
|
# curl -fsSL https://deejanuz.github.io/frametop/uninstall.sh | bash
|
||||||
|
#
|
||||||
|
# or ~/frametop/uninstall.sh. It doesn't use the rest of the repo, so it also works when
|
||||||
|
# ~/frametop is gone or broken.
|
||||||
|
#
|
||||||
|
# It never stops what you're using now: the input relay carries the keyboard and mouse, and the
|
||||||
|
# desktop runs from the repo. So it goes in two steps:
|
||||||
|
# 1. Frametop stops starting. The launcher's Desktop entry goes back to the stock desktop, and
|
||||||
|
# Frametop's services, SteamVR driver, menu entries, and system files (our eye tracker's
|
||||||
|
# frame grabber and the Bluetooth fixes, with sudo) are removed. What runs now keeps running
|
||||||
|
# until you restart the headset.
|
||||||
|
# 2. After the restart, run it again. It deletes the code (~/frametop) and, if you want, your
|
||||||
|
# settings and the build container.
|
||||||
|
# When nothing of Frametop is running, one run does both.
|
||||||
|
#
|
||||||
|
# Options (piped, they go after "bash -s --"):
|
||||||
|
# --dir DIR where the repo is, if not ~/frametop
|
||||||
|
# --dry-run show what it would do, and change nothing
|
||||||
|
set -euo pipefail
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: uninstall.sh [--dir DIR] [--dry-run]
|
||||||
|
piped: curl -fsSL https://deejanuz.github.io/frametop/uninstall.sh | bash -s -- [options]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry=0
|
||||||
|
apps=$HOME/.local/share/applications
|
||||||
|
override=$apps/deckard-nested-desktop.desktop
|
||||||
|
relay_unit=$HOME/.config/systemd/user/frametop-input-relay.service
|
||||||
|
driver=$HOME/.local/share/frametop/ft_pointer
|
||||||
|
vrpathreg=/opt/steamvr/bin/linuxarm64/vrpathreg
|
||||||
|
handsctl=$HOME/.local/bin/ft-handsctl
|
||||||
|
eyegrab_files=(/etc/systemd/system/frametop-eyegrab.service /etc/frametop/ft-eyegrab)
|
||||||
|
bt_files=(/etc/systemd/system/steamframe-bt-fixups.service /etc/systemd/system/bluetooth.service.d/steamframe.conf
|
||||||
|
/etc/steamframe/bt-fixups.sh)
|
||||||
|
|
||||||
|
step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
|
||||||
|
run() { # run a command, or with --dry-run, show it
|
||||||
|
if [ "$dry" = 1 ]; then printf ' would run: %s\n' "$*"; else "$@"; fi
|
||||||
|
}
|
||||||
|
ask() { # ask "question" default(y|n)
|
||||||
|
local hint answer
|
||||||
|
if [ "$dry" = 1 ]; then echo "$1 [dry run: yes]"; return 0; fi
|
||||||
|
hint=$([ "$2" = y ] && echo "Y/n" || echo "y/N")
|
||||||
|
read -r -p "$1 [$hint] " answer </dev/tty || answer=
|
||||||
|
answer=${answer:-$2}
|
||||||
|
[[ $answer =~ ^[Yy] ]]
|
||||||
|
}
|
||||||
|
exists() { local f; for f in "$@"; do [ -e "$f" ] && return 0; done; return 1; }
|
||||||
|
size() { du -shc "$@" 2>/dev/null | tail -1 | cut -f1; }
|
||||||
|
|
||||||
|
# Is this folder Frametop's code? Only then is it deleted.
|
||||||
|
is_repo() { [ "$1" != "$HOME" ] && [ -f "$1/desktops.sh" ] && [ -f "$1/session/frametop-session.sh" ]; }
|
||||||
|
find_repo() {
|
||||||
|
local p
|
||||||
|
[ -n "$dir" ] && { echo "$dir"; return; }
|
||||||
|
# The launcher entry and the relay's unit point into the repo, until step 1 removes them.
|
||||||
|
p=$(sed -n 's#^Exec=\(.*\)/session/frametop-session\.sh.*#\1#p' "$override" 2>/dev/null | head -1)
|
||||||
|
[ -z "$p" ] && p=$(sed -n 's#^ExecStart=/usr/bin/python3 \(.*\)/input/input-relay\.py.*#\1#p' "$relay_unit" 2>/dev/null | head -1)
|
||||||
|
[ -z "$p" ] && [ -f "${BASH_SOURCE[0]:-}" ] && p=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
echo "${p:-$HOME/frametop}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frametop's programs that are running now (step 1 leaves them running until the restart).
|
||||||
|
running() {
|
||||||
|
local n out=()
|
||||||
|
for n in ft-screens ft-pointer ft-powerd ft-eyegrab ft-camd ft-hands; do
|
||||||
|
pgrep -x "$n" >/dev/null && out+=("$n")
|
||||||
|
done
|
||||||
|
pgrep -f '[i]nput/input-relay\.py' >/dev/null && out+=("input relay")
|
||||||
|
pgrep -f '[g]aze/ft-gazed' >/dev/null && out+=("gaze service")
|
||||||
|
pgrep -f '[f]rametop-session\.sh' >/dev/null && out+=("desktop session")
|
||||||
|
echo "${out[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local units=() entries=() names=() sys=0 repo now f
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case $1 in
|
||||||
|
--dir) dir=${2:?--dir needs a folder}; shift ;;
|
||||||
|
--dry-run) dry=1 ;;
|
||||||
|
-h|--help) usage; return 0 ;;
|
||||||
|
*) echo "unknown option: $1" >&2; usage >&2; return 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! { grep -qx 'ID=steamos' /etc/os-release && grep -qE '^VARIANT_ID="?vr"?$' /etc/os-release; } 2>/dev/null; then
|
||||||
|
echo "This uninstalls Frametop from a Steam Frame (SteamOS, VR variant). Run it in a terminal on the headset." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$dry" = 0 ] && ! { : </dev/tty; } 2>/dev/null; then
|
||||||
|
echo "This asks questions, and there's no terminal to ask in. Run it in one (over SSH: ssh -t)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[ "$dry" = 1 ] && echo "Dry run: nothing changes."
|
||||||
|
repo=$(find_repo)
|
||||||
|
|
||||||
|
# Step 1: what makes Frametop start. Nothing here stops a running program.
|
||||||
|
[ -f "$override" ] && grep -q 'Frametop' "$override" || override=
|
||||||
|
units=("$HOME"/.config/systemd/user/frametop-*.service)
|
||||||
|
for f in ft-input-settings ft-display-settings ft-layout-reset ft-screens-toggle ft-remote-settings ft-gazeprobe; do
|
||||||
|
[ -e "$apps/$f.desktop" ] && entries+=("$apps/$f.desktop")
|
||||||
|
done
|
||||||
|
[ -e "$apps/frametop-handrec.desktop" ] && entries+=("$apps/frametop-handrec.desktop")
|
||||||
|
entries+=("$apps"/frametop-profile-*.desktop) # one per layout profile (layout/ft_layout.py)
|
||||||
|
[ -L "$handsctl" ] || handsctl=
|
||||||
|
exists "${eyegrab_files[@]}" "${bt_files[@]}" && sys=1
|
||||||
|
|
||||||
|
if [ -n "$override" ] || [ ${#units[@]} -gt 0 ] || [ ${#entries[@]} -gt 0 ] || [ -d "$driver" ] ||
|
||||||
|
[ -n "$handsctl" ] || [ "$sys" = 1 ]; then
|
||||||
|
step "Step 1 of 2: stop Frametop from starting"
|
||||||
|
echo "This removes:"
|
||||||
|
[ -n "$override" ] && echo " - the launcher's Desktop entry (Launch a program -> Desktop opens the stock desktop again)"
|
||||||
|
for f in "${units[@]}"; do echo " - the service $(basename "$f")"; done
|
||||||
|
[ -d "$driver" ] && echo " - the 3D mouse's SteamVR driver (ft_pointer)"
|
||||||
|
[ ${#entries[@]} -gt 0 ] && echo " - ${#entries[@]} menu entries (Frametop Display Settings, Input Settings, ...)"
|
||||||
|
[ -n "$handsctl" ] && echo " - $handsctl"
|
||||||
|
exists "${eyegrab_files[@]}" && echo " - our eye tracker's frame grabber (a system service: needs your password)"
|
||||||
|
exists "${bt_files[@]}" && echo " - the Bluetooth fixes (system files: needs your password)"
|
||||||
|
echo "What runs now keeps running until you restart the headset, so your keyboard, mouse, and"
|
||||||
|
echo "this terminal keep working. Your settings and the code stay for now."
|
||||||
|
ask "Uninstall Frametop?" n || { echo "Nothing changed."; return 0; }
|
||||||
|
|
||||||
|
[ -n "$override" ] && run rm -f "$override"
|
||||||
|
if [ ${#units[@]} -gt 0 ]; then
|
||||||
|
for f in "${units[@]}"; do names+=("$(basename "$f")"); done
|
||||||
|
run systemctl --user disable "${names[@]}" 2>/dev/null || true
|
||||||
|
run rm -f "${units[@]}"
|
||||||
|
run systemctl --user daemon-reload
|
||||||
|
fi
|
||||||
|
if [ -d "$driver" ]; then
|
||||||
|
if [ -x "$vrpathreg" ]; then
|
||||||
|
LD_LIBRARY_PATH=$(dirname "$vrpathreg") run "$vrpathreg" removedriver "$driver" ||
|
||||||
|
echo "warning: SteamVR's vrpathreg couldn't unregister the driver; SteamVR may log that it's missing" >&2
|
||||||
|
fi
|
||||||
|
run rm -rf "$driver"
|
||||||
|
fi
|
||||||
|
[ ${#entries[@]} -gt 0 ] && run rm -f "${entries[@]}"
|
||||||
|
[ -n "$handsctl" ] && run rm -f "$handsctl"
|
||||||
|
if [ "$sys" = 1 ]; then
|
||||||
|
echo "The system files need your password (sudo)."
|
||||||
|
if ! run sudo bash -c '
|
||||||
|
for u in frametop-eyegrab steamframe-bt-fixups; do systemctl disable $u.service 2>/dev/null; done
|
||||||
|
rm -f "$@"
|
||||||
|
rmdir /etc/frametop /etc/steamframe /etc/systemd/system/bluetooth.service.d 2>/dev/null
|
||||||
|
systemctl daemon-reload; true' sys "${eyegrab_files[@]}" "${bt_files[@]}"; then
|
||||||
|
echo "warning: the system files weren't removed (no password?). Run this again to retry." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ "$dry" = 1 ] || echo "Frametop no longer starts."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: delete what's left, once nothing of Frametop runs.
|
||||||
|
now=$(running)
|
||||||
|
if [ -n "$now" ]; then
|
||||||
|
step "Restart the headset to finish"
|
||||||
|
echo "Still running from before: $now."
|
||||||
|
echo "They stop when the headset restarts. After that, run this again to delete the code"
|
||||||
|
echo "($repo) and, if you want, your settings and the build container:"
|
||||||
|
echo
|
||||||
|
if [ "$repo" = "$HOME/frametop" ]; then
|
||||||
|
echo " curl -fsSL https://deejanuz.github.io/frametop/uninstall.sh | bash"
|
||||||
|
else
|
||||||
|
echo " curl -fsSL https://deejanuz.github.io/frametop/uninstall.sh | bash -s -- --dir $(printf %q "$repo")"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
if ask "Restart the headset now? This closes everything open, in VR and on the desktop." n; then
|
||||||
|
run systemctl reboot || echo "Couldn't restart it from here: restart the headset from Steam's power menu." >&2
|
||||||
|
fi
|
||||||
|
[ "$dry" = 1 ] || return 0
|
||||||
|
echo "Dry run: after the restart, step 2 would go like this."
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Step 2 of 2: delete what's left"
|
||||||
|
if [ -d "$repo" ] && is_repo "$repo"; then
|
||||||
|
if [ -f "$repo/.git" ]; then
|
||||||
|
echo "Leaving $repo: it's a git worktree. Remove it with git worktree remove."
|
||||||
|
else
|
||||||
|
local def=y
|
||||||
|
if [ -n "$(git -C "$repo" status --porcelain --untracked-files=no 2>/dev/null)" ] ||
|
||||||
|
[ -n "$(git -C "$repo" log --branches --not --remotes --oneline 2>/dev/null | head -1)" ]; then
|
||||||
|
echo "$repo has changes of its own (git -C $repo status)."
|
||||||
|
def=n
|
||||||
|
fi
|
||||||
|
if ask "Delete the Frametop code in $repo ($(size "$repo"))?" "$def"; then
|
||||||
|
cd "$HOME"
|
||||||
|
run rm -rf "$repo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [ -e "$repo" ]; then
|
||||||
|
echo "Leaving $repo: it doesn't look like Frametop's code."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuilt by the desktop at each start, so nothing to keep.
|
||||||
|
run rm -rf "$HOME/.local/share/frametop/apps" "$HOME/.local/share/kwin/decorations/kwin4_decoration_qml_frametop" \
|
||||||
|
"$HOME/.cache/frametop"
|
||||||
|
|
||||||
|
local settings=("$HOME"/.config/frametop.conf* "$HOME"/.config/frametop-*.json* "$HOME/.config/frametop-remote"
|
||||||
|
"$HOME/.config/frametop" "$HOME/.local/state/frametop")
|
||||||
|
local kept=()
|
||||||
|
for f in "${settings[@]}"; do [ -e "$f" ] && kept+=("$f"); done
|
||||||
|
if [ ${#kept[@]} -gt 0 ]; then
|
||||||
|
echo "Your settings: the screen layout and profiles, button maps, gaze calibration, the remote"
|
||||||
|
echo "desktop password, and the Frametop desktop's own Plasma setup (${kept[*]/#$HOME/\~})."
|
||||||
|
ask "Delete your settings too? Keep them to pick up where you left off if you reinstall." n &&
|
||||||
|
run rm -rf "${kept[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local recs=()
|
||||||
|
for f in "$HOME/.local/share/frametop/eyes" "$HOME/.local/share/frametop/hands"; do [ -e "$f" ] && recs+=("$f"); done
|
||||||
|
if [ ${#recs[@]} -gt 0 ]; then
|
||||||
|
ask "Delete your eye and hand recordings in ~/.local/share/frametop ($(size "${recs[@]}"))?" n &&
|
||||||
|
run rm -rf "${recs[@]}"
|
||||||
|
fi
|
||||||
|
[ "$dry" = 1 ] || rmdir "$HOME/.local/share/frametop" 2>/dev/null || true
|
||||||
|
|
||||||
|
if command -v podman >/dev/null && podman container exists dev 2>/dev/null; then
|
||||||
|
echo "The build container (dev) holds Frametop's compilers and libraries, 1-2 GB. If you put"
|
||||||
|
echo "anything else in it, that goes too."
|
||||||
|
if ask "Delete the build container?" n; then
|
||||||
|
run "$HOME/.local/bin/distrobox" rm --force dev </dev/null
|
||||||
|
run podman image rm registry.fedoraproject.org/fedora-toolbox:44 >/dev/null 2>&1 || true
|
||||||
|
echo "distrobox stays in ~/.local/bin, for any other containers. To remove it too:"
|
||||||
|
echo " ~/dev/src/distrobox/uninstall --prefix ~/.local"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Done"
|
||||||
|
echo "Frametop is uninstalled."
|
||||||
|
}
|
||||||
|
|
||||||
|
dir=
|
||||||
|
main "$@"
|
||||||
Reference in new issue
Block a user