Userspace Linux driver + reverse-engineered protocol for Actions Micro AM8268N (1de1:f201) USB display panels, as used in Powerology and similar dual-screen laptop extenders. JSON-RPC control, H.264 video, real monitors via evdi.
0
stars
1
commits
Python
primary language
Sep 10, 2026
updated
A userspace Linux driver for USB display panels built on the Actions
Microelectronics AM8268N (1de1:f201, firmware am_8268N_ezcast-usb_usbdisplay),
as found in portable dual-screen laptop monitor extenders — including
Powerology dual-screen extenders and other rebadges of the same reference
design (reported as QuattroTx CT10 by the firmware).
If your panels power on but show "no signal" on Linux, ship with a Windows
USBDisplay.exe on a virtual CD drive, and appear in lsusb as
1de1:f201 Actions Microelectronics Co. USB_DISPLAY, this is for you.
These panels ship with a Windows-only application and have no Linux support of
any kind — they are not DisplayLink, not udl/udlfb, not gm12u320, and not
libam7xxx. This repository contains a reverse-engineered protocol
specification and a working driver that presents each panel as a real monitor.
Developed against a Powerology dual-screen extender (QuattroTx CT10) on
Linux Mint 22 with kernel 7.0, Cinnamon/X11.
Note: some newer revisions of these extenders use DisplayPort MST over USB-C alt mode instead of this USB protocol. Those need no driver at all — they work natively. Check
lsusbfor1de1:f201: if it is absent and your screens appear asDP-*outputs, you do not need this repository. A working UCSI/Type-C stack is required for that path, which on some laptops means a newer kernel.
evdi, so it appears in
Display Settings and can be arranged, made primary, and assigned a resolutionevdi, which is
upstream)evdi at allFull details in docs/PROTOCOL.md. In short, each panel is a USB device exposing two vendor-defined HID interfaces:
| Interface | Report ID | Carries |
|---|---|---|
| 0 | 1 | JSON-RPC 2.0 control messages, plain UTF-8 |
| 1 | 2 | H.264 Annex-B video, Constrained Baseline, level 4.2 |
Reports are a fixed 4096 bytes: a 16-byte header (report ID, big-endian payload length, and width/height for video) followed by 4080 bytes of payload.
The control channel is human-readable. The panel announces itself with
notify-app-accessories-are-ready, hands over its EDID via
common.data_passthrough, and the host replies with app-launched and
notify-projection-started before streaming begins.
V 48-63Hz,
and its decoder is level 4.2 (~1080p64 max). Forcing 120 Hz makes the panel
firmware crash and re-enumerate. make_edid.py exists if you want to
experiment, but do so knowing that.protocol.py,
evdi_display.py, and the pixel path in extend.py) is display-server
agnostic, but start-panels.sh uses xrandr for provider attachment and
output placement, and mirror mode uses x11grab. See Wayland below.evdi >= 1.14.15 (1.14.15 added kernel 7.0 support; 1.15.0 covers 7.2).
Distro packages are often far older — Ubuntu 24.04 ships 1.14.2, which does
not compile against kernel 7.0.ffmpeg with libx264, and optionally h264_nvenc or h264_qsvlibdrm-dev, gcc, make, dkms to build evdistart-panels.shSkip this if your distro already ships evdi >= 1.14.15 and it loads on your
kernel (modinfo evdi).
sudo apt install build-essential dkms libdrm-dev
git clone --branch v1.15.0 https://github.com/DisplayLink/evdi
cd evdi
make -C module
make -C library
Optionally shorten a 5-second startup delay in libevdi (it waits for the X server to take DRM master, then proceeds as a slave anyway):
patch -p0 < /path/to/actions-usb-display/patches/libevdi-shorten-master-wait.patch
make -C library
Then register it with DKMS so it survives kernel updates:
sudo /path/to/actions-usb-display/driver/install-dkms.sh "$PWD"
If you have a conflicting distro package, remove it first:
sudo apt purge evdi-dkms libevdi1
The panels are HID devices, so a udev rule is needed to reach them without root:
printf 'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1de1", ATTRS{idProduct}=="f201", MODE="0660", GROUP="plugdev", TAG+="uaccess"\n' \
| sudo tee /etc/udev/rules.d/99-actions-usbdisplay.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
Make sure you are in the plugdev group (id -nG).
If your unit has a mode switch, it must be in the Windows position, and the cable must go to its host port (not a power-only port).
python3 driver/probe.py
Expected output, per panel:
=== 3-4.1 serial=88983c9e speed=480Mb ===
if00 class=0x03 driver=usbhid hidraw=/dev/hidraw5
output report payload: [4095] bytes
/dev/hidraw5: accessible
if01 class=0x03 driver=usbhid hidraw=/dev/hidraw6
output report payload: [12287] bytes
/dev/hidraw6: accessible
If it says PERMISSION DENIED, step 2 did not take effect.
sudo modprobe evdi initial_device_count=2
./driver/start-panels.sh extend
You should end up with your built-in screen plus one output per panel, all
listed by xrandr --query and arrangeable in your desktop's Display Settings.
Mirror the built-in screen instead (no evdi needed):
./driver/start-panels.sh mirror
Stop:
./driver/start-panels.sh stop
mkdir -p ~/.config/systemd/user
cp systemd/usb-panels.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now usb-panels.service
The unit runs start-panels.sh watch, which polls for the dock and
starts/stops streams as it is plugged and unplugged.
Note it targets default.target, not graphical-session.target — desktops
that do not run their session under systemd (Cinnamon among them) never
activate the latter, so a unit wired to it silently never starts.
ENC=h264_nvenc FPS=60 ./driver/start-panels.sh extend # default
ENC=libx264 FPS=30 ./driver/start-panels.sh extend # battery friendly
ENC=h264_qsv ./driver/start-panels.sh extend # Intel iGPU
Measured on an i7-1355U with an RTX 3050, two panels:
| Encoder | fps | ffmpeg CPU |
|---|---|---|
libx264 ultrafast @30 | 29.8 / 29.8 | 133% / 130% |
h264_nvenc @60 | 50 / 57 | 146% / 132% |
libx264 ultrafast @60 | 50 / 54 | 212% / 195% |
backlog 0 in the logs means the USB link is keeping up; a growing backlog
means it is not, and lowering --bitrate is the fix.
Per-panel logs land in /tmp/panel0.log and /tmp/panel1.log.
Both panels show the same image, or only one monitor appears.
Both clients grabbed the same evdi device. evdi_check_device(N) indexes
/dev/dri/cardN, not evdi device N — the real GPUs occupy numbers in
between, so evdi may be card0 and card3. The driver enumerates through
sysfs to avoid this; check that each log names a different evdi node.
A panel is black and the log says no mode assigned.
Your desktop left the new monitor switched off. start-panels.sh enables it
externally; if you run extend.py by hand, enable the output yourself. Never
call xrandr from inside the client — setting a mode on an evdi output blocks
in X until the client services the modeset event, which deadlocks.
no free evdi device after a crash.
A SIGKILLed client leaves the connector reporting connected with no owner.
Increase initial_device_count, or reload the module.
Panels never appear in xrandr.
The evdi providers need attaching to the render GPU:
xrandr --setprovideroutputsource <sink-id> <source-id>.
start-panels.sh does this automatically.
Startup blanks the desktop for several seconds.
Adding monitors makes X rebuild its screen layout. The launcher batches
modesets into single xrandr invocations to minimise it, but some cost is
unavoidable.
The driver core is display-server agnostic — evdi creates a real DRM
connector, and the pixel path reads from evdi rather than from X. Wayland
compositors generally auto-enable new DRM connectors, which removes the
provider-attachment and modeset orchestration entirely.
What would need changing: drop the vestigial xrandr calls in extend.py,
replace the arrangement logic in start-panels.sh (mostly by deleting it), and
swap x11grab for kmsgrab in mirror mode. Untested.
| Script | Purpose |
|---|---|
probe.py | Enumerate panels, map interfaces to hidraw nodes |
interrogate.py | Read-only HID interrogation |
extend.py | One panel as an evdi virtual monitor (the driver) |
stream.py | Mirror a screen region, no evdi |
replay.py | Replay captured frames — useful for protocol work |
pcapng.py | Standalone USBPcap/pcapng reader, no tshark needed |
extract_frames.py | Pull H.264 access units out of a capture |
make_edid.py | Patch the EDID to advertise a different refresh rate |
start-panels.sh | Launcher: extend, mirror, stop, watch |
install-dkms.sh | Register an evdi source tree with DKMS |
USB traffic from the vendor's Windows application was captured with USBPcap and
Wireshark, then decoded with the pcapng.py reader in this repository. The
control channel turned out to be plain-text JSON-RPC, and the video channel
standard H.264 — extracting the stream and decoding it with ffmpeg produced
the exact desktop that had been on the panels, which confirmed the framing.
No vendor binaries, disassembly, or captures are included here: the executable is copyrighted, and the captures contain images of a private desktop.
MIT for the code in this repository. evdi is a separate GPL-2.0 project by
DisplayLink and is not included.
Not affiliated with or endorsed by Actions Microelectronics, DisplayLink, or any panel vendor. Reverse engineering was performed for interoperability on hardware I own.
1 commits
Python
84.0%
Shell
16.0%
Userspace Linux driver + reverse-engineered protocol for Actions Micro AM8268N (1de1:f201) USB display panels, as used in Powerology and similar dual-screen laptop extenders. JSON-RPC control, H.264 video, real monitors via evdi.
0
stars
1
commits
Python
primary language
Sep 10, 2026
updated
A userspace Linux driver for USB display panels built on the Actions
Microelectronics AM8268N (1de1:f201, firmware am_8268N_ezcast-usb_usbdisplay),
as found in portable dual-screen laptop monitor extenders — including
Powerology dual-screen extenders and other rebadges of the same reference
design (reported as QuattroTx CT10 by the firmware).
If your panels power on but show "no signal" on Linux, ship with a Windows
USBDisplay.exe on a virtual CD drive, and appear in lsusb as
1de1:f201 Actions Microelectronics Co. USB_DISPLAY, this is for you.
These panels ship with a Windows-only application and have no Linux support of
any kind — they are not DisplayLink, not udl/udlfb, not gm12u320, and not
libam7xxx. This repository contains a reverse-engineered protocol
specification and a working driver that presents each panel as a real monitor.
Developed against a Powerology dual-screen extender (QuattroTx CT10) on
Linux Mint 22 with kernel 7.0, Cinnamon/X11.
Note: some newer revisions of these extenders use DisplayPort MST over USB-C alt mode instead of this USB protocol. Those need no driver at all — they work natively. Check
lsusbfor1de1:f201: if it is absent and your screens appear asDP-*outputs, you do not need this repository. A working UCSI/Type-C stack is required for that path, which on some laptops means a newer kernel.
evdi, so it appears in
Display Settings and can be arranged, made primary, and assigned a resolutionevdi, which is
upstream)evdi at allFull details in docs/PROTOCOL.md. In short, each panel is a USB device exposing two vendor-defined HID interfaces:
| Interface | Report ID | Carries |
|---|---|---|
| 0 | 1 | JSON-RPC 2.0 control messages, plain UTF-8 |
| 1 | 2 | H.264 Annex-B video, Constrained Baseline, level 4.2 |
Reports are a fixed 4096 bytes: a 16-byte header (report ID, big-endian payload length, and width/height for video) followed by 4080 bytes of payload.
The control channel is human-readable. The panel announces itself with
notify-app-accessories-are-ready, hands over its EDID via
common.data_passthrough, and the host replies with app-launched and
notify-projection-started before streaming begins.
V 48-63Hz,
and its decoder is level 4.2 (~1080p64 max). Forcing 120 Hz makes the panel
firmware crash and re-enumerate. make_edid.py exists if you want to
experiment, but do so knowing that.protocol.py,
evdi_display.py, and the pixel path in extend.py) is display-server
agnostic, but start-panels.sh uses xrandr for provider attachment and
output placement, and mirror mode uses x11grab. See Wayland below.evdi >= 1.14.15 (1.14.15 added kernel 7.0 support; 1.15.0 covers 7.2).
Distro packages are often far older — Ubuntu 24.04 ships 1.14.2, which does
not compile against kernel 7.0.ffmpeg with libx264, and optionally h264_nvenc or h264_qsvlibdrm-dev, gcc, make, dkms to build evdistart-panels.shSkip this if your distro already ships evdi >= 1.14.15 and it loads on your
kernel (modinfo evdi).
sudo apt install build-essential dkms libdrm-dev
git clone --branch v1.15.0 https://github.com/DisplayLink/evdi
cd evdi
make -C module
make -C library
Optionally shorten a 5-second startup delay in libevdi (it waits for the X server to take DRM master, then proceeds as a slave anyway):
patch -p0 < /path/to/actions-usb-display/patches/libevdi-shorten-master-wait.patch
make -C library
Then register it with DKMS so it survives kernel updates:
sudo /path/to/actions-usb-display/driver/install-dkms.sh "$PWD"
If you have a conflicting distro package, remove it first:
sudo apt purge evdi-dkms libevdi1
The panels are HID devices, so a udev rule is needed to reach them without root:
printf 'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1de1", ATTRS{idProduct}=="f201", MODE="0660", GROUP="plugdev", TAG+="uaccess"\n' \
| sudo tee /etc/udev/rules.d/99-actions-usbdisplay.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
Make sure you are in the plugdev group (id -nG).
If your unit has a mode switch, it must be in the Windows position, and the cable must go to its host port (not a power-only port).
python3 driver/probe.py
Expected output, per panel:
=== 3-4.1 serial=88983c9e speed=480Mb ===
if00 class=0x03 driver=usbhid hidraw=/dev/hidraw5
output report payload: [4095] bytes
/dev/hidraw5: accessible
if01 class=0x03 driver=usbhid hidraw=/dev/hidraw6
output report payload: [12287] bytes
/dev/hidraw6: accessible
If it says PERMISSION DENIED, step 2 did not take effect.
sudo modprobe evdi initial_device_count=2
./driver/start-panels.sh extend
You should end up with your built-in screen plus one output per panel, all
listed by xrandr --query and arrangeable in your desktop's Display Settings.
Mirror the built-in screen instead (no evdi needed):
./driver/start-panels.sh mirror
Stop:
./driver/start-panels.sh stop
mkdir -p ~/.config/systemd/user
cp systemd/usb-panels.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now usb-panels.service
The unit runs start-panels.sh watch, which polls for the dock and
starts/stops streams as it is plugged and unplugged.
Note it targets default.target, not graphical-session.target — desktops
that do not run their session under systemd (Cinnamon among them) never
activate the latter, so a unit wired to it silently never starts.
ENC=h264_nvenc FPS=60 ./driver/start-panels.sh extend # default
ENC=libx264 FPS=30 ./driver/start-panels.sh extend # battery friendly
ENC=h264_qsv ./driver/start-panels.sh extend # Intel iGPU
Measured on an i7-1355U with an RTX 3050, two panels:
| Encoder | fps | ffmpeg CPU |
|---|---|---|
libx264 ultrafast @30 | 29.8 / 29.8 | 133% / 130% |
h264_nvenc @60 | 50 / 57 | 146% / 132% |
libx264 ultrafast @60 | 50 / 54 | 212% / 195% |
backlog 0 in the logs means the USB link is keeping up; a growing backlog
means it is not, and lowering --bitrate is the fix.
Per-panel logs land in /tmp/panel0.log and /tmp/panel1.log.
Both panels show the same image, or only one monitor appears.
Both clients grabbed the same evdi device. evdi_check_device(N) indexes
/dev/dri/cardN, not evdi device N — the real GPUs occupy numbers in
between, so evdi may be card0 and card3. The driver enumerates through
sysfs to avoid this; check that each log names a different evdi node.
A panel is black and the log says no mode assigned.
Your desktop left the new monitor switched off. start-panels.sh enables it
externally; if you run extend.py by hand, enable the output yourself. Never
call xrandr from inside the client — setting a mode on an evdi output blocks
in X until the client services the modeset event, which deadlocks.
no free evdi device after a crash.
A SIGKILLed client leaves the connector reporting connected with no owner.
Increase initial_device_count, or reload the module.
Panels never appear in xrandr.
The evdi providers need attaching to the render GPU:
xrandr --setprovideroutputsource <sink-id> <source-id>.
start-panels.sh does this automatically.
Startup blanks the desktop for several seconds.
Adding monitors makes X rebuild its screen layout. The launcher batches
modesets into single xrandr invocations to minimise it, but some cost is
unavoidable.
The driver core is display-server agnostic — evdi creates a real DRM
connector, and the pixel path reads from evdi rather than from X. Wayland
compositors generally auto-enable new DRM connectors, which removes the
provider-attachment and modeset orchestration entirely.
What would need changing: drop the vestigial xrandr calls in extend.py,
replace the arrangement logic in start-panels.sh (mostly by deleting it), and
swap x11grab for kmsgrab in mirror mode. Untested.
| Script | Purpose |
|---|---|
probe.py | Enumerate panels, map interfaces to hidraw nodes |
interrogate.py | Read-only HID interrogation |
extend.py | One panel as an evdi virtual monitor (the driver) |
stream.py | Mirror a screen region, no evdi |
replay.py | Replay captured frames — useful for protocol work |
pcapng.py | Standalone USBPcap/pcapng reader, no tshark needed |
extract_frames.py | Pull H.264 access units out of a capture |
make_edid.py | Patch the EDID to advertise a different refresh rate |
start-panels.sh | Launcher: extend, mirror, stop, watch |
install-dkms.sh | Register an evdi source tree with DKMS |
USB traffic from the vendor's Windows application was captured with USBPcap and
Wireshark, then decoded with the pcapng.py reader in this repository. The
control channel turned out to be plain-text JSON-RPC, and the video channel
standard H.264 — extracting the stream and decoding it with ffmpeg produced
the exact desktop that had been on the panels, which confirmed the framing.
No vendor binaries, disassembly, or captures are included here: the executable is copyrighted, and the captures contain images of a private desktop.
MIT for the code in this repository. evdi is a separate GPL-2.0 project by
DisplayLink and is not included.
Not affiliated with or endorsed by Actions Microelectronics, DisplayLink, or any panel vendor. Reverse engineering was performed for interoperability on hardware I own.
1 commits
Python
84.0%
Shell
16.0%