15 August 2026

Turning a Zebra TC57 into a Wireless Barcode and NFC Scanner with a Python Server

I recently built a demo that takes a Zebra TC57 enterprise Android device and streams both barcode scans and NFC card taps over WiFi to a Python server running on a nearby phone. No cloud, no MDM, no special middleware — just DataWedge, Automate, and about 150 lines of Python. The result works well. Getting there involved a series of non-obvious gotchas that I want to document thoroughly.

The Setup

The Xiaomi Redmi Note 8 Pro acts as a WiFi hotspot. The TC57 connects to it as the only client. A Python server runs on the Xiaomi in Pydroid 3 and listens on two ports:

  • TCP 9100 — receives barcode scans via DataWedge IP Output
  • HTTP 9101 — receives NFC taps via an Automate flow on the TC57
Zebra TC57
  ├── Barcode scan → DataWedge IP Output → TCP 9100 → server
  └── NFC tap     → Automate flow       → HTTP GET 9101 → server

Xiaomi Redmi Note 8 Pro (WiFi hotspot)
  └── Python server (Pydroid 3)

The one awkward wrinkle: MIUI randomises the hotspot subnet on every restart, so the server IP changes. Both halves of the system handle this differently.


Part 1: Barcode Scanning with DataWedge

The TC57 ships with DataWedge pre-installed — Zebra's data capture middleware that intercepts scanner input and routes it wherever you want. The relevant profile settings:

Section Setting Value
IP Output Enabled
IP Output Protocol TCP
IP Output IP address (auto-configured by server)
IP Output Port 9100

Gotcha 1: DataWedge sends no newline and closes the connection

DataWedge opens a fresh TCP connection for each scan and closes it immediately after — there is no persistent session and no newline terminator on the data. A naive server that only flushes on \n will detect the connection but print nothing:

def handle_client(conn, addr):
    buffer = b""
    while True:
        chunk = conn.recv(4096)
        if not chunk:
            break
        buffer += chunk
        while b"\n" in buffer:
            line, buffer = buffer.split(b"\n", 1)
            barcode = line.decode("utf-8", errors="replace").strip()
            if barcode:
                print(f"[{timestamp()}] SCANNED: {barcode}")
    # Flush remaining data — DataWedge sends no trailing newline
    if buffer:
        barcode = buffer.decode("utf-8", errors="replace").strip()
        if barcode:
            print(f"[{timestamp()}] SCANNED: {barcode}")

Without the final flush after the loop, scans arrive silently. The connection log shows Connected: 192.168.x.x:port but the barcode is never printed.

Handling the Changing Hotspot IP

Because MIUI randomises the subnet on every hotspot restart, hardcoding the server IP in DataWedge is not viable. The server reads its own hotspot interface IP at startup and pushes it into the DataWedge profile automatically via a broadcast intent:

def get_ap0_ip():
    out = subprocess.check_output(["ip", "-4", "addr", "show", "ap0"]).decode()
    for line in out.splitlines():
        if line.strip().startswith("inet "):
            return line.split()[1].split("/")[0]
    return "0.0.0.0"

def configure_datawedge(ip):
    config = json.dumps({
        "PROFILE_NAME": "DWDemo",
        "CONFIG_MODE": "UPDATE",
        "PLUGIN_CONFIG": {
            "PLUGIN_NAME": "IP_OUTPUT",
            "PARAM_LIST": {
                "ip_output_enabled": "true",
                "ip_output_ip_addr": ip,
                "ip_output_port": "9100",
                "ip_output_protocol": "TCP"
            }
        }
    })
    subprocess.run([
        "am", "broadcast",
        "-a", "com.symbol.datawedge.api.ACTION",
        "--es", "com.symbol.datawedge.api.SET_CONFIG", config
    ])

Every time the server starts, the TC57 is automatically pointed at the correct IP. No manual reconfiguration ever needed.


Part 2: NFC Scanning with Automate

DataWedge does not have a built-in NFC input plugin that feeds into IP Output — NFC on Zebra devices requires either EMDK (a custom Android app) or a separate automation tool. Rather than building a full Android app, I used Automate by LlamaLab, which was already installed on the TC57.

The finished flow is:

  1. Begin
  2. NFC tag scanned — tag type: Any — stores UID in nfc_id
  3. Shell command/system/bin/getprop dhcp.wlan0.gateway — stores result in gateway
  4. Variable settrim(gateway) in expression mode — overwrites gateway with clean value
  5. HTTP request — GET http://{gateway}:9101/nfc?uid={nfc_id}
  6. Loop back to block 2

Getting to this required fixing seven separate issues.

Gotcha 2: NFC block ignores credit cards by default

The "NFC tag scanned" block defaults to tag type "Automate", which only catches NDEF-formatted tags. Credit cards, Mifare Plus, and ISO 14443-4 tags are silently ignored — the block never fires. The fix is to change the tag type setting to Any. Once changed, every NFC technology including EMV payment cards is detected.

Gotcha 3: ip route show default returns nothing on the TC57

To make an HTTP request, the TC57 needs to know the server IP. The Xiaomi is always the DHCP gateway, so reading the gateway address is equivalent to reading the server IP. The obvious Linux approach fails:

ip route show default
# returns nothing

The TC57's routing table has no default gateway entry. The correct Android-specific command is:

getprop dhcp.wlan0.gateway
# returns: 192.168.244.7

This wasted significant time. Always verify shell commands on the actual device.

Gotcha 4: Automate's shell has no /system/bin in PATH

Calling getprop dhcp.wlan0.gateway in Automate's Shell command block produced no output. Running echo hello worked fine, confirming the block itself worked. The Shell command block in Automate runs in a restricted environment that does not include /system/bin in PATH. The fix is to use the full absolute path:

/system/bin/getprop dhcp.wlan0.gateway

Gotcha 5: Pipes do not work in Automate's Shell command block

getprop appends a trailing newline to its output. Automate stores this literally, which turns the HTTP URL into http://192.168.244.7\n:9101/... — an invalid host. The natural fix is to pipe through tr:

/system/bin/getprop dhcp.wlan0.gateway | tr -d '\n'

This produces an empty result. Automate's Shell command block does not support pipes. The | character is passed as a literal argument rather than interpreted by the shell. Several other attempts also failed:

  • awk commands using $3 — Automate treats $ as a variable sigil, producing an "illegal character" error
  • trim(gateway) in a Variable set block — this appeared to work but cleared the variable, because at that point the Shell command was also failing (the PATH issue had not yet been diagnosed)

The working fix is a Variable set block after the Shell command, with the value field switched to expression mode (the = toggle in Automate):

  • Variable: gateway
  • Value: trim(gateway)

Once the PATH issue was fixed and gateway was actually being populated, trim() successfully stripped the newline.

Gotcha 6: Automate HTTP block plain text vs expression fields

The HTTP request block has two field types that look identical but behave differently. Plain text fields use {variable} substitution. Expression fields use direct variable names and string operators like "http://" + gateway.

Putting "http://" + gateway + ":9101/..." into the URL field (which is plain text) produced a URISyntaxException — the literal quote characters ended up in the URL. The correct approach for the URL field is plain {variable} substitution:

http://{gateway}:9101/nfc?uid={nfc_id}

Gotcha 7: Debugging with a Dialog block

When the HTTP request was still failing with an empty host (http://:9101/...), it was unclear whether the Shell command or the Variable set block was the problem. Adding a temporary Dialog message block between them with message {gateway} made the variable's actual content visible in notifications. This revealed that {gateway} was empty — confirming the Shell command PATH issue rather than a trim issue.

The Dialog message in notifications does not show trailing whitespace or newline characters. When the fix was in place and the dialog showed 192.168.244.7, it looked correct — but the \n was still there and still broke the URL. The newline only became visible in the error message: Invalid host: http://192.168.244.7\n:9101/....


The Final Server Output

With everything connected:

[14:02:11] DataWedge updated → 192.168.244.7:9100
[14:02:11] HTTP NFC server on 192.168.244.7:9101
[14:02:11] TCP barcode server on 192.168.244.7:9100
[14:02:44] Connected: 192.168.244.2:54321
[14:02:44] SCANNED: 012345678901
[14:03:15] NFC: 05861DBCE2F200

Barcodes and NFC taps from an enterprise rugged device, streamed over a local WiFi hotspot to a Python script on a phone — no cloud required.


Complete Issue Reference

Problem Root Cause Fix
Barcode connects but nothing prints DataWedge sends no newline; buffer only flushed on \n Flush buffer on connection close
DataWedge IP needs manual update after restart MIUI randomises hotspot subnet Push IP via SET_CONFIG broadcast on server startup
ip route show default returns nothing TC57 has no default gateway in routing table Use getprop dhcp.wlan0.gateway instead
NFC block never fires for credit cards Tag type defaults to NDEF only Set tag type to Any
getprop not found in Automate shell /system/bin not in PATH Use full path /system/bin/getprop
Pipes produce empty output in Shell command Automate does not interpret \| as a pipe Use a Variable set block with trim() expression instead
$3 causes "illegal character" error Automate treats $ as variable sigil Avoid $-based shell syntax entirely
trim() emptied the variable Variable was already empty — PATH bug not yet fixed Fix root cause first; trim() works once variable is populated
URISyntaxException with "http://" + gateway URL field is plain text, not expression — quotes become literals Use {variable} substitution in URL plain text fields
Dialog shows correct IP but URL still fails Dialog trims display; \n invisible in notifications Error message reveals the newline; fix with trim() in Variable set
HTTP request "Invalid host" with newline getprop appends \n; stored literally Variable set block: trim(gateway) in expression mode

No comments:

Post a Comment