A passive Bluetooth Low Energy (BLE) scanner built on a DFRobot Beetle ESP32-C3. It listens for BLE advertising packets and alerts (buzzer + LED) when it detects the radio signature of Meta Ray-Ban / Snap smart glasses nearby.
Smart glasses broadcast BLE advertising frames. While the MAC address and service UUIDs are randomized or non-persistent, the SIG-assigned company ID in the manufacturer-specific data field is mandatory and immutable. The detector reads that company ID from every advertisement and matches it against a known list.
This replicates the detection method of the open-source Nearby Glasses app by Yves Jeanrenaud (https://github.com/yjeanrenaud/yj_nearbyglasses), and uses the same company IDs and signal threshold so the hardware and the app agree.
| Company ID | Vendor |
|---|---|
0x01AB |
Meta Platforms, Inc. |
0x058E |
Meta Platforms Technologies, LLC |
0x0D53 |
Luxottica Group (makes Ray-Ban Meta) |
0x03C2 |
Snap, Inc. (Spectacles) |
Default RSSI threshold is -75 dBm (~10-15 m open air, 3-10 m indoors), matching the Nearby Glasses default. Tunable in firmware.
- Detects presence of an advertising device — not whether anyone is recording.
- Cannot distinguish glasses from other devices sharing the same company IDs (e.g. Meta Quest headsets) — a known false-positive class.
- Detection is best while glasses are powered on, pairing, or active. Glasses that are idle and connected to a phone may not advertise continuously.
| Ref | Part |
|---|---|
| ERF-0063 | Beetle ESP32-C3 (RISC-V) |
| PRO-0031 | Breadboard 16x5 cm, 830 points |
| PR1880 | Active buzzer module |
| PRO-0243 | DuPont male-male jumper cables (20 cm, 40 pcs) |
| BAT-0054 | LiPo battery 400 mAh / 3.7 V (802030) |
| CMP-0002 | Mini momentary push button |
The DFR0868 box also includes a snap-on expansion board (M0027.00725) and a 2.54 mm 8-pin header strip. Headers are not pre-soldered — you supply a soldering iron + solder and a USB-C cable (neither is in the box).
See SETUP.md for wiring, toolchain, flashing, and testing.
Firmware: firmware/lens_canary/lens_canary.ino
This firmware is the detection core of Lens Canary — a pocket detector that buzzes when camera glasses come near. The device is being built; the code that does the detecting is here, and it is the whole trick.
What it can and cannot do, stated plainly: it knows a pair of glasses is present, not whether anyone is recording. Nothing in this repo can tell you that, and any claim otherwise is false. Gear from the same vendor — a powered-on Meta Quest, for instance — can trip the same company ID.
The included PR1880 is an active buzzer — a single, fixed, fairly loud tone,
on/off only. Quieter or more pleasant alternatives, all keeping the same 3-pin
VCC/GND/Signal wiring on GPIO1 unless noted:
| Want | Component | Audible? | Code change |
|---|---|---|---|
| Silent / tactile | Vibration motor module (coin/ERM with onboard driver) | No | None — drop-in |
| Single soft tone | Passive buzzer + PWM duty | Quiet tone | Minor |
| Short tune / melody | Passive buzzer or small speaker | Melody | tone() melody |
| Premium haptic taps | DRV2605L driver + LRA | No | I2C (4-wire) |
Notes:
- Vibration: buy a module with an onboard transistor + flyback diode, not a bare motor. A bare motor is inductive (~80 mA) and will damage a GPIO if driven directly. The 400 mAh LiPo runs a coin motor fine through the module's driver.
- Tunes / tones: requires a passive buzzer or speaker — the active buzzer
has a built-in oscillator and physically cannot play notes. A commented-out
playTune()stub is in the firmware, ready to wire in. - No sampled audio: the ESP32-C3 has no DAC, so melodies are PWM square-wave ("chiptune") tones — great for jingles, not MP3-style playback.
- Multiple alerters can coexist on different GPIO pins (e.g. silent vibration plus an optional audible mode).
A credit card is 85.6 x 54 mm. Every component fits inside that outline flat, with room to spare:
| Part | Footprint | Thickness |
|---|---|---|
| Beetle ESP32-C3 | ~22 x 20 mm | ~5 mm |
| LiPo 802030 (400 mAh) | 30 x 20 mm | 8 mm |
| 0.96" OLED | ~27 x 27 mm | ~4 mm |
| Buzzer / button | small | - |
Footprint is not the constraint - thickness is. Stacking the board on the battery lands around 13-15 mm (card-sized, key-fob thick). To slim it: lay the battery beside the board rather than under it, or swap to a flatter cell (e.g. 402025, ~4 mm, ~200 mAh) and trade runtime for thinness.
A coin/watch cell (CR2032) is tempting for a "swap it, never charge it" product, but it cannot run continuous active BLE scanning — a current-delivery problem, not a capacity one. Active scanning pulls ~80-130 mA; a CR2032 is rated for single-digit mA continuous, and its high internal resistance makes the voltage sag and brown out the ESP32 under that load. You'd get resets, not runtime.
What works instead:
- User-replaceable LiPo — put the rechargeable cell on a connector instead of soldered in. Dead pack = pop in a fresh one. Same "swap when dead" UX, no coin-cell physics. The clean answer.
- A replaceable cell that can source the current — 2xAAA (~3 V, ~1000 mAh) works electrically but is bulky and fights the credit-card form factor.
- Coin cell + supercapacitor + heavy duty-cycle — the cap supplies the peak scan current while the cell recharges it during sleep. Legit, but it forces the detector to sleep most of the time, missing glasses that pass during the gaps. Bad trade for a "warn me now" wearable.
The real appeal isn't "watch battery," it's no charging + swap when dead — deliver that with a swappable cell that can push ~100 mA. None of this affects the stationary SKU (USB powered). For coin-cell months you'd need a different architecture — a dedicated ultra-low-power BLE sniffer that hardware-filters adverts and wakes the MCU only on a match — beyond the ESP32-C3 scan model and over-engineering for now.
Chemistry also drives shipping and liability: alkaline/NiMH AAA sidestep the lithium dangerous-goods rules a LiPo triggers.
A 0.96" SSD1306 OLED (I2C, 4 wires: VCC/GND/SDA/SCL) can show a rough "warmer/colder" gauge driven by the matched hit's RSSI.
62E2- I2C remap: the C3's default I2C pins (GPIO8/9) are strapping pins we avoid.
Remap to any free GPIO with
Wire.begin(sda, scl)(GPIO0/4/5/6/7/20/21 are free). - Gauge: track the strongest matching hit's RSSI per scan, smooth it with an
exponential moving average (RSSI is jittery), and map roughly
-30 dBm -> full,-90 dBm -> emptyas a bar graph orWEAK / MEDIUM / STRONG. - Honest limits: RSSI swings with orientation, body-blocking, and reflections, so this is a relative "getting warmer" indicator, not a distance readout. Moving toward the source trends the bars up - that's the useful behavior.
- Power: an always-on OLED shortens the 400 mAh runtime; blank it and wake the screen only on a hit to save battery.
- Pin budget: adding SDA/SCL on top of buzzer + button is where the snap-on expansion board (M0027.00725) earns its keep - the bare Beetle has few pads.
Driver library: U8g2 or Adafruit_SSD1306 + Adafruit_GFX. A commented-out OLED + RSSI-bars stub is in the firmware, ready to wire in.
- Nearby Glasses (app this is modeled on): https://github.com/yjeanrenaud/yj_nearbyglasses
- DFRobot Beetle ESP32-C3 wiki: https://wiki.dfrobot.com/SKU_DFR0868_Beetle_ESP32_C3
MIT — see LICENSE. Use it, fork it, sell what you build with it.
The Lens Canary name and logo are not covered by the MIT grant. The code is open so you can verify what the device does; the name is how people know which one they're buying. Build your own, call it your own.