PUEO

Surveillance

These are this fork's, plus the bench beacon that proves the radio side of them works. Everything else in the menus arrived with ESP32-DIV and was touched only where the pin map or the shared bus forced it, which is a different list. That one is under what's different.

Surveillance

Called Spotter until 0.4.0, and still Spotter in the source. The old name did not say what you get; the list is plate readers, body cameras, smart glasses and vehicle modules. It moved out of Bluetooth at the same time, along with the drone detector: neither is a Bluetooth feature, both listen on WiFi and BLE. They sat behind a tile called More until 0.4.13 and now have their own, Detect.

The mark Surveillance opens on: an owl in orange silhouette perched facing forward, both eyes wide, with straight sight-lines running out to six pieces of surveillance hardware arranged above it - a pole-mounted licence-plate camera, a body-worn camera, a dashcam, a CCTV dome, a car and a radio mast. The word Surveillance sits below in white. The Surveillance screen listing six detections, each three lines deep. Three red rows read ALPR Flock Safety, ALPR Flock SSID marked with a red double asterisk, and BODYCAM Axon Enterprise heard over BLE. An orange row beneath them reads VEHICLE KARR BT module, also over BLE. Two grey rows at the bottom read ALPR Liteon and ALPR LAA, not a vendor. Each row carries a MAC address, the radio, signal strength, a sighting count and how long it has been in range. The dwell alert for a tracker. An owl in orange silhouette perches on a bar with five broadcasting antennas ranged along it, filling most of a black screen. Below the mark, DWELL in orange, then AirTag 12 min in white, then may be travelling with you in grey, and Hunt can walk you to it in orange. The same dwell alert for something that is not a tracker. The identical owl and antenna mark, then DWELL in orange, Flock Safety camera 12 min in white, and in range this whole time in grey. There is no fourth line and no suggestion to open Hunt.

The mark it opens on, the list, and the dwell alert in both its forms. The last two differ by one line: a BLE tracker is offered Hunt can walk you to it, a camera is not, because Hunt lists trackers and nothing else.

Surveillance is one of six features this fork added rather than inherited,
and the one with the most to say. It listens for
hardware that announces itself to anyone in range, and does nothing else.
Two hundred and eighty-three signatures over ten kinds: plate readers and
their accessories, body cameras, fixed cameras and doorbells, smart glasses,
item trackers, vehicle modules, gunshot sensors, pentest hardware and
mesh nodes. Every OUI in
there is checked against the IEEE registry by check_oui_registry.py, and
every Bluetooth identifier against the SIG's assigned numbers, before it
counts as a strong finding.

Two of those kinds are not surveillance gear, and are here because knowing
they are nearby is worth something on its own. Pwnagotchi beacons from a
fixed de:ad:be:ef:de:ad with its stats in the payload, so it announces
itself more clearly than most of what it is listening for. Meshtastic
nodes advertise a 128-bit service of their own and get a kind of their own
with it, rather than being filed under something they are not.

What announces itself

ALPR cameras broadcast WiFi probe requests hunting for the network they
upload to. They do it whether or not a hotspot is there and whether or not
anyone is listening, so a receiver hears them without transmitting anything.
Smart glasses advertise over BLE, and Meta's put both a Luxottica company
ID and a Meta service UUID in the same packet. Body cameras carry WiFi so
they can offload to a dock, which makes an Axon unit audible the same way.
00:25:DF is Axon Enterprise's own registered block, so unlike most of what
follows it is strong evidence of Axon hardware in range, though, not of a
camera specifically, because the same block covers their docks, their in-car
Fleet systems and their TASERs.

That block is matched on BLE as well as WiFi, which is what 0.3.2 added, but
only it and Flock's, and only against a public address. The table holds
135 OUI rows now, and the 61 strong ones are the
ones that reach BLE: Ring, Hikvision, Axis, Verkada, Avigilon, Wyze and
Flipper among them, every one a block IEEE assigned to the company named on
the row.

Fifteen are graded likely. Seven are Motorola Solutions and Genetec,
because Motorola is the public-safety radio industry before it is a camera
company and one of its addresses is more often a handheld radio. The other
eight are Zigbee coordinators, Hue, SmartThings, Aqara and IKEA, a grade down
for the same reason read the other way: the block belongs to the company, and
the company sells more than the hub.

The remaining 58 are weak, contract manufacturers and module vendors
and the radio blocks pentest kit is built on: 23 Liteon, two Espressif, two
USI, Murata, Realtek, Telink, Alfa. Pwnagotchi's de:ad:be
is here too, weak as a prefix because it is a convention rather than an
allocation and anything at all can wear it. The whole
de:ad:be:ef:de:ad is a strong row in its own table,
where six bytes make the claim that three cannot.

