Surveillance-device detector for the ESP32-2432S028R ("Cheap Yellow Display").
MuleSkin-CYD sniffs the 2.4 GHz airwaves for known wireless signatures of Flock Safety cameras, Axon body cameras, recording glasses, card skimmers, AirTags, drones, proximity beacons and pentest hardware. It runs standalone on a bare CYD board — no PC, no extras, just plug it into USB.
The UI is a neon radar: the hooded-MuleSkin artwork on the splash, a sweeping radar scope behind the main screen, full-screen ALERT cards, and headlines in Orbitron.
A fork of SquachWatch-CYD by skizzophrenic. The detection engine, the board ports and most of what follows are their work; this fork rebrands it as MuleSkin, reworks the look and the main screen, and leaves out the board-to-board squad mesh. Like the original, it is GPL-3.0.
| Type | What | How |
|---|---|---|
FLOCK |
Flock Safety ALPR cameras | 29 WiFi OUI prefixes + BLE name + company ID 0x09C8 (the Penguin battery packs' radio; Medium unless the name agrees) |
AXON |
Axon body cameras, TASERs, LE equipment | 3 WiFi OUI + SSID prefixes AB2-/AB3-/AB4-/AXON- |
META |
Camera glasses — Ray-Ban Meta, Snap Spectacles | BLE service UUID 0xFD5F + Meta / Luxottica / Snap company IDs |
SKIMMER |
Bluetooth card skimmers (HC-05/06/03, RN42, BT04-A; HC-08 and FCD_1608 by rules set 4) | BT Classic name match + SPP UUID 0x1101 + 3 OUI |
RAVEN |
Raven gunshot detector | Service UUIDs 0x3100–0x3500 |
AIRTAG |
Apple AirTag / Find My trackers | Company ID 0x004C + Find My payload check. The Find My beacon itself (4C 00 12 19) is what a tag sends only when it is away from its owner -- HIGH, "AWAY FROM OWNER" on the alert and in the push, and the only AirTag that counts toward WITH YOU; any other 30-byte Apple advert (AirPods pairing too) is LOW, logged and never alerted |
DRONE |
Remote ID drones, and DJI / Tello drones that send none over WiFi | Service UUID 0xFFFA, then the ASTM F3411 message decoded — aircraft position, altitude, serial, and the operator's location; DJI's 10 registered OUI and the Tello's TELLO-/RMTT- network (rules set 4) |
ALPR |
Motorola Solutions / Genetec plate readers | 6 WiFi OUI |
CAMERA |
Generic / covert IP cameras | 17 WiFi OUI (Wyze, Amazon, Tuya, Verkada, Avigilon, Axis, …) |
SAMSUNG_TAG |
Samsung Galaxy SmartTag / SmartTag+ | BLE service UUID / service data 0xFD5A; its tag state (bits 5-7 of the first byte) 2 or 3 is lost / long lost -- away from its owner, HIGH, Away-Owner -- else MED |
GOOGLE_TAG |
Google Find My Device trackers (Chipolo, Pebblebee, Moto Tag), and any tracker on the DULT standard | Service data under 0xFEAA with the tracker frame type 0x40/0x41 -- not the plain Eddystone beacons that share the UUID. 0x41 (Google's unwanted-tracking mode = DULT's separated state) is away from its owner, HIGH; 0x40 MED. And DULT's own 0xFCB2 service data with the near-owner bit clear: a separated tracker of any brand |
TILE |
Tile BLE trackers | BLE service UUID 0xFEED / 0xFEEC |
RING |
Ring doorbells / cameras | 15 WiFi OUI (Ring LLC's registered block + Amazon's) |
DEAUTH |
WiFi deauthentication floods | Rate-detected burst, not a signature |
EVILTWIN |
Rogue / spoofed access points | One SSID beaconing from two BSSIDs that disagree about encryption |
IBEACON |
Retail proximity beacons | Exact Apple header 4C 00 02 15 — off by default, see below |
HACKER |
Flipper Zero, Pwnagotchi, WiFi Pineapple, ESP deauthers, karma APs | Flipper's service UUIDs 0x3080–0x3083, company ID 0x0E29 and OUI 0C:FA:22; the Pwnagotchi's own beacon payload; Pineapple_ and pwned SSIDs; and any access point answering to five network names in five minutes from one address (beacons and probe responses) -- a karma AP, Karma-AP |
Every hardware prefix in the firmware was checked against the IEEE registry rather than against other detectors. Of 76 rows: 32 High, 4 Medium, 40 Low.
That grading matters most on FLOCK, where exactly one of 29 prefixes is
registered to Flock Safety and the rest are the generic Espressif and Liteon
parts they build on — real evidence, shared with every dev board on earth.
ALERT FILTER is a minimum-confidence gate, so setting it to High keeps a
passing ESP32 in the log without taking over the screen.
The audit also removed 00:0E:58, which sat here for eleven releases
labelled "Vigilant" and is registered to Sonos. Every speaker in range
was being logged as a plate reader.
IBEACON ships switched off — not a judgement about importance, one about
volume. One shop can put more beacons in range than this device would
otherwise see all week. It is one tap away in DETECTION FILTER.
The table above is what is built in. On top of it, a board with a saved WiFi
network fetches a signed rule set from flasher.oillie.cloud at every boot
check (web-flasher/signatures.txt): extra address prefixes and WiFi
network names, used only when the signature checks out. Rules only ever add:
a prefix the firmware already knows keeps its built-in meaning. Rule set 2
adds 44 prefixes read out of the IEEE registry -- WatchGuard, Digital Ally
and BodyWorn body cams, ShotSpotter, Neology and Kapsch plate readers, and
Axis, Hanwha, Hikvision, Dahua, Uniview, Reolink, Amcrest and other camera
makers -- and three camera setup hotspots (davinci, HAP_, DAP-).
- ESP32-2432S028R ("Cheap Yellow Display" / CYD) — about $15. Built-in 320×240 ILI9341 TFT, XPT2046 resistive touch, and an onboard microSD card slot.
- Elecrow CrowPanel Advance 7.0 — experimental: an ESP32-S3 with an
800×480 RGB panel and GT911 touch, via the
crowpanel7build. It renders at 400×240 doubled, on purpose. See board setup and testing.
That's it. No GPS, no extra modules. The CYD is the whole device. A small speaker on its SPEAK connector is optional, and stays silent unless you switch BUZZER on.
Other boards have builds of their own -- platformio.ini has one
[env:...] each, with what is and is not confirmed on it. The newest is the
RockBase NM-CYD-C5 ([env:nm-cyd-c5]): the classic 2.8" CYD's glass on
an ESP32-C5, the first RISC-V chip here, with dual-band Wi-Fi 6, 16 MB of
flash and 8 MB of PSRAM. It runs the whole firmware and is on the web flasher
as a BETA; docs/NM-CYD-C5.md has the esptool route and
every pin. Before it, the Freenove ESP32-S3
Display 2.8" (FNK0104B, [env:freenove-s3]): an S3 with capacitive touch, an
SDMMC card slot, a WS2812 status light and a battery connector. Pins in
docs/PINOUT.md.
Two more came over from upstream SquachWatch in v3.6.0, both BETA on the flasher until they have run here:
- Sunton ESP32-3248S035C (
[env:cyd35c-fast]): the 3.5" CYD with capacitive touch -- the resistive 3.5"'s ST7796 panel, and a GT911 on I2C (SDA 33, SCL 32, reset 25, INT 21), the 3.2" capacitive's touch chip. Contributed upstream by VV-B0Y (PR #24). - Freenove ESP32-S3 Display 4.0" (FNK0104S,
[env:freenove-s3-40]): the Freenove 2.8"'s board with a 320×480 ST7796; touch, card, light and battery on the same pins. Its 480×320 frame lives in PSRAM. Contributed upstream by T0x0pl4sm4 (PR #51). If the picture tears or speckles, build with-DSPI_FREQUENCY=40000000. - Freenove ESP32-S3 Display 3.5" (FNK0104N,
[env:freenove-s3-35], from upstream v1.34.0): the Freenove 2.8"'s board with a 320×480 ST77922 on a four-lane QSPI bus TFT_eSPI cannot drive, and touch inside the same chip.st77922_*.cppown the bus, the panel and the touch; the frame (480×320, in PSRAM) goes out from the loop, changed bands only, rotated on the way. Its SD (SDMMC 2-7), LED (40), battery (8) and backlight (41) pins differ. - LilyGo T-Deck Plus (
[env:tdeck-plus], from upstream v1.36.0): an S3 handheld with a 2.8" ST7789, GT911 touch, an SD slot on the display's bus, a battery, a GPS (WARDRIVE to the card) and a real keyboard, which types into the WiFi password and the PIN pad. Its backlight is a 16-step chip that counts pulses, so it is stepped rather than PWM'd; landscape only, keyboard down. The trackball, the LoRa radio and the speaker sit this one out. - M5Stack StickS3 (
[env:sticks3], BETA) and Cardputer ADV ([env:cardputer-adv], experimental), from upstream v1.31.0: 240x135 screens with no touch. The keys drive a cursor through each screen's stops -- on the StickS3 the side button steps, the front one presses, the side one held goes home; on the Cardputer the arrows or TAB step, ENTER or space presses, ESC goes home. The screens have small layouts of their own: a compact alert card, a one-line-a-row LOG, tighter lists, a paged DIAGNOSTICS and MORE INFO, a shorter PIN pad. No SD on either yet; the Cardputer's optional EXT SCREEN panel is not ported. - Marauder V8 (
[env:marauder-v8], from upstream v1.36.0): justcallmekoko's ESP32-C5 board -- the NM-CYD-C5's SPI bus with an ILI9341 at 40 MHz, resistive touch, an SD slot, a MAX17048 battery gauge (BATTERY on the SYSTEM page, "+" while charging), a GPS on GPIO14 (the CYD GPS code: WARDRIVE to a WiGLE file on the card), its activity LED as the status light, and 5 GHz scanning. Its own 8 MB partition table.
No build tools, no IDE, no cloning anything — flash a board straight from your browser:
Works in Firefox, Chrome, Edge, or Brave on desktop. Pick your board (2.8" CYD, AWOK 2.4", RL Phantom 2.4", Freenove 3.2", or in beta the 3.5", the LilyGo T-Watch S3, the Freenove ESP32-S3 2.8", the Elecrow CrowPanel 7" and the RockBase NM-CYD-C5), plug in, click Connect & Install, done. A T-Watch has its clock set for it once the install finishes.
The same page talks to a plugged-in board over the cable, and says which one it is as it connects -- its name, its Bluetooth name and its firmware:
- Set Time & Zone sends this computer's clock and time zone.
- Back up / Restore settings keeps your settings across an install that erases the board.
- Ignored devices lists the devices the board never alerts for, and removes or adds one.
- Away days lists the trackers the board has heard on days away from home (three makes NEAR YOU ON N DAYS), the home network it goes by and how many of its routers it has learned -- and removes one, or all.
- Push options picks exactly which types PUSH ALERTS sends, and quiet hours for PUSH AT NIGHT of their own.
- Download log saves every detection the board has kept as a spreadsheet, and charts it on the page: by type, by hour of the day, and the devices seen on more than one day. Crash report reads what it kept about its last crash.
- The log's CSV has each sighting's confidence, an away column (a tracker that said it was away from its owner) and away_days (the days the board has heard it away from home), and the charts can show only those.
- As it connects, the page says which board it is and when this browser last backed it up -- and nudges when that was never, or over a month ago.
- Self-test checks the board's parts -- memory, touch, both radios, the black box, SD card, settings, clock, WiFi, update keys, rules -- and lists anything that needs a look.
- Live view shows what the board is detecting right now, live -- trackers away from their owner first, in red -- and ignores a device from its row.
- What the alerts mean (
/alerts/, linked under the legend): WITH YOU, NEAR YOU ON N DAYS, AWAY FROM OWNER, fake-tag spam, karma/evil-twin WiFi, confidence -- and what to do about each. - The page's footer shows the site's last redeploy: the release, and where it stopped if it failed.
Every release is also on GitHub Releases with the bins for every board attached, and the page keeps the previous releases in a version picker.
Three steps:
- Install PlatformIO (CLI or VS Code extension).
- Clone the repo:
git clone https://github.com/muleskin/MuleSkin-CYD cd MuleSkin-CYD - Build and flash:
For a board that is not the default three, name its build, e.g.
pio run -t upload
pio run -e freenove-s3 -t upload.
The first build pulls the TFT_eSPI, XPT2046, and NimBLE-Arduino libraries; after that it's incremental.
A full beginner-friendly walkthrough is in docs/BUILD.md.
- Plug the CYD into USB-C.
- The splash runs for about three seconds: the MuleSkin artwork, ears
twitching, with the release under the wordmark and the build's own
version on the INITIALIZING line (from
git describe, so a working-tree build says so). - The main screen appears: the radar sweeping, the time at the top centre once the clock is set, and live per-type counters in alphabetical order. Everything heard in the last minute is a blip on the scope in its type's colour -- nearer the centre the stronger its signal, lighting up as the arm passes and fading a minute after it was last heard. (The bearing is only a fixed place per device: the board can't tell direction.) Tap a blip for that device's panel -- WATCH, IGNORE, HUNT or what it is.
- The soft buttons at the bottom:
< WIFI TIME >— join your saved WiFi network just long enough to set the clock (see The clock).< LOG >— open the rolling 200-entry detection log; tap it again to return to the main screen. On the LOG screen a second button,[ CLR ], wipes the log and returns.< IN MEETING >— a red do-not-disturb sign, IN A MEETING in big white letters, with BACK to return. It never dims, no alert takes it over, and the status light and buzzers stay quiet; detection keeps running and logging behind it. Tap the sign for a timer -- BACK IN 15, 30 or 60 MIN -- and it takes itself down when the time is up.
- When something is detected, the device flashes a full-screen ALERT: a header strip in the detection's own colour with the type in the headline face, a data plate with the vendor, the device's own name where it broadcasts one, its MAC and a signal meter, and a gauge showing what was found with the instrument grid over it. Tap anywhere to dismiss early, or it clears itself after 60 seconds.
- A tracker travelling with you gets its own alert. An AirTag, Tile, SmartTag or Find My Device tag that has stayed in range for twenty minutes while the devices around you kept changing -- you are moving and it is not being left behind -- raises the alert card with WITH YOU N MIN across the top, once per tag. A tag near its owner changes its address every quarter of an hour, so it never gets that far; one planted on you keeps its address for about a day. Ignore a tag to stop it.
If a microSD card is present, every detection is also appended to
MuleSkin-<day>.log (CSV: ts,type,rssi,mac,channel,vendor,ssid).
There is no GPS, and the board never joins a network to scan. It joins one
to tell the time: < WIFI TIME > on the main screen joins your saved
network (the one marked USE, or the strongest saved one in range), asks a time
server, and lets go, pausing detection only while the radio is busy; with no
network saved it opens WIFI NETWORKS to add one. The boot-time update check
and UPDATE OVER WIFI set the clock the same way while the radio is up anyway.
AUTO TIME (on the SYSTEM page, on by default) keeps it right on its own:
with a network saved, the board rejoins it once a day from the main screen --
and once a couple of minutes after a boot that didn't set the clock -- for the
few seconds a time server takes. Once the time is real it shows at the top
centre of the main screen, 12-hour with AM/PM. Tap it for TODAY: a bar
for every hour since midnight, the types seen most, and the first and last
sighting -- counted from the black box, so a restart doesn't lose the morning. The zone is yours to pick, and there are three ways: the web
flasher's Set Time & Zone button sends this computer's clock and zone
down the same cable right after flashing; the first time the clock is set
with no zone chosen, a card on the main screen asks, with the live time in
the zone it shows so you can see when it's right; and TIME ZONE on the
SYSTEM page changes it later. Daylight saving takes care of itself. Without
a saved network the clock can still be set over serial with a TIME <epoch>
line at 2,000,000 baud, and ZONE US EASTERN sets the zone the same way.
Until the clock is set, timestamps count from boot. The board keeps a
note of the time in flash every ten minutes, and a cold boot with no clock
starts from that note: not the right time, since nobody knows how long the
power was off, but never earlier than the note, which keeps the day count
honest. Such a clock is used for the date only; the LOG times, the night
tag and the hour lines wait for a real answer.
Once it is set, the LOG shows the real time of each catch (or the date, for one from another day), and the alert card says AT NIGHT for anything caught between eleven and five. NIGHT DIM on the APPEARANCE page (10PM-6AM, 11PM-7AM or 12AM-7AM) runs the screen at a quarter of its brightness through those hours, full again for half a minute after a touch or an alert; scanning carries on at full rate.
Nothing is detected while the board is joined to a network, attacks included. To stay associated the radio has to sit on the access point's channel, so WIFI TIME, AUTO TIME, the boot-time update check and UPDATE OVER WIFI turn the WiFi sniffer off first (no DEAUTH, EVIL TWIN, pentest-gear or other WiFi catches) and stop BLE scanning too (no trackers). The board never stays connected: WIFI TIME gives the join 20 seconds and the time server about 6, then lets go, and detection resumes the moment the radio is free, so the blind spot is a few seconds and at most about 26. An update over WiFi is blind for as long as the download and flash take. While the main screen is up, an amber PAUSED pill in the title bar shows when scanning is off.
One consequence: an evil twin or a deauth flood aimed at the board's own join isn't flagged while it happens. A flood still shows up as a join that fails ("Could not join" on the WIFI TIME screen), and it's caught as soon as scanning resumes if it's still going.
The RGB LED on the back of the 2.8" CYD (on the front of the RL Phantom, and a WS2812 on the Freenove ESP32-S3 2.8") tells you what the screen is doing without the screen. A slow breathe in the theme's colour when nothing is happening; three flashes and a hold in the detection's own colour when something is, for as long as the alert card is up; cyan while an update downloads and green or red for how it went. It goes dark on the lock screen and through a wipe, so a duress restart looks like any other restart from the back too.
Settings → APPEARANCE → STATUS LIGHT: the master switch, alerts on or off, idle breathe or solid or off, an idle colour that follows the theme, the background, or one of nine fixed colours, brightness in seven steps, and a TEST row that plays an alert and the idle in four seconds. Boards whose LED pins have not been checked (the AWOK and the 3.5") compile it out and say so on that screen.
BUZZER (Settings, off by default) chirps once for a device the board has never logged before (not at night or with the screen dimmed), and for every WITH YOU, whenever it comes. The CrowPanel 7 has a buzzer; on the 2.8" CYD, plug a small 8 ohm speaker into the two-pin SPEAK connector.
PUSH ALERTS (Settings, every board, off by default) sends each alert to your phone as a real push notification -- lock screen, phone in your pocket, no page left open -- through the free ntfy app on iPhone or Android. The board joins one of its saved WiFi networks for a couple of seconds to send it -- home, work, or your phone's Personal Hotspot (on an iPhone, turn on Maximize Compatibility: the board needs 2.4 GHz) -- and lets go.
Set it up from the flasher page, SET UP PUSH ALERTS: it makes a long random topic, you subscribe to it in the ntfy app, and it sends the setup to the board over the cable and has it post a test. Once it is on, three more rows tune it:
- PUSH WHAT -- WITH YOU only; IMPORTANT (the default: WITH YOU plus Flock, Axon, plate readers, Raven, cameras, camera glasses, skimmers, WiFi attacks and attack tools -- Flipper, Pineapple, karma APs -- not every passing AirTag); ALL; or CHOSEN, the types ticked in the web flasher's PUSH OPTIONS.
- PUSH AT NIGHT -- through NIGHT DIM's hours (11 PM-5 AM without them), or quiet hours of its own set in PUSH OPTIONS: WITH YOU only (the default), or as set.
- DAILY SUMMARY -- one quiet push at 8 PM: the day's count, its top three types, the first and last sighting, how many were pushed and held back, and the board's firmware version and settings store -- a board left behind shows without being plugged in -- and, the day a new signed rules set arrives, which one.
Alerts go as they happen: a WITH YOU at once and loud, the rest a few seconds after the first, so a burst is one join rather than ten; joins are at least twenty seconds apart, and each costs detection a couple of seconds of blindness. With no saved network in range they wait -- the eight newest, for up to half an hour, with an amber PUSH n pill in the title bar -- and go at the next chance, with the time they happened. If the best network in range will not take the board, it tries the next one. Each device keeps one notification: on Android (and ntfy's web app) a camera that keeps alerting replaces its own notification rather than stacking up -- the iPhone app, as of ntfy 1.7, still shows each one.
One push per device an hour: a camera you sit beside all afternoon, or a tracker drifting in and out of range, is one notification, not forty. And at most 30 a day: past that the board says so once and keeps the rest, and the evening summary counts them. WITH YOU always goes, either way.
- PUSH AT HOME -- while the board can see your home network (the one marked USE in WIFI NETWORKS): AS SET (the default), IMPORTANT at most, or WITH YOU only. Only ever quieter: a faked beacon of your network can hush routine pushes, never add any.
- Ignore on a notification adds that device to the board's ignore list the next time the board joins WiFi -- for your own AirTag, your own doorbell; Snooze 1 day quiets it for a day (or until a restart) -- for a neighbour's camera. Either way the board answers with a quiet "ignored" or "snoozed" push once it has done it, with an Undo button of its own. Pause 2 h holds that board's ordinary pushes for two hours -- a meeting, a drive -- with WITH YOU still coming through, and an Undo too. On your own ntfy server it needs a one-time step, deploy/README.md.
- Near you on several days: a tracker away from its owner seen on three or more different days gets the alert -- "NEAR YOU ON N DAYS" -- and a push, once a day each. Only days heard away from home count (home: the WiFi network marked USE, heard in the last few minutes), so a neighbour's tag beside a board that never leaves the house never raises it; a tag following you is heard in different places. With no home network set, every day counts, and one Ignore settles a tag you know, on every board. Home is recognised by its routers, not just its name: each time the board joins the USE network it learns the access point it went through (up to four; a router's other band counts as the same box), so a hotspot of the same name elsewhere -- "xfinitywifi" -- is not home. The LOG shows a tracker's away days beside it ("2d away", in yellow) before it gets to three, and the first minutes after a restart, before home has been heard, count as neither.
- What to do, one scan away: a WITH YOU, NEAR YOU ON N DAYS or AWAY FROM OWNER alert shows a QR code (in place of the gauge) to that alert's part of flasher.oillie.cloud/alerts.
- Several boards on one topic: an Ignore or Snooze pressed on any board's alert is applied by all of them -- your own AirTag is yours everywhere -- but only the board that sent the alert says so back. A device ignored (or un-ignored) on one board's own screen, or over its cable, is passed to the others the same way. Give each a board name in SET UP PUSH ALERTS ("Car", "Desk") so you can tell their pushes apart.
A server that says "too many" (HTTP 429) is not asked again two minutes later: the board waits as long as it says, or 15 minutes, then 30, 60 and 120. With your own ntfy server, deploy/README.md also has a check that pushes "quiet" when a board has sent nothing for a day.
The self-test's push alerts line and DIAGNOSTICS' PUSH row say how many went today, how many the limits held back, and how the last one went -- sent, or why not (WiFi did not join, the server refused the token, a rate limit, the IGNORE topic failing) -- so a push that quietly stops arriving shows on the board too. DIAGNOSTICS' HOME row says whether the board hears the home network right now, and the 8 PM summary ends with the day's pushed and held-back counts.
It uses the public ntfy.sh unless told otherwise: nothing to install, and the topic is the password. For alerts that stay on your own server, behind an account, run ntfy there -- deploy/README.md has the setup, including Web Push for an iPhone (the ntfy web app on the home screen), which avoids the daily limit ntfy.sh puts on relaying a self-hosted server's alerts to the iPhone app. Either way the board posts in plain HTTP; TLS needs more memory than it has.
Nothing installs on its own unless you ask it to. At boot, a board with a saved WiFi network joins it for about a second, asks flasher.oillie.cloud for the latest version of its own build, and lets go again, all before Bluetooth starts; UPDATE CHECK on the SYSTEM page turns that off. When there is a newer one, the SYSTEM row reads UPDATE, and UPDATE FIRMWARE names the version until you install it.
AUTO UPDATE on the SYSTEM page (off by default) installs it for you, at night: during NIGHT DIM's hours (1 to 5 AM without them), from the main screen, after ten minutes untouched and unlocked. It shows a 30 second countdown with SKIP first, then joins the saved WiFi, installs, checks and restarts; anything that fails leaves the old version running. The daily AUTO TIME join checks for a new release too, so a board that is never restarted still hears about one.
It has two speeds. AFTER 3 DAYS waits three days from when that board first heard of the release; EARLY goes the first night. Set the board you test on to EARLY and the rest to AFTER 3 DAYS: a release that turns out bad shows itself on the test board, and once it is pulled from the site the others never install it.
UPDATE FIRMWARE installs it three ways: over WiFi from the site, over USB from the web flasher, or over Bluetooth from a browser in range -- UPDATE OVER BLUETOOTH on the board, then flasher.oillie.cloud/update on a computer or phone with Web Bluetooth (Chrome, Edge; Bluefy on an iPhone), and the six-digit code the board shows. Every image is checked against the release key before it is installed, whichever way it came.
Up to v3.1.8 the board kept its settings in 20 KB of flash, and a board that had been in use for a while could fill it. A full store is erased whole at boot -- WiFi networks, push alerts, the touch calibration, everything. From v3.2.0 the store is 128 KB and somewhere else in flash, and that takes a new partition table, which only a USB install can write. An update over WiFi or Bluetooth still works but keeps the old 20 KB store; the self-test's "settings store" line says which one a board has.
A board still on the small store says so: a card on the main screen on its first three boots, and MuleSkin mentions it now and then after that.
So, once per board: plug it in, Connect & Install from the web flasher without ticking "Erase device". The first boot moves every setting into the new store by itself and erases the old copy. If you do erase (or to be safe anyway), Back up settings first and Restore settings after.
WIFI NETWORKS on the SYSTEM page is where the board keeps the networks it knows: up to six, with USE marking the one it tries first. ADD picks one from a scan and takes the password on the board's keyboard; it is not checked by joining, since joining means giving Bluetooth up until a restart, but at the next boot check, and each row then says how that went: joined, wrong password, or not found. At boot the board scans, joins the USE network if it is there and otherwise the strongest saved one that is, so home and work both just work. The update flow does the same, and only shows its own list when none of the saved networks is in range. REMOVE takes one off the list.
- Arrows on NEARBY. Each device shows a green up-arrow when it has come closer since its last reading and a red down-arrow when it has moved away; under four dB of change shows nothing, which is what a still device does.
- First of its kind. The first time this board ever catches a type, the card says so.
- SNOOZE on an alert. Quiets that one device until the board restarts. It is still scanned, counted and logged; only the alert stops. IGNORE is the same thing kept for good.
MuleSkin forked from SquachWatch-CYD v1.28.0. These later upstream changes were ported (v3.5.0), adapted where the two have grown apart:
- Update failures that say why (v1.33): a sign-in page, a network that blocks the site, an https-only redirect, a slow or failing site, or no connection -- each its own sentence, with the detail after it ("(HTTP 403)"). Fetches retry up to three times. When the router's name lookup fails, the board reaches flasher.oillie.cloud by the last address the name had (upstream uses GitHub Pages' fixed addresses; this site is our own).
- About 70 KB of flash back (v1.33): C++ exceptions off everywhere (no catch exists in the firmware), and fixed colours as constants.
- 5 GHz on the NM-CYD-C5 (v1.31, PR #27 by quietradio): a short 5 GHz slice after every 2.4 GHz sweep, WIFI BANDS in Settings, "5G CH 149" on the alert, and the black box at last (it sat inside the C5's app slot).
- Raven (v1.36, #41): only its five service numbers match; the old range called an NVIDIA Shield a gunshot detector.
- A guessed clock stays a guess across a restart (v1.36), and the away days count only on a trusted clock.
- POCKET MODE (v1.34): the BOOT button puts the screen out and only the button brings it back; an alert lights it for its card, then it is dark again.
- BRIGHT has real - and + buttons (v1.35).
- SD log by date (v1.36, PR #46 by jotra7): /MuleSkin-YYYYMMDD.log with the real time in each row (/MuleSkin-nodate.log until the clock is trusted). The security wipe now really removes them: the rename had left it matching nothing.
- GPS on the CYD (v1.36, PR #46 by jotra7):
cyd-gpsandcyd-ili9341-gpsbuilds for an ATGM336H on GPIO35 -- a position on each SD row and on the alert, the clock without WiFi, WARDRIVE to a WiGLE file on the card, the fix in DIAGNOSTICS. Not on the flasher until tested on hardware. (Upstream's emulator GPS replay and main-screen GPS label were not brought over.)
MuleSkin-CYD/
├── platformio.ini
├── README.md
├── LICENSE
├── docs/
│ ├── FAQ.md (what it does, hardware, legality)
│ ├── DESIGN.md (the contract — single source of truth)
│ ├── BUILD.md (friendly walkthrough)
│ ├── PINOUT.md (CYD pin map)
│ ├── DETECTIONS.md (per-signature provenance)
│ ├── MULESKINWARE-AESTHETIC.md (CSS → RGB565 mapping)
│ ├── demo.gif (the artwork at the top of this file)
│ └── radar.gif (the radar background's source; see tools/)
├── include/
│ ├── state.h (DetectionType, Detection, Confidence)
│ ├── theme.h (palette, backgrounds, icons, chrome)
│ ├── signatures.h (the tables and their lookups)
│ ├── detection.h
│ ├── remote_id.h (ASTM F3411 decoder)
│ ├── clock.h (wall clock: NTP at the boot check, zones, the calendar)
│ ├── ignore_list.h (per-device alert suppression)
│ ├── status_light.h (the RGB LED and its rules)
│ ├── settings.h
│ ├── muleskin.h (the mascot -- switched off: MASCOT_SHOWN)
│ ├── bangers_font.h (generated 1bpp headline face: Orbitron)
│ ├── muleskin_art.h (generated: the splash artwork)
│ ├── radar_art.h (generated: the radar background's model)
│ ├── cyd_user_setup.h (TFT_eSPI config for the CYD)
│ └── ui_*.h
├── src/
│ ├── main.cpp (setup/loop, state machine, touch)
│ ├── theme.cpp (backgrounds, per-type icons, chrome)
│ ├── muleskin.cpp (the mascot's state; not drawn)
│ ├── signatures.cpp
│ ├── detection.cpp (WiFi promiscuous + NimBLE scan)
│ ├── remote_id.cpp
│ ├── clock.cpp
│ ├── ignore_list.cpp
│ ├── pet.cpp
│ ├── push_queue.cpp (PUSH ALERTS: which alerts go, and when)
│ ├── status_light.cpp
│ ├── sd_log.cpp
│ └── ui_*.cpp
├── tools/
│ ├── make_boot_art.py (docs/input.png -> muleskin_art.h)
│ ├── make_radar.py (docs/radar.gif -> radar_art.h)
│ └── gen_headline_font.py (Orbitron -> bangers_font.h)
├── test/ (host tests -- `make -C test`, no framework)
└── sim/ (PC simulator — compiles src/ natively)
├── Makefile (`make` builds the simulator)
├── *.h (Arduino/TFT_eSPI/NVS shims)
├── screens_check.py (CI: the main screens against golden/)
├── golden/ (the reference renders it compares with)
├── make_readme_demo.py (renders a simulator clip, docs/demo1.gif)
└── make_social.py (renders the repo's social preview card)
GNU General Public License v3.0 (GPL-3.0). See LICENSE.
- skizzophrenic, author of SquachWatch-CYD, the project MuleSkin-CYD is forked from. The firmware, the detection signatures, the web flasher, the simulator and the tests all started there.
- Flock Safety OUI research: @NitekryDPaul,
DeFlockJoplin,
colonelpanichacks/flock-you(MIT). - Axon / skimmer / SSID prefix data: compiled with assistance from Gemini (Google), expanded against public sources.
- AirTag manufacturer-data format: public Apple FindMy spec.
- Ported from SquachWatch after the fork: quietradio (5 GHz on the NM-CYD-C5, PR #27), jotra7 (GPS, wardriving and the dated SD log, PR #46), VV-B0Y (the 3.5" capacitive, PR #24) and T0x0pl4sm4 (the Freenove S3 4.0", PR #51); the Marauder V8 bring-up is skizzophrenic's, on justcallmekoko's board.
- AWOK 2.4" board port (ESP32-Marauder V6.1 hardware): bkbroiler, who did the actual pin-mapping and shared-bus touch-calibration work that made this board possible.
Shipping. CI (.github/workflows/ci.yml) builds every flasher board, runs
the host tests, and renders the main screens in the emulator and compares them
with the references in sim/golden/ on each push. tools/release.sh 3.1.5 --name ... --note ... --push stamps the manifests with the version and the release notes the
boards show, tags and pushes it; the tag builds a GitHub Release with every
board's bins (.github/workflows/release.yml). Getting it onto
flasher.oillie.cloud, which the boards check for updates, is one command on
the server -- see deploy/README.md -- which builds the
bins, signs them and the rule set with the release key, and deploys.
Detection is reliable for the high-priority targets (Flock, Axon, skimmer, camera glasses). Remote ID, iBeacon and Google's tracker frame are exact-format matches. Raven and generic ALPR are best-effort — see docs/DETECTIONS.md for per-signature provenance and the confidence each one earns.
Verified on real hardware. There is also a PC simulator in sim/ that
compiles the actual src/ against shims, and a host test suite in test/
(make -C test) covering the decoders, the signature tables and the
simulator's own fidelity to the display library.

