Skip to content

A desktop app that talks straight to the scanner

A cross-platform desktop app that reads barcode scanners and label printers directly, processes inventory counts locally, and keeps running through the warehouse's dead cell coverage.

TallyrigNorthgate DC · Goods In · Dock 2GI-03
2 scannersPrinterServer unreachable · 46 held2:13 PM
In hand · read at 2:13:21 PMMatches PO-55120 line 3
Pallet8841Garden peas 12 × 900 gHarrow Vale Foods · 48 casesSSCC 150123450000088416 60 bytes over serial, none typed
Put away toC-12Frozen−18 °C or colder 62 m · aisle C, level 2Scan the bay label at C-12 to finishRouteReprint
Read off the labelGS1-128 · ]C1
(00)SSCC150123450000088416pallet 8841, check digit 6 verified
(02)Contained GTIN05012345678900Garden peas 12 × 900 g
(17)Use by27031403/14/2027
(10)BatchL2291variable length, ended by GS
(37)Count4848 cases
Scans this shift3,406since 6:00 AM
Put-aways43146 held on this terminal
Refused at bay7wrong zone or full
Codes typed0no keyboard path
Waiting for a scan8 pallets behind 8841
PalletProductBay
8842Oven chips 4 × 2.5 kgFrozen60 cases · Dock 2C-14
8843Whole milk 6 × 2 LChilled72 cases · Dock 2A-07
8844Plain yogurt 12 × 500 gChilled54 cases · Dock 2A-09
8845Basmati rice 4 × 5 kgAmbient40 cases · Dock 3F-22
8846Fish fillets 10 × 1 kgFrozen36 cases · Dock 3C-15
8847Chopped tomatoes 24 × 400 gAmbient84 cases · Dock 3F-31
8848Mixed berries 8 × 1 kgFrozen48 cases · Dock 4D-03
8849Salted butter 20 × 250 gQuarantine30 cases · Dock 4Q-02
v4.8.2 · signedHID 1F3A:0A21 · COM4 · COM3 tallyrig.db · WAL, 46 movements held

How the work was scoped

Industry

Warehousing & Fulfillment

Duration

11 weeks

Cooperation model

Fixed price

Services
Desktop app buildHardware integrationSigned installer & auto-update
Integrations
StripeSendGridSegmentSentry
Technologies
ElectronTypeScriptReactSQLitenode-serialportCode signing & notarization
Team
1 Project lead1 Product designer2 Frontend engineers1 Backend engineer1 QA engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

Direct hardware access

Talks to scanners and label printers over USB and serial, which a browser can't do.

Barcode scanners and label printers speak over USB HID and serial, which a browser can't reach. Electron with node-serialport talks to them directly, so a scan lands in the app with no keyboard-wedge workaround, and a label prints to the right printer on the right stock with no print dialog. Device configuration is per terminal, so each station knows its own hardware.

What shipped
  • Direct USB HID and serial access, with no keyboard-wedge hack
  • Labels print to the right printer and stock without a dialog
  • Per-terminal device configuration
