From 5dbbb34096476d178bb50394c482dbb7f8a0a5d1 Mon Sep 17 00:00:00 2001 From: Jacob Nelson Date: Sun, 17 May 2026 18:36:35 -0500 Subject: [PATCH] Initial commit: JCD3198 second HDMI driver setup for Fedora MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables the MCT T6 USB Station (0711:5601) chip on the J5 Create JCD3198 dock, which drives the 4K30 HDMI port with no native Linux kernel driver. Stack: EVDI 1.14.16 (kernel module + libevdi) → t6evdi userspace daemon (mobilepixels-linux-driver, patched for EVDI 1.14 API) → MCT T6 over USB. Includes DKMS registration for evdi.ko (auto-rebuilds on kernel updates) and a Mutter D-Bus layout script that restores monitor positions by serial number after each daemon restart, working around the incrementing DVI-I-N connector name assigned by EVDI on every reconnect. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 207 ++++++++++++++++++++++++++++++++++++++++ bin/fix-monitor-layout | 85 +++++++++++++++++ bin/t6evdi-apply-layout | 12 +++ systemd/evdi.conf | 1 + systemd/t6evdi.service | 13 +++ 5 files changed, 318 insertions(+) create mode 100644 README.md create mode 100755 bin/fix-monitor-layout create mode 100755 bin/t6evdi-apply-layout create mode 100644 systemd/evdi.conf create mode 100644 systemd/t6evdi.service diff --git a/README.md b/README.md new file mode 100644 index 0000000..fff26c8 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# J5 Create JCD3198 — Second HDMI (4K30) Driver on Linux + +## Problem + +The JCD3198 dock has two HDMI outputs: + +| Port | Chip | Linux support | +|------|------|--------------| +| 4K60 HDMI | DP alt mode via USB-C | Native — works out of the box | +| 4K30 HDMI | MCT T6 USB Station (`0711:5601`) | Requires userspace driver | + +The T6 chip has no mainline Linux kernel driver. It presents as USB class `ff-00-00` +(vendor-specific) with no driver bound. A userspace daemon bridges it through the EVDI +virtual display framework. + +## System + +| Item | Value | +|------|-------| +| Machine | LG Gram 17Z90Q (`jake@gram`) | +| OS | Fedora 43, GNOME Wayland | +| Left monitor | LG FHD — vendor `GSM`, serial `0x01010101` (via T6 / EVDI) | +| Right monitor | Acer SA270 — vendor `ACR`, serial `0x13102591` (via DP alt mode, stable) | + +## Architecture + +``` +GNOME Wayland compositor + │ + ▼ +evdi.ko — virtual DRM device (card0, connector DVI-I-N) + │ libevdi.so + ▼ +t6evdi — reads frames from EVDI, encodes JPEG, sends over USB bulk transfer + │ libusb-1.0 + ▼ +MCT T6 chip (USB 0711:5601) → 4K30 HDMI port +``` + +## Installed Files + +| Repo path | Installed to | Purpose | +|-----------|-------------|---------| +| `bin/fix-monitor-layout` | `/usr/local/bin/fix-monitor-layout` | Python script — applies monitor layout via Mutter D-Bus | +| `bin/t6evdi-apply-layout` | `/usr/local/bin/t6evdi-apply-layout` | Shell wrapper — called by systemd after t6evdi starts | +| `systemd/t6evdi.service` | `/etc/systemd/system/t6evdi.service` | Systemd service — starts daemon, auto-restarts on crash | +| `systemd/evdi.conf` | `/etc/modules-load.d/evdi.conf` | Loads evdi.ko at boot | + +The `t6evdi` binary itself lives at `/usr/local/bin/t6evdi` (built from source, not in this repo). + +## First-Time Build + +### 1. Dependencies + +```bash +sudo dnf install -y libusb1-devel turbojpeg-devel libdrm-devel git \ + bison elfutils-libelf-devel flex openssl-devel python3-dbus + +# kernel-devel must match the running kernel exactly. +# If not available via dnf, fetch from Koji: +KVER=$(uname -r) +VER=$(echo $KVER | cut -d- -f1) +REL=$(echo $KVER | cut -d- -f2) +sudo rpm -i "https://kojipkgs.fedoraproject.org/packages/kernel/$VER/$REL/x86_64/kernel-devel-$KVER.rpm" +``` + +### 2. Build EVDI (kernel module + library) + +EVDI 1.14.16 supports kernels through 6.20. The top-level Makefile passes +`-Werror=sign-compare` which trips on Fedora kernel headers — patch it before building. + +```bash +git clone --depth=1 https://github.com/DisplayLink/evdi.git +cd evdi +``` + +In `Makefile`, replace the `module:` recipe with: +```makefile +module: + CFLAGS="-isystem./include -isystem./include/uapi -Wextra -Wall -Wno-sign-compare -Wno-error $(CFLAGS)" $(MAKE) -C module $(MFLAGS) +``` + +Then build and install: +```bash +make module && make library +sudo make install # installs evdi.ko + libevdi.so +sudo cp library/evdi_lib.h /usr/include/ +sudo ldconfig +sudo modprobe evdi +echo "evdi" | sudo tee /etc/modules-load.d/evdi.conf +``` + +### 3. Register EVDI with DKMS + +DKMS rebuilds `evdi.ko` automatically after each kernel update (`AUTOINSTALL=yes`). + +```bash +sudo cp -r module /usr/src/evdi-1.14.16 +sudo cp module/dkms.conf /usr/src/evdi-1.14.16/ +sudo dkms add evdi/1.14.16 +sudo dkms build evdi/1.14.16 +sudo dkms install evdi/1.14.16 +``` + +### 4. Build the T6 userspace daemon + +Source: https://github.com/rfxDarth/mobilepixels-linux-driver +Supports `0711:5601` alongside the MobilePixels devices it was written for. + +One API fix required — `evdi_connect` was renamed `evdi_connect2` in EVDI 1.14+: + +```bash +git clone --depth=1 https://github.com/rfxDarth/mobilepixels-linux-driver.git +cd mobilepixels-linux-driver/evdi_t6_1 +# Edit main.c ~line 1117: +# evdi_connect(..., 0, 0) → evdi_connect2(..., 0, 0) +make +sudo cp T6evdi /usr/local/bin/t6evdi +``` + +### 5. Install service and layout scripts + +```bash +sudo cp bin/fix-monitor-layout bin/t6evdi-apply-layout /usr/local/bin/ +sudo chmod +x /usr/local/bin/fix-monitor-layout /usr/local/bin/t6evdi-apply-layout +sudo cp systemd/t6evdi.service /etc/systemd/system/ +sudo cp systemd/evdi.conf /etc/modules-load.d/ +sudo systemctl daemon-reload +sudo systemctl enable --now t6evdi.service +``` + +## After a Kernel Update + +DKMS rebuilds `evdi.ko` automatically — nothing to do. If it fails: + +```bash +sudo dkms build evdi/1.14.16 -k $(uname -r) +sudo dkms install evdi/1.14.16 -k $(uname -r) +# If kernel-devel isn't in repos, fetch it from Koji (see step 1) +``` + +## How the Monitor Layout Fix Works + +**Problem:** The DRM connector name for the EVDI display (`DVI-I-N`) increments every +time t6evdi restarts. GNOME's `monitors.xml` matches saved layouts by connector name, +so every restart lands the LG monitor on the wrong side. + +**Solution:** `fix-monitor-layout` calls the Mutter `DisplayConfig` D-Bus API directly, +matching monitors by **serial number** rather than connector name. It calls +`ApplyMonitorsConfig` with `method=2` (persistent), which also rewrites `monitors.xml` +correctly for the current connector. + +`t6evdi-apply-layout` is the shell wrapper called by systemd. It sleeps 6 seconds to +give GNOME time to enumerate the new display, then runs `fix-monitor-layout` as `jake` +via `runuser` with the correct session bus address. + +Run the fix manually (without restarting the daemon): +```bash +DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /usr/local/bin/fix-monitor-layout +``` + +## Troubleshooting + +**Second monitor black after restart:** +```bash +sudo systemctl restart t6evdi.service +# Layout fix fires automatically ~6 seconds later +``` + +**Monitors on wrong sides (layout fix didn't run):** +```bash +DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /usr/local/bin/fix-monitor-layout +``` + +**Check daemon health:** +```bash +systemctl status t6evdi +journalctl -u t6evdi -n 30 +sudo dmesg | grep evdi | tail -10 +``` + +Healthy log output looks like: +``` +monitor state =1 +EDID_ret= 128 +edid edid_size = 256 +set mode finish +mct_dpms_handler: dpms_mode = 0 +``` + +**EDID_CheckSum error loop:** No monitor connected to the 4K30 HDMI port. Daemon +retries indefinitely — plug in the monitor and it will proceed. + +**DKMS status:** +```bash +dkms status evdi +``` + +## Known Limitations + +- Refresh rate capped at 75Hz at 1080p (T6 chip / USB bandwidth constraint) +- UID 1000 is hardcoded in `t6evdi-apply-layout` — update if jake's UID changes +- The 6-second sleep in `t6evdi-apply-layout` is a heuristic; increase it if GNOME is + slow to enumerate the display under load +- Layout fix only runs on service start, not on login. If the display is already + connected but mis-positioned at login, run `fix-monitor-layout` manually +- The t6evdi codebase is immature — do not use `make install` from the mobilepixels repo diff --git a/bin/fix-monitor-layout b/bin/fix-monitor-layout new file mode 100755 index 0000000..a1d43e2 --- /dev/null +++ b/bin/fix-monitor-layout @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Re-apply dual-monitor layout after t6evdi restarts. + +Matches monitors by serial number (not connector name), so it works +regardless of what DRM connector name t6evdi assigns on each restart. + +LG FHD (GSM, serial 0x01010101) → left (x=0) +Acer SA270 (ACR, serial 0x13102591) → right (x=1920), primary +""" + +import sys +import dbus + +BUS = 'org.gnome.Mutter.DisplayConfig' +PATH = '/org/gnome/Mutter/DisplayConfig' + +LG_SERIAL = '0x01010101' +ACER_SERIAL = '0x13102591' + + +def find_mode(modes, width=1920, height=1080, rate=74.973): + """Return mode ID matching resolution+rate, falling back to resolution only.""" + for mode_id, mw, mh, mr, *_ in modes: + if int(mw) == width and int(mh) == height and abs(float(mr) - rate) < 0.5: + return str(mode_id) + for mode_id, mw, mh, *_ in modes: + if int(mw) == width and int(mh) == height: + return str(mode_id) + return None + + +def main(): + try: + bus = dbus.SessionBus() + except dbus.exceptions.DBusException as e: + print(f"ERROR: cannot connect to session bus: {e}", file=sys.stderr) + sys.exit(1) + + proxy = bus.get_object(BUS, PATH) + iface = dbus.Interface(proxy, BUS) + + serial, monitors, _, _ = iface.GetCurrentState() + + connectors = {} + mode_ids = {} + + for (connector, vendor, product, mon_serial), modes, props in monitors: + s = str(mon_serial) + if s in (LG_SERIAL, ACER_SERIAL): + connectors[s] = str(connector) + mode_ids[s] = find_mode(modes) + + errors = [] + for label, key in [('LG FHD', LG_SERIAL), ('Acer SA270', ACER_SERIAL)]: + if key not in connectors: + errors.append(f"{label} not detected") + elif mode_ids[key] is None: + errors.append(f"no 1920x1080 mode on {label}") + if errors: + print(f"ERROR: {'; '.join(errors)}", file=sys.stderr) + sys.exit(1) + + logical = [ + (0, 0, 1.0, 0, False, + [(connectors[LG_SERIAL], mode_ids[LG_SERIAL], {})]), + (1920, 0, 1.0, 0, True, + [(connectors[ACER_SERIAL], mode_ids[ACER_SERIAL], {})]), + ] + + iface.ApplyMonitorsConfig( + dbus.UInt32(serial), + dbus.UInt32(2), # 2 = persistent + logical, + {}, + signature='uua(iiduba(ssa{sv}))a{sv}' + ) + + print( + f"Applied: {connectors[LG_SERIAL]} (LG FHD) at x=0, " + f"{connectors[ACER_SERIAL]} (Acer SA270) at x=1920 [primary]" + ) + + +if __name__ == '__main__': + main() diff --git a/bin/t6evdi-apply-layout b/bin/t6evdi-apply-layout new file mode 100755 index 0000000..557aefc --- /dev/null +++ b/bin/t6evdi-apply-layout @@ -0,0 +1,12 @@ +#!/bin/bash +# Called by t6evdi.service after startup. +# Waits for GNOME to enumerate the new display, then uses the Mutter +# DisplayConfig DBus API to apply the correct monitor positions by serial +# number — independent of whatever connector name t6evdi assigns. + +sleep 6 + +exec runuser -u jake -- env \ + DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \ + XDG_RUNTIME_DIR=/run/user/1000 \ + /usr/local/bin/fix-monitor-layout diff --git a/systemd/evdi.conf b/systemd/evdi.conf new file mode 100644 index 0000000..0576d51 --- /dev/null +++ b/systemd/evdi.conf @@ -0,0 +1 @@ +evdi diff --git a/systemd/t6evdi.service b/systemd/t6evdi.service new file mode 100644 index 0000000..3090bc8 --- /dev/null +++ b/systemd/t6evdi.service @@ -0,0 +1,13 @@ +[Unit] +Description=MCT T6 USB display daemon (JCD3198 second HDMI) +After=systemd-udev-settle.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/t6evdi +ExecStartPost=/usr/local/bin/t6evdi-apply-layout +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target