Initial commit: JCD3198 second HDMI driver setup for Fedora
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 <noreply@anthropic.com>
This commit is contained in:
207
README.md
Normal file
207
README.md
Normal file
@@ -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
|
||||
85
bin/fix-monitor-layout
Executable file
85
bin/fix-monitor-layout
Executable file
@@ -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()
|
||||
12
bin/t6evdi-apply-layout
Executable file
12
bin/t6evdi-apply-layout
Executable file
@@ -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
|
||||
1
systemd/evdi.conf
Normal file
1
systemd/evdi.conf
Normal file
@@ -0,0 +1 @@
|
||||
evdi
|
||||
13
systemd/t6evdi.service
Normal file
13
systemd/t6evdi.service
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user