DEV Community

Cover image for We Spent Days Fighting a Zebra Card Printer. So You Don't Have To.
Alex Radulovic
Alex Radulovic

Posted on • Originally published at purpleowl.io

We Spent Days Fighting a Zebra Card Printer. So You Don't Have To.

If you've ever tried to programmatically control a Zebra ZC350 card printer, you already know the pain. And if you haven't, let me save you some time: it's considerable. I want to save you the pain, even if you are a competitor.

Feed a card. Encode an NFC chip. Print a badge. Eject it. Sounds simple. It wasn't.

We built an open-source bridge called Dazzle because a client needed automated card issuance inside a larger workflow, and getting from "nice hardware" to "working software" was much harder than it should have been.

If you're going to have zebra problems, you might as well answer them with Dazzle. Bonus points if you get the reference.

At one point we spent hours staring at APDU responses that all came back 6900, trying to figure out whether we were talking to the card, the reader, the SAM, or something undocumented in between.

So we open-sourced the thing we wish we'd had on day one.

What Dazzle Does

Dazzle is a .NET 8 helper that runs as a child process and speaks JSON over stdin/stdout.

Your app, whether it's Node.js, Python, or anything else that can spawn a process and read lines, sends commands like:

  • feed
  • smartcard.connect
  • smartcard.transmit
  • eject

The helper talks to the Zebra printer and its internal smart-card encoder, then sends structured results back.

The goal is simple: keep the ugly vendor-specific parts out of your app code.

A typical flow looks like this:

const printer = new ZebraCardPrinter();
await printer.start();
await printer.connect("192.168.1.100");

await printer.feed();
const readers = await printer.getReaders();
await printer.smartcardConnect(readers.contactless);

const uid = await printer.smartcardTransmit("FFCA000000");
console.log("Card UID:", uid.response);

await printer.smartcardDisconnect();
await printer.eject();
Enter fullscreen mode Exit fullscreen mode

The point is to make card issuance feel like normal application code.

Why We Built It

At PurpleOwl, we build custom business software for small and mid-sized companies. In this case, the project needed NFC-enabled ID card issuance as part of a larger operational workflow.

The Zebra ZC350 hardware is solid. The integration story was not.

What Made This Hard

Here are the problems that made us build Dazzle.

1. The SDK Isn't Distributed Like a Normal Dependency

There is no clean npm install or dotnet add package path for the Zebra Card SDK.

You download a large installer, dig through C:\Program Files\Zebra Technologies\ for DLLs, manually copy them into your project, and add references one by one.

Every new machine gets the same ritual. No normal dependency management. No clean reproducible setup.

2. USB Discovery Was Surprisingly Fragile

Finding a USB-connected printer should have been a solved problem.

Instead, the relevant class could live in a separate assembly that was not always loaded, and the property holding the USB address changed names across SDK versions:

  • SymbolicName
  • UsbSymbolicName
  • Address
  • DeviceId

We ended up using reflection to locate the type, invoke the discovery method, and probe multiple property names until one worked.

3. Connections Could Fail Without Looking Failed

The TCP connection to the printer can drop while still leaving you with an object that appears usable.

So IsConnected might look fine right up until the next SDK call hangs, throws from somewhere deep in the stack, or returns nonsense.

Dazzle wraps operations with reconnection logic because trusting the reported connection state turned out to be a mistake.

4. "Card Present" Did Not Mean "Card Ready"

After feeding a card into the encoder station, there is a delay before the PC/SC reader fully recognizes it.

During that window, the card can show up as present but unpowered. Connect too early and you get RemovedCard even though the card is physically there.

So Dazzle waits until the card is both present and powered before trying to connect.

The Cindy0 Problem

This one cost us the most time.

We connected to the printer's internal smart-card encoder over PC/SC, decoded the ATR's historical bytes to ASCII, and got:

Cindy0

Every APDU came back 6900.

Read UID? 6900.

Authenticate? 6900.

Write? 6900.

We assumed we were talking to the wrong slot, maybe the SAM instead of the contactless card, so we tried configuration changes, different share modes, transparent RF commands, IOCTLs, and a lot of other dead ends.

The real answer was stranger: Cindy0 is the Elatec TWN4 reader's virtual slot. It is not the card. It is a command channel that accepts the reader's "Simple Protocol" wrapped in APDUs.

So standard PC/SC card commands were failing because we were not actually talking to a card.

The actual contactless card appears on a different logical slot, but only after you use that command channel to search for a tag first.

That detail exists in the TWN4 PC/SC docs. It is not surfaced clearly in the Zebra integration story.

Five hours disappeared because a virtual slot was named Cindy0.

What Ships In The Repo

Dazzle includes:

  • ZebraCardHelper, the .NET helper that manages printer and encoder communication, APDUs, tunneling, and reconnection logic
  • zebra-card-printer.js, a Node.js wrapper that handles process lifecycle, JSON messaging, request/response correlation, and timeouts
  • PAINPOINTS.md, a write-up of the gotchas, race conditions, and undocumented behaviors we ran into

If you are doing anything with Zebra card printers and NFC encoding, that pain-points document alone may save you a lot of time.

Why JSON Over stdio

We used JSON over standard input/output because it is simple and portable. Each request has an id, and each response echoes it back.

{ "id": 1, "cmd": "connect", "ip": "192.168.1.100" }
{ "id": 1, "ok": true, "data": { "connected": true } }

{ "id": 2, "cmd": "feed" }
{ "id": 2, "ok": true, "data": { "fed": true } }

{ "id": 3, "cmd": "smartcard.transmit", "apdu": "FFCA000000" }
{ "id": 3, "ok": true, "data": { "response": "04a23b1a9070809000" } }
Enter fullscreen mode Exit fullscreen mode

That keeps the integration language-agnostic. Node.js, Python, Go, or anything else that can spawn a child process and read lines can use it.

Who This Is For

If you're building a system that issues NFC-encoded cards, employee badges, access cards, membership cards, or anything similar on Zebra ZC350 hardware, this project is for you.

It is also for the developer who is already deep in the Zebra SDK and wondering whether the weirdness is their fault.

It probably is not.

Why We Open-Sourced It

We're a five-person shop. We build custom ERP, CRM, and PSA systems. We built Dazzle because a client needed it, and we released it because it felt wasteful to let every future developer rediscover the same problems from scratch.

The hardware is solid. The integration layer does not need to be this punishing.

If Dazzle saves someone else from losing half a day to Cindy0, publishing it was worth it.

Links

If you try it and run into edge cases we haven't covered yet, I'd genuinely love to hear what you found.

Top comments (0)