Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
746edca
Add CH9350L UART protocol specification documentation
sjmf May 2, 2026
5d41c8f
Create gitignored tmp/ directory for investigative purposes
sjmf May 3, 2026
506d3c7
Document CH9350L protocol findings (towards #13)
sjmf May 3, 2026
f101e8a
Add some notes on USB HID interpretations for observed protocol behav…
sjmf May 3, 2026
c0f2768
docs(ch9350): document STATUS bit-field and reattach replay, towards #13
sjmf May 3, 2026
f5359c5
docs(ch9350l): document state-2 mode behaviour from empirical capture…
sjmf May 3, 2026
b8e6be1
docs(ch9350l): document states 3 and 4 from empirical capture and dat…
sjmf May 3, 2026
f50aed9
docs(ch9350l): document states 2/3/4 from datasheet §3.3-3.5 and capt…
sjmf May 3, 2026
9837f11
docs(ch9350l): rework redundancies, ambiguities, and stale claims (to…
sjmf May 3, 2026
e2d612e
docs(ch9350l): update docs regarding HID Digitizers for state-3/4 cur…
sjmf May 4, 2026
0f18e48
docs(ch9350l): swap labelling spellings to British English (towards #13)
sjmf May 4, 2026
070907a
Refactor DataComm into ABC + CH9329Comm subclass, towards #13
sjmf May 4, 2026
194f9ae
Migrate mouseop to high-level mouse API; fix scroll wire format (towa…
sjmf May 4, 2026
f908389
Move CH9329Comm into its own module (towards #13)
sjmf May 4, 2026
e2fa502
Split CH9329 tests into their own file (towards #13)
sjmf May 4, 2026
206b0eb
Add CH9350Comm with states 2/3/4 (Phase A), towards #13
sjmf May 4, 2026
b540f93
Add CH9350Comm state 0/1 paired mode (towards #13)
sjmf May 4, 2026
0f2d03c
Wire LED echo for CH9350Comm states 2/3/4 (towards #13)
sjmf May 4, 2026
b129893
Wire CH9350Comm through BaseOp + kvm-control CLI (towards #13)
sjmf May 4, 2026
8b47470
Wire CH9350Comm via DataCommManager + kvm-control CLI flag (towards #13)
sjmf May 5, 2026
4c487fa
Wire CH9350Comm via DataCommManager + kvm-control CLI flag (towards #13)
sjmf May 5, 2026
7565913
Add protocol documentation for ch9329 for parity
sjmf May 5, 2026
cefed15
Update references to specific chip to generalise across both supporte…
sjmf May 5, 2026
d6f940a
(docs) Generate documentation homepage from a script instead of redun…
sjmf May 5, 2026
4b49abd
(docs) Update README and add SUPPORTED_DEVICES documentation for CH93…
sjmf May 5, 2026
e48e09a
Change run as module vs run as script import strategy
sjmf May 5, 2026
29c0da2
Fix type casting for mouse movement deltas in CH9350Comm class (towar…
sjmf May 5, 2026
0c501ed
Add support for composite mouse descriptor in CH9350Comm class (towar…
sjmf May 5, 2026
3307d4f
(docs) Document CH9350L state-0/1 absolute mouse limitation (towards …
sjmf May 10, 2026
822d39e
Revert composite mouse descriptor support in CH9350Comm (towards #13)
sjmf May 10, 2026
574715d
Improve CH9350Comm absolute-mouse handling for relative-only states (…
sjmf May 10, 2026
33f8529
(docs) add CH9329 and CH9350L user guides (towards #13)
sjmf May 10, 2026
c27f82d
Fix CH9350Comm state-3/4 absolute mouse over-scaling (towards #13)
sjmf May 10, 2026
b53f45f
Add debug log for state-0/1/2 abs->rel mouse translation (towards #13)
sjmf May 10, 2026
28371ca
Recalibrate CH9350Comm state-3/4 absolute range to 14-bit (towards #13)
sjmf May 10, 2026
0c8f233
Recalibrate CH9350Comm state-3/4 absolute range to 10-bit (towards #13)
sjmf May 10, 2026
7e63507
Document CH9350L state-3/4 absolute X/Y range (towards #13)
sjmf May 10, 2026
85d759d
Document target-side pointer acceleration in state 0/1/2 (towards #13)
sjmf May 10, 2026
5c43bc4
Fix drag broken by on_move clearing held button (towards #13)
sjmf May 10, 2026
a46ca74
Fix CH9350L abs coordinate range in DataComm docstring (towards #13)
sjmf May 10, 2026
58020f7
Clamp wheel delta to ±127 in state-0/1 mouse frame (towards #13)
sjmf May 10, 2026
1f6ebc7
Type comm_manager as DataCommManager via TYPE_CHECKING import (toward…
sjmf May 10, 2026
1ff0de6
Use -1 sentinel for CH9329 state in _PROTOCOL_OPTIONS (towards #13)
sjmf May 10, 2026
e8ce224
Add tests for ch9350 internals, parse_frames edge cases, manager erro…
sjmf May 10, 2026
dd0b53a
Bump year and version number
sjmf May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ jobs:
- name: Install MkDocs and Material theme
run: pip install mkdocs-material

- name: Generate docs homepage from README
run: python scripts/generate_docs_index.py

- name: Deploy to GitHub Pages
run: mkdocs gh-deploy --force
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea/
.vscode/
.claude/
tmp/
CLAUDE.md
.DS_Store
._.DS_Store
Expand All @@ -22,6 +23,7 @@ uv.lock

# MkDocs build output
site/
docs/index.md

# Icon generation intermediate files
assets/icon.iconset/
Expand Down
73 changes: 42 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Serial KVM Controller (CH9329)
# Serial KVM Controller (CH9329 and CH9350L)

[![PyPI](https://img.shields.io/pypi/v/kvm-serial)](https://pypi.org/project/kvm-serial/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
[![Black](https://img.shields.io/badge/code%20style-black-black)](https://github.com/sjmf/kvm-serial/actions/workflows/lint.yml)
[![Run Tests](https://img.shields.io/github/actions/workflow/status/sjmf/kvm-serial/test.yml?label=Unit%20Tests)](https://github.com/sjmf/kvm-serial/actions/workflows/test.yml)
[![codecov](https://img.shields.io/codecov/c/gh/sjmf/kvm-serial)](https://codecov.io/gh/sjmf/kvm-serial)

A Software KVM, using the CH9329 UART Serial to USB HID controller.
A Software KVM for UART-to-USB-HID bridge chips (CH9329 and CH9350L).

Control your computers using an emulated keyboard and mouse!

This app and python module allows you to control to a second device using a CH9329 module (or cable)
and a video capture device. You can find these from vendors on eBay and AliExpress for a low price.
However, there is very little software support available for these modules, and CH9329
protocol documentation is sparse.
This app and python module allows you to control a second device using a UART-to-USB-HID bridge chip
(CH9329 or CH9350L) and a video capture device. You can find these from vendors on eBay and AliExpress
for a low price. However, there is very little software support available for these modules, and protocol
documentation is sparse.

This software captures keyboard and mouse inputs from the local computer, sending these over a
serial UART connection to the CH9329 USB HID module, which will output USB HID mouse and keyboard
serial UART connection to the bridge chip, which will output USB HID mouse and keyboard
movements and scan codes to the remote computer.

The `kvm_serial` package provides options for running the GUI, or as a script providing flexible options.
Expand Down Expand Up @@ -44,36 +44,43 @@ The GUI app will do a lot of the work for you: it will enumerate video devices a
and give you a window to interact with the guest in. Application settings can be changed from the
menus (File, Options, View), for example if the app doesn't select the correct devices by default.

kvm-serial supports both CH9329 and CH9350L bridge hardware. See the user guides for hardware-specific setup:
- [CH9329 User Guide](docs/CH9329_GUIDE.md) — cables, wiring, and usage for CH9329 modules
- [CH9350L User Guide](docs/CH9350L_GUIDE.md) — dipswitch configuration, working states, and usage for CH9350L modules
- [SUPPORTED_DEVICES.md](docs/SUPPORTED_DEVICES.md) — protocol and feature comparison

## Kit List

This module requires a little bit of hardware to get going. You will need:

* CH9329 module or cable
* A UART-to-USB-HID bridge chip (CH9329 or CH9350L) — optionally with an assembled cable or module
* Video capture card (e.g. HDMI)

You can likely get everything you need for under £30, which is incredible when compared to the
price of a KVM crash cart adapter.

### CH9329 module/cable assembled as cables
### Bridge Module/Cable

_PLEASE NOTE: I am a hobbyist. I have no affiliation with any manufacturer developing or selling CH9329 hardware._
_PLEASE NOTE: I am a hobbyist. I have no affiliation with any manufacturer developing or selling bridge hardware._

[![Home-made serial KVM module](https://wp.finnigan.dev/wp-content/uploads/2023/11/mini-uart.jpg)](https://wp.finnigan.dev/?p=682)
*A home-made serial KVM module: CH9329 module soldered to SILabs CP2102. CH340 works, too.*

So, I don't have a specific vendor to recommend, but if you put "*CH9329 cable usb*" into a search
engine, you will find the right thing. Just make sure what you buy has "CH9329" in the name: a USB-A
to USB-A cable won't do, and can damage your machine.
Pre-assembled cables and modules are available from eBay and AliExpress:

- **CH9329 cables:** Search for "*CH9329 cable usb*". Just make sure it has "CH9329" in the name;
a USB-A to USB-A cable won't do and can damage your machine. See the [CH9329 User Guide](docs/CH9329_GUIDE.md)
for full hardware and wiring details.
- **CH9350L modules:** Less common than CH9329 but available; typically come as breakout boards
with serial connector and dipswitches. See the [CH9350L User Guide](docs/CH9350L_GUIDE.md) for
dipswitch configuration and working state selection.

The modules have a USB-A male connector on one end, and serial connector on the other. The cables
have USB-A both ends, as they are already put together and should pretty much be plug-and-play: just
make sure it's the right way around. I just soldered a CH9329 module to a UART transceiver chip
myself, as above.
You can build your own by soldering a bridge chip to a UART transceiver chip (e.g. SILabs CP2102 or CH340).

### Video capture card
### Video Capture Card

You also need a capture card that takes the display output from your remote machine, and presents it
as a USB device to your local system. I found the "*UGREEN Video Capture Card HDMI to USB C Capture
You also need a capture card that takes the display output from your remote machine and presents it
as a USB device to your local system. The "*UGREEN Video Capture Card HDMI to USB C Capture
Device*" was a good balance of price versus value. The more you spend on a capture device, the more
responsive your video feed will likely be (to a point). HDMI and VGA hardware is available.

Expand Down Expand Up @@ -110,9 +117,7 @@ pip install -e ".[dev]"

A script called `control.py` is also provided for use directly from the terminal, so you can also control remotes from a headless environment! (e.g. Pi to Pi!)

Packages must be installed first. Use your preferred python package manager. E.g.:


Packages must be installed first. Use your preferred python package manager, e.g. `pip`, `uv`

Usage examples for the `control.py` script:

Expand All @@ -124,7 +129,7 @@ python -m kvm_serial.control
uv run kvm-control

# Run with mouse and video support; use a Mac OSX serial port:
python -m kvm_serial.control -ex /dev/cu.usbserial-A6023LNH
python -m kvm_serial.control -e /dev/cu.usbserial-A6023LNH

# Run the script using keyboard 'tty' mode (no mouse, no video)
python control.py --mode tty /dev/tty.usbserial0
Expand All @@ -134,16 +139,22 @@ sudo python control.py --mode usb /dev/tty.usbserial0

# Increase logging using --verbose (or -v), and use COM1 serial port (Windows)
python control.py --verbose COM1
```

Use `python control.py --help` to view all available options. Keyboard capture and transmission is the default functionality of control.py: a couple of extra parameters are used to enable mouse and video. For most purposes, the default capture mode will suffice.
# Use CH9350L in state 3 (absolute mouse — recommended for desktop use)
python control.py --ch9350 --ch9350-state 3 /dev/cu.usbserial-XXXX

# Use CH9350L in state 2 (BIOS keyboard + relative mouse — for BIOS/UEFI use)
python control.py --ch9350 --ch9350-state 2 /dev/cu.usbserial-XXXX

# Use CH9350L in state 0 (full descriptor handshake)
python control.py --ch9350 --ch9350-state 0 /dev/cu.usbserial-XXXX
```

Mouse capture is provided using the parameter `--mouse` (`-e`). It uses pynput for capturing mouse input and transmits this over the serial link simultaneously to keyboard input. Appropriate system permissions (Privacy and Security) may be required to use mouse capture.
Use `python control.py --help` to view all available options. By default, the CH9329 protocol is used; pass `--ch9350` to switch to CH9350L protocol. See the [CH9329 User Guide](docs/CH9329_GUIDE.md) and [CH9350L User Guide](docs/CH9350L_GUIDE.md) for hardware-specific setup and usage.

Video capture is provided using the parameter `--video` (`-x`). It uses OpenCV for capturing frames from the camera device. Again, system permissions for webcam access may need to be granted.
Mouse capture is provided using the parameter `--mouse` (`-e`). Appropriate system permissions (Privacy and Security) may be required on macOS.

See [MODES.md](./docs/MODES.MD) for more information on the various other options to the script.
Implementations are provided for all the main python input capture methods.
For live video, use the GUI (`kvm-gui`). See [MODES.md](docs/MODES.md) for keyboard capture mode options.

## Troubleshooting

Expand All @@ -158,4 +169,4 @@ With thanks to [@beijixiaohu](https://github.com/beijixiaohu), the author of the
Thank you, once again, to everyone who has [contributed](CONTRIBUTING.md) to this project.

## License
(c) 2023-25 Samantha Finnigan and contributors (except where acknowledged) and released under [MIT License](LICENSE.md).
(c) 2023-26 Samantha Finnigan and contributors (except where acknowledged) and released under [MIT License](LICENSE.md).
172 changes: 172 additions & 0 deletions docs/CH9329_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# CH9329 User Guide

The CH9329 is a UART-to-USB-HID bridge chip. It presents itself on the target PC as a composite
USB keyboard and mouse device, and receives framed commands over a TTL UART from the host
controller (kvm-serial). It is the simpler of the two supported bridge chips: no pairing handshake
or dipswitch configuration is needed, and no special flags are required in kvm-serial. CH9329 is
the default protocol.

> **Protocol reference:** [CH9329 Protocol Specification](CH9329_PROTO.md)
> **Supported devices overview:** [Supported Devices](SUPPORTED_DEVICES.md)

---

## Hardware

### What you need

- A CH9329 module or cable (UART-to-USB-HID)
- A USB-to-UART adapter to connect the CH9329 to your host computer, **unless** using a
pre-assembled all-in-one cable
- A USB video capture card (optional, for video feed from the remote machine)

### Pre-assembled cables and modules

Pre-assembled CH9329 cables are sold on eBay and AliExpress. Search for **"CH9329 cable usb"**.

There are two common form factors:

| Form factor | Description |
|-------------|-------------|
| **Cable** | USB-A at both ends. One end connects to the host (serial), the other to the target (USB HID). Plug-and-play — no soldering required. Make sure it has the CH9329 chip label; a plain USB-A to USB-A cable has no serial conversion and **will not work**. |
| **Module** | A small breakout board with serial header pins. Requires a separate USB-to-UART adapter (CP2102, CH340, FTDI, etc.) wired to the host computer. |

> _Note: the author has no affiliation with any manufacturer or seller._

### DIY wiring

[![Home-made serial KVM module](img/mini-uart.jpg)](https://wp.finnigan.dev/?p=682)
*A home-made serial KVM module: CH9329 chip soldered to a SILabs CP2102 USB-to-UART adapter. CH340 works too.
Note: this photo shows the 5 V pins connected — see the warning below before replicating this wiring.*

You can build your own module by wiring a CH9329 chip to a USB-to-UART adapter:

```
Host PC (USB) ──► USB-to-UART adapter (e.g. CP2102 / CH340)
│ TX ──► RX ┐
│ RX ◄── TX ├─ CH9329 module
│ GND ─── GND ┘
USB-A (target PC)
```

The CH9329 operates at 3.3 V or 5 V TTL. Default baud rate is **9600 bps**; the chip also supports
1200, 2400, 4800, 14400, 19200, 38400, 57600, and 115200 bps (note: 115200 is not supported at
3.3 V). For most uses, keep the default 9600 bps.

> **⚠ Warning — do not connect the 5 V power pins together.**
> The CH9329 module is already powered by the target machine's USB port. If you also connect
> the 5 V output of the USB-to-UART adapter to the CH9329's VCC pin (as per the image above),
> you create two power sources driving the same rail. This can backfeed voltage into one or
> both USB ports, which may damage hardware that lacks modern reverse-power protection. Wire
> only **TX, RX, and GND** between the adapter and the CH9329; leave the 5 V / VCC pins unconnected.

---

## Physical Setup

1. Connect the CH9329 module/cable to the **target machine** via USB.
2. Connect the serial end (or USB-to-UART adapter) to your **host machine** via USB.
3. Verify the device appears as a serial port on the host:
- **macOS/Linux:** `/dev/cu.usbserial-XXXX` or `/dev/ttyUSB0`
- **Windows:** `COM3`, `COM4`, etc. (check Device Manager → Ports)
4. On the target machine, the CH9329 should enumerate as a USB keyboard + mouse. No drivers
are needed on the target side.

> **Driver installation:** If the serial port does not appear on your host, you may need to install
> a USB-to-UART driver. See [INSTALLATION.md](INSTALLATION.md) for platform-specific instructions.

---

## GUI Usage

No special configuration is needed for CH9329 — it is the default protocol.

1. Launch the GUI (`python -m kvm_serial` or the packaged executable).
2. Open **File → Connect** (or the connection toolbar).
3. Select your serial port from the drop-down list and click **Connect**.
4. If using a video capture card, select the camera under **Options → Camera**.
5. The app will begin forwarding your keyboard and mouse input to the remote machine.

The GUI detects CH9329 automatically; no additional menu options are required.

---

## Headless / CLI Usage

Use `kvm_serial.control` for terminal-based usage without a GUI (e.g. Raspberry Pi to Pi, or
headless servers):

```bash
# Basic keyboard forwarding only (default mode)
python -m kvm_serial.control /dev/cu.usbserial-A6023LNH

# With mouse capture enabled
python -m kvm_serial.control --mouse /dev/cu.usbserial-A6023LNH

# With video, use the GUI: python -m kvm_serial

# Windows COM port
python -m kvm_serial.control COM3

# Verbose logging
python -m kvm_serial.control --verbose /dev/cu.usbserial-A6023LNH

# Using uv
uv run kvm-control /dev/cu.usbserial-A6023LNH
```

CH9329 is the default protocol; no `--ch9350` flag is needed. Run with `--help` to see all options.

### Keyboard capture modes

By default, `curses` mode is used for keyboard-only, and `pynput` when mouse or video is enabled.
Other modes can be selected with `--mode`:

```bash
python -m kvm_serial.control --mode tty /dev/cu.usbserial-A6023LNH
python -m kvm_serial.control --mode usb /dev/cu.usbserial-A6023LNH # requires root
```

See [MODES.md](MODES.md) for a full comparison of capture modes and their trade-offs.

---

## Troubleshooting

### Serial port not detected

- Check that the USB-to-UART driver is installed. See [INSTALLATION.md](INSTALLATION.md).
- On Linux, your user may need serial port access: `sudo usermod -a -G dialout $USER` (then log out and back in).
- On macOS, run `ls /dev/cu.*` to list available serial ports after connecting the device.

### No keyboard/mouse response on the target

- Ensure the USB end of the CH9329 is connected to the **target** machine and a USB HID device
has enumerated (check the target's device manager or `lsusb`).
- Double-check you have not connected the USB-A ends the wrong way around (target ↔ host reversed).
- Try a different USB port or cable.
- Check baud rate: kvm-serial defaults to 9600 bps which matches the CH9329 factory default.
If the chip has been reconfigured to a different baud rate, pass `--baud <rate>`.

### Keyboard input not captured on the host

- On macOS, pynput requires **Input Monitoring** permission (System Settings → Privacy & Security).
- Try `--mode curses` or `--mode tty` as alternatives that do not require this permission.

### Mouse coordinates appear wrong

- CH9329 uses a 12-bit absolute coordinate space (0–4095 × 0–4095).
- kvm-serial maps screen/scene coordinates to this range automatically in the GUI.
- In headless mode, absolute mouse is sent when `--mouse` is active.

---

## Further Reading

- [CH9329 Protocol Specification](CH9329_PROTO.md) — full frame format, command reference, and worked examples
- [MODES.md](MODES.md) — keyboard capture mode comparison
- [INSTALLATION.md](INSTALLATION.md) — platform-specific driver and permission setup
- [SUPPORTED_DEVICES.md](SUPPORTED_DEVICES.md) — comparison of CH9329 and CH9350L
Loading
Loading