Docker for Hardware Simulation in Dev/CI: Emulating Kiosks, Scanners, and Printers to Reproduce Angular 20+ Defects Fast

Docker for Hardware Simulation in Dev/CI: Emulating Kiosks, Scanners, and Printers to Reproduce Angular 20+ Defects Fast

Spin up a repeatable device lab in Docker—barcode scanner WebSockets, ESC/POS printer mocks, and kiosk states—wired into Nx + Cypress in minutes.

Make hardware boring. When a kiosk or scanner is “just another container,” bugs stop hiding in the field and start failing fast in CI.
Back to all posts

I’ve shipped airport kiosks that had to keep printing boarding passes when Wi‑Fi hiccuped, and retail scanners that swallowed barcodes faster than the UI could paint. The common thread: defects were “field only.” The fix: Dockerized device labs I could spin up locally or in CI to reproduce issues in minutes.

This is a focused playbook for Angular 20+ teams. We’ll emulate scanners over WebSockets, printers over HTTP/IPP with ESC/POS output, and kiosk device states, then wire all of it into an Nx + Cypress + GitHub Actions pipeline. Along the way, we’ll use Signals + SignalStore to make device state explicit and testable.

The airport kiosk that wouldn’t print—and why Docker solved it

A real outage, reproduced in 6 minutes

For a major airline, a kiosk intermittently “printed” without actually cutting paper. In the field, logs were noisy and time-limited. I stood up a Docker printer emulator that dropped ACKs 1 in 10 times. In six minutes, we reproduced it, fixed the ack/timeout logic, and added a deterministic Cypress spec.

  • Printer accepted jobs but never cut paper

  • UI showed “Printed” without verifying device acks

  • Wi‑Fi packet loss masked error handling

Why now matters (2025 roadmaps)

If you plan to upgrade to Angular 20+ (or 21 soon), flaky device flows will block you. A Docker device lab makes upgrades and incident response safer. If you need an Angular consultant to set this up, I’m available for remote engagements.

  • Q1 is hiring season for Angular talent

  • Angular 21 beta is near; CI must be green to upgrade safely

Why Docker hardware simulation matters for Angular 20+ teams

Measurable outcomes I’ve seen

When your “hardware” is a container with log history and togglable behaviors, every bug becomes reproducible. You can also stress test: burst 50 scans/second, inject 3% packet loss, or delay print acks by 2 seconds.

  • 10x faster defect reproduction (hours → minutes)

  • 40–60% drop in flaky E2E tests

  • Fewer hotfixes; easier canary rollbacks

Enterprise constraints it solves

We avoid privileged runners and USB/IP hacks by standardizing on WS/HTTP interfaces that mirror production gateway services. You get portability and deterministic builds.

  • No USB passthrough needed in CI

  • Works on GitHub Actions, Jenkins, and Azure DevOps

  • Repeatable across Nx apps and preview stacks

Reference architecture: device simulators, Angular adapter, and CI harness

// device.store.ts — SignalStore for device state (Angular 20, @ngrx/signals)
import { Injectable, inject } from '@angular/core';
import { SignalStore, withState, withMethods } from '@ngrx/signals';
import { toSignal } from '@angular/core/rxjs-interop';
import { webSocket } from 'rxjs/webSocket';
import { retryBackoff } from 'backoff-rxjs';

interface DeviceState {
  scannerConnected: boolean;
  lastScan?: { value: string; ts: number };
  printerConnected: boolean;
  printQueue: number;
  errors: string[];
}

@Injectable({ providedIn: 'root' })
export class DeviceStore extends SignalStore(
  withState<DeviceState>({ scannerConnected: false, printerConnected: false, printQueue: 0, errors: [] }),
  withMethods((store) => {
    const scanner$ = webSocket<any>({ url: 'ws://localhost:9001' }).pipe(
      retryBackoff({ initialInterval: 500, maxInterval: 5000, randomizationFactor: 0.25 })
    );

    const msgs = toSignal(scanner$, { initialValue: null });

    function connectScanner() {
      const m = msgs();
      if (!m) return;
      if (m.type === 'connected') store.patch({ scannerConnected: true });
      if (m.type === 'scan') store.patch({ lastScan: m.payload });
      if (m.type === 'error') store.patch({ errors: [...store.state().errors, m.message] });
    }

    async function print(payload: Blob) {
      const res = await fetch('http://localhost:9100/print', { method: 'POST', body: payload });
      const json = await res.json();
      if (json.status === 'queued') store.patch({ printQueue: store.state().printQueue + 1 });
    }

    return { connectScanner, print };
  })
) {}

# simulators/scanner/scanner.js — WS + REST for deterministic tests
const { WebSocketServer } = require('ws');
const express = require('express');
const app = express();
app.use(express.json());