TallyrigNorthgate DC · Devices on GI-03GI-03
2 scannersPrinterServer unreachable · 47 held2:14 PM
What this terminal is talking toConfigured for GI-03 only · read directly over USB HID and serial, no keyboard wedgeKeyboard input of codes: offRescan ports
Handheld scannerPallet labels at the dockConnected
SerialCOM4 · 9600 8N1ProfileGS1-128, symbology id on, suffix CR
last 60 minin 18,240 · out 0 bytes
Ring scannerBay labels at put-awayConnected
USB HIDHID 1F3A:0A21ProfileBay labels only
last 60 minin 6,912 · out 0 bytes
Label printer4 × 6 in frozen-stock labelsConnected
SerialCOM3 · 115200 8N1ProfileVendor escape set B, stock 4 × 6 in
last 60 minin 1,296 · out 41,388 bytes
Pallet archDock 2 drive-through readerMissing · 6 days ago
USBnot presentProfileUnplugged 09/11 7:52 AM
last 60 minin 0 · out 0 bytes
Every byte on the portsnewest first · 5 of 1,184 this hour
TimePortBytesRawDecoded
2:14:09.412→ COM36121B 40 1B 4C 02 1D 21 11 43 2D 31 32 …Bay label C-12 · escape set B · 4 × 6 in
2:14:09.508← COM3406 00 01 0DPrinter ack · label 1 of 1 done
2:14:07.036← HID125D 43 30 43 2D 31 32 …Bay label C-12 · frozen · matches pallet 8841
2:13:21.884← COM4605D 43 31 30 30 31 35 30 31 32 33 34 …Pallet 8841 · SSCC, GTIN, use-by, batch, count
2:12:41.217← HID125D 43 30 43 2D 31 31 …Bay label C-11 · frozen · matches pallet 8839
v4.8.2 · signedHID 1F3A:0A21 · COM4 · COM3 tallyrig.db · WAL, 47 movements held
On screen

What this terminal is talking to: a handheld scanner on serial, a ring scanner on USB HID and the label printer on its own port, the pallet arch unplugged six days ago, and the bytes each port has carried in the last hour.

TallyrigNorthgate DC · Sync queueGI-03
2 scannersPrinterServer unreachable · 47 held2:14 PM
The server cannot be reachedSince 1:31 PM. Counting carries on: every put-away is written to this terminal’s disk as it is scanned.47put-aways
held here
Held on this terminalNot yet on the server
MovePalletProductBayWrittenState
#204178841Garden peas 12 × 900 gFrozenC-122:14:07 PM on disk
#204168839Sweetcorn 12 × 1 kgFrozenC-112:12:41 PM on disk
#204158838Cheddar block 8 × 2 kgChilledA-022:11:16 PM on disk
#204148836Porridge oats 10 × 1 kgAmbientF-182:09:52 PM on disk
#204138835Ice cream tubs 6 × 2 LFrozenD-012:08:30 PM on disk
#204128834Double cream 12 × 600 mlChilledA-052:06:58 PM on disk
#204118833Pasta shells 12 × 1 kgAmbientF-242:05:21 PM on disk
#204108831Prawns 10 × 900 gFrozenC-102:03:44 PM on disk
#204098830Sliced ham 20 × 400 gChilledA-062:02:05 PM on disk
#204088829Canned beans 24 × 415 gAmbientF-292:00:33 PM on disk
#204078828Spinach 10 × 1 kgFrozenC-091:58:50 PM on disk
Still working offline
Scanners and printerlocal ports
Bay and zone rulescached at 6:00 AM
Order lines for todaycached at 1:30 PM
Writes to tallyrig.dbper scan
Reconciled one movement at a time
When the link returns, each movement is sent and confirmed on its own. A link that drops halfway keeps what it confirmed.Earlier today · 10:05 AMlink up 41 s
15confirmed, kept
8carried to the next try
Nothing is marked sent until the server confirms it
v4.8.2 · signedHID 1F3A:0A21 · COM4 · COM3 tallyrig.db · WAL, 47 movements heldlast reach attempt 2:14:02 PM · retry in 20 s
On screen

The server cannot be reached: forty-seven put-aways held on the terminal itself, each written to disk as it was scanned, the scanners and bay rules still working, and an earlier link that dropped halfway keeping the fifteen movements it had confirmed.

Local-first counts

Inventory processing runs on the terminal and reconciles when a connection returns.

Counts write to a local SQLite database first and reconcile with the server when a connection returns, so a warehouse dead spot doesn't stop a stock take. Reconciliation runs per movement, which means a partial sync makes real progress instead of failing whole and starting over. The operator sees pending counts explicitly, so nobody has to assume everything landed.

What shipped
  • SQLite-first writes; a dead spot doesn't stop a count
  • Reconciled per movement, so partial syncs make progress
  • Pending counts shown explicitly, never assumed sent
