Meshtastic contact-QR nametag kiosk for a Raspberry Pi + a Niimbot label printer.
DM the kiosk node the word print over the mesh and it prints your nametag:
a black HELLO-MY-NODE-IS header, your short/long name (emoji included),
node ID, a shared-contact QR (https://meshtastic.org/v/#...) that
anyone can scan in the Meshtastic app (>= 2.6) to add you as a contact,
PKC public key included, and the Meshtastic M-PWRD mark.
Built at DEFCON 34. 🦞
-
Raspberry Pi with a Meshtastic node attached (USB serial radio, or meshtasticd with a LoRa hat — use
--tcp localhostfor the latter) -
A Niimbot label printer, USB-C cable strongly recommended (2.4 GHz at DEFCON is a war zone). Pick yours with
--printer:--printerhead labels layout d110(default)12 mm / 96 dots continuous roll, or --die-cut30x15 mmdynamic-length banner, or a compact QR-left card b148 mm / 384 dots 50x30 mm die-cut landscape card: header band, name block, QR right The 50x30 mm stock is 50 mm across the roll and 30 mm along the feed, so the B1 card is laid out across the head instead of along it. Its QR gets 3 printer dots per module against the D110's 2, which means full long names survive into the QR payload instead of being truncated.
On the Pi:
sudo apt install -y python3-venv fonts-dejavu-core
python3 -m venv ~/badge
~/badge/bin/pip install labeltasticThat puts a labeltastic command in ~/badge/bin/. The emoji font ships
with the package; fonts-dejavu-core is the text font and comes from the
system. Python 3.11 or newer.
labeltastic sorts out which USB serial port is the radio and which is the
printer by USB vendor/product IDs (the D110 is a CH340; radios are
CP210x/CH9102/ESP32/nRF52/RP2040) and prints its port table at startup.
Just run it:
~/badge/bin/labeltastic --sample --dry-run # render only, no radio needed
~/badge/bin/labeltastic --test # one real test print
~/badge/bin/labeltastic # run the kiosk
~/badge/bin/labeltastic --printer b1 --die-cut # kiosk on a B1--sample renders a canned node, so --sample --dry-run works with
nothing plugged in at all. It writes the PNG (--out to redirect) and then
runs the real bitmap encoder over it, reporting packet count and per-third
pixel counts — the only check on the wire format you can make without the
printer in front of you.
If it can't tell the ports apart, it exits with the table and tells you
what to pass — use the stable names in /dev/serial/by-id/ for
--serial / --printer-port so a replug can't shuffle them. The B1's
USB IDs aren't known here, so pass --printer-port explicitly for one.
If labels come out flipped, add --flip (the old --rotate 270 still works).
labeltastic-provision gets nodes ready to ship with a pairing QR code. It watches USB, and
for each node plugged in it:
- reads the node's BLE address (
DeviceMetadata.ble_address), - writes a connect link for a random 6-digit PIN and its QR code to
provisioned/<BLE MAC>.png, - appends the node ID, hardware model, address, PIN and link to
provisioned/provisioned.csv, - sets Bluetooth to
FIXED_PINwith that PIN (the node reboots), and - reconnects and reads the PIN back, recording the row's
status:verified,mismatch(the node did not take it),unverified(it never came back) orwrite failed. Ship onlyverifiedrows.
A node with WiFi on is skipped: ESP32 firmware keeps Bluetooth off while WiFi is configured. A port that fails is retried twice, then left until it is replugged.
It installs with labeltastic (pip install labeltastic puts labeltastic-provision
next to labeltastic), and needs no printer.
labeltastic-provision # watch USB, provision each new node
labeltastic-provision --port /dev/ttyACM0 # provision one node and exit
labeltastic-provision --link web # https:// links, Android onlyNodes already plugged in at start are left alone unless you pass
--include-existing. Ports that look like a Niimbot printer are never
touched. provisioned.csv holds every PIN in clear text: treat it as a secret.
Needs firmware that reports ble_address (meshtastic/firmware#12281). On older
firmware the node is skipped; --macaddr-fallback uses the node's MAC instead,
which is the BLE address on nRF52 only, so it skips nodes that report WiFi. The PIN in the link needs
a Meshtastic Android release with meshtastic/Meshtastic-Android#7856; older
releases open the Connections screen and ignore it.
The connect link is specified in meshtastic/design#177:
meshtastic://meshtastic/connections?address=x<BLE MAC>&pin=<PIN>
The Android app also accepts the same path under https://meshtastic.org. The
iOS app does not open either form yet (meshtastic/Meshtastic-Apple#2955), so an
iPhone user pairs by hand with the PIN from provisioned.csv.
| parameter | value |
|---|---|
address |
the device address with its transport prefix: x + BLE MAC (xAC:27:6E:CD:5A:99), t + host:port for TCP (t192.168.1.1:4403), or n to disconnect |
pin |
optional, BLE only: the node's fixed pairing PIN, exactly 6 digits. Anything else is ignored and pairing asks as usual |
Opening either link shows a "Connect to this device?" prompt with the address and PIN; nothing connects until the user confirms. On Connect the app copies the PIN to the clipboard (marked sensitive) and pairs. Up to Android 16 it enters the PIN itself; on Android 17 the system pairing dialog asks and the PIN can be pasted.
The default, --link app, writes the meshtastic:// form. --link web writes
the https:// form, which opens only the Android app: meshtastic.org has no
page at /connections and does not hand it to the iOS app. Without the app
installed, neither form opens anything useful. To try a link on a phone over
adb:
adb shell am start -a android.intent.action.VIEW \
-d "meshtastic://meshtastic/connections?address=xAC:27:6E:CD:5A:99&pin=317338"uv sync --extra dev
ruff check . && mypy && python scripts/check_spdx.py && pytestNone of it needs a printer or a radio. CONTRIBUTING.md has
the details, AGENTS.md the module map and the invariants that
will bite you (starting with: head_px must be a multiple of 24, or the
printer prints blank and reports success).
- DM-triggered only, with a per-node cooldown — broadcast triggers would melt the label roll on a con mesh.
- Long names are truncated inside the QR payload (not on the printed label) to keep QR modules >= 2 printer dots (3 on the B1's roomier card), or phones can't scan the thermal print.
- Print density is capped per model (
--density): 3 on the D110, 5 on the B1. src/labeltastic/assets/NotoEmoji-Regular.ttfis Google's monochrome Noto Emoji, vendored from google/fonts under the SIL Open Font License 1.1 (assets/OFL.txt).- niimprint is vendored under
src/labeltastic/_vendor/(MIT, © kjy00302 — AndBondStyle's repo is the maintained fork of that original) because upstream's packaging pins Python to 3.11.x, which modern Pi OS is way past, while the code itself runs fine on 3.14. See its README for the two local changes. - The M-PWRD mark is redrawn with PIL primitives from
M-PWRD_BW_Border.svgin meshtastic/design — the geometry is a dozen coordinates, which beats putting an SVG rasteriser (and native cairo) on the Pi for one 40-dot glyph. It is supersampled and hard thresholded rather than left anti-aliased, because niimprint dithers any grey it is handed and that turns a mark this small into noise. Only the banner prints the whole logo, in a column of its own — it can just grow. The B1 card and the compact 30x15 layout print the bare Ms, no border and no wordmark: PWRD is a fifth of the frame's height, so a logo sized to fit the space those two had spare sets it under a millimetre.

