---
title: "USB Switcher (headless ⇄ desktop auto-switch)"
description: "The AI machine is a **dual-use** box: normally it is a **headless inference server** (no display manager, both GPUs busy), but a human can occasionally plug a monitor into it via a **USB monitor/displ…"
section: ai-docs
raw: "usb-switcher.md"
source: ai-generated
tags: ai, llama.cpp, docker
last-updated: 2026-09-14
---

# USB Switcher (headless ⇄ desktop auto-switch)

The AI machine is a **dual-use** box: normally it is a **headless inference server** (no display manager, both GPUs busy), but a human can occasionally plug a monitor into it via a **USB monitor/display switcher** to get an interactive desktop. A small udev-driven automation handles the two sides of that toggle automatically — no manual `systemctl` juggling when the monitor is plugged in or pulled out.

The source files are:

- `99-usb-switcher.rules` — a **udev rule** that fires on connect/disconnect.
- `usb-switcher-on.sh` — runs when the switcher is **plugged in**.
- `usb-switcher-off.sh` — runs when the switcher is **pulled out**.

All three log to `/var/log/usb-switcher.log`.

---

## 1. The udev rule — `99-usb-switcher.rules`

```udev
ACTION=="add",    SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
    ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="0610", \
    RUN+="/usr/local/bin/usb-switcher-on.sh"
ACTION=="remove", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
    ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="0610", \
    RUN+="/usr/local/bin/usb-switcher-off.sh"
```

- **Device fingerprint:** `idVendor 05e3` (Genesys Logic) /
  `idProduct 0610`. This identifies the specific USB switcher; only this   device triggers the scripts.
- **`ACTION=="add"`** → `usb-switcher-on.sh`; **`ACTION=="remove"`** →
  `usb-switcher-off.sh`.
- The rule file must live in `/etc/udev/rules.d/` (hence the `99-…` name for
  ordering) and be owned by the host — the `RUN+=` paths must be   `/usr/local/bin/usb-switcher-{on,off}.sh`.

---

## 2. On connect — `usb-switcher-on.sh`

```bash
#!/bin/bash
# Runs as root via udev — use absolute paths only
LOG=/var/log/usb-switcher.log
echo "[$(date '+%F %T')] switcher connected" >> "$LOG"

/usr/bin/systemctl stop llama-primary.service    2>>"$LOG"
/usr/bin/systemctl stop llama-secondary.service  2>>"$LOG"
/usr/bin/systemctl start gdm.service            2>>"$LOG"

# Switch the monitor to the DisplayPort/USB-C input (see step 4)
/usr/bin/ddcutil setvcp 60 0x1b 2>>"$LOG" || true
```

**What it does (headless → desktop):**

1. **Stops both inference servers** (`llama-primary`, `llama-secondary`) —
   releases the GPUs so they can serve the desktop.
2. **Starts the display manager** (`gdm.service`) — brings up the interactive
   login/desktop.
3. **Switches the monitor input** with
   `ddcutil setvcp 60 0x1b` — VCP feature `60` (input source), value `0x1b`,    i.e. the **DisplayPort / USB-C** input. (`|| true` so a DDC failure doesn't    abort the rest.)

---

## 3. On disconnect — `usb-switcher-off.sh`

```bash
#!/bin/bash
LOG=/var/log/usb-switcher.log
echo "[$(date '+%F %T')] switcher disconnected" >> "$LOG"

/usr/bin/systemctl stop gdm.service              2>>"$LOG"
/usr/bin/systemctl start llama-primary.service   2>>"$LOG"
/usr/bin/systemctl start llama-secondary.service 2>>"$LOG"

/usr/bin/ddcutil setvcp 60 0x11 2>>"$LOG" || true
```

**What it does (desktop → headless):**

1. **Stops the display manager** (`gdm.service`).
2. **Restarts both inference servers** — returns the machine to its default
   headless-inference role.
3. **Switches the monitor input back** with
   `ddcutil setvcp 60 0x11` — VCP feature `60`, value `0x11` (the non-DP    input).

---

## State model

```text
          plug in switcher (add)                     pull out switcher (remove)
 ┌────────────────────────────────────────┐   ┌────────────────────────────────────────┐
 │ HEADLESS INFERENCING (default)         │   │  DESKTOP (interactive)                 │
 │ • gdm STOPPED                         │   │  • gdm RUNNING                         │
 │ • llama-primary RUNNING               │   │  • llama-primary STOPPED               │
 │ • llama-secondary RUNNING             │   │  • llama-secondary STOPPED             │
 └────────────────────────────────────────┘   └────────────────────────────────────────┘
```

So the **default state is headless inference** (both llama servers up, no display manager); plugging in the USB switcher flips it to desktop and pulls it back flips it to inference.

## Dependencies & notes

- **`gdm.service`** — the display manager is the "desktop" half; it is stopped
  in the default headless state.
- **`ddcutil`** — used to switch the monitor's active input. It requires the
  monitor to expose DDC/CI over the connection and (usually) the X/DRM   context to be available. The `|| true` means a failed input switch is   logged but non-fatal.
- **Runs as root via udev** — the scripts use absolute paths (`/usr/bin/…`)
  because udev runs them in a minimal environment.
- **Ordering is important:** on connect the servers are stopped *before* gdm
  starts (free the GPUs first); on disconnect gdm is stopped *before* the   servers start (avoid a GPU contention window).
- **Log:** `/var/log/usb-switcher.log` records each transition with a
  timestamp, plus any errors from the individual commands.

> **Verify on the host:** that `gdm.service` is actually the display manager in
> use (vs `lightdm`/`sddm`), that `ddcutil` is installed and can reach the
> monitor, and that the VCP values `0x1b` / `0x11` match the monitor's input
> source numbering. See [Uncertainties & Notes](uncertainties-and-notes.md).

---

## Source Disclaimer

- [x] AI Generated
- [ ] Human Generated
- [x] AI Edited
- [ ] Human Edited