TallyrigNorthgate DC · UpdatesGI-03
2 scannersPrinterServer · in sync3:02 PM
Releasev4.8.2from v4.8.1 · signed and notarized
Terminals on this build18 of 1812 Windows · 6 macOS
Rolled back0over two days on the floor
Site pinNot pinnedpin before a peak week
How it rolled out
Canary09/141 terminal · 0 rolled backGI-01 alone, a full shift on the new build
Floor · day 109/159 terminals · 0 rolled backInstalled on next restart
Floor · day 209/168 terminals · 0 rolled backInstalled on next restart
Windows installerCode-signed
macOS appSigned and notarized
Pin Northgate DC to v4.8.2Every terminal on this site stays on this build until the pin is lifted. New releases wait.Pin through peakSchedule
Every terminal18 on v4.8.2
IdLocationOSStageInstalledBuild
GI-01Goods In · Dock 1WindowsCanary09/14 6:12 AM 4.8.2
GI-02Goods In · Dock 1WindowsDay 109/15 6:04 AM 4.8.2
GI-03Goods In · Dock 2WindowsDay 109/15 6:05 AM 4.8.2
GI-04Goods In · Dock 3WindowsDay 109/15 6:09 AM 4.8.2
GI-05Goods In · Dock 4WindowsDay 109/15 6:11 AM 4.8.2
CH-01Chilled · Aisle AmacOSDay 109/15 1:58 PM 4.8.2
CH-02Chilled · Aisle BmacOSDay 109/15 2:03 PM 4.8.2
FZ-01Frozen · Aisle CWindowsDay 109/15 2:10 PM 4.8.2
FZ-02Frozen · Aisle DWindowsDay 109/15 2:12 PM 4.8.2
FZ-03Frozen · Blast roomWindowsDay 109/15 2:31 PM 4.8.2
AM-01Ambient · Aisle FmacOSDay 209/16 6:02 AM 4.8.2
AM-02Ambient · Aisle GmacOSDay 209/16 6:07 AM 4.8.2
AM-03Ambient · Aisle HWindowsDay 209/16 6:15 AM 4.8.2
QA-01Quarantine cageWindowsDay 209/16 9:40 AM 4.8.2
DS-01Dispatch · Bay 1WindowsDay 209/16 1:55 PM 4.8.2
DS-02Dispatch · Bay 2WindowsDay 209/16 2:01 PM 4.8.2
OF-01Stock officemacOSDay 209/16 2:20 PM 4.8.2
OF-02Shift lead deskmacOSDay 209/16 2:26 PM 4.8.2
v4.8.2 · signedHID 1F3A:0A21 · COM4 · COM3 tallyrig.db · WAL, in syncupdate channel: stable · checked 3:00 PM

Managed updates

On screen

Eighteen terminals, one build: a canary terminal first, then the Windows and macOS terminals on the floor over two days, installed on restart with nothing rolled back, and a pin that holds the site on this build through a peak week.

A signed auto-update channel keeps every terminal current without a manual reinstall.

Installers are code-signed on Windows and notarized on macOS, because an unsigned build is one an IT team won't deploy and an operator will click past. Updates roll out on a staged channel (a canary terminal first, then the floor) and can be pinned per site, so a peak week isn't interrupted by a release nobody asked for.

What shipped
  • Signed on Windows, notarized on macOS
  • Staged rollout: canary terminal before the floor
  • Version pinning per site through peak weeks
Introduction

What we were brought in to do

Inventory counts ran through a browser tab that couldn't see the barcode scanners or label printers on the floor, so staff retyped codes by hand and counts drifted from what was actually on the shelves.

A distribution facility running inventory counts through a browser tab on terminals bolted to the racking. The scanners and label printers on those terminals connect over USB and serial, so the browser could see neither. Staff were scanning into a handheld, reading the number aloud, and typing it in. The engagement started after a quarterly count that took three days and still disagreed with the shelves.

Desktop Engineering

Where the old way broke

The warehouse's scanners and printers connect over USB and serial, which a browser sandbox can't reach, and the facility's cell coverage is unreliable enough that a web app dropped mid-count regularly.

