A cross-platform .NET 10 print service that connects to a web application via SignalR and provides remote printing. Automatically detects the operating system and uses Windows Print (PowerShell/WMI) on Windows or CUPS (lpstat, lp) on Linux/macOS.
Web App (BasicWeigh, etc.)
|
+-- SignalR Hub (/scaleHub)
|
+-- Web Print Service (this)
|
+-- SumatraPDF (silent PDF printing)
|
+-- Physical Printer (USB, Network, Shared)
The service needs a silent PDF printing tool to send tickets to printers. It auto-detects in this order:
- SumatraPDF (recommended) — auto-installed via
wingetif not found - PDFtoPrinter.exe — if placed in the service directory
- Fallback — uses
Start-Process -Verb PrintTo(may open a dialog, not recommended for unattended use)
To manually install SumatraPDF:
winget install SumatraPDF.SumatraPDF
Or download from https://www.sumatrapdfreader.org/free-pdf-reader
Web App (BasicWeigh, etc.)
|
+-- SignalR Hub (/scaleHub)
|
+-- Web Print Service (this)
|
+-- CUPS (lpstat, lp, lpoptions)
|
+-- Physical Printer (USB, Network, IPP)
- Cross-Platform — automatically uses Windows Print or CUPS based on the OS
- Windows Integration — uses PowerShell
Get-Printer,Start-Process,Out-Printerto manage and print to local and network printers - CUPS Integration — uses
lpstat,lp,lpoptionsto manage and print to local printers on Linux/macOS - SignalR Connection — connects to any web app's SignalR hub (outbound only, works behind firewalls)
- Printer Discovery — announces available CUPS or Windows printers to the web app on connect/reconnect
- PDF Printing — downloads ticket PDFs from the web app and prints locally
- Test Print — send a test page to any connected printer from the web UI
- Swagger API — local REST API for configuration, testing, and diagnostics
- SQLite Settings — persistent configuration stored locally (no appsettings.json edits needed)
- Forever Retry — exponential backoff, never gives up reconnecting
The installer provisions everything: CUPS, the bundled BIXOLON POS CUPS
driver, a queue named TicketPrinter for the USB BIXOLON SRP-F310II
ticket printer (override with --printer-name / --ppd), the .NET runtime,
and a systemd service. The SignalR ServiceId defaults to the Pi's hostname —
give each print box a unique hostname before installing.
git clone https://github.com/GTMichelli-Dev/web-print-service.git /tmp/wps
bash /tmp/wps/deploy/install.sh http://your-server:5110
rm -rf /tmp/wpsRe-run the same command to update; settings survive. See
deploy/install.sh --help for all options (--printer-name, --service-id,
--rotate, --media-size, ...). After install, assign
<hostname>:TicketPrinter on Foundation's Setup → Printers page.
Download web-print-service-win-x64.zip from
Releases, unzip
it on the PC the printer is attached to, and from an admin prompt in that
folder:
INSTALL.bat https://your-serverSelf-contained — no .NET, no SDK and no git needed on that PC. Full option set and the printing gotchas that bite on Windows are in Install on Windows.
# Windows
cd WebPrintService
dotnet run
# Linux / Raspberry Pi
sudo apt-get install cups
dotnet runReturns service health, print system type, and printer count.
Response:
{
"status": "ok",
"printSystem": "Windows",
"printSystemAvailable": true,
"printerCount": 3,
"printers": [
{ "printerId": "HP LaserJet", "displayName": "HP LaserJet", "status": "idle", "enabled": true, "isDefault": true }
]
}Returns API documentation as JSON including all endpoints, parameters, response formats, and SignalR methods.
List all printers with their status.
Response:
[
{
"printerId": "HP_LaserJet_Pro",
"displayName": "HP LaserJet Pro",
"status": "idle",
"isDefault": true,
"enabled": true
}
]Get detailed status of a specific printer.
Example: GET /api/printers/HP_LaserJet_Pro/status
Response:
{
"printerId": "HP_LaserJet_Pro",
"status": "printer HP_LaserJet_Pro is idle. enabled since Mon 24 Mar 2026 08:00:00 AM CDT"
}Send a test page to a printer. No request body needed.
Example: POST /api/printers/HP_LaserJet_Pro/test
Response:
{
"success": true,
"message": "Sent to HP_LaserJet_Pro"
}Get current service settings.
Response:
{
"id": 1,
"serviceId": "default",
"serverUrl": "http://localhost:5110",
"signalRHub": "/scaleHub"
}Update service settings. Triggers a reconnect to the web app.
Request:
{
"serviceId": "office-printer",
"serverUrl": "http://192.168.1.100:5110"
}Response:
{
"success": true,
"message": "Settings saved. Service restarting..."
}| Direction | Method | Description |
|---|---|---|
| Service -> Hub | JoinPrintGroup(serviceId) |
Join the PrintClients group |
| Service -> Hub | PrintServiceReady(announcement) |
Announce printers on connect |
| Service -> Hub | PrinterListResponse(data) |
Respond to printer list request |
| Service -> Hub | PrintResult(result) |
Report print job result |
| Service -> Hub | TestPrintResult(result) |
Report test print result |
| Hub -> Service | PrintTicket(data) |
Print a ticket PDF |
| Hub -> Service | GetPrinterList |
Request printer list |
| Hub -> Service | TestPrint(printerId) |
Send test page to printer |
| Hub -> Service | ReloadConfig |
Restart the service |
Uses PowerShell commands:
Get-Printer— enumerate all local and network printersStart-Process -Verb PrintTo— print PDFs (or SumatraPDF if installed)Out-Printer— print text filesrundll32 shimgvw.dll— print images- Optional: SumatraPDF for better PDF printing
Uses CUPS command-line tools:
lpstat -p -d— enumerate printers and defaultlp -d <printer>— print fileslpoptions -p <printer>— get printer options/description- CUPS web interface at https://localhost:631
SSH into the Pi and run:
git clone https://github.com/GTMichelli-Dev/web-print-service.git /tmp/wps
bash /tmp/wps/deploy/install.sh https://basicscale.scaledata.net
rm -rf /tmp/wpsWith options:
git clone https://github.com/GTMichelli-Dev/web-print-service.git /tmp/wps
bash /tmp/wps/deploy/install.sh https://basicscale.scaledata.net \
--service-id office --port 5230
rm -rf /tmp/wpsFor private repos, git will prompt for credentials. You can also use a deploy key or GitHub token.
Options:
| Option | Default | Description |
|---|---|---|
--service-id <id> |
default |
Unique ID for this service instance |
--port <port> |
5230 |
API/Swagger port |
--branch <branch> |
master |
Git branch to install |
--install-dir <path> |
/opt/web-print-service |
Install location |
What the script does automatically:
- Detects Pi architecture (arm64, armv7l, x64)
- Installs CUPS (printer system)
- Installs the .NET 10 SDK and runtime permanently (skips download on future updates)
- Downloads latest source from GitHub
- Builds for the Pi's architecture
- Installs to
/opt/web-print-service - Preserves existing database on updates
- Registers and starts as a systemd service
Prerequisites: Just internet access and git (pre-installed on Raspberry Pi OS). No .NET, no CUPS — the script installs everything. The .NET SDK is installed permanently so future updates skip the download.
To update: Run the same command again. The script stops the service, updates files, preserves your database, and restarts.
After install:
- Swagger:
http://<pi-ip>:5230/swagger - CUPS Admin:
http://<pi-ip>:631 - Logs:
sudo journalctl -u web-print-service -f - Restart:
sudo systemctl restart web-print-service
deploy/install.ps1 is the Windows counterpart to install.sh, with
deploy/INSTALL.bat as a double-clickable wrapper so nobody has to touch the
machine's execution policy.
Get the package. Every tagged release ships a prebuilt, self-contained
web-print-service-win-x64.zip — the .NET runtime is bundled, so the target PC
needs no .NET, no SDK and no git. Download it from
Releases and
unzip it on the PC the printer is attached to.
To build the same package by hand — publish into an app folder next to the
scripts:
dotnet publish PiPrintService.csproj -c Release -r win-x64 --self-contained true -o C:\Temp\web-print\app
copy deploy\install.ps1 C:\Temp\web-print\
copy deploy\INSTALL.bat C:\Temp\web-print\
copy deploy\package-README.txt C:\Temp\web-print\README.txtInstall. From an admin prompt in that folder:
INSTALL.bat https://basicscale.scaledata.netOr drive the script directly for the full option set:
powershell -ExecutionPolicy Bypass -File install.ps1 -WebUrl https://basicscale.scaledata.net -ServiceId scalehouse| Option | Default | Description |
|---|---|---|
-WebUrl |
(required) | Base URL of the web app. Must match its real scheme and port. |
-ServiceId |
computer name | Names this PC on the web app's Printers page. |
-Port |
5230 |
Local Swagger / diagnostic API port. |
-InstallDir |
C:\Services\WebPrintService |
Install location. |
-ServiceName |
WebPrintService |
Windows service name. |
-TestPrinter |
(none) | Print a test page to this printer at the end. |
-ResetDb |
off | Start from a clean database. Destroys the ServiceId, ServerUrl and printer settings — a timestamped backup is taken first regardless. |
-SkipUrlCheck |
off | Install even if the SignalR hub probe fails. |
-SkipPdfTool |
off | Don't try to install SumatraPDF when none is found. |
The script is idempotent — re-run it to update. It will:
- Validate the arguments, find the binaries, and probe the web app's
SignalR hub — a redirect or a 404 is reported before anything is
installed, since either leaves the service reconnecting forever. (Negotiate
is a POST; an
http→httpsredirect downgrades it to GET and the hub answers 405 for good.) - Stop the service and wait for it to actually stop (it holds its own
.exe; copying too early fails with a file lock), plus stop any copy left running from a console. - Back up the database to the Desktop, timestamped.
- Copy the binaries, excluding the database and its
-wal/-shmcompanions. - Write
ServerUrland the listen port intoappsettings.json. - Check that a silent PDF printer is installed for all users, installing
SumatraPDF via
winget --scope machineif not — see Silent PDF printing. - Create the service if missing — automatic startup, and configured to restart itself on failure (5s, 15s, then every 60s), since a scale-house PC is rarely watched. An existing service has its path corrected and startup set to automatic.
- Start it, poll
/api/status/healthuntil it answers, and list the printers the service can see — failing loudly rather than reporting success over a dead service. - Apply
ServiceIdandServerUrlthrough the API, then read them back.
Step 9 matters: appsettings.json only seeds the database on the first run, so
on a machine that has been running for months, editing the config file alone
would change nothing.
Manage it afterwards:
Get-Service WebPrintService
Restart-Service WebPrintService
Invoke-RestMethod http://localhost:5230/api/printersTickets are PDFs, and printing one without a dialog needs a helper. The service
looks for PDFtoPrinter.exe in its install folder, then
C:\Program Files\PDFtoPrinter\PDFtoPrinter.exe, then
C:\Program Files\SumatraPDF\SumatraPDF.exe. Without one it falls back to
Start-Process -Verb PrintTo, which wants a desktop — under a service account
there isn't one, so tickets quietly never come out.
All-users installs only. The service runs as LocalSystem, whose
LocalAppData is C:\Windows\System32\config\systemprofile\AppData\Local, so
an ordinary winget install SumatraPDF.SumatraPDF — which installs per-user —
is invisible to it. It then works perfectly when you double-click a PDF and not
at all from the service. The installer asks for --scope machine for exactly
this reason, and says so when that fails.
Fixing it later needs no reinstall: drop PDFtoPrinter.exe into the install
folder, or install SumatraPDF for all users, and the next print finds it.
Printers follow the same rule. A printer added while logged in as yourself
— including a shared \\server\printer — belongs to your profile and is
invisible to LocalSystem. Either add it for all users, or point the service at
that account in services.msc → WebPrintService → Log On. The
installer prints the list of printers it can actually see; an empty list, or
one missing the ticket printer, is this.
Run it in the foreground — for development, or to see an error the service swallows:
cd PiPrintService
dotnet runOr, on a machine where it is already installed:
net stop WebPrintService
"C:\Services\WebPrintService\PiPrintService.exe"Initial config via appsettings.json:
{
"Print": {
"ServerUrl": "https://your-server",
"Port": "5230"
}
}After first run, use the API to update settings (persists to SQLite):
curl -X PUT http://localhost:5230/api/settings \
-H "Content-Type: application/json" \
-d '{"serverUrl": "https://your-server", "serviceId": "office-printer"}'