Two monitors with built-in USB touchscreens, one X server spanning both — but touches land on the wrong screen, or touch doesn’t work at all. This post covers identifying the physical devices, mapping each touchscreen to its monitor persistently, and how to swap left/right without re-cabling.
Problem
On a dual-monitor kiosk setup (two 1920×1080 screens side-by-side, 3840×1080 desktop), each monitor has an integrated USB touchscreen. X11 maps every touchscreen to the entire desktop by default, so touching the middle of the left screen lands the pointer at x≈1920 — the left edge of the right monitor.
Two separate failure modes showed up:
- Touch dead entirely —
xinput listshows no touch devices even thoughlsusbsees them. - Touch alive but wrong screen — touches land on the other half of the desktop.
Failure mode 1: Paused input fds (dead touch)
If xinput list shows only the virtual core devices, check which VT is foreground:
sudo fgconsole # active console VT
ps aux | grep [X]org | grep -o "vt[0-9]*" # VT X is bound toIf X runs on vt7 but the console is on tty1, systemd-logind hands Xorg paused device fds — the devices are added but never deliver input, and the X server never recovers once it missed the resume. Fix live:
sudo chvt 7 # bring X's VT to the foreground
systemctl restart kiosk.service # or whatever starts your X sessionPermanent fix: Xorg has a -novtswitch flag, but removing it alone didn’t help here — unprivileged (logind-managed) X cannot chvt at all. The reliable fix is a tiny watcher service that keeps the console on X’s VT (and re-switches after X restarts, since X exit returns the console to the previous VT):
[Unit]
Description=Keep console on kiosk VT
After=kiosk.service
PartOf=kiosk.service
[Service]
Type=simple
ExecStart=/bin/sh -c 'while systemctl is-active --quiet kiosk.service; do [ "$(cat /sys/class/tty/tty0/active)" = tty7 ] || chvt 7; sleep 2; done'
[Install]
WantedBy=kiosk.serviceDo not try ExecStartPost=/usr/bin/chvt 7 inside a systemd unit that uses PAMName=login + TTYPath= — the post-start process registers its own PAM session on the same VT, fails with VirtualTerminalAlreadyTaken, and systemd kills the main process. Ask me how I know.
Identify which device is which screen
Two identical USB touchscreens can’t be told apart by name. Use the USB topology path — it’s stable across reboots as long as cables stay in the same ports:
for e in /dev/input/event*; do
n=$(cat /sys/class/input/$(basename $e)/device/name 2>/dev/null)
p=$(cat /sys/class/input/$(basename $e)/device/phys 2>/dev/null)
case "$n" in *Touch*|*touch*) echo "$e $n $p" ;; esac
done/dev/input/event2 Weida Hi-Tech CoolTouch® System usb-0000:03:00.4-1.1.4/input0
/dev/input/event5 Weida Hi-Tech CoolTouch® System usb-0000:03:00.4-1.1.2.4/input0To learn which port is the physical left screen, watch for event traffic while touching a known monitor:
for e in event2 event5; do
(sudo timeout 60 dd if=/dev/input/$e bs=48 2>/dev/null | wc -c > /tmp/c_$e) &
done
wait
for e in event2 event5; do echo "$e: $(cat /tmp/c_$e) bytes"; done
# the nonzero node is the screen you just touchedEach touchscreen also exposes a “Mouse” companion interface (absolute pointer events on a separate event node). It will also generate clicks if left unconstrained — map both.
Test live: xinput
Map each device to its output (or set the matrix directly):
DISPLAY=:0 xinput map-to-output 8 DisplayPort-0 # left monitor device
DISPLAY=:0 xinput map-to-output 6 DisplayPort-1 # right monitor device
# equivalent explicit form (row-major 3x3 CTM):
DISPLAY=:0 xinput set-prop 8 "Coordinate Transformation Matrix" 0.5 0 0 0 1 0 0 0 1
DISPLAY=:0 xinput set-prop 6 "Coordinate Transformation Matrix" 0.5 0 0.5 0 1 0 0 0 1The matrix maps normalized device coordinates into screen space: 0.5 horizontal scale confines the device to half the desktop width; the 0.5 offset in m13 shifts the right device onto the right half.
Make it persistent: a self-healing mapper script
The “proper” static approaches all failed on this logind-managed, unprivileged X server:
xorg.conf.dInputClass+MatchDevicePathon/dev/input/by-path/*— Xorg opens/dev/input/eventN, so the glob never matchesxorg.conf.dInputClass+MatchTagon udevTAG+=— tags were set, InputClass never applied- udev
LIBINPUT_CALIBRATION_MATRIX— present in the udev DB, never picked up by libinput via the logind fd path - plain
xinput— dies with the X session, and the CTM is wiped when logind resumes/recreates devices after a VT switch
What works: a small daemon started from the window-manager autostart that resolves /dev/input/by-path/* → xinput id via the Device Node property and enforces the CTM every few seconds:
#!/usr/bin/env bash
# 1.1.4 = LEFT monitor, 1.1.2.4 = RIGHT monitor (USB topology ports)
LEFT_PORT="1.1.4"; RIGHT_PORT="1.1.2.4"
LEFT_MATRIX="0.5 0 0 0 1 0 0 0 1"
RIGHT_MATRIX="0.5 0 0.5 0 1 0 0 0 1"
# xinput prints floats with 6 decimals, comma-separated
LEFT_PREFIX="0.500000, 0.000000, 0.000000"
RIGHT_PREFIX="0.500000, 0.000000, 0.500000"
set_ctm() { # $1=eventN, $2=matrix, $3=expected prefix (skip if already set)
local node="/dev/input/$1" id dn cur
for id in $(xinput list --id-only 2>/dev/null); do
dn=$(xinput list-props "$id" 2>/dev/null | sed -n 's/.*Device Node.*"\(.*\)"/\1/p')
[ "$dn" = "$node" ] || continue
cur=$(xinput list-props "$id" 2>/dev/null | sed -n 's/.*Coordinate Transformation Matrix.*:\s*//p')
case "$cur" in
"$3"*) ;;
*) xinput set-prop "$id" "Coordinate Transformation Matrix" $2 ;;
esac
done
}
while :; do
for link in /dev/input/by-path/*usb*:1.0-event*; do
[ -e "$link" ] || continue
node=$(basename "$(readlink -f "$link")")
case "$link" in
*":$LEFT_PORT:"*) set_ctm "$node" "$LEFT_MATRIX" "$LEFT_PREFIX" ;;
*":$RIGHT_PORT:"*) set_ctm "$node" "$RIGHT_MATRIX" "$RIGHT_PREFIX" ;;
esac
done
sleep 5
doneand in the openbox autostart:
/usr/local/bin/kiosk-touchmap.sh &Verify:
DISPLAY=:0 xinput list-props 8 | grep "Coordinate Transformation Matrix"How to swap left and right
If the ports are cabled opposite to the labels (common — there’s no way to know remotely which USB port feeds which panel), don’t re-cable:
- Edit
/usr/local/bin/kiosk-touchmap.sh - Exchange
LEFT_PORTandRIGHT_PORT - Restart X:
systemctl restart kiosk.service(or your session service) — the mapper picks it up
For a quick live test without editing files, swap the two xinput map-to-output targets and touch each screen.
Matrix cheat sheet for other layouts
Row-major 3×3, applied as x' = m11·x + m13, y' = m22·y + m23 (normalized 0..1 device coords → normalized screen coords):
| Layout | Matrix |
|---|---|
| Full desktop | 1 0 0, 0 1 0, 0 0 1 |
| Left half | 0.5 0 0, 0 1 0, 0 0 1 |
| Right half | 0.5 0 0.5, 0 1 0, 0 0 1 |
| Top half | 1 0 0, 0 0.5 0, 0 0 1 |
| Bottom half | 1 0 0, 0 0.5 0.5, 0 0 1 |
| Third of three | 0.333 0 N, 0 1 0, 0 0 1 where N = 0, 0.333, 0.666 |