Two failures compounded. Transcription introduced errors at a rate that made the count itself untrustworthy, and the facility's cell coverage dropped often enough that the browser tab lost its session mid-count and discarded the entries. Staff had learned to count in short bursts and save constantly, which is slower and still lost work.

We built a cross-platform desktop app with direct hardware access to the scanners and printers, local processing so counts don't depend on a live connection, and a code-signed auto-update channel so every terminal stays current without a manual reinstall.

What we built together

  1. 01

    Scoped the hardware integration against the warehouse's actual scanner and printer models

    Two of the facility's printers turned out to need vendor-specific escape sequences, which no generic driver assumption would ever have produced.

  2. 02

    Built local-first inventory processing that reconciles once a connection is available

    Counts write to a local database first and reconcile per movement when a connection returns, so a partial sync makes progress instead of failing whole.

  3. 03

    Wired direct USB/serial access to scanners and label printers

    Two of the facility's printers needed vendor-specific escape sequences, the kind of thing you only find by testing against the actual hardware.

  4. 04

    Set up code signing, notarization, and an auto-update channel across all terminals

    Signed and notarized builds roll out on a staged channel (a canary terminal before the floor), with version pinning available per site through peak weeks.

Process

Phase by phase

  1. Phase 1: Scope hardware

    The models actually on the floor

    Scoped the integration against the warehouse's real scanner and printer models, with no generic driver assumptions.

    • Hardware inventory
    • Integration spec
  2. Phase 2: Local-first

    Counting without a connection

    Built inventory processing that completes locally and reconciles once connectivity is available.

    • Local store
    • Reconciliation logic
  3. Phase 3: Wire devices

    USB and serial

    Implemented direct device access for scanners and label printers, with per-model quirks handled explicitly.

    • Device layer
    • Printer templates
  4. Phase 4: Distribute

    Signed and self-updating

    Set up code signing, notarization, and an auto-update channel across every terminal.

    • Signed installers
    • Update channel
    • Rollout plan
TallyrigNorthgate DC · Cycle countGI-03
2 scannersPrinterServer · in sync3:40 PM
Aisle C · frozenCycle count against what the system expected · every bay and case scanned, quantities by stepper9 of 9 bays counted4 off · 9 cases
Bays
BayProduct · reasonExpectedCountedDiff
C-04Fish fingers 12 × 900 gMatched · D. Okafor4242
C-05Oven chips 4 × 2.5 kg3 cases put away to C-06 by mistake, same product6057−3
C-06Oven chips 4 × 2.5 kgReceives the 3 cases missing from C-055760+3
C-07Chicken breasts 6 × 2 kgMatched · D. Okafor3636
C-08Peas & carrots 12 × 900 gMatched · M. Reyes4848
C-09Spinach 10 × 1 kg2 damaged cases moved to quarantine, never booked4240−2
C-10Prawns 10 × 900 gMatched · M. Reyes3030
C-11Sweetcorn 12 × 1 kg1 case picked short, confirmed after the count window4948−1
C-12Garden peas 12 × 900 gMatched · M. Reyes4848
Drift by bay, in cases
C-040
C-05−3
C-06+3
C-070
C-080
C-09−2
C-100
C-11−1
C-120
How this count was taken
Bay label scanned before countingCase GTIN scanned, never typedQuantity set with the stepperSaved to disk as each bay closes
v4.8.2 · signedHID 1F3A:0A21 · COM4 · COM3 tallyrig.db · WAL, in synccount CC-0917-C · saved per bay
On screen

A cycle count in aisle C: nine bays counted against what the system expected, four of them off by nine cases in total, each with the reason it drifted, and every bay and case scanned, none keyed.

Operational results after launch

+31%

Count accuracy

0

Re-typed codes

100%

Terminals on latest build

Count accuracy is measured against a controlled recount of the same aisles, before and after, never against the system's own figures. Retyped codes is literal: there's no keyboard path for a code anymore. Terminals on latest build is measured from the update channel's own check-in telemetry.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

About our collaboration

A cross-functional team of 5 worked on a fixed price basis over 11 weeks, covering Desktop app build, Hardware integration, Signed installer & auto-update. We shipped in two-week increments, each one releasable and reviewed with them before it merged. Decisions were written down as they were made, so the reasoning outlived the people who made it.

Hardware was scoped against the facility's actual scanner and printer models in week one, with no generic driver assumptions, and that's the step that decides whether this kind of integration works at all. Builds went onto two real terminals from week four, and the update channel was tested by shipping deliberately trivial releases before anything important depended on it.

What we'd carry into the next one

03
  1. 01

    The dead cell coverage was the requirement, not an edge case. Local-first was the only workable shape.

    Poor coverage wasn't an edge case to handle. It was the operating condition, and a design that treats the network as optional is a different design from the ground up, not a hardened version of the old one.

  2. 02

    Generic driver assumptions fail on real hardware; scoping to the actual models saved the integration.

    The models mattered: two of the facility's printers needed vendor-specific escape sequences that no generic driver assumption would have produced.

  3. 03

    With dozens of terminals to keep current, code signing and notarization belong in the plan from day one.

    With dozens of terminals, an unsigned build is one IT won't deploy and an operator will click past. Signing is what makes the update channel usable at all.

One pallet, four steps

From the bytes on the port to the bay, a code never passes through a keyboard.

Pallet 8841 as the terminal sees it: the frame read off the serial port, the label fields parsed out of it, the checks against its order line, and the bay scan that accepts or refuses it. Switch tabs, or use the arrow keys once one is focused.

The handheld scanner reads the pallet label and writes the frame to serial port COM4. The app reads the port itself, so there's no text field that has to have focus and no keystrokes standing in for a barcode.

COM4 · 9600 8N160 / 60 bytes
5D]43C3113003003113553003113223333443553003003003003003883883443113663003223003553003113223333443553663773883993003003113773223773003333113443113004CL3223223993111DGS3333773443880DCR

]C1 names the symbology, GS closes the variable batch field, CR ends the frame. Frame complete.

Pallet 8841 so far
  1. Serial read
  2. 2Label parsed
  3. 3Checked against the order
  4. 4Accepted or refused
Keystrokes carrying a code: 0At every step. There's no keyboard path for a code, so there's nothing to retype.

Reveal timings are illustrative. The values follow pallet 8841 through the same data the screens above are drawn from.

Architecture

From the label on a pallet to the build on every terminal

Everything from the port to the database runs on the terminal and treats the network as optional. The server is where counts reconcile; a count never depends on it.

  1. 01 · Hardware
    Scanners and label printersIntegrated against the facility's actual models. Two printers needed vendor-specific escape sequences, handled explicitly.
  2. 02 · Device layer
    USB HID and serial via node-serialportA scan lands in the app with no keyboard-wedge workaround, and a label prints to the right printer without a dialog.
  3. 03 · Engine
    Inventory processing on the terminalCounts complete locally, so a dropped connection doesn't stop a stock take or discard its entries.
  4. 04 · State
    SQLite first, reconciled per movementA partial sync keeps the movements it confirmed, and pending counts are shown, never assumed sent.
  5. 05 · Delivery
    Signed auto-update channelSigned on Windows, notarized on macOS, a canary terminal before the floor, and a version pin per site.

What the floor can rely on

A count that survives the building it runs in

A dead spot doesn't lose a count

Counts are written to the terminal's own database first and reconcile per movement when a connection returns. The operator sees exactly what's pending, with nothing assumed.

A code cannot be mistyped

Scanners are read directly over USB HID and serial. There's no keyboard path for a code anymore, so there's no transcription step for an error to slip in through.

No release nobody asked for

Builds are signed and notarized, reach a canary terminal before the floor, and can be pinned per site, so an update never interrupts a peak week.

Need software that talks to the hardware on your floor and keeps counting when the signal drops? Scope your build in 3 minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.