hidws is a lightweight C daemon that bridges WebSocket clients to USB HID
devices, letting web apps talk to HID devices remotely over the network
without direct USB access.
It also ships hid-list, a small diagnostic tool that lists all USB HID
devices and probes the reports they support.
Both programs link against the hidapi libusb backend and do not
require kernel HID support (/dev/hidraw*), so they work even on systems
without a HID/INPUT kernel subsystem.
sudo apt install libhidapi-dev libwebsockets-devThe programs link with -lhidapi-libusb at build time. libhidapi-dev must
ship the precompiled libhidapi-libusb.so backend (usbfs, libusb).
To read/write HID devices as a regular user:
sudo usermod -aG plugdev $USERLog out and back in for the change to take effect.
Create /etc/udev/rules.d/99-hid.rules:
# Permissions for HID and USB devices (covers both hidraw and libusb backends)
SUBSYSTEM=="hidraw", MODE="0666"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0664", GROUP="plugdev"
Then reload:
sudo udevadm control --reload-rules && sudo udevadm triggermake
sudo make install # PREFIX=/usr/localThis builds both hidws and hid-list.
MIPS/uClibc note: on the Freetz-EVO MIPS toolchain (GCC 13.4.0, uClibc-ng,
-march=34kc -msoft-float), compilinghidwswith-O1or higher miscompiles the reader thread and crashes with a NULL deref insidehid_read_timeout(only-O0is stable there). On other platforms the default flags are fine. If you hit aSegmentation faultright after[hid] Reader thread started, rebuild with-O0.
./hidws [port] [--cert FILE] [--key FILE] [--no-ssl] [--config FILE] [--token SECRET] [--user USER --password PASS] [--allow ip/cidr,...]Default port is 9001. Point your WebSocket client to ws://<host>:9001.
The HID device is released automatically when the client disconnects.
Credentials and the IP allowlist may also be provided via the config file
(--config FILE / $HIDWS_CONFIG) or the environment variables
HIDWS_TOKEN, HIDWS_USER, HIDWS_PASSWORD, HIDWS_ALLOW — see
Access control.
By default (./hidws [port]) hidws serves both plain ws:// and
encrypted wss:// on the same port, using a temporary self-signed
certificate kept only in memory (no file, no disk writes, regenerated on
every start). A warning is printed to the log and shown on the diagnostic
page.
./hidws 9001 # ws:// + wss://, temporary in-memory cert
./hidws 9001 --no-ssl # ws:// only (no TLS at all)
./hidws 9001 --cert server.crt --key server.key # persistent cert--cert FILE [--key FILE]: use a persistent certificate. If the cert file does not exist yet and hidws was built with SSL support, a self-signed certificate/key pair is generated automatically at first start (RSA-2048, valid 10 years, CN/SANfritz.box).--keyis optional: if omitted, it is derived from the cert path by replacing the extension with.key(server.crt->server.key).--no-ssl: force plainws://only (no TLS). Useful to keep an explicit "no TLS" mode, e.g. from an init script.- Both schemes are served by the same listening socket, so
ws://<host>:9001andwss://<host>:9001work side by side. This is handy on HTTPS-hosted web apps (e.g. GitHub Pages) which block plainws://connections: the app can fall back towss://.
Note: the self-signed certificate is not trusted by clients, so browsers must be told to accept it (a one-time warning) or the app must be configured to allow self-signed certificates. With a temporary in-memory cert the exception is re-asked on every service restart; with a persistent cert (
--cert) the exception stays valid across restarts.
All access control is disabled by default.
Credentials and the IP allowlist can be supplied through three sources (lowest to highest precedence):
-
Config file —
--config FILE(or theHIDWS_CONFIGenvironment variable) reads a simplekey=valuefile (#comments allowed):# /etc/hidws.conf token=mysecret user=alice password=s3cret allow=192.168.178.0/24,10.0.0.0/8
-
Environment variables —
HIDWS_TOKEN,HIDWS_USER,HIDWS_PASSWORD,HIDWS_ALLOW(comma-separated allowlist):HIDWS_TOKEN=mysecret ./hidws 9001 HIDWS_USER=alice HIDWS_PASSWORD=s3cret ./hidws 9001 HIDWS_ALLOW=192.168.178.0/24,10.0.0.0/8 ./hidws 9001
-
Command line — overrides the previous sources:
./hidws 9001 --token mysecret # bearer token ./hidws 9001 --user alice --password s3cret # user/password ./hidws 9001 --config /etc/hidws.conf
Prefer a config file or environment variables over command-line options so the secrets don't show up in
psoutput or shell history.
./hidws 9001 --token mysecret # bearer token
./hidws 9001 --user alice --password s3cret # user/passwordWhen any credential is configured, every client session starts
unauthenticated: all HID commands (list, open, send_report, ...) are
answered with {"type":"error","message":"not authenticated"} until the
client authenticates with one of the following methods:
-
Application-level (recommended for browser clients; no credential in the URL):
{"cmd":"auth","token":"mysecret"} {"cmd":"auth","user":"alice","password":"s3cret"}Success →
{"type":"auth_ok"}. Failure →{"type":"error","message":"auth failed"}; after 5 consecutive failures the connection is closed (brute-force guard). -
?token=in the WebSocket URL (handy for quick manual use; the token travels in the request line):ws://<host>:9001/?token=mysecret wss://<host>:9001/?token=mysecret -
Authorizationheader (for non-browser clients, e.g. curl/scripts):Authorization: Bearer mysecret Authorization: Basic base64(alice:s3cret)
If no credential is configured, an auth command is accepted idempotently
(always answered auth_ok), so clients that always send it keep working on
both open and protected servers.
./hidws 9001 --allow 192.168.178.0/24,10.0.0.0/8
./hidws 9001 --allow 192.168.178.2
./hidws 9001 --config /etc/hidws.conf # allow= in the file
HIDWS_ALLOW=192.168.178.0/24 ./hidws 9001 # or env var--allow takes a comma-separated list of IPv4 addresses and/or CIDR prefixes.
Connections from any other address are dropped before the HTTP/WebSocket
handshake. With no --allow, every address is accepted.
For exposure beyond the LAN, use
wss://(--cert) on top of the token: without TLS the token travels in clear text.
Opening http://<host>:<port>/ or https://<host>:<port>/ in a browser
serves a small diagnostic page that confirms the daemon is reachable and
shows version, port and the available WebSocket endpoints, plus "Test ws://"
/ "Test wss://" buttons that open a real WebSocket to the server and report
success/failure. When a temporary in-memory certificate is in use, the page
shows a warning banner about it.
This is the convenient way to confirm access and (for wss://) to accept the
browser's one-time security exception for the self-signed certificate: visit
https://<host>:9001/, accept the warning, then reload the page and
press "Test wss://".
./hid-listPrints every USB HID device (path, VID/PID, manufacturer, product, serial) and probes its feature and input reports.
All messages are JSON over WebSocket.
| Command | Payload |
|---|---|
auth |
{"cmd":"auth","token":SECRET} or {"cmd":"auth","user":U,"password":P} |
list |
{"cmd":"list"} |
open |
{"cmd":"open","vendorId":<int>,"productId":<int>} |
send_report |
{"cmd":"send_report","reportId":<int>,"data":[<bytes>]} |
send_feature_report |
{"cmd":"send_feature_report","reportId":<int>,"data":[<bytes>]} |
close |
{"cmd":"close"} |
| Type | Payload |
|---|---|
auth_ok |
{"type":"auth_ok"} |
device_list |
{"type":"device_list","devices":[{"vendorId":...,"productId":...,"productName":"..."}]} |
opened |
{"type":"opened","vendorId":...,"productId":...,"productName":"...","usagePage":...,"usage":...,"interfaceNumber":...,"busType":...,"releaseNumber":...,"serialNumber":"...","reportDescriptor":[...]} |
input_report |
{"type":"input_report","reportId":<int>,"data":[<bytes>]} |
ok |
{"type":"ok"} |
error |
{"type":"error","message":"..."} |
closed |
{"type":"closed"} |
openedalso carries the hidapi device info (usagePage,usage,interfaceNumber,busType,releaseNumber,serialNumber) and the raw HID report descriptor as a byte array (reportDescriptor), so clients can render the same report collections (input/output/feature with report IDs and items) as a local WebHID connection.
- https://ircama.github.io/fiiocontrol, repository https://github.com/Ircama/fiiocontrol
- https://ircama.github.io/kt02h20-control, repository https://github.com/Ircama/kt02h20-control
- https://ircama.github.io/Audiocular-Aura, repository https://github.com/Ircama/Audiocular-Aura
- https://ircama.github.io/fiiocontrol-oss, respository https://github.com/Ircama/fiiocontrol-oss
- https://ircama.github.io/webhid-explorer, respository https://github.com/Ircama/webhid-explorer
- https://ircama.github.io/walkplay, repository https://github.com/Ircama/walkplay
- Access control (
--token/--user/--password/--allow, env varsHIDWS_TOKEN/HIDWS_USER/HIDWS_PASSWORD/HIDWS_ALLOW, or a config file withtoken=/user=/password=/allow=) is off by default; it only activates when at least one credential is configured. - The KT02H20 family (FiiO JA11 etc.) uses OUTPUT reports; frontends must send data with
send_report(report ID 0x02), not feature reports. input_reportdata received over WebSocket includes the report-ID byte as the first element for numbered input reports; WebHID strips it, so remote frontends must strip it to match.
European Union Public Licence 1.2 (EUPL-1.2). See LICENSE.