Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/windows-printer-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Windows printer recovery tests
on:
pull_request:
paths:
- 'src/windows-printer-*.js'
- 'src/windows-printer-*.ps1'
- 'src/windows-print-queue.js'
- 'src/hardware-manager.html'
- 'tests/windows-printer-discovery.test.js'
- 'tests/windows-print-queue.test.js'
- 'tests/kot-multi-printer-recovery.test.js'
- '.github/workflows/windows-printer-tests.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
printer-recovery:
name: Windows USB discovery and queue recovery
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts
# Synthetic device records and transport only; no printing or power changes.
- run: node --test tests/windows-printer-discovery.test.js tests/windows-print-queue.test.js tests/kot-multi-printer-recovery.test.js
45 changes: 43 additions & 2 deletions docs/WINDOWS-PRINTER-RECOVERY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pooling is refused because it can route a ticket to another device.
USB presence is checked with Windows PnP, mapping USBPRINT's device-specific
`PortName` to the configured queue. A persistent SWD PrintQueue instance, or
another printer with the same driver, does not prove physical presence.
Unknown mappings stop safely and ask for an explicit physical instance ID.
Only currently present matches are eligible. Unknown mappings and enumeration failures wait safely for another check; ambiguous or stale bindings remain distinct and never choose another device automatically.

The helper attempts to clear `WorkOffline` only when the matching device is
present. It uses the current user's permissions. If Windows refuses this
Expand Down Expand Up @@ -86,7 +86,8 @@ Idle keep-alive is enabled by default for configured USB ESC/POS queues,
and can be disabled in Hardware Manager. After at least 30 seconds quiet,
the same RAW writer sends only `ESC @` (1B 40), without feed, text, or cut.
It requires verified USB presence, matching port/device, an empty Windows
queue, and both printer statuses explicitly Idle. Busy or unknown status
queue, and primary printer status explicitly Idle. Extended Idle (3) or Unknown (2)
is accepted for drivers such as POS-80C; unknown primary or busy status
is not permission to reset a printer's buffer. Initialization disabled also
disables keep-alive. This is not a bidirectional status acknowledgement.

Expand Down Expand Up @@ -123,3 +124,43 @@ arrive once; the queue's “Sent to printer” remains a Windows observation.

Windows references: [Win32_Printer properties](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-printer)
and [Get-PnpDevice](https://learn.microsoft.com/en-us/powershell/module/pnpdevice/get-pnpdevice).

## Discovery correction and old job recovery

The health probe reads the configured queue and matches its port to currently present physical USBPRINT devices. Historical registry entries are not evidence of presence. A single present match is usable; multiple present matches, enumeration failures and missing mappings remain distinct. A saved physical instance ID never silently changes to another device. Expected ports remain frozen per job.

The queue retries temporary discovery failures with its existing 2/5/10-second delays, then waits for recovery. Startup, background polling and repeated enqueue can migrate only the exact old USB identification failure when the durable record explicitly says it was never submitted and has no accepted, observed or spooler-ID evidence. The original ID and bytes are retained. Submitted or uncertain jobs are never replayed because an empty spooler does not prove non-delivery.

## What the shopkeeper sees

Open **Hardware Manager → Receipt Printer → Windows printer health & recovery**. Check the printer queue, expected port, physical identity, current discovery result and job status. A stale binding needs the physical printer and port checked before saving a new binding; existing pending jobs retain their original destination.

The same section audits the current Windows power plan, read-only, at most once per minute. Enabled or unavailable settings display instructions:

- For a dedicated counter, keep mains power connected. In **Control Panel → Power Options → Change plan settings**, set **Put the computer to sleep** to **Never** while serving.
- Under **Change advanced power settings → Sleep**, check **Hibernate after**. Battery settings have a battery-life tradeoff.
- For USB disconnections after idle, temporarily test **USB settings → USB selective suspend → Disabled**. Restore the setting if it does not improve the fault. Microsoft normally recommends selective suspend remain enabled; this is an isolation step, not a universal requirement.
- If necessary, inspect the printer's USB hub in **Device Manager → Properties → Power Management**. Where available, temporarily clear **Allow the computer to turn off this device to save power**, and restore it if ineffective. The app cannot verify every hub/firmware setting or organizational policy.

No power plan, driver, port, Windows security setting or spooler service is changed by this audit. Screen lock is different from system sleep. The app's existing power blocker does not prove USB hardware stays connected.

## Remove the diagnostic helper before testing

The separate `posnic-printer-keepalive.ps1` in the diagnostic handoff is not part of the product. Stop that helper and remove its shortcut from the Windows Startup folder (`shell:startup`) on the affected computer. Do not stop all PowerShell processes or delete unrelated shortcuts. Hardware Manager warns when the helper is detected, and optional app idle commands are suppressed while it is running or the audit is unavailable. This patch does not remotely remove diagnostic scripts.

Idle ESC/POS initialization is serialized with the app's print jobs, requires a quiet interval and an empty Windows queue, and accepts the observed POS-80C combination of primary Idle (3) and extended Unknown (2). Unknown primary status still blocks it. ESC @ resets printer state; it is not a documented universal wake command and an empty spooler does not establish that every external writer or physical buffer is idle. Disable idle initialization if unsupported. No paper feed/cut is used for initialization.

## Verification on the affected counter

Do not replay diagnostic job metadata or actual orders. Keep a record of the selected reception/kitchen queues and their existing ports. After removing the separate helper, use a clearly labelled test ticket:

1. Print once to each destination and verify the physical destination and formatting/cut.
2. Leave the counter idle for its usual failure interval. Record discovery status and power settings, then print a fresh test ticket.
3. Disconnect/reconnect the kitchen USB connection with one new test job pending. Verify one physical copy after reconnect and no reception reroute.
4. Repeat the same idle interval with the kitchen printer on a short direct USB cable, then compare with the extender. Record USB arrival/removal times, power and cable/extender model. A repeated physical disappearance despite the software fixes needs this controlled hardware test.

“Sent to printer” means Windows accepted/released the job, not proof of paper output. If the outcome is uncertain, inspect the paper and queue before explicitly requesting a duplicate.

Automated tests use synthetic jobs and mock transports; Windows-only tests execute the actual PowerShell resolver with synthetic device records. They cannot establish that an extender or printer remains physically connected indefinitely.

References: [Microsoft powercfg options](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/powercfg-command-line-options), [USB selective suspend](https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/usb-selective-suspend).
9 changes: 7 additions & 2 deletions src/hardware-ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,14 @@ function setupHardwareIPC(hardwareManager, kotManager, billManager) {
});

