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) —
deploy\install.ps1downloads the portableSumatraPDF.exeinto<install dir>\tools\if none is already present - PDFtoPrinter.exe — if placed in the service directory
- Fallback — uses
Start-Process -Verb PrintTo(may open a dialog, and does nothing at all under a service account)
To provision it by hand — on a PC with no internet, or where a proxy blocked the
installer's download — grab the 64-bit portable build from
https://www.sumatrapdfreader.org/download-free-pdf-viewer and copy it to
C:\Services\WebPrintService\tools\SumatraPDF.exe. No installer needed, and
the next print picks it up.
winget is deliberately not used: it is absent on Server LTSC and on plenty of
stock Windows builds, and its default per-user install lands where the LocalSystem
service account cannot read it.
Orientation. SumatraPDF is invoked with
-print-settings "disable-auto-rotation". Left to itself it rotates the page 90
degrees to match the printer's default paper orientation, which turns portrait
tickets landscape on any PC whose printer defaults that way — while a PC using the
shell fallback prints the same file portrait. Forcing the authored orientation
makes every PC agree. Changing the printer's orientation in Windows does not fix
it: this is content rotation, not paper orientation.
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.
A printer several companies share serves every company's site. Give each site's URL. On Linux, separate them with spaces or commas; on Windows, put the list in quotes, separated by commas (unquoted, Windows splits it at each comma):
bash /tmp/wps/deploy/install.sh https://agnw.scaledata.net https://crs.scaledata.net
INSTALL.bat "https://agnw.scaledata.net,https://crs.scaledata.net"
powershell -ExecutionPolicy Bypass -File install.ps1 -WebUrl "https://agnw.scaledata.net,https://crs.scaledata.net"They are kept as one comma-separated serverUrl. The service opens one
connection per site, each with its own retries, so a site that is down does not
affect the others. Every site sees the printers as its own and assigns them on
its own Setup → Printers page. A print job fetches its PDF from the site
that sent it.
# 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 each web app in serverUrl.
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 printersSumatraPDF -print-to <printer> -print-settings "disable-auto-rotation" -silent— print PDFs, staged into<install dir>\tools\bydeploy/install.ps1Start-Process -Verb PrintTo— PDF fallback when no silent printer is present (wants a desktop, so it does nothing under a service account)Out-Printer— print text filesrundll32 shimgvw.dll— print images
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 download 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 the service account can read is present,
downloading the portable
SumatraPDF.exeinto<install dir>\tools\if 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 tools\SumatraPDF.exe in its install folder, then
PDFtoPrinter.exe in the same folder, then the machine-wide
C:\Program Files\... copies of either. 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.
Step 5 downloads it, and does not use winget. The installer fetches the
64-bit portable SumatraPDF.exe over HTTPS straight into
<install dir>\tools\. winget was tried first and is the wrong tool here twice
over: plenty of scale-house PCs have no winget at all (Server LTSC, or any box
without the Store's App Installer), and where it exists its default per-user
scope drops the exe into the installing admin's profile — invisible to
LocalSystem, whose LocalAppData is
C:\Windows\System32\config\systemprofile\AppData\Local. That install then
works perfectly when you double-click a PDF and not at all from the service.
A portable exe next to the binaries has neither problem.
If the download is blocked, the installer warns and carries on. Fixing it later
needs no reinstall: copy SumatraPDF.exe into <install dir>\tools\ (or drop
PDFtoPrinter.exe into the install folder) and the next print finds it. Re-running
the installer also does not wipe tools\ — the binary copy leaves it alone.
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"}'serverUrl takes one URL or several, separated by commas, semicolons, spaces
or new lines — see Several sites.