const wss = new WebSocketServer({ port: 9001 });
let sockets = [];
wss.on('connection', ws => {
  sockets.push(ws);
  ws.send(JSON.stringify({ type: 'connected' }));
  ws.on('close', () => (sockets = sockets.filter(s => s !== ws)));
});

app.post('/emit', (req, res) => {
  const value = req.body.value || 'TEST-123';
  const msg = JSON.stringify({ type: 'scan', payload: { value, ts: Date.now() } });
  sockets.forEach(s => s.readyState === 1 && s.send(msg));
  res.json({ ok: true });
});

app.listen(9002, () => console.log('scanner REST on :9002'));

# simulators/printer/printer.js — HTTP endpoint capturing print jobs + flaky acks
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();

app.post('/print', (req, res) => {
  const id = Date.now();
  const file = fs.createWriteStream(path.join('/data', `${id}.bin`));
  req.pipe(file);
  file.on('finish', () => {
    // 10% of the time, drop the ack to reproduce field bugs
    if (Math.random() < 0.1) return res.destroy();
    res.json({ status: 'queued', id });
  });
});

app.get('/health', (_, r) => r.json({ ok: true }));
app.listen(9100, () => console.log('printer on :9100'));

1) Device simulators (Node + WS/HTTP)

Simulators mimic real protocols, not DOM hacks. The scanner pushes JSON events; the printer accepts buffered jobs and emits acks/errors; kiosk state flips through a REST API so Cypress can script failures.

  • Scanner over WebSocket with server-push

  • Printer over HTTP/IPP-like endpoint storing ESC/POS/PDF

  • Kiosk state via REST: online/offline, paper-low, jam

2) Angular adapter with Signals + SignalStore

Keep device logic in a dedicated DeviceStore. The UI reads signals: connected(), lastScan(), printQueue(), error(). This keeps templates declarative and E2E tests stable.

  • Centralize device state

  • Typed events and error taxonomy

  • Exponential backoff + jitter

3) CI harness (Nx + Cypress + Actions)

Compose brings up services at known ports, Cypress drives scenarios by hitting REST endpoints, and Actions/Jenkins collects logs for triage.

  • Docker Compose starts simulators

  • Cypress posts events to simulators

  • Artifacts include logs and captured prints

Docker Compose and GitHub Actions pipeline

# docker-compose.yml
version: '3.9'
services:
  scanner:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./simulators/scanner:/app
    command: sh -c "npm i ws express && node scanner.js"
    ports: ["9001:9001", "9002:9002"]
  printer:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./simulators/printer:/app
      - ./artifacts/prints:/data
    command: sh -c "npm i express && node printer.js"
    ports: ["9100:9100"]

# .github/workflows/e2e.yml
name: e2e
on: [push, pull_request]
jobs:
  cypress:
    runs-on: ubuntu-latest
    services:
      scanner:
        image: node:20-alpine
        options: >-
          --health-cmd "wget -qO- localhost:9002 || exit 1" --health-interval 3s --health-timeout 2s --health-retries 20
        ports:
          - 9001:9001
          - 9002:9002
        volumes:
          - ${{ github.workspace }}/simulators/scanner:/app
        command: sh -c "cd /app && npm i ws express && node scanner.js"
      printer:
        image: node:20-alpine
        options: >-
          --health-cmd "wget -qO- localhost:9100/health || exit 1" --health-interval 3s --health-timeout 2s --health-retries 20
        ports:
          - 9100:9100
        volumes:
          - ${{ github.workspace }}/simulators/printer:/app
          - ${{ github.workspace }}/artifacts/prints:/data
        command: sh -c "cd /app && npm i express && node printer.js"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx nx run web:build
      - run: npx http-server dist/apps/web -p 4200 &
      - run: npx wait-on http://localhost:4200
      - run: npx cypress run --browser chrome --record false
      - uses: actions/upload-artifact@v4
        with: { name: print-artifacts, path: artifacts/prints }

// cypress/e2e/scanner.cy.ts
it('accepts a barcode and routes to order page', () => {
  cy.visit('/scan');
  // inject deterministic scan
  cy.request('POST', 'http://localhost:9002/emit', { value: 'ORDER-ABC-123' });
  // assert UI reacts via DeviceStore (Signals)
  cy.findByRole('status', { name: /scanner connected/i }).should('exist');
  cy.findByText(/ORDER-ABC-123/).should('be.visible');
});

Compose file for local dev and CI

One file powers both local dev and CI. Volumes capture artifacts (scans, prints) for triage.

GitHub Actions with Nx + Cypress

This is the pattern I drop into Jenkins and Azure DevOps as well—identical steps, different YAML.

  • Boot simulators as services

  • Wait for health endpoints

  • Run Cypress headless in parallel

UI states, accessibility, and telemetry

<!-- status-banner.component.html -->
<p-toast></p-toast>
<div role="status" aria-live="polite" class="status-banner" [class.error]="device.errors().length">
  <ng-container *ngIf="device.scannerConnected(); else off">
    <i class="pi pi-barcode"></i> Scanner connected
  </ng-container>
  <ng-template #off>
    <i class="pi pi-ban"></i> Scanner offline — retrying…
  </ng-template>
</div>

.status-banner {
  padding: var(--space-2);
  background: var(--surface-50);
  &.error { background: var(--red-50); color: var(--red-900); }
}

Accessible device indicators

I pair PrimeNG components with design tokens for success/warn/error and density. Live regions announce device changes to screen readers—critical for ADA compliance in kiosks.

  • role=status for live updates

  • High-contrast tokens for error states

  • Keyboard-first retry flows

Typed events + Firebase logs

I log scan and print events to Firebase/GA4 with a correlation ID so we can chart percentiles on dashboards. That’s how we proved a 42% reduction in failed print acks after shipping retry logic.

  • Event schema versioning

  • Sample-rate noisy events

  • Correlate scan→print latency

When to hire an Angular developer for legacy rescue

Signals migration meets devices

If your app is mid-upgrade (12–15 → 20) and device flows are flaky, bring in an Angular expert who has stabilized kiosks and scanners. I’ll stand up the Docker lab, add a SignalStore, and leave you with deterministic tests.

  • Zoneless + Signals can surface race conditions

  • Device gateways hide USB/driver variance

Typical timeline

For a telecom ads platform, we reproduced barcode latency spikes and cut mean scan→render to <120ms via WebSocket + Signals. For an airline kiosk, we removed phantom success toasts and added printer acks with retries and backoff.

  • 2–3 days to baseline simulators and Compose

  • 1–2 weeks to add DeviceStore + tests

  • Parallel track to upgrade Angular safely

What to measure next

Reliability and UX KPIs

Hook metrics into Angular DevTools profiling, GA4, and your logs. If you use Firebase, stream logs with correlation IDs. For dashboards, I use D3/Highcharts with WebSocket updates and data virtualization for long sessions.

  • MTTR for device incidents

  • Scan→route p95

  • Print ack p99 and error taxonomy coverage

Related Resources

Key takeaways

  • Dockerized device simulators make field-only bugs reproducible in minutes, not days.
  • Model device state in Angular with Signals + SignalStore to drive offline, retry, and UI indicators.
  • Use WebSocket for scanner input and HTTP/IPP for print to mirror real protocols without USB passthrough.
  • Wire simulators into Nx + Cypress and GitHub Actions for deterministic, parallelizable CI.
  • Instrument everything—typed event schemas, Firebase logs, and GA4—to prove reliability gains.

Implementation checklist

  • Define typed event schemas for scanner, printer, and kiosk states.
  • Create Node-based simulators: WS for scanner, HTTP/IPP for print, REST for device state flips.
  • Expose simulators via Docker Compose; tag and version images.
  • Add an Angular DeviceStore (Signals + SignalStore) to track connection, last events, and errors.
  • Implement exponential backoff, jitter, and offline banners with accessible ARIA states.
  • Write Cypress tests that post events to simulators and assert UI updates.
  • Run simulators as GitHub Actions/Jenkins services; wait-on health endpoints.
  • Collect metrics: mean time-to-reproduce, test flake rate, print render size, and Lighthouse/UX deltas.

Questions we hear from teams

What does an Angular consultant do for Docker hardware simulation?
Stand up scanner/printer/kiosk simulators in Docker, add a Signals-based DeviceStore, wire Nx + Cypress tests, and integrate logs/metrics. The outcome: reproducible defects, stable CI, and measurable reliability gains.
How long does it take to set up a device lab for Angular 20+?
Baseline simulators and Compose typically take 2–3 days. Adding a DeviceStore, retries, and 8–12 deterministic Cypress specs usually takes 1–2 weeks, depending on legacy complexity and CI platform.
How much does it cost to hire an Angular developer for this work?
Most teams invest 2–4 weeks of senior Angular engineering. I offer fixed-scope packages for setup plus training, or flexible retainers. Book a discovery call for a tailored estimate within 48 hours.
Can we run this on GitHub Actions, Jenkins, or Azure DevOps?
Yes. The same Docker images and health checks work across GitHub Actions, Jenkins, and Azure DevOps. I provide drop-in YAML and scripts plus artifact uploads for print/scan logs.
Will this work with Firebase Hosting or SSR?
Yes. The simulators run as sidecar services; your Angular app can be SSR or static on Firebase Hosting. The DeviceStore remains framework-agnostic and works with Signals and zoneless setups.

Ready to level up your Angular experience?

Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.

Hire Matthew – Remote Angular Expert, Available Now Review Your Angular Build and CI Strategy

NG Wave

Angular Component Library

A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.

Explore Components
NG Wave Component Library

Related resources