Nzyme Alerts Go Mobile - Building a Wireless Bandit Pager

When I first set up Nzyme and took it to BSides London, it was missing something pretty important: a way to alert me properly when chaos broke out.
At the time, the only supported alerting mechanism was email, and while that worked in theory, it didn’t quite fit the “real-time incident response” vibe I was going for.
So, I wrote a script to query the Nzyme database directly and used Pushover to send those alerts straight to my phone.
That setup worked brilliantly 😄 every time I powered up a rogue access point or triggered a Wi-Fi attack, my phone would light up and make a wonderfully dramatic klaxon sound.
Then came the SSID spam incident (oops). Hundreds of alerts later, my free Pushover quota was toast.
It was time to evolve and build something better.
The Idea: A Physical Alert Badge
Instead of relying on my phone I wanted a hardware alert device, something that would light up, beep, and generally make a fuss whenever Nzyme spotted a wireless bandit.
I’d tinkered with Badger 2040 and MagTag devices before, but this time I came across a brilliant post by Ayan Pahwa:

In his article, Ayan builds a cloud-connected badge using CircuitPython on the Adafruit PyBadge (a credit card sized device that can run CircuitPython or Arduino). It’s packed with fun features:
- full-colour display
- built-in speaker
- Li-Po battery support
- buttons and NeoPixels
Luckily the PyBadge also has co-processor support via FeatherWing, a pluggable board that brings extra capability, for example the Adafruit Airlift FeatherWing which has an ESP32 co-processor with Wi-Fi radio so we can connect to a wireless access point or hotspot.
This looked perfect for what I wanted - a portable device that can be battery powered, connects to Wi-Fi, has a colour screen and can make noises when an alert is triggered.
Add to this the simplicity of CircuitPython: just plug in the device and write code.
A Quick Win: Syslog Support
Right as I started planning, Nzyme added support for sending alerts to syslog which made life much easier. Instead of scraping the database, I could just tail syslog and process alerts however I wanted.
That became the backbone of my new alerting pipeline.
The Plan

Conceptually, I wanted this flow:
nzyme node → syslog → MQTT → badge → alert sound + display
Here’s how it fits together:
- Nzyme sends alerts to syslog
- A lightweight script tails syslog and publishes matching entries to MQTT
- The badge subscribes to the MQTT topic
- When an alert arrives, the badge lights up, plays a sound, and shows the alert on screen
Stage 1 - Nzyme Alert Configuration
We need to configure Nzyme to send alerts to Syslog. Nzyme has a nice 'events and actions' configuration, and it's simple enough to create a 'wildcard' alert that fires an alert on all detection types.

I'm sending all of my alerts to the local syslog to keep things simple, but you could use a remote syslog service if you wanted 😄
Stage 2 - Syslog to MQTT Script
The script is running on the syslog host, in my case this is the same host as the Nzyme node. It watches syslog for alerts matching the 'Syslog Hostname' I configured in Nzyme (nzyme-alert), and then publishes the alert to my MQTT service.
I won't cover setting up MQTT, but there are Ubuntu packages that make it simple. You could even use a SaaS MQTT service (e.g. HiveMQ) if you wanted.
#!/bin/bash
LOG_FILE="/var/log/syslog"
PATTERN="nzyme-alert"
tail -F "$LOG_FILE" | grep --line-buffered "$PATTERN" | while read -r line
do
ALERT_STRING=$(echo "${line}" | awk '{for(i=4;i<=NF;++i)printf $i" "; print ""}' | sed 's/"//g')
/usr/bin/mqtt pub -i nzymealertscript -h mqtt.server.here -p 8883 \
-u mqtt.username -pw 'mqtt.password' -r -t alerts -q 1 -m "${ALERT_STRING}" -d
done
Inelegant, but it works
I also created a systemd unit file to start this script on boot.
[Unit]
Description=nzyme-syslog-monitor
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
Restart=always
RestartSec=5
User=root
ExecStart=/opt/nzyme-syslog-monitor.sh
[Install]
WantedBy=multi-user.target
Stage 3 - Building the Badge
You can find the PyBadge for sale directly from Adafruit. At this time it's out of stock. You could instead get the EdgeBadge instead... same hardware layout, plus a few TensorFlow features I definitely don’t need (yet).

Since I'm using CircuitPython to build my application, I installed that on the device (see here) and downloaded Mu editor which is a nice, lightweight IDE with serial console support for the device.
As mentioned earlier, for Wi-Fi, I added the Adafruit Airlift FeatherWing ESP32 co-processor.

A quick bit of soldering later and I had:
- the badge with AirLift attached
- a Li-Po battery
- and an external speaker (because subtlety is overrated)
After tweaking the code for the latest CircuitPython libraries, I added graphics, icons, and alert sounds 😄 the fun bits.
Below is the code and a list of the libraries I've used from CircuitPython. Since it's python I didn't heavily comment the code, it's adaptable and genuinely I enjoyed writing it 😀

import board
import busio
import neopixel
from os import getenv
from digitalio import DigitalInOut
from adafruit_pybadger import pybadger
import adafruit_minimqtt.adafruit_minimqtt as MQTT
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_connection_manager
sound = getenv("SOUND")
ssid = getenv("SSID")
ssid_pass = getenv("SSID_PASS")
ssid2 = getenv("SSID2")
ssid_pass2 = getenv("SSID_PASS2")
ssid3 = getenv("SSID3")
ssid_pass3 = getenv("SSID_PASS3")
mqtt_host = getenv("MQTT_HOST")
mqtt_port = getenv("MQTT_PORT")
mqtt_user = getenv("MQTT_USER")
mqtt_pass = getenv("MQTT_PASS")
# esp connection pins
esp32_cs = DigitalInOut(board.D13)
esp32_ready = DigitalInOut(board.D11)
esp32_reset = DigitalInOut(board.D12)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
pool = adafruit_connection_manager.get_radio_socketpool(esp)
sslcontext = adafruit_connection_manager.get_radio_ssl_context(esp)
def wificonnect():
while not esp.is_connected:
try:
if not esp.is_connected:
esp.connect_AP(ssid, ssid_pass)
if not esp.is_connected:
esp.connect_AP(ssid2, ssid_pass2)
if not esp.is_connected:
esp.connect_AP(ssid3, ssid_pass3)
except OSError as e:
print("could not connect to AP, retrying: ", e)
continue
# print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi)
def connect(mqtt_client, userdata, flags, rc):
print(f"Connected to MQTT Broker {mqtt_host}")
mqtt_client.subscribe("alerts", 1)
def subscribe(mqtt_client, userdata, topic, granted_qos):
print(f"Subscribed to {topic} with QOS level {granted_qos}")
def message(client, topic, message):
if message == "":
return
print(f"New message on topic {topic}: {message}")
if "with unexpected BSSID" in message:
parts = message.split()
network_name = parts[2]
pybadger.show_badge(
name_string=network_name, hello_string='Unexpected', my_name_is_string="BSSID for", hello_scale=2, my_name_is_scale=2, name_scale=2
)
elif "on unexpected frequency" in message:
parts = message.split()
network_name = parts[2]
pybadger.show_badge(
name_string=network_name, hello_string='Unexpected', my_name_is_string="frequency for", hello_scale=2, my_name_is_scale=2, name_scale=2
)
elif "Bandit" in message and "detected in range" in message and "Pwnagotchi" in message:
parts = message.split()
pybadger.show_badge(
name_string=parts[4], hello_string="Bandit", my_name_is_string=parts[1], hello_scale=2, my_name_is_scale=2, name_scale=2
)
elif "Bandit" in message and "detected in range" in message and "WiFi Pineapple" in message:
parts = message.split()
pybadger.show_badge(
name_string=parts[4], hello_string="Bandit", my_name_is_string="WiFi Pineapple", hello_scale=2, my_name_is_scale=2, name_scale=2
)
else:
pybadger.show_badge(
name_string=message, hello_scale=1, my_name_is_scale=1, name_scale=1
)
if sound != 0:
pybadger.play_file(file_name="alert.wav")
mqtt_client.publish("alerts", "", True, 1)
def mqconnect():
mqtt_client.connect()
pybadger.show_business_card(image_name="nzymelogo.bmp")
if sound != 0:
pybadger.play_file(file_name="startup.wav")
mqtt_client = MQTT.MQTT(
client_id="badge",
broker=mqtt_host,
port=mqtt_port,
username=mqtt_user,
password=mqtt_pass,
socket_pool=pool,
is_ssl=False,
# ssl_context=sslcontext,
recv_timeout=11,
connect_retries=3,
socket_timeout=9,
)
mqtt_client.on_connect = connect
mqtt_client.on_subscribe = subscribe
mqtt_client.on_message = message
while True:
try:
pybadger.auto_dim_display()
mqtt_client.loop(10)
except Exception as e:
try:
print("Re-Connecting: ", e)
wificonnect()
mqconnect()
except Exception as f:
print("Re-Connecting Failed: ", f)
print("polling....")
code.py
The Badge
Now whenever Nzyme detects a wireless network bandit, or a rogue access point, the badge lights up, plays a sound, and shows the alert in near real-time!
No phone apps, no cloud subscriptions, and no rate limits - just a self-contained little device that yells at me when the Wi-Fi gets spicy 🔥
And yes, it looks very cool handing off a lanyard at conferences.



Demo Time
That's it for now... next up I want to explore some other Nzyme features including Bluetooth device identification (for all the Flipper Zero's out there) and UAV tracking.
I'll take Nzyme to BsidesLondon in December 2025, so if you want to know more just find me on the conference floor!