A WiFi scan only hears the ones probing for a network, whereas a BLE scan
hears everything advertising in the room. Espressif's block on that list would
report a shelf of dev boards as surveillance hardware, and the device doing
the scanning is an ESP32. The public-address condition
matters for a subtler reason: a random BLE address encodes its type in the top
two bits rather than a vendor, and 00:25:DF begins 00, which is exactly the
shape of a non-resolvable private address. The strongest entry in the table is
also the one a randomised address could wear by chance.

KARR, and the cars carrying one

Vehicle modules joined the list in 0.3.3. KARR is a dealer-installed
immobiliser and remote control, and UC San Diego's BLE
Theft Auto (USENIX Security 2026) found it second only to Tesla among BLE
devices on San Diego's highways (608 of 4,314 unique devices) because
dealerships fit one to every car on the lot. Every module shares one
hard-coded key. It advertises a name of QT or DR followed by exactly eight
characters, which is the whole signature.

At least a million vehicles carry it, and in roughly half of those the
owner does not know, the dealer fitted it and they never bought the
service.
That is the case for a passive detector in one sentence: the question "is one
of these in my car" currently has no answer that does not involve asking the
dealership that installed it.

That exactness is new machinery. Name signatures matched on a prefix alone,
case-insensitively, which is fine for Spectacles and useless for
DR, a two-letter prefix claims every device whose name starts that
way, and a match is what puts a row on the screen. So a signature can now
declare the exact length it expects.

It is graded likely, and it names a module rather than a finding. The
vulnerability was patched on 2026-07-20, but the fix is applied by hand and
nothing in the advertisement says whether it has been. The row means a KARR
module is in range. It does not mean the car is vulnerable.

Applied by hand is doing a lot of work in that sentence. Most KARR modules
have BLE and nothing else (no cellular, so no over-the-air update), and the
patch has to be pushed to each one. Owners who never bought the service have
no account to authenticate with, so the vendor lets them present their
VIN instead, against a database of the cars modules were pre-installed
in. A fix that requires each owner to discover a device they did not know
about, and then prove which car they own, is not one you should assume has
been applied to the module in front of you. It is also not one you can rule
out from a scan, which is why the row says what it says.

The wardriver will not upload them. The same paper describes the attack
beginning with a name-pattern query against a public wardriving database
(1,141,798 of those QT addresses were pulled from WiGLE exactly that way), so
adding fresh sightings with coordinates would be contributing to that index.
The WiGLE conversion drops them and reports how many it dropped.

The reason that matters is in the same paper. Of fifty modules sampled from
WiGLE, 31 of them (62%) had been seen three or more times inside a
100 m radius: not a car passing on a road, but a car that sleeps somewhere. A
wardriving database is a list of where things are parked, and for this
particular class of thing that is most of an address.

The log on the card keeps them. Withholding from a public database and lying
to the operator about what their own radio heard are different things, and
only the first is wanted.

Receive only, and deliberately so. Surveillance never transmits, never
associates, never deauthenticates. It puts the radio in promiscuous mode and
runs a passive BLE scan, which is what any WiFi analyser does. That is a
legal statement as much as a technical one: listening to a broadcast is a
different act from injecting a frame, and this stays on the listening side.

Tyre sensors, and which kind these are

Not the ones your car reads. Factory TPMS is a valve-stem transmitter on
315 MHz or 433 MHz, talking to a receiver in the car, and nothing here
decodes it. The CC1101 tunes across both, so the hardware could, and no
feature does. If that is what you came for, it is not here.

These are the Bluetooth ones. Aftermarket valve-cap sensors that pair
with a phone app, and a handful of vendors who put BLE in the sensor itself:
Schrader, Goodyear, Pacific, Huf, FOBO, TireCheck. Strong on a vendor's own
company ID, because nothing else uses one. The four wheel-position rows are
only Likely: they are company 0x0001, which is Nordic's, so the
payload byte is doing all the work and the grade says so. The SIG's own tyre
pressure service is Weak for the same reason the other allocated services
are, since anything implementing the standard lands on it.

Worth more than they look. A tyre sensor broadcasts a stable identifier
for one wheel, continuously, and the owner did not choose it and cannot
switch it off. Four of them is a vehicle fingerprint that survives a change
of number plate, and following a car by its tyres is a known technique rather
than a hypothetical one. That is the same argument as KARR: something
bolted to a car, broadcasting, that its owner never agreed to.

