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:
2026-05-17 18:36:35 -05:00
commit 5dbbb34096
5 changed files with 318 additions and 0 deletions

85
bin/fix-monitor-layout Executable file
View 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()