# ═══════════════════════════════════════════════════════════════════
# ZEBRA ZD421 PRINT RELAY — NO ADMIN REQUIRED
# Run on the Windows PC with the USB printer.
#
# HOW TO USE:
# 1) Right-click this file → "Run with PowerShell"
# 2) Windows may ask for firewall permission — click "Allow"
# 3) Leave it running. The WLMT0 scanner sends labels here.
# 4) Press Ctrl+C to stop.
#
# CONFIGURATION: Change $printerName below to match YOUR printer.
# To find the exact name run in PowerShell: Get-Printer | Select Name
# ═══════════════════════════════════════════════════════════════════
# ─── CHANGE THIS to your printer's exact Windows name ───
$printerName = "ZDesigner ZD421-203dpi ZPL"
# ─── Port (must match the URL entered in the Knack module) ───
$port = 3000
# ─── Raw Printer Helper (Win32 API — sends ZPL bytes to USB printer) ───
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class RawPrinterHelper
{
[StructLayout(LayoutKind.Sequential)]
public struct DOCINFOA
{
[MarshalAs(UnmanagedType.LPStr)] public string pDocName;
[MarshalAs(UnmanagedType.LPStr)] public string pOutputFile;
[MarshalAs(UnmanagedType.LPStr)] public string pDataType;
}
[DllImport("winspool.Drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true)]
public static extern bool OpenPrinter(string szPrinter, out IntPtr hPrinter, IntPtr pd);
[DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true)]
public static extern bool ClosePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "StartDocPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true)]
public static extern bool StartDocPrinter(IntPtr hPrinter, int level, [In] ref DOCINFOA di);
[DllImport("winspool.Drv", EntryPoint = "EndDocPrinter", SetLastError = true)]
public static extern bool EndDocPrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "StartPagePrinter", SetLastError = true)]
public static extern bool StartPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "EndPagePrinter", SetLastError = true)]
public static extern bool EndPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "WritePrinter", SetLastError = true)]
public static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, out int dwWritten);
public static bool SendRawData(string printerName, byte[] data)
{
IntPtr hPrinter;
if (!OpenPrinter(printerName, out hPrinter, IntPtr.Zero)) return false;
DOCINFOA di = new DOCINFOA();
di.pDocName = "ZPL Label";
di.pDataType = "RAW";
if (!StartDocPrinter(hPrinter, 1, ref di)) { ClosePrinter(hPrinter); return false; }
if (!StartPagePrinter(hPrinter)) { EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return false; }
IntPtr pBytes = Marshal.AllocCoTaskMem(data.Length);
Marshal.Copy(data, 0, pBytes, data.Length);
int written;
bool ok = WritePrinter(hPrinter, pBytes, data.Length, out written);
Marshal.FreeCoTaskMem(pBytes);
EndPagePrinter(hPrinter);
EndDocPrinter(hPrinter);
ClosePrinter(hPrinter);
return ok;
}
}
"@
# ─── Helper: build an HTTP response string ───
function Send-Response ($client, [int]$statusCode, [string]$statusText, [string]$body) {
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$header = "HTTP/1.1 $statusCode $statusText`r`n"
$header += "Access-Control-Allow-Origin: *`r`n"
$header += "Access-Control-Allow-Methods: POST, GET, OPTIONS`r`n"
$header += "Access-Control-Allow-Headers: Content-Type`r`n"
$header += "Content-Type: text/plain; charset=utf-8`r`n"
$header += "Content-Length: $($bodyBytes.Length)`r`n"
$header += "Connection: close`r`n"
$header += "`r`n"
$headerBytes = [System.Text.Encoding]::UTF8.GetBytes($header)
$stream = $client.GetStream()
$stream.Write($headerBytes, 0, $headerBytes.Length)
if ($bodyBytes.Length -gt 0) { $stream.Write($bodyBytes, 0, $bodyBytes.Length) }
$stream.Flush()
$client.Close()
}
# ─── Start TCP Listener (does NOT require admin) ───
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Any, $port)
$listener.Start()
} catch {
Write-Host ""
Write-Host "ERROR: Could not bind to port $port." -ForegroundColor Red
Write-Host "Another program may be using that port. Try changing `$port above." -ForegroundColor Yellow
Write-Host ""
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "=====================================================" -ForegroundColor Green
Write-Host " ZEBRA ZD421 PRINT RELAY RUNNING" -ForegroundColor Green
Write-Host " ** NO ADMIN REQUIRED **" -ForegroundColor Green
Write-Host "=====================================================" -ForegroundColor Green
Write-Host ""
Write-Host " Listening on port : $port"
Write-Host " Printer : $printerName"
Write-Host ""
# Show local IP addresses so user knows what to enter in the module
$ips = Get-NetIPAddress -AddressFamily IPv4 |
Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.PrefixOrigin -ne "WellKnown" } |
Select-Object -ExpandProperty IPAddress
foreach ($ip in $ips) {
Write-Host " URL for module : http://${ip}:${port}/print" -ForegroundColor Cyan
}
Write-Host ""
Write-Host " Enter this URL in the Knack module Printer Settings."
Write-Host " Press Ctrl+C to stop the relay."
Write-Host ""
# ─── Main Loop ───
try {
while ($true) {
$client = $listener.AcceptTcpClient()
$stream = $client.GetStream()
$stream.ReadTimeout = 5000
# Read the full HTTP request
$buffer = New-Object byte[] 65536
$total = 0
try {
do {
$n = $stream.Read($buffer, $total, $buffer.Length - $total)
$total += $n
} while ($stream.DataAvailable -and $total -lt $buffer.Length)
} catch {}
if ($total -eq 0) { $client.Close(); continue }
$raw = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $total)
# Parse first line: METHOD /path HTTP/1.x
$firstLine = ($raw -split "`r`n")[0]
$parts = $firstLine -split ' '
$method = if ($parts.Length -ge 1) { $parts[0].ToUpper() } else { "" }
# OPTIONS → CORS preflight
if ($method -eq "OPTIONS") {
Send-Response $client 204 "No Content" ""
continue
}
# GET → health check
if ($method -eq "GET") {
Send-Response $client 200 "OK" "RELAY_OK|$printerName"
continue
}
# POST → print ZPL
if ($method -eq "POST") {
# Extract body (after the blank line)
$bodyStart = $raw.IndexOf("`r`n`r`n")
$zpl = ""
if ($bodyStart -ge 0) { $zpl = $raw.Substring($bodyStart + 4) }
if ([string]::IsNullOrWhiteSpace($zpl)) {
Send-Response $client 400 "Bad Request" "EMPTY_PAYLOAD"
continue
}
$zplBytes = [System.Text.Encoding]::UTF8.GetBytes($zpl)
$ok = [RawPrinterHelper]::SendRawData($printerName, $zplBytes)
$ts = Get-Date -Format "HH:mm:ss"
if ($ok) {
Write-Host " $ts | OK | $($zplBytes.Length) bytes sent to printer" -ForegroundColor Green
Send-Response $client 200 "OK" "OK"
} else {
Write-Host " $ts | FAIL | Could not send to '$printerName'" -ForegroundColor Red
Send-Response $client 500 "Server Error" "PRINT_FAILED"
}
continue
}
# Anything else
Send-Response $client 405 "Method Not Allowed" "METHOD_NOT_ALLOWED"
}
} finally {
$listener.Stop()
}0 views