One of the most tedious parts of testing a barcode scanning pipeline is coming up with test data. You need something to scan, and ideally something that changes between scans so you can verify the data is live rather than cached. QR codes containing timestamps solve this perfectly — every scan is unique, the content is human-readable, and you can verify the round-trip time just by looking at what got logged.
This is QR-Time: a minimal Python web app that displays a QR code containing the current date and time, refreshing every five seconds. It runs in an LXC container and is accessible on the local network as https://qr-time.lan.
What It Does
Point a browser at https://qr-time.lan and you get a full-screen QR code. Every five seconds the image is replaced with a fresh one encoding the current second. Aim the Zebra TC57 at the screen, scan, and the timestamp arrives at the server exactly like any barcode — because as far as DataWedge is concerned, it is one.
2026-04-16 14:03:22
That string is what gets scanned and forwarded via DataWedge IP Output to the Python server described in the previous post. It is a self-contained end-to-end test of the entire scanning pipeline with no physical labels or static test cards needed.
The Stack
The app is about as minimal as a Flask app gets:
from flask import Flask, Response
import qrcode
import io
from datetime import datetime
app = Flask(__name__)
@app.route('/qr')
def qr():
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
img = qrcode.make(ts, border=2)
buf = io.BytesIO()
img.save(buf, 'PNG')
buf.seek(0)
return Response(buf.getvalue(), mimetype='image/png',
headers={'Cache-Control': 'no-store'})
Each request to /qr generates a fresh QR code on the fly. The Cache-Control: no-store header prevents the browser from serving a stale image when the JavaScript updates the src.
The HTML page loads the image and replaces it every five seconds using setInterval:
function refresh() {
document.getElementById('qr').src = '/qr?' + Date.now();
}
setInterval(refresh, 5000);
Appending Date.now() as a query parameter is the simplest way to bust the browser cache without any server-side state.
Infrastructure
The app runs in an LXC container managed by Incus, with Caddy providing HTTPS and a .lan hostname on the local network.
Browser / TC57
└── https://qr-time.lan (Caddy, host)
└── http://10.140.20.30:5000 (Flask, qr-time container)
Creating the container was straightforward, with one gotcha: the default Incus profile on this machine uses macvlan networking on the wireless interface rather than a bridge. Containers created with the default profile get a veth peer attached directly to the physical NIC, which means they are unreachable from the host. The fix is a one-line override to put the container on the incusbr0 bridge instead:
incus config device add qr-time eth0 nic name=eth0 network=incusbr0
Once on the bridge, the container gets a static IP via systemd-networkd and is reachable at 10.140.20.30.
Caddy handles the rest — a three-line config file and a reload:
qr-time.lan {
tls internal
reverse_proxy 10.140.20.30:5000
}
tls internal uses Caddy's built-in certificate authority, which means HTTPS works on the local network with no external dependencies. The .lan hostname resolves via dnsmasq, which routes all *.lan queries to 127.0.0.1 where Caddy is listening.
The Result
Scan the screen with the TC57 and the server logs:
[14:03:22] Connected: 192.168.244.2:54823
[14:03:22] SCANNED: 2026-04-16 14:03:20
The two-second delta between the timestamp encoded in the QR and the time it appears in the log is the full round-trip: QR generation, screen refresh, scanner trigger, DataWedge processing, TCP send, server receive. Not bad for a stack assembled from a phone hotspot, a Python script in Pydroid 3, and a five-second refresh loop.
The full source for the receiving server is in the previous post. QR-Time is the transmitting end.
No comments:
Post a Comment