Headless Debian/Ubuntu service for USB keyboard-wedge barcode scanners. It
reads scans from a configured Linux input device, stores completed scans in a
persistent SQLite queue, and forwards them to
industrial-scanner-logger
over TCP.
This is the Linux counterpart to the Windows
usb-scanner-client. It does
not provide a GUI. It is meant to run continuously under systemd and keep the
scanner ready for industrial use.
This app requires the Industrial Scanner Logger server app to accept and process scan events:
https://github.com/cosmicc/industrial-scanner-logger/tree/dev
linux-usb-scanner-client does not classify scans, write the production scan
database, handle duplicates, or serve the scanner dashboard. Its job is to keep
the Linux USB scanner available, buffer scans locally when needed, and forward
each scan to industrial-scanner-logger over TCP.
This project is the Linux service version of the Windows USB Scanner Client app:
https://github.com/cosmicc/usb-scanner-client
Use the Windows app on Windows scanner workstations. Use this Linux service on Debian or Ubuntu scanner hosts that need the same TCP scan forwarding behavior without a GUI.
- Current version:
1.0.0. - Runs as
linux-usb-scanner-client.serviceon Debian and Ubuntu. - Runs a separate
linux-usb-scanner-client-monitor.servicefor degraded-state beep alerts. - Stores runtime settings in
/etc/linux-usb-scanner-client.conf. - Reads one explicitly configured USB keyboard-wedge scanner from
/dev/input/event*. - Completes scans only when the scanner sends Enter, CR, LF, or CRLF.
- Sends each scan to the logger as UTF-8 barcode text followed by
CRLF. - Uses TCP port
55256by default, matchingindustrial-scanner-logger. - Opens and holds the TCP connection only while the configured USB scanner is available.
- Does not connect to the server when the scanner is missing, inaccessible, or misconfigured.
- Buffers scans in
/var/lib/linux-usb-scanner-client/scans.sqlite3if the server is unavailable. - Keeps the SQLite scan queue in persistent
/var/libstorage across service restarts, app updates, and host reboots. - Drains queued scans in capture order after the server is reachable again.
- Provides CLI health output with scanner state, server state, queue depth, backlog age, queue storage free space, heartbeat, and recent errors.
- Reports current-day and previous-day UTC scan totals in the quick health-check script without printing barcode payloads.
- Beeps continuously during degraded states when alerting is enabled.
- Uses the audio card first for beep alerts, falls back to the system speaker,
and keeps console bell as a final fallback when
backend = auto. - Writes installed service, monitor, and updater logs to
/var/log/linux-usb-scanner-client.log. - Avoids logging raw barcode values.
The server derives scanner_id from the last octet of the client computer's
IPv4 address. This client does not send a scanner ID in the payload.
All client-generated scan metadata and health timestamps are UTC ISO-8601
values ending in Z. The TCP protocol itself sends only barcode frames, because
that is what industrial-scanner-logger currently accepts. The logger remains
responsible for its own server-side receive timestamp.
From the project directory on Debian 12 or newer, or Ubuntu 22.04 or newer:
sudo scripts/install.shAll shell scripts under scripts/ must be run as root. They fail immediately
with a sudo reminder when run as a normal user, including --help requests.
The installer:
- verifies
systemctl, theinputgroup, andpython33.10 or newer before installing the service; - installs required Debian/Ubuntu packages when missing:
acl,ca-certificates,git,python3,python3-dev,python3-pip,python3-venv,build-essential, andalsa-utils; - attempts to install the optional
beeppackage for system-speaker alerting, and continues with a warning if that package is unavailable; - creates a
linux-usb-scanner-clientsystem user and group; - adds the service user to the
inputgroup so it can read/dev/input/event*; - installs the app under
/opt/linux-usb-scanner-client; - creates a Python virtual environment;
- installs
/etc/linux-usb-scanner-client.confif it does not already exist; - installs and starts
linux-usb-scanner-client.service; - installs and starts
linux-usb-scanner-client-monitor.service; - installs and starts
linux-usb-scanner-client-update.timer; - verifies that the app service, alert monitor service, and update timer are enabled and active before reporting success.
The installer is safe to rerun from a source checkout or from the installed
/opt/linux-usb-scanner-client directory. When the source directory is already
the install directory, it skips the application file copy instead of copying the
tree onto itself.
If the app is already installed, sudo scripts/install.sh acts as an update:
it refreshes application files, systemd units, dependencies, and the virtual
environment, then restarts the app service, alert monitor service, and update
timer. It does not delete or recreate /opt/linux-usb-scanner-client,
/etc/linux-usb-scanner-client.conf, /var/lib/linux-usb-scanner-client, or
/var/log/linux-usb-scanner-client.log.
The installer grants the linux-usb-scanner-client service user read/traverse
access to the app directory where scripts/install.sh is running from and to
/opt/linux-usb-scanner-client. It uses ACLs when available so private parent
directories can stay private instead of making the app tree world-readable.
The existing config is preserved unless you run:
sudo scripts/install.sh --overwrite-configThe installer uses Debian/Ubuntu apt-get, so the host needs access to
configured apt repositories during installation or upgrade. Debian 11's default
Python is too old for this package; use Debian 12 or newer, Ubuntu 22.04 or
newer, or provide a supported python3 3.10+ interpreter before running the
installer.
List keyboard-like input devices:
/opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client list-devicesExample output:
/dev/input/event4 name='Honeywell USB Keyboard' vendor_id=0x0c2e product_id=0x0901 phys='usb-0000:00:14.0-1/input0'
The config comments in /etc/linux-usb-scanner-client.conf show how to map the
list-devices output into device_path, vendor_id, product_id, and
device_name. Prefer vendor_id plus product_id because /dev/input/event*
numbers can change after a reboot or USB reconnect.
Edit /etc/linux-usb-scanner-client.conf and set a specific scanner matcher:
[scanner]
vendor_id = 0x0c2e
product_id = 0x0901
device_name = Honeywell
grab_device = truePrefer vendor_id and product_id, or device_path, over a broad name-only
match. The service will not read input until at least one matcher is configured.
This prevents accidentally capturing a normal keyboard.
Then restart:
sudo systemctl restart linux-usb-scanner-clientTo restart the whole installed app after config, unit, script, or app-file changes without rebooting:
sudo /opt/linux-usb-scanner-client/scripts/restart-services.shFrom a source checkout, run sudo scripts/restart-services.sh. The helper
reloads systemd, resets failed unit state, restarts the scanner service,
restarts the alert monitor service, and restarts the auto-update timer. It does
not run the root-owned one-shot auto-update service by default; pass
--run-update-check only when you intentionally want an immediate configured
update check.
Set the logger target in /etc/linux-usb-scanner-client.conf:
[server]
host = 10.10.10.5
port = 55256
connect_timeout = 5
send_timeout = 1
retry_interval = 5
poll_interval = 1
tcp_keepalive = trueWhen the scanner is present, this service opens a persistent TCP connection to the logger. When the scanner is absent, it closes or avoids the TCP connection so the logger can report the scanner as missing.
Human-readable health:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client healthHuman-readable health is ANSI-colored by default: healthy/up values are green, warnings are yellow, down/error values are red, labels are cyan, and paths or targets use accent colors for readability.
Plain text health:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client health --no-colorJSON health:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client health --jsonHealth includes:
- service heartbeat freshness;
- scanner availability and matched device;
- server connection state;
- queue pending count;
- oldest pending scan timestamp;
- queue database size and free space on the queue filesystem;
- maximum pending retry attempts;
- last scan time and scan length;
- last delivery time;
- auto-update state and last remote version seen;
- alert monitor state, active beep pattern, and last monitor check;
- recent scanner or server error.
Raw barcode payloads are intentionally not printed in health output.
The queue database itself contains raw scans, so installed deployments keep it
restricted to the service identity. Run health with sudo unless your operator
account has been intentionally granted access.
Quick installed-system diagnostic:
sudo /opt/linux-usb-scanner-client/scripts/check-health.shThe script summarizes the CLI health report, USB keyboard-like scanner devices,
server connection state, queue/storage state, current-day and previous-day UTC
scan totals, alert monitor state, update state, systemd unit active/enabled
state, and the /var/log/linux-usb-scanner-client.log file. From a source
checkout, run sudo scripts/check-health.sh.
Installed deployments write app, monitor, and updater logs to:
/var/log/linux-usb-scanner-client.logThe Python application logger writes to the path configured by
logging.log_file, which defaults to that file. The installed systemd units also
append stdout and stderr to the same file so tracebacks and command output are
available outside the journal. Adjust logging.log_level to control Python app
logging volume:
[logging]
log_file = /var/log/linux-usb-scanner-client.log
log_level = INFOValid levels are DEBUG, INFO, WARNING, ERROR, and CRITICAL. Raw
barcode payloads are not written to the log.
Every completed scan is inserted into the SQLite queue before delivery. If the TCP server is unavailable or a send fails, the row stays pending with attempt metadata and a UTC retry timestamp. Once the scanner is present and the server can be reached, queued scans are sent in the order they were captured.
Pending scans are retained forever until successfully sent. Sent scan metadata
is retained for seven days by default. Automated cleanup never removes pending
scans.
The queue database is stored under /var/lib/linux-usb-scanner-client/, which
is persistent across reboots and is preserved by install/update runs.
The health check reports free space on the queue database filesystem and marks
health degraded when it drops below buffer.storage_min_free_mb. Provision more
disk or move buffer.database_path before that threshold is reached; the app
will not delete pending scans to make room.
To explicitly clear pending queued scans, run:
sudo /opt/linux-usb-scanner-client/scripts/clear-scan-queue.shThe clear script stops the app and monitor services, asks you to type CLEAR,
deletes pending scan rows from the SQLite queue, vacuums the database so barcode
payloads are removed from the file, and restarts services that were running. It
preserves sent scan metadata by default. Add --include-sent only when you want
to remove all rows from the scans table. Add --yes for non-interactive use.
The installer also starts linux-usb-scanner-client-monitor.service. This is a
separate monitor that checks the health data written by the scanner service and
plays the highest-priority active beep pattern:
5quick beeps every interval when the scanner app service is not running or its heartbeat is stale.3quick beeps every interval when the configured USB scanner is not detected.1quick beep every interval when the Industrial Scanner Logger server is not contactable.
Configure the monitor in /etc/linux-usb-scanner-client.conf:
[alerting]
enabled = true
interval_seconds = 5
backend = auto
server_unavailable_beeps = 1
scanner_unavailable_beeps = 3
app_not_running_beeps = 5Restart the monitor after changing alerting settings:
sudo systemctl restart linux-usb-scanner-client-monitorbackend = auto tries ALSA audio through aplay first, then the Linux beep
command for system-speaker alerts, then a console bell as a final fallback.
Different Debian and Ubuntu installs expose different sound paths: the aplay
backend may require ALSA output to be configured, while the beep backend may
require the beep package and system speaker support. Use enabled = false to
keep the monitor service running without making sound.
If you want alerts to use only the audio card, set backend = aplay. If
backend = beep is selected, the system speaker is used. Some Linux beep
package and driver combinations can make an audible system-speaker beep while
returning a nonzero exit status with no error text; the monitor treats that as a
best-effort beep and logs one warning instead of failing every alert interval.
Both the scanner app service and the alert monitor service use systemd
Restart=always. The alert monitor also disables systemd start-rate limiting so
it keeps coming back after repeated process failures.
Test one pattern:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client monitor --test-beep scanner_unavailableEvaluate monitor state once without beeping:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client monitor --onceAuto-update is installed as a separate root-owned systemd timer so the scanner
service can continue running as the restricted linux-usb-scanner-client user.
The timer runs every 15 minutes, but it does nothing until explicitly enabled in
/etc/linux-usb-scanner-client.conf:
[updates]
enabled = true
repository_url = https://github.com/cosmicc/linux-usb-scanner-client.git
branch = main
service_name = linux-usb-scanner-client.serviceWhen enabled, the updater maintains a Git checkout under
/var/lib/linux-usb-scanner-client/updates/, fetches the configured branch,
reads its pyproject.toml version, and compares it to the installed package
version. If GitHub main has a newer version, it stops the app service, runs
the new branch's scripts/install.sh, and the installer restarts the app.
Security note: enabling auto-update allows a root systemd service to run the install script from the configured repository branch. Only enable it for a repository and branch you control.
Manual update check:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client auto-update --check-only --forceManual forced reinstall from the configured branch, even when versions match:
sudo /opt/linux-usb-scanner-client/venv/bin/linux-usb-scanner-client auto-update --forceRemove only service integration:
sudo scripts/uninstall.shThis preserves config, queued scans, logs, install files, and service identity.
Remove SQLite state, including queued scans, while still preserving config, logs, install files, and service identity:
sudo scripts/uninstall.sh --purgeUse --purge only when queued scans and local state are no longer needed.
/opt/linux-usb-scanner-client, /etc/linux-usb-scanner-client.conf, and
/var/log/linux-usb-scanner-client.log are always left intact.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m pip install -e .
PYTHONPATH=src python -m unittest discover -s testsValidate shell scripts after editing installer behavior:
bash -n scripts/install.sh scripts/uninstall.sh scripts/check-health.sh scripts/restart-services.sh scripts/clear-scan-queue.shRuntime script checks should be run with sudo; non-root execution is expected
to stop immediately before option parsing.