sercomm-printserver is a command-line tool and Python library for the SerComm family of
network print servers: the SerComm PS301 and the same hardware sold by TRENDnet, Perle, Netgear
and Linksys (one to three parallel ports, on some models a serial or USB port). It finds
them on the network, reads their status and configuration, changes settings, restarts them,
prints to them over LPD or raw sockets, and recovers a device whose address or password is
lost.
These devices are managed over FTP through "virtual files", and some of those files are actions:
reading RESET restarts the device, reading DEFAULTC wipes its settings. The tool never touches
them by accident. Anything that changes the device needs --yes, and device-config set /
import show the changes first. factory-default, password clear, store-ip and
arp factory-reset also need the device's name (--confirm-name). Every device command can
answer in JSON, so scripts get the same information people do.
What is known about the devices, models and firmware versions is in docs/Manual-PS301.md (German) and docs/config-keys.md.
The CLI is built on rich-click, lib_layered_config (defaults, app, host and user files, .env
and environment), lib_log_rich and lib_cli_exit_tools. Static ARP entries for the address
recovery are written natively through ipscout.
- The project targets Python 3.10 and newer.
- Runtime dependencies are declared with >= floors in pyproject.toml. Dev dependencies (pytest, ruff, pyright, bandit, etc.) are declared the same way.
- CI tests CPython 3.10 through 3.15 (taken from the pyproject classifiers) on
ubuntu-latestandwindows-latest, and the newest of them onmacos-latest.
uv is an ultrafast Python package manager written in Rust (10-20x faster than pip/poetry).
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"uvx sercomm_printserver@latest --help# One-time install from PyPI (isolated environment, added to PATH)
uv tool install sercomm-printserver
# or from the git repository
uv tool install --from "git+https://github.com/bitranox/sercomm-printserver.git" sercomm-printserver
# Pick the interpreter explicitly if needed, e.g. --python 3.14
# Update (requires network)
uv tool upgrade sercomm-printserver
# Run
sercomm-printserver --helpuv venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
uv pip install sercomm_printserverFor alternative install paths (pip, pipx, source builds, etc.), see
INSTALL.md. All supported methods register both the
sercomm_printserver and sercomm-printserver commands on your PATH.
This repository commits a uv.lock. It pins every package the project needs, transitive
dependencies included, to an exact version and to the hashes of that version's files. The
exclude-newer setting under [tool.uv] in pyproject.toml adds a cooldown: uv leaves any
release younger than that out of the lock. CI installs from the lock, so a hijacked release that
gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
match is refused.
That protection stays in this repository. The installable package declares only lower bounds
(>=) for its dependencies, so a plain pip install takes the newest release of every transitive
dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
hijacked version. pipx install and uv tool install without options do the same.
To install the command line tool with a cooldown of your own (uv then skips any release younger than 7 days, transitive ones included), or to run the exact tree our CI tests:
uv tool install --exclude-newer "7 days" \
--exclude-newer-package sercomm-printserver=false \
--exclude-newer-package ipscout=false \
--exclude-newer-package lib-log-rich=false \
--exclude-newer-package lib-layered-config=false \
--exclude-newer-package lib-cli-exit-tools=false \
sercomm-printserver
git clone https://github.com/bitranox/sercomm-printserver.git && cd sercomm-printserver && uv sync --locked && uv run sercomm_printserver --helpIf you depend on sercomm_printserver in your own project, give that project the same cooldown (7 days here, as
an example) and commit the uv.lock that uv add sercomm_printserver writes:
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = {sercomm-printserver = false, ipscout = false, lib-log-rich = false, lib-layered-config = false, lib-cli-exit-tools = false}The exemptions cover this package and the libraries it shares an author and a release process with. Without them, a cooldown leaves out every release of this tool for its first week, and a release whose floor names a young library version cannot be installed at all (uv answers "No solution found"). Every other dependency keeps the cooldown.
See CONFIG.md for detailed documentation on the layered configuration system, including precedence rules, profile support, and customization best practices.
uv tool install sercomm-printserver
sercomm-printserver discover # find print servers on this host's networks
sercomm-printserver device-info --host 192.0.2.52 # name, MAC, firmware, addresses
sercomm-printserver status --host 192.0.2.52 # each port and its printer
sercomm-printserver print lpd L1 page.prn --host 192.0.2.52Run sercomm-printserver <command> --help for every option.
sercomm-printserver discover [--network 192.0.2.0/24] # reads the FTP greeting only, never logs in
sercomm-printserver device-info # PSINF: names, MAC, firmware, addresses
sercomm-printserver firmware # firmware version, no login needed
sercomm-printserver status # port state and printer status
sercomm-printserver check # wedged ports (see "A wedged port"); exit 3 on one
sercomm-printserver queue L2 [--long] # LPD queue state (default L1); sends no print dataWithout --network, discover searches this host's networks up to 4096 addresses in all and
lists the networks beyond that budget under skipped. Named networks (--network can be given
more than once) that hold more than 4096 host addresses together are refused (exit 22) before
anything is probed. --timeout is the wait for each host's greeting (default 3 s).
sercomm-printserver device-config show # the whole CONFIG, typed
sercomm-printserver device-config get IP_ADDR # one value, by token or line number
sercomm-printserver device-config set DHCP_MODE=Disable IP_ADDR=192.0.2.60 # shows the diff
sercomm-printserver device-config set DHCP_MODE=Disable IP_ADDR=192.0.2.60 --yes # writes it
sercomm-printserver device-config export backup.cfg
sercomm-printserver device-config import backup.cfg --yesA write restarts the device and waits until it answers again, at its new address when the
change moved it. The wait reads the device's SNMP uptime (community public): a device whose
SNMP agent does not answer fails after 120 s with exit 69, even though the change was applied.
--no-apply stores the change without the restart; it takes effect at the next reset --yes.
reset --no-wait returns without waiting.
device-config import takes a file as device-config export wrote it. A CONFIG is about 2 KB, so
a file larger than 64 KiB is refused (exit 22) without being read whole.
sercomm-printserver reset --yes
sercomm-printserver password set --yes # hidden prompt, or --from-file FILE
sercomm-printserver password clear --yes --confirm-name PS_EXAMPLE
sercomm-printserver store-ip --yes --confirm-name PS_EXAMPLE
sercomm-printserver factory-default --yes --confirm-name PS_EXAMPLEpassword set never takes the password on the command line: it asks with a hidden prompt, or
reads the first line of --from-file (only that line is read; a line ends at \n, \r\n or
\r). The device stores at most 19 printable ASCII characters; anything else is refused
(exit 22).
After factory-default the device restarts with its stored address set to 0.0.0.0, DHCP
switched on and its name empty. It answers wherever a DHCP server puts it (measured: the old
address again, so the address alone does not show the reset happened). Without a DHCP server it
has no usable address: bring it back with arp assign-ip. Its login is then the default name
SC plus the last six hex digits of its MAC, and --confirm-name takes only that name. Export
its configuration first (device-config export) if you want to restore it afterwards.
A password does not protect these devices: the firmware lets anyone on the network clear it without logging in. Keep them on a network you trust.
sercomm-printserver print lpd L1 page.prn # LPD queue L1..L8
cat page.prn | sercomm-printserver print lpd L2 -
sercomm-printserver print raw 1 page.prn # RAW port of logical printer 1Only L1..L3 have a RAW port, and RAW data goes wherever that logical printer is routed, just like
an LPD job. Data that no printer takes stays queued and keeps the port busy until a restart
(reset --yes), so never send a test job, LPD or RAW, to a port without a printer. An LPD job
there fails after --timeout with exit 76, because the device holds back its acknowledgement.
print lpd names the job job unless --job-name says otherwise. An empty job is refused
(exit 22) before the device is contacted.
Both commands stream the data: a job of any size is read and sent in pieces of at most 64 KiB, so
it never sits in memory whole. LPD puts the size in front of the data, so print lpd from a pipe first
copies the data to a temporary file (in TMPDIR); when that fails, for example on a full disk,
it exits 74 before the device is contacted. A file that grows or shrinks while print lpd sends
it stops the job without its end-of-file byte and exits 65: an LPD server discards such a job, but
this device prints data as it arrives, so part of it may be on paper. The reported size is the
number of bytes sent, also for a pipe.
print raw succeeds as soon as the device took the bytes, whatever the port is doing: a port held
by another job accepts a new one, and a job that blocks the port afterwards goes unnoticed. With
--verify it waits for the port the printer is routed to to read Idle, sends, and waits again,
each for up to --verify-timeout seconds (default 30; raise it for a long job;
--verify-timeout without --verify is a usage error). A port still busy
before the send exits 75 with nothing sent, so sending later is safe. After the send it exits 110:
the data was sent, so do not resend it blindly. Idle means the port is free again, not that a
page came out.
sercomm-printserver print raw 3 label.dpl --verifyprint raw reads the device's configuration first (one FTP session). A printer on the serial
port gets its data at the port's baud rate, and a RAW connection opened before the device has
passed the previous job on loses its data and wedges the port. So for a printer routed to SP the
connection stays open until the job has crossed the serial line (bytes / (baud / bits per frame),
plus 0.25 s), about 2.4 s for 2 KB at 9600 8N1. A printer on a parallel or USB port gets no wait.
A port can be left reading Printing, occupied by Direct Socket, 0 bytes serviced, with no
client connected: a RAW connection that lost its data (another program opened one too early, or a
job was sent while the device still passed the previous one on). Every later job to it is
accepted and lost. status cannot tell this from a client that holds the port, since an open
connection reads the same. check can: it makes a bare connect, with no data, to each RAW port
routed there. A wedged port accepts it, a held one refuses it. A port counts as wedged only if
every such connect got in, so a live job is never reported as one. A port that reads like a wedge
but has no RAW port routed to it (only L1..L3 have one) cannot be probed and is reported
unprobed; it does not count as wedged.
sercomm-printserver check # idle / printing / held / wedged / unprobed per port; exit 3 on a wedge
sercomm-printserver check --reset --yes # restart only if a port is wedged, then check againOnly a restart clears a wedge, and it restarts every port, so --reset acts only when a port is
wedged and exits 0 only once the check after the restart finds none. --reset without --yes is
refused (exit 77). With --json, a found wedge gives an envelope with "ok":false and the full
answer under data. print raw --verify names a
wedge, or another client, in its exit 75 and 110 messages. Not found by check: a RAW port that
refuses every connect while status reads it idle (print raw reports that one when it cannot
connect), and an idle port whose printer does not answer.
The device answers any address this host maps to its MAC, which is how the vendor manuals
configure a new unit ("ARP trick"). arp borrows a free address in this host's subnet for the
call and removes the ARP entry afterwards. It needs root on Linux (or both CAP_NET_ADMIN, to
write the ARP entry, and CAP_NET_RAW, to check by an ARP request that the address is free), root
on macOS, or Administrator on Windows. An address another machine holds, or one this host cannot
check, is refused before anything is written (exit 22 and 13). --interface picks the network
interface; assign-ip keeps the device's netmask and gateway unless --netmask / --gateway
are given, and always switches DHCP off.
sudo sercomm-printserver arp reach --mac 00:c0:02:12:34:56 --ip 192.0.2.77
sudo sercomm-printserver arp assign-ip --mac 00:c0:02:12:34:56 --ip 192.0.2.77 --yes
sudo sercomm-printserver arp factory-reset --mac 00:c0:02:12:34:56 --ip 192.0.2.77 --yes --confirm-name SC123456Every device command takes --json (indented) or --json-bare (one line). stdout then carries
only the envelope; anything meant for a person goes to stderr. Success and failure look like this:
{"ok":true,"command":"firmware","data":{"host":"192.0.2.52","firmware":"6022"},"skipped":[]}
{"ok":false,"command":"firmware","error":"ConfigurationError","message":"no device given: pass --host or set [device] host in the configuration"}A usage error (an unknown option, print raw 4) is reported by the command-line parser before
any JSON is written: exit 2 and a usage message on stderr. Exit codes do not depend on the format.
| Exit code | Meaning |
|---|---|
| 0 | done |
| 1 | any other failure, such as an ARP table this host could not change |
| 2 | usage error: a missing or invalid option or argument |
| 3 | check: a port is wedged (with --reset: still wedged after the restart) |
| 13 | this host lacks a right (arp: the ARP table or sending an ARP request; config-deploy: a system dir) |
| 22 | invalid argument, such as an empty print job, a password the device cannot store or a CONFIG file over 64 KiB |
| 65 | print lpd: the file changed size while it was sent; do not resend blindly |
| 69 | the device did not answer, or did not come back after a restart; no print data reached it, so a resend is safe |
| 74 | print lpd: a pipe could not be spooled to TMPDIR, nothing was sent |
| 75 | print raw --verify: the port stayed busy before the send, nothing was sent |
| 76 | the device answered something this firmware family does not send, or stored a value other than the one sent (rare: run the command again) |
| 77 | refused: confirmation missing, the device wants a login, or a wrong password |
| 78 | configuration error, such as no device address |
| 110 | print raw: the data was sent and then the connection was lost, or (--verify) the port was not read as idle; do not resend blindly |
from sercomm_printserver import DeviceEndpoint, PrintJob, open_print_server
server = open_print_server(DeviceEndpoint(host="192.0.2.52"))
server.print_lpd("L1", PrintJob(data=b"hello\r\n\f", name="greeting"))
print(server.info().firmware_version)A big job is streamed from where it lies instead of being read into memory: PrintData.from_path
reads a file (re-opened on every send), PrintData.from_stream an open binary stream such as
sys.stdin.buffer (readable once). PrintJob, print_raw and print_raw_verified take a
PrintData or plain bytes, and print_raw and print_lpd return the number of bytes sent.
from sercomm_printserver import PrintData
sent = server.print_raw(1, PrintData.from_path("manual.pcl"))
server.print_lpd("L2", PrintJob(data=PrintData.from_path("report.ps"), name="report"))PrintServer.check() and reset_if_wedged() are the library side of check and
check --reset --yes. discover_print_servers(), PrintServer.read_config(), write_config()
and reset(), and arp_trick with IpscoutNeighborTable cover the rest. The
Python API reference describes every public name, its arguments and the
exceptions it raises.
sercomm-printserver config # the merged configuration
sercomm-printserver config --section device
sercomm-printserver config-deploy --target user # Linux: ~/.config/sercomm-printserver/config.toml
sercomm-printserver --set device.host=192.0.2.52 status
sercomm-printserver config-generate-examples --destination ./examples # one placeholder file per layer
sercomm-printserver info # package metadata (name, version, ...)See CONFIG.md for the precedence, profiles and every key.
The repository is also a Claude Code plugin with a skill for operating these devices:
/plugin marketplace add bitranox/sercomm-printserver
/plugin install sercomm-printserver@sercomm-printserver
The [device] section names the print server the CLI will talk to. It ships empty; set it in
a configuration file, a .env file or the environment:
[device]
host = "192.0.2.52" # IPv4 address or host name of the print server
login = "" # FTP login; empty means "ask the device for its name"
password = "" # FTP password; redacted in `config` output
timeout = 10.0 # seconds per network operation
raw_port_base = 9100 # RAW (direct socket) port of logical printer L1export SERCOMM_PRINTSERVER___DEVICE__HOST=192.0.2.52
sercomm-printserver config --section deviceSee CONFIG.md for every key and its environment variable.
- Install Guide
- Development Handbook
- Contributor Guide
- Configuration
- Python API - the library reference
- Changelog
- Module Reference
- Device notes (German) and CONFIG keys
- License