epad-signature-pad-hid-driver
Unofficial Python driver for the ePadLink "ePad" USB signature pad (VID 0x04DF,
PID 0x0012), talking to it directly over raw USB HID.
- What is this device?
- The problem
- How this driver solves it
- Install
- Usage
- What
watchsaves - Handling errors
- How it works
- Library layout
- Documentation
- Development
- License
What is this device?
This is an ePadLink "ePad" USB electronic signature capture pad, made by
Interlink Electronics / ePadLink. When plugged in, it identifies itself over USB
as "ePadLink USB ePad", with USB vendor ID 0x04DF and product ID 0x0012. If
your pad looks like the photo above, this driver is for your hardware.
The problem
ePadLink's own official software — the "Universal Installer" driver plus the "SigCaptureWeb" Chrome extension — only supports Windows and Linux. There is no macOS build at all (confirmed directly against ePadLink's own site). Even on a supported OS, using the pad the official way means installing their proprietary driver and a browser extension first.
How this driver solves it
This driver does not use any ePadLink software. Instead, it reads the pad's own standard USB HID report descriptor directly off the device — a real, self-describing thing every USB HID device publishes about itself — and decodes it into a plain 6-byte report layout (X, Y, pressure, touch state, two buttons). This descriptor was fetched live from real hardware, not guessed or reverse-engineered from captured traffic.
It talks to the pad over raw USB HID using the cross-platform
hidapi library, which works the same way on
macOS, Windows, and Linux. So this driver works on any of those three operating
systems, with no ePadLink software installed at all.
This is a from-scratch driver based on the device's own published HID descriptor, and it is not affiliated with ePadLink / Interlink Electronics.
Install
uv add epad-signature-pad-hid-driver # or: pip install epad-signature-pad-hid-driver
Or clone and run locally:
git clone https://github.com/CaptainCodeAU/epad-signature-pad-hid-driver.git
cd epad-signature-pad-hid-driver
uv sync
Usage
Confirm the pad is connected:
uv run epad-signature-pad-hid-driver
Print live decoded pen data for a fixed window (default 15s):
uv run epad-signature-pad-hid-driver capture 15
Auto-save a signature on every pen touch — starts recording the moment the pen touches down, keeps recording through pen lifts (e.g. between letters), and only stops once there has been no touch at all for a few seconds. Saves the result as three files (see below), waits 2s, then listens for the next touch. Runs until Ctrl+C:
uv run epad-signature-pad-hid-driver watch
Convert a saved file back to a picture — reads a .json or .inkml file
saved by watch (or by save_json/save_inkml as a library) and renders it to
PNG. Also works on a whole directory at once:
uv run epad-signature-pad-hid-driver convert output/signature_20260831T224512_123.json signature.png
uv run epad-signature-pad-hid-driver convert output/ pngs/ # every .json/.inkml in output/, one PNG each
As a library — see examples/ (start with
examples/README.md for what each script shows):
from epad_signature_pad_hid_driver import capture, render_signature
samples = []
capture(seconds=5.0, on_sample=samples.append)
render_signature(samples, "output/signature.png")
As a live browser demo — see examples/web_demo/: a
small local server streams live decoded pen data to a page that draws the
signature on a <canvas> as it happens, with a Save button.
What watch saves
Every captured signature is saved as three files, all sharing one timestamp,
e.g. signature_20260831T224512_123.{png,json,inkml}:
-
.png— a plain rendered picture of the strokes, for a human to look at. -
.json— every single reading captured, in order, not just the final shape: time, X, Y, pressure, and touch state for each reading. This matters because a picture alone can't be replayed or re-rendered differently later, and can't be stored in a system that only holds structured data, like a database. This file can do both, because it keeps the real movement, not just a static image. Load it back withload_json()and re-render it any time withrender_signature(). -
.inkml— the same movement data, saved in InkML, a real open standard from the W3C (the standards body for the web) made specifically for digital pen/ink data. It's included so this data can also be opened directly by other ink and handwriting tools outside this project, not just custom code. Load it back withload_inkml().
If watch is stopped mid-signature (via its stop switch — see below), the
files are still saved, but marked as truncated (a "truncated": true field in
the JSON, a matching InkML annotation, _partial in the filename, and
WatchResult.truncated if you're using watch() as a library) rather than
looking like an ordinary, complete signature.
About the JSON format
The JSON file names its own format and version inside itself, as
"format": "epad-pen-samples-v1". Naming and versioning it this way means a
future format change can't be mistaken for the current one — any reader can
check the field before trusting the data. The format is deliberately simple JSON,
not a custom binary layout, so any programming language or database can read it
with zero special libraries. This is different from the .inkml file, which
needs an XML/InkML-aware reader to open properly.
About the InkML format
The .inkml file's <traceFormat> lists its own channels, so a reader always
knows what each point's numbers mean. A file saved before button channels were
added has 4 columns per point (X, Y, pressure, time); a file saved by the current
version has 6 (the same four, plus B1/B2 for the two side buttons). Both load
correctly through load_inkml() — it reads the <traceFormat> list rather than
assuming a fixed column count.
Handling errors
Every exception this package raises on purpose is an EpadError:
| Exception | Also a... | Raised when |
|---|---|---|
PadNotFoundError |
OSError |
The pad couldn't be opened — not plugged in, or already open in another program (these two cases are not distinguishable — see docs/HARDWARE_NOTES.md) |
EmptyCaptureError |
ValueError |
render_signature() was given zero pen-down samples |
InvalidFormatError |
ValueError |
A .json/.inkml file is malformed, or its extension isn't recognized |
Each also inherits the plain built-in exception type this package already raised
for it before EpadError existed, so except OSError / except ValueError code
keeps working unchanged. The CLI catches all of these itself and prints a plain
Error: ... message instead of a raw traceback.
How it works
The pad exposes a standard 6-byte USB HID input report (no Report ID byte):
| Bytes | Field |
|---|---|
| byte 0, bit 0 | button1 |
| byte 0, bit 1 | button2 |
| byte 0, bits 2-4 | vendor-defined 3-bit field (meaning undocumented) |
| byte 0, bit 5 | touch |
| byte 0, bit 6 | in_range |
| bytes 1-2 | X, 16-bit little-endian |
| bytes 3-4 | Y, 16-bit little-endian |
| byte 5, bits 0-6 | pressure (0-127) |
This layout was read directly from the pad's own HID report descriptor
(hid.device.get_report_descriptor()), not guessed from captured traffic.
See src/epad_signature_pad_hid_driver/protocol.py
for the full decode (and its reverse, encode_report(), used by the file loaders).
For the actual descriptor bytes, how they were read, what's confirmed by
live testing versus what's still unconfirmed, and why no existing vendor
document could have supplied this layout, see
docs/HARDWARE_NOTES.md.
Library layout
The package is split by job, not one big file:
| Module | Job |
|---|---|
protocol.py |
PenSample, decode_report()/encode_report(), the device IDs. Pure data in/out — no USB I/O, no hid import. |
device.py |
open_pad(), PadConnection (a with-statement wrapper), run(). The only module that imports hid. |
session.py |
capture(), watch(), WatchResult — built on protocol.py + device.py. |
exceptions.py |
EpadError and its three subclasses (see above). |
formats.py |
save_json()/load_json(), save_inkml()/load_inkml(). |
render.py |
render_signature(). |
cli.py |
The command-line interface. |
All of these are re-exported from the top-level package, so
from epad_signature_pad_hid_driver import ... keeps working for every name it
already exported, plus the new ones (encode_report, load_json, load_inkml,
PadConnection, and the exceptions above). open_pad()/capture()/watch() all
take optional vendor_id/product_id parameters, so a different pad model can be
pointed at without code changes — see
docs/ADDING_A_MODEL.md if you're adding support for one.
Documentation
| Doc | What's in it |
|---|---|
docs/HARDWARE_NOTES.md |
The hardware backstory: the pad's device identity, its full HID report descriptor and how it was read, every live-confirmed behaviour (sample rate, the pad's exclusivity, what unplugging it mid-read raises), and why no existing vendor document could have supplied the byte layout this driver uses. |
docs/ADDING_A_MODEL.md |
A step-by-step guide for adding support for a different pad model from the same vendor later, without guessing. |
examples/README.md |
An index of the numbered example scripts and which to read first. |
examples/web_demo/README.md |
How to run the live browser demo, its --replay flags, and what it does and doesn't verify. |
Development
uv sync --extra dev # install with dev dependencies
uv run pytest # run tests
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy src/ # type check
License
MIT — see LICENSE.
Metadata
Release files for epad_signature_pad_hid_driver 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| epad_signature_pad_hid_driver-0.1.0.tar.gz | 177.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| epad_signature_pad_hid_driver-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 201.4 kB
Release files / epad_signature_pad_hid_driver-0.1.0.tar.gz
| Download URL | epad_signature_pad_hid_driver-0.1.0.tar.gz |
|---|---|
| Size | 177.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d2ab22d473b0d505a705994ec445379a44eee6f20ba39ccaf27b570f968eb067
|
|
BLAKE2b-256 checksum How to use checksums |
30e6ae45b16b19157423d7efbf2fae38a049897c8bbd5617422e585f05b865cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / epad_signature_pad_hid_driver-0.1.0-py3-none-any.whl
| Download URL | epad_signature_pad_hid_driver-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9422bfa2a008f983d40003c5c0dc2aab1b110bb389cf87922dab697964203ae2
|
|
BLAKE2b-256 checksum How to use checksums |
5333d8e873d396a6c7aade82e553e232c7a5b1f71f775870ff050b262bc83244
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|