// Printer Handlers
ipcMain.handle('printer:recovery-status', () => {
ipcMain.handle('printer:recovery-status', async () => {
const queue = hardwareManager.getWindowsPrintQueue();
return queue ? { supported: true, bindings: queue.bindings,
let systemSettings;
if (queue) {
try { systemSettings = await queue.transport.systemSettings(); }
catch (_) { systemSettings = { guidance: ['Windows power settings could not be checked. Review Sleep, Hibernate and USB selective suspend manually in Power Options.'] }; }
}
return queue ? { supported: true, systemSettings, bindings: queue.bindings,
health: [...queue.health.values()].map(item => item.value), keepAlive: queue.keepAlive, jobs: queue.list() } : { supported: false };
});
ipcMain.handle('printer:recovery-configure', async (_event, printer, binding) => {
Expand Down
17 changes: 14 additions & 3 deletions src/hardware-manager.html
Original file line number Diff line number Diff line change
Expand Up @@ -394,11 +394,16 @@ <h1>🔧 Hardware Manager</h1>
<label>Expected port <input id="wrPort" placeholder="Detected on first use" maxlength="128"></label>
<label>Physical USB instance ID (optional) <input id="wrPnp" placeholder="USBPRINT\…" maxlength="512"></label>
<label><input type="checkbox" id="wrInitialize" checked> Initialize ESC/POS before sending</label>
<label><input type="checkbox" id="wrKeepAlive" checked> Keep USB ESC/POS printer awake while Posnic is running</label>
<p>Sends a paperless initialization after 30 seconds idle. No paper feed or cut. Requires ESC/POS initialization above. Pauses while printing or disconnected.</p>
<label><input type="checkbox" id="wrKeepAlive" checked> Send idle ESC/POS initialization (supported printers only)</label>
<p>Sends ESC @ after 30 seconds idle, without feed or cut. This resets printer state and is not a guaranteed wake command. Enable only if your printer supports it. Pauses during app printing or disconnection. Do not run the separate diagnostic keep-alive helper alongside it.</p>
<button type="button" onclick="wrSave()">Save recovery settings</button>
<p>Changing these settings affects new jobs only. Pending jobs retain their original printer and port. No ports are swapped automatically.</p>
<div id="wrMessage" role="status"></div>
<h4>Windows settings for reliable printing</h4>
<div id="wrPower" role="status"></div>
<p>Recommended for a dedicated counter: keep it plugged in and prevent sleep/hibernate while serving. Battery settings affect battery life. These checks do not change Windows settings.</p>
<p>If a USB printer still disappears: in Device Manager, inspect its USB hub → Properties → Power Management, where available, and temporarily test with “Allow the computer to turn off this device to save power” cleared. Restore it if there is no improvement. Posnic cannot verify every hub or printer firmware setting.</p>
<p>For an extender connection, test the same printer with a short direct USB cable across the usual idle period. Check extender power and cables. Initialization cannot restore a device that Windows cannot see.</p>
<pre id="wrHealth" style="white-space:pre-wrap;"></pre>
<pre id="wrKeepAliveStatus" style="white-space:pre-wrap;"></pre>
<div id="wrJobs" role="status" style="max-height:240px;overflow:auto;"></div>
Expand Down Expand Up @@ -2141,10 +2146,16 @@ <h3 style="margin-top:0;">Hear the whole thing</h3>
if (!status.supported) return;
document.getElementById('windowsRecovery').style.display = 'block';
wrBindings = status.bindings || {};
const power = document.getElementById('wrPower');
power.replaceChildren();
const messages = status.systemSettings?.guidance || ['Power settings have not been checked.'];
(messages.length ? messages : ['Checked Sleep, Hibernate and USB selective suspend: all disabled in the current power plan.']).forEach(message => {
const row = document.createElement('p'); row.textContent = message; power.appendChild(row);
});
document.getElementById('wrKeepAliveStatus').textContent = Object.values(status.keepAlive || {}).map(p =>
p.printer + ' | ' + p.status + ' | ' + new Date(p.at).toLocaleTimeString()).join('\n');
document.getElementById('wrHealth').textContent = (status.health || []).map(p =>
p.printer + ' | ' + p.port + ' | USB present: ' + p.present + ' | WorkOffline: ' + p.workOffline + ' | Windows status: ' + p.printerStatus).join('\n');
p.printer + ' | ' + p.port + ' | USB: ' + (p.discovery || p.present) + ' | WorkOffline: ' + p.workOffline + ' | Windows status: ' + p.printerStatus).join('\n');
const list = document.getElementById('wrJobs');
list.replaceChildren();
(status.jobs || []).slice().reverse().forEach(job => {
Expand Down
Loading
Loading