N7le/powerology-actions-usb-display-linux-driver

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

actions-micro
drm
evdi
h264
hid
linux
linux-driver
portable-monitor
powerology
protocol
reverse-engineering
usb
usb-display

README

powerology-actions-usb-display-linux-driver

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 lsusb for 1de1:f201: if it is absent and your screens appear as DP-* 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.

What it does

  • Presents each panel as a genuine display via evdi, so it appears in Display Settings and can be arranged, made primary, and assigned a resolution
  • Streams H.264 to the panels at 1920x1080, measured at 50-57 fps per panel with hardware encoding
  • Drives both panels of a dual-screen unit independently
  • Requires no custom kernel module of its own (only evdi, which is upstream)
  • Also offers a simple mirror mode that needs no evdi at all

Protocol summary

Full details in docs/PROTOCOL.md. In short, each panel is a USB device exposing two vendor-defined HID interfaces:

InterfaceReport IDCarries
01JSON-RPC 2.0 control messages, plain UTF-8
12H.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.

Limitations

  • 60 Hz is a hardware ceiling. The panel's own EDID declares 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.
  • 1920x1080. That is the panel's native timing.
  • X11 only for the launcher. The driver core (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.
  • Shared USB 2.0 bandwidth. Two panels on one bus cannot both sustain 30 Mbps; the default is 18 Mbps each.

Requirements

  • 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_qsv
  • Python 3.8+ (standard library only)
  • libdrm-dev, gcc, make, dkms to build evdi
  • X11 session for start-panels.sh

Setup

1. Build and install evdi

Skip 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

2. Grant access to the panels

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).

3. Connect the dock and verify

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.

4. Load evdi

sudo modprobe evdi initial_device_count=2

5. Start the panels

./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

6. Optional: start automatically

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.

Tuning

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:

Encoderfpsffmpeg CPU
libx264 ultrafast @3029.8 / 29.8133% / 130%
h264_nvenc @6050 / 57146% / 132%
libx264 ultrafast @6050 / 54212% / 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.

Troubleshooting

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.

Wayland

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.

Tools

ScriptPurpose
probe.pyEnumerate panels, map interfaces to hidraw nodes
interrogate.pyRead-only HID interrogation
extend.pyOne panel as an evdi virtual monitor (the driver)
stream.pyMirror a screen region, no evdi
replay.pyReplay captured frames — useful for protocol work
pcapng.pyStandalone USBPcap/pcapng reader, no tshark needed
extract_frames.pyPull H.264 access units out of a capture
make_edid.pyPatch the EDID to advertise a different refresh rate
start-panels.shLauncher: extend, mirror, stop, watch
install-dkms.shRegister an evdi source tree with DKMS

How the protocol was recovered

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.

License

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.

Contributors

N7le

1 commits

N7le/powerology-actions-usb-display-linux-driver

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

actions-micro
drm
evdi
h264
hid
linux
linux-driver
portable-monitor
powerology
protocol
reverse-engineering
usb
usb-display

README

powerology-actions-usb-display-linux-driver

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 lsusb for 1de1:f201: if it is absent and your screens appear as DP-* 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.

What it does

  • Presents each panel as a genuine display via evdi, so it appears in Display Settings and can be arranged, made primary, and assigned a resolution
  • Streams H.264 to the panels at 1920x1080, measured at 50-57 fps per panel with hardware encoding
  • Drives both panels of a dual-screen unit independently
  • Requires no custom kernel module of its own (only evdi, which is upstream)
  • Also offers a simple mirror mode that needs no evdi at all

Protocol summary

Full details in docs/PROTOCOL.md. In short, each panel is a USB device exposing two vendor-defined HID interfaces:

InterfaceReport IDCarries
01JSON-RPC 2.0 control messages, plain UTF-8
12H.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.

Limitations

  • 60 Hz is a hardware ceiling. The panel's own EDID declares 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.
  • 1920x1080. That is the panel's native timing.
  • X11 only for the launcher. The driver core (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.
  • Shared USB 2.0 bandwidth. Two panels on one bus cannot both sustain 30 Mbps; the default is 18 Mbps each.

Requirements

  • 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_qsv
  • Python 3.8+ (standard library only)
  • libdrm-dev, gcc, make, dkms to build evdi
  • X11 session for start-panels.sh

Setup

1. Build and install evdi

Skip 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

2. Grant access to the panels

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).

3. Connect the dock and verify

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.

4. Load evdi

sudo modprobe evdi initial_device_count=2

5. Start the panels

./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

6. Optional: start automatically

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.

Tuning

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:

Encoderfpsffmpeg CPU
libx264 ultrafast @3029.8 / 29.8133% / 130%
h264_nvenc @6050 / 57146% / 132%
libx264 ultrafast @6050 / 54212% / 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.

Troubleshooting

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.

Wayland

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.

Tools

ScriptPurpose
probe.pyEnumerate panels, map interfaces to hidraw nodes
interrogate.pyRead-only HID interrogation
extend.pyOne panel as an evdi virtual monitor (the driver)
stream.pyMirror a screen region, no evdi
replay.pyReplay captured frames — useful for protocol work
pcapng.pyStandalone USBPcap/pcapng reader, no tshark needed
extract_frames.pyPull H.264 access units out of a capture
make_edid.pyPatch the EDID to advertise a different refresh rate
start-panels.shLauncher: extend, mirror, stop, watch
install-dkms.shRegister an evdi source tree with DKMS

How the protocol was recovered

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.

License

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.

Contributors

N7le

1 commits

Languages

Python

84.0%

Shell

16.0%