What the REC tag is telling you

While a capture is running the tag reads REC and the row
count, in red. Three things change it, and all three turn it orange,
because all three mean the file is not what you would otherwise assume.

REC 412 !7 is seven frames the capture ring dropped
because the card could not keep up. A capture that is quietly incomplete is
worse than one that says so: before this, the only evidence was rows that did
not add up to what the radio saw. The count appears only when it is not zero.

log off is SD logging switched off in Settings,
either for Surveillance or by the master switch, with a card sitting in the
slot. no SD is the card itself.

Filtering what you see

Nine kinds and three confidence levels is a lot of rows in a car park. The
left button opens a filter: toggle any of the ten kinds, and set a floor of
everything, likely and up, or strong only.

Two things about it are deliberate, and both exist because a filtered list
and a quiet street look identical.

It only hides rows. Everything is still detected, still counted, still
written to the card. A filter that stopped recording would make the capture
depend on what the screen happened to be showing while it ran, which is not
something you can read six months later.

It resets when you leave the screen. The way a remembered filter fails is
silent: you set strong-only in a car park, walk somewhere that matters, read
an empty screen and conclude there is nothing there. And while any filter is
on, the header counts both ways, hits 4/13 rather than hits 13, with the
second number being the one that has not changed.

The signature table

The signatures are the part that goes stale, so they live in their own file,
away from the matching logic. The contents of that file are on the
signature list page; what follows is what each
table can see. Vendors change contract manufacturers, buy new
OUI blocks and revise firmware; the code that compares three bytes does not.

There are nine tables, and the shape matters more than the contents. A vendor
in the list goes out of date; what a table can see does not, and it is
what explains why something was or was not found.

The whole address              1 row.  For hardware with no vendor block at
                               all, only a hardcoded joke: the Pwnagotchi
                               advertises de:ad:be:ef:de:ad on every unit.
WiFi source MAC prefix      135 rows. The first three bytes, which IEEE
                               assigns to a manufacturer. The oldest and
                               bluntest of these.
Network name, anchored        5 rows. An SSID or BLE name matched from the
                               start, optionally at an exact length.
Network name, anywhere       78 rows. The same, matched in the middle. A
                               Pineapple's management SSID has whatever the
                               owner typed in front of it.
BLE company or service       47 rows. The manufacturer ID in an
                               advertisement, or a 16-bit service UUID.
BLE advertised name           7 rows. The name a device puts in its own
                               advertisement, which it chose and can change,
                               so it is the softest of the BLE tables.
128-bit service UUID          2 rows. Somebody's own protocol rather than
                               an allocation out of the shared SIG space,
                               so a hit is worth much more.
Manufacturer data             5 rows. The bytes past the company ID. This
                               is what distinguishes an AirTag from every
                               other Apple device in range: company 0x004C
                               is an iPhone, and the type byte 0x12 is a
                               Find My accessory separated from its owner.
Service data                  3 rows. The bytes past a service UUID. Both a
                               Find My Device tag and a shop's beacon
                               advertise 0xFEAA; the frame type says which.
                               One of these is DULT, the cross-vendor
                               advertisement a tracker is supposed to send
                               when it is separated from its owner, which
                               makes it the one row that covers vendors who
                               have not shipped yet.

The last three arrived together, and the AirTag is the reason. Surveillance
had Tile, Samsung SmartTag and Eddystone and no Find My rule at all, so the
most common tracker there is did not appear on the screen whose job is
finding trackers. It could not: nothing in the file could read a byte past a
company ID. The gap was a missing kind of question, not a missing vendor.

Where a batch of the vendors came from: Flock Safety's own IEEE block, the
Flock-, bare Flock and Flock Camera net. network names, the Penguin battery
pack's BLE name and XUNTONG company ID, the Raven camera's GATT UUIDs, and
the 33 WiFi prefixes that the flock-you and flock-finder community
collections have between them.

Those 33 are mostly not what they look like. Resolved against the IEEE
registry, twenty-three belong to Liteon and two to Espressif: contract
manufacturers and a module vendor whose blocks sit in an enormous amount of
unrelated consumer hardware. One is registered to nobody, and one has the
locally administered bit set, which is to say it is a randomised address
rather than a vendor block at all. Reported as a camera on their own they
would be wrong far more often than right, so each carries the name the
registry actually gives it and each counts as weak evidence only.

Which is what the confidence scoring is for. A bare OUI is a hint. Two
independent fields agreeing, a weak prefix on the same MAC as a Flock-
network name, is a finding, and the screen says which of the two it is
showing you. A detector that cries wolf is one you stop believing.

