Skip to content

About

Meshtastic contact-QR nametag kiosk for Niimbot label printers — DM a node over the mesh, get a printed badge

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

HELLO MY NODE IS

CI PyPI Python License: GPL-3.0 CLA assistant Ruff

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. 🦞

Two printed nametags resting on a Niimbot label printer: the landscape card layout on 50x30 mm stock above, showing the M-PWRD mark, a caterpillar emoji, the short name WPG, the long name Wandering Packet Goblin, the node ID !deadbeef and a contact QR; the compact layout below, with its QR on the left and the mark bottom-right

Hardware

  • Raspberry Pi with a Meshtastic node attached (USB serial radio, or meshtasticd with a LoRa hat — use --tcp localhost for the latter)

  • A Niimbot label printer, USB-C cable strongly recommended (2.4 GHz at DEFCON is a war zone). Pick yours with --printer:

    --printer head labels layout
    d110 (default) 12 mm / 96 dots continuous roll, or --die-cut 30x15 mm dynamic-length banner, or a compact QR-left card
    b1 48 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.

Close-up of the card layout as printed, showing the header band, emoji, names, node ID and QR at full detail

Setup

On the Pi:

sudo apt install -y python3-venv fonts-dejavu-core
python3 -m venv ~/badge
~/badge/bin/pip install labeltastic

That 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).

Provisioning nodes for QR pairing

labeltastic-provision gets nodes ready to ship with a pairing QR code. It watches USB, and for each node plugged in it:

  1. reads the node's BLE address (DeviceMetadata.ble_address),
  2. writes a connect link for a random 6-digit PIN and its QR code to provisioned/<BLE MAC>.png,
  3. appends the node ID, hardware model, address, PIN and link to provisioned/provisioned.csv,
  4. sets Bluetooth to FIXED_PIN with that PIN (the node reboots), and
  5. reconnects and reads the PIN back, recording the row's status: verified, mismatch (the node did not take it), unverified (it never came back) or write failed. Ship only verified rows.

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 only

Nodes 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.

Connect links

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"

Development

uv sync --extra dev
ruff check . && mypy && python scripts/check_spdx.py && pytest

None 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).

Notes

  • 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.ttf is 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.svg in 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.

About

Meshtastic contact-QR nametag kiosk for Niimbot label printers — DM a node over the mesh, get a printed badge

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages