Edge & hardware

When Software Can't Reach the Hardware It Controls

Every POS system eventually runs into the same wall: the software lives in the cloud, but the thing it needs to control is a physical box wired into a till on someone's counter. Here's how I bridged that gap on FranchiseTech, and the one decision in it I'd defend under questioning.

Every POS system eventually runs into the same wall: the software lives in the cloud, but the thing it needs to control — a cash drawer, a receipt printer — is a physical box wired into a till on someone's counter, on a network the cloud has no route to. You can't fetch() a drawer open from a cloud server. Something has to run on-site.

That "something" turned into one of the more interesting systems I built as CTO of FranchiseTech, a POS platform for small food and retail businesses in Romania. Here's how it's put together.

The franchisetech Connector app running on a SUNMI terminal, showing the bridge service running on port 17878, SUNMI built-in printer status, and an Open Drawer Now button
The connector running on an actual till during a site visit — bridge live on port 17878, drawer API available, status reported as "closed" until proven otherwise.

The shape of the problem

A cash drawer has no network connection of its own. It's a solenoid wired into the printer's drawer-kick port, and the printer relays a pulse to it on receiving a specific byte sequence — for most ESC/POS printers, 1B 70 00 19 FA. So "open the drawer" really means "get these five bytes onto the printer," and the printer sits on the shop's LAN or a till's USB port — unreachable from the cloud.

The fix is a small connector that runs on the till itself, listens on 127.0.0.1:17878, and translates one abstract request into whatever the locally-attached hardware actually understands.

Standardizing instead of chasing vendor SDKs

My first version of this was tied to a single manufacturer's proprietary SDK. It worked, for that one terminal model, and it meant every new hardware combination a customer brought in was a new integration to write. That's not a sustainable shape for a product selling into small businesses who source their own hardware.

So I rebuilt the connector around the actual common denominator across POS hardware — the ESC/POS command set, sent over whichever transport is present, USB or LAN — with the connector auto-detecting which one is live rather than making the customer configure it. Vendor quirks (some printers want the drawer pulse on pin 2, some pin 5, some a longer pulse) get handled as fallback attempts within that shared protocol, not as separate per-manufacturer integrations.

franchisetech Connector app screen showing Bridge Service running on port 17878, and a Connection Method selector: Auto, WiFi/LAN, USB, with the note 'Tries USB first, falls back to WiFi. Recommended.'
Auto-detect in the actual settings screen — USB first, WiFi/LAN fallback, no configuration required.

The connector is an Android app running a small embedded HTTP server, with a handful of endpoints:

GET  /health       — public, no auth
GET  /status       — configured mode, printer reachability
POST /open-drawer  — fire the actual hardware command
POST /diagnose     — walk every hardware path and explain what's wrong

/open-drawer requires a pairing token generated on-device and copied into the main product's settings, plus an Origin check locked to the production domain — an unauthenticated local server would let any page open on that device fire the till's cash drawer, which is exactly the kind of "fine in dev, dangerous in the field" shortcut worth catching early.

The distinction that actually mattered

The system is never allowed to say a drawer opened — only that a command was sent. That sounds pedantic until you think about what "success" means to a cashier: if the app says "opened" and the drawer didn't move because a cable's loose, the cashier trusts the software and walks away from cash that isn't secured.

So the connector reports command_sent the instant bytes leave the device, with zero claim about what happened physically after. A separate record — created only from an explicit human action during setup, "yes, I watched it open" — is the only thing allowed to represent real confirmation. That split isn't UI wording; it's two different database tables in the audit trail, so no future feature can quietly collapse "we told it to" into "it happened."

Software shouldn't get to claim a physical event occurred just because it didn't get an error back.

What's still unfinished

The built-in-printer path for terminals with hardware wired straight into the OS is currently detect-only — it can tell you the manufacturer's service is present but doesn't drive it directly yet, falling back to "use an external printer." A reasonable boundary for a first version, not a finished one.

If there's a theme here, it's that distributed, physically-real systems don't get a single "it worked" bit. You model the states in between — sent, unconfirmed, confirmed, failed — and refuse to collapse them just because a simpler API would be nicer to call.

Built as CTO of FranchiseTech, a HORECA/POS platform for hospitality businesses in Romania.

← More writing