When the address stops helping

The OUI is already being designed around. One of those 33 prefixes has the
locally administered bit set, meaning it is not a vendor block but a made-up
address, and flock-finder reports newer units randomising precisely to beat
prefix matching. A camera that changes its address defeats the table above
completely, and it defeats the list it would have appeared in too: the hit
table is 48 rows keyed on the address, so one rotating radio fills it on its
own.

What does not change when the address does is the radio. A probe request
carries a set of information elements whose identity, order and contents are
chosen by the chipset, the driver and the supplicant rather than by the
network being looked for. Hashing that gives a fingerprint a device cannot
randomise away without becoming a different device.

Two things are deliberately kept out of the hash: the network name, which is
the one field that varies between two probes from the same radio, and the
channel, because Surveillance hops channels every 260 ms and hashing it would
give one camera thirteen fingerprints. Everything else contributes, and
the element ids contribute even where their contents do not, because which
elements a driver emits, and in what order, is itself the signature.

There is no fingerprint table, and that is deliberate. Values for one need
packets captured off a real camera, and none have been taken here. Seeing a
row appear on the panel is not a capture.
FlipDeFlock has some; this fork moved to GPL-3.0 partly so they could be
used, with attribution and without its name, which its trademark file keeps
separate from its code. They are still somebody else's captures rather than
mine. The fingerprint is shown on screen to be written down, and the table
stays empty until somebody stands next to a camera.

Three things work with no table at all, which is why they were built first.
A rotating radio collapses to one row rather than filling the list, five
hundred rotations leave forty-seven rows free. A made-up address is marked
rnd, which is exactly the case where the OUI beside it means nothing. And
every row carries how long the device has been in range, because a handset
walks past and a camera is bolted to a pole.

That collapsing is the one place this can quietly be wrong, so it is narrow:
two addresses only fold together if both are made up. Two cameras of a model
share an element set, and a rule that merged on the fingerprint alone would
report one where there are two, which is the wrong direction for a
detector to fail in.

All of that is in 0.2.2, and in the download. The capture is on the Log
button and it writes to the card, not anywhere else.

The dwell alarm

Ten minutes is when it stops being a number and starts being an alert. The
duration column has been on every row since 0.2.2, which is a number you have
to be looking at to read, and the case this feature is for is the one where
you are not looking at it. In 0.4.3 the first row to cross ten minutes brings
up the mark above, the device and how long, and two rising notes.

Shown once, then gone. It fades rather than staying, because this is a
passive monitoring screen and a banner that stayed would be covering the list
you wanted to read. It re-arms when the last dwelling device goes quiet, so
something that leaves and comes back says so twice, which is the case worth
hearing about twice.

The two screens differ by one line, and that line is the whole point. Hunt
lists BLE trackers and nothing else. A dwelling plate reader or body camera
will not appear in its picker, so offering it anyway would be sending someone
to an empty screen and calling it advice. A tracker gets Hunt can walk you
to it; a camera gets the duration and no suggestion.

May be is doing real work in the other line. Pueo has no position
of its own. It cannot tell a tracker moving with you from a beacon you have
been sitting beside for ten minutes, and from here the two are identical. It
reports how long, and points at the tool that would settle it. It does not
decide.

The sound is the half that works when the screen does not. The 3.5" board
drives a speaker from GPIO 26, which is how a device in a bag or face down on
a table can raise an alarm at all. Two caveats, both worth knowing before
deciding the firmware is broken: the 2.8″ board had that pin free but spent
GPIO 4 on a radio's chip select and is silent by construction here, and the
3.5" ships with no speaker fitted. The output comes out to a 1.25 mm
two-pin connector and you solder your own.

The firmware used to assert GPIO 4 as an amplifier enable alongside this,
which was read off a datasheet for a board this is not built on. On the real
one GPIO 4 is the RGB LED's red channel, so what that line did was blink the
LED for the length of every beep. Removed on 2026-09-23. The beeps sit inside the hold
rather than adding to it, so the alert lasts the same time whether or not it
makes a sound.

Finding an unknown tracker

Surveillance finds, Hunt locates, and the step between them is yours. The two
screens answer different questions and neither answers the one that matters
on its own.

1. Leave Surveillance running. It is passive, so there is nothing to start
and nothing that announces you. Put the device in a bag or face down; the
dwell alarm is audible and that is what the speaker is for. Filter to
TRACKER if a car park is producing more rows than you want to read.

2. Wait for the dwell alarm. Ten minutes of continuous presence is what
turns a row into an alert. A tracker that has been with you that long is
interesting; a beacon in the shop you are sitting in has been there longer
and looks identical from here.

3. Separate the two by moving. This is the part Pueo cannot do, and it
is worth being plain about why: not the firmware, the device. There is no
GNSS fix behind this screen and no dead reckoning. It
hears a radio and knows how long it has heard it, and from inside the device
"a tag in my coat" and "a beacon by the till" are the same observation. Walk
somewhere else, somewhere a fixed beacon cannot follow, and wait. What comes
with you is the thing worth chasing.

4. Walk it down with Hunt. Pick the row and the gauge opens. It reads
signal strength, smoothed, with a green tick at the best reading so far and
WARMER / COLDER in large letters you can read at arm's length. It is not
distance and it does not pretend to be: an unobstructed tag across a room can
read stronger than one under a seat two feet away.

Sweep rather than stare. Turn slowly on the spot and watch where the needle
peaks, move a few paces that way, and let the reading settle before judging
it. Your own body between the device and the tag is worth 10 dB, which is
useful: block it deliberately and the direction it drops is the direction it
is in.

What will not appear, and why that matters

A screen with nothing on it is not evidence of nothing. These are all
invisible to a passive listener and none of them are unusual:

  a tracker in its paired state   Apple's separated-from-owner broadcast is
                                  type 0x12. A tag still near its owner says
                                  0x07 and Surveillance leaves it alone,
                                  deliberately: somebody's own keys beside
                                  them are not surveillance.
  anything cellular-only          an LTE tracker with no BLE advertisement
                                  is silent on every radio this device has.
  anything switched off           a tag with a dead cell, or one that only
                                  wakes periodically, between wakes.
  5 GHz Wi-Fi                     the ESP32 is 2.4 GHz. A 5 GHz-only access
                                  point does not exist as far as this is
                                  concerned.
  a wired or recording-only       a dashcam writing to a card announces
  device                          nothing at all.

So the honest reading of a quiet screen is "nothing that announces itself is
here". That is a real and useful statement, and it is narrower than "nothing
is here". A tool that blurs the two gives a false sense of security.

Handshakes

Handshakes, in the capture that is already running. Packet Monitor writes
every frame it hears to a pcap on the card, data frames included, which is
where a WPA handshake travels. It now recognises them. The 802.1X payload is
found by computing the header length from the frame control field rather
than testing a fixed offset (a frame can carry a fourth address or an HT
Control field, and then the payload is somewhere else), and the four
messages are told apart by three bits of the Key Information field. The
header shows how many networks have M2 plus M3, which is the pair worth
having.

Recognising them was not enough, because they were being dropped.
The pcap pool drops frames when it fills, which on a busy channel is
constantly. That is harmless for ordinary traffic and fatal here: the four
messages arrive together, in a burst, at the moment the channel is busiest,
and losing one makes the other three worthless.

A bigger pool was the wrong fix, and the board said so. Raised from
three slots to sixteen, it reported four key frames seen and four not
written. A bigger pool fills later; the burst still arrives after it has
filled. Key frames have eight slots of their own now that nothing else can
take, so ordinary traffic cannot evict them however heavy it gets.

Every capture ends with one line saying what happened:

  written=266 dropped=56 keyframes_lost=0

Fifty-six ordinary frames the general pool could not hold, which is expected
and was always happening. No key frame lost. The condition that destroyed
every handshake is still present in the traffic and no longer reaches the
frames that matter.

Proved rather than argued: a capture dropping 1,387 of 4,268 frames, 32 per
cent, while four key frames arrived consecutively and every one reached the
card. Those files were then checked at byte level, not with the parser they
are a fixture for, against Wireshark's own WPA reference capture.

Forcing a handshake, by deauthenticating somebody so their device
reconnects, is not wired into this. The decision logic exists and stops
itself on capture or after six bursts, and nothing calls it: that half
transmits at other people's equipment, and it is a different thing from
listening.

That is about the capture rather than about the device. WiFi Deauther
came with ESP32-DIV, is still in the Wi-Fi menu as a tool of its own, and
Stealth Mode gates it like every other transmitter. What is absent is the
automatic version, a capture that nudges whatever it happens to be watching
with nobody choosing a target.

What has and has not been tested

One row has come off real hardware. A Flock ALPR raised one on the
panel, which is the ALPR signatures and the matching path confirmed together,
on one device, on one occasion. Nothing was captured or logged off it, so
there is no artefact to point at and nothing to build a fingerprint from.

The rest of the table has not been near the hardware it names. Those
signatures come out of public research and other people's firmware
teardowns, not out of packets captured off a device. See Status.