Skip to content

fentas/blep.fyi

Repository files navigation

blep mascot
blep

Stars License CI Site Kotlin Multiplatform

Platforms

 

blep flow: pick a device, track it warm/cold, found it

 

blep turns your phone — or your watch — into a warm/cold pointer for the Bluetooth Low Energy devices already around you. Pick a device and blep walks you to it: a single animated arrow and a background colour that shifts from cool to warm as you close in. No extra hardware, no maps, no accounts.

The same scan runs the other way too: blep watches for unwanted trackers travelling with you — an AirTag, Tile or SmartTag someone may have slipped into your bag — and, because it's a finder, doesn't just warn but points you to it. (Anti-tracking ↓)

It's a minimalist, cross-platform Kotlin Multiplatform app (iOS, Android, Apple Watch, Wear OS) plus a small static PWA landing page at blep.fyi. Everything runs on-device — no accounts, no analytics, no data collection.

 

🐾 How it works — the body-shielding technique

A phone's BLE radio is roughly omnidirectional, so raw RSSI tells you how far but not which way. blep makes it directional using your own body:

Hold the phone flat against your chest. Your torso absorbs ~10–20 dB of 2.4 GHz signal, so the reading is strongest when the target is in front of you.

From there it's a guided loop driven entirely by the change in signal (ΔRSSI):

Step Phase What you do blep says
1 Calibrate Hold at your chest, stay still Hold at your chest
2 Axis sweep Turn slowly in place Warmer · turn back
3 Vector walk Walk forward Keep going · stop
4 Reorient Re-sweep as needed Turn again
5 Pinpoint Kneel, search low Almost there
6 Done 🎉 Finished — congratulations!

RSSI is noisy and multipath-prone, so this is an assistive heuristic, not a precise locator. Every threshold lives in TrackingTuning and is unit-tested.

 

🛰 Sensor fusion & the live map

The body-shielding loop is the floor, not the ceiling. When motion sensors are present blep fuses them into a parallel spatial tracker — and degrades cleanly back to RSSI-only when they aren't:

  • Compass + accelerometer + step counter + GPS dead-reckon the path you walk (real step distance indoors; GPS-fused outdoors). Turning the phone no longer fools the tracker — signal swings during reorientation are discounted.
  • A recursive Bayesian particle filter triangulates the target from the RSSI sampled along that path: each reading constrains it to a sphere, and the intersections across your route localise it. It accumulates evidence over time, learns the environment's path-loss as it goes, and can even follow a moving target.
  • A barometer adds altitude, so the filter is 3-D — it can tell you the target is one floor up / down.
  • The screen is a no-map radar: your heading wedge (green toward / red away), a signal-coloured trail with a warm "fog", and the predicted target as a pulsing glow with an uncertainty ellipse — plus turn-by-turn copy ("turn 30° left · ~8 m") and a Geiger-counter haptic + tone that quickens as you close in. On phone, Wear OS, and Apple Watch.
  • At point-blank the live signal trumps the estimate: rather than a stuck triangulated distance, it switches to a plain "It's right here" with a look-around hint. The whole UI is light / dark / system themable, with independent sound and vibration toggles.

It all lives in core/spatial — pure and unit-tested like the rest, with platform sensor providers behind an expect/actual boundary.

 

🛡 Is something following you?

A finder run in reverse is a tracker detector. Tap "Is something tracking you?" and blep watches the same advertisements for a tag that's travelling with you:

  • Known tracker kinds — Find My (AirTag), Tile, Samsung SmartTag and other beacons are recognised by their manufacturer data / service UUIDs, including a Find My device advertising in separated-from-owner mode near you.
  • Address-rotation correlation — a privacy tracker rotates its BLE address to stay anonymous, but gives itself away by reappearing at the same close range again and again. The un-correlation is the correlation — the trick AirTag detectors that key on a stable address miss.
  • Cross-session memory (opt-in) — a small on-device log of close encounters by tracker kind + time. A tag that keeps showing up across separate hours — and, if you allow coarse location, across separate places — gets flagged as likely following you.
  • Background watch (opt-in) — a foreground service / periodic worker keeps an eye out while the app is closed and notifies you, with a battery-minded interval (it skips actively ranging your own paired devices). On Wear OS the watch runs only the lighter periodic (~30-min) check — no continuous foreground service — to spare the watch battery; the phone does the continuous watch.
  • Sensitivity profiles — Relaxed / Balanced / Strict retune the thresholds; surfaced both in Settings and on the safety screen.

Because blep is a finder, every alert is actionable: hit Find and it walks you to the tag with the same warm/cold guidance. Everything stays on-device; location is opt-in, coarse, and never leaves the phone. It lives in core/safety (SafetyScanner · TrackerDetector · SafetyHistory) — pure and unit-tested.

Warning

Tracker detection is new and not yet widely field-tested. It can produce false negatives (miss a real tracker) and false positives (flag a harmless device). Treat it as a helpful signal — not a guarantee — and don't rely on it alone for your safety. blep is a free side project, so real-world use is the testing: it gets better as people use it, and bug reports of a missed or mis-flagged tracker are very welcome. Design notes: docs/detection.md.

 

🔔 Don't leave it behind

The inverse of "is something following you?" — tether a device (a tag on your keys, a bag, earbuds, even your phone) and blep alerts you (notification + vibrate, per your system settings) the moment it slips out of Bluetooth range, and again when it's back, so you notice before you walk off without it. On the phone the device panel has the toggle; on Wear OS you long-press a device in the list. The direction — leave / return / both (default both) — is configurable in Settings.

It works in all three scan modes — in-app, the continuous foreground service, and the periodic background worker — backed by a small persistent PresenceMonitor in core/tether that turns presence samples into debounced leave/return events (shared by phone and watch, unit-tested). Tethering a device keeps the foreground watch alive so a leave is caught promptly.

Two detection tracks, chosen automatically by whether the device is paired:

  • Paired / bonded devices (earbuds, a car, a watch, a bonded phone) are caught the instant they disconnect or reconnect via the OS connection-state hook (ACTION_ACL_DISCONNECTED / ACTION_ACL_CONNECTED) — event-driven, no scanning, and rotation-proof (a bond resolves to the device's stable identity address). A short debounce rides out a momentary blip. It runs in the foreground service (which a tether keeps alive), so it works with the app closed; a BOOT_COMPLETED receiver re-arms it after a reboot.
  • Unpaired advertising tags use the scan-based PresenceMonitor.

On Wear OS the watch also warns you if you walk off without your phone: it watches the watch↔phone companion link (the Wear Data Layer — the system delivers connect/disconnect events, no scanning, no foreground service), debounced so a blip doesn't cry wolf. Toggle it in the watch's Settings.

The Apple Watch / iPhone side is still in progress.

 

🔗 Phone ↔ watch sync

Your phone and watch act like one device. Over the Wear Data Layer they keep a small shared state in step and relay live events:

  • Converged state — favourites, device names, the tether set and a couple of settings (sensitivity, left-behind direction) sync both ways. It's a tiny last-write-wins CRDT (core/sync/SyncState), so the two converge no matter the order things sync — and a removal (un-favourite, rename-clear) really propagates instead of being resurrected by a stale replica.
  • Relayed events — the phone, in your pocket with the better antenna and battery, runs the continuous scan and buzzes the watch when it finds a tracker or a tethered thing drops out of range (MessageClient). The watch gets phone-grade detection for almost no watch battery.
  • PresenceCapabilityClient reports whether the peer is reachable + nearby (the foundation for splitting scan duty by who's better placed).

Every category is gated by a Sync settings menu (master switch + per-category, so you can share favourites but keep names private). The whole engine is pure and unit-tested (SyncManager

In progress: the watch reflecting synced names/favourites in its own list, live scan fusion (relaying sightings for better coverage), and the Apple side (WatchConnectivity is the WCSession analog of the Data Layer). Design in docs/sync.md.

 

🗂 Repository layout

.
├── app/                      Kotlin Multiplatform project
│   ├── core/                 Pure tracking logic + BLE scanner (shared, tested)
│   │   └── src/
│   │       ├── commonMain/   RssiFilter · SignalTrend · DeviceTable ·
│   │       │                 TrackingSession · BleScanner (expect) ·
│   │       │                 spatial/ (DeadReckoner · ParticleTargetEstimator ·
│   │       │                 StepCounter · PathLossCalibrator · MotionProvider) ·
│   │       │                 safety/ (SafetyScanner · TrackerDetector ·
│   │       │                 SafetyHistory · TrackerTuning) ·
│   │       │                 sync/ (SyncManager · SyncState LWW CRDT) ·
│   │       │                 tether/ (PresenceMonitor)
│   │       ├── commonTest/   JVM-runnable unit tests
│   │       ├── kableMain/    Kable scanner (Android + Apple share this)
│   │       └── jvmMain/      Fake scanner for tests/preview
│   ├── composeApp/           Shared Compose UI as a KMP library (Android + iOS):
│   │                         Discovery (favorites + paired) · Tracking ·
│   │                         Safety · Settings · background-scan service
│   ├── androidApp/           Android phone app (launcher, manifest, signing)
│   ├── wearApp/              Wear OS app (Wear Compose)
│   ├── iosApp/               SwiftUI shell for iPhone (XcodeGen)
│   ├── watchApp/             SwiftUI watchOS app (uses shared BlepCore)
│   └── zeppApp/              Zepp OS mini-app (Amazfit) — find-only
├── web/                      Vite + Tailwind PWA for blep.fyi
├── design/tokens.md          Design tokens shared by app + web
└── .github/workflows/        CI (tests + cross-platform compile) + Pages deploy

The core module is deliberately free of platform and UI dependencies so the tracking logic is provable on a plain JVM. The Compose phone app and the Wear app each provide a thin state holder (BlepController / WearController) over the same TrackingSession; the watchOS app does CoreBluetooth in Swift and feeds RSSI into that same Kotlin engine — no Flow bridging.

 

🏗 Architecture

        ┌─────────────────────── :core (commonMain, pure) ───────────────────────┐
        │  RssiFilter (EMA, ΔRSSI) → SignalTrend (peak/drop) → TrackingSession    │
        │  DeviceTable (dedup/sort/TTL)        BleScanner (expect)  TrackingStatus │
        └───────▲───────────────────────────────────▲─────────────────▲──────────┘
                │ actual (Kable)                     │ actual (Kable)   │ actual (fake)
            androidMain ─┐                       appleMain ─┐         jvmMain
                         └── kableMain (shared) ◄───────────┘
   ┌─────────────┴───────────┐     ┌────────────┴───────────┐   ┌──────┴───────┐
   │ composeApp (Android+iOS)│     │ wearApp (Wear Compose) │   │ watchApp     │
   │ BlepController + screens│     │ WearController         │   │ SwiftUI +    │
   └─────────────────────────┘     └────────────────────────┘   │ BlepCore     │
                                                                 └──────────────┘

 

🔧 Building

The toolchain is pinned with mise (mise.toml): JDK 21 + Gradle. Run mise install once, or bring your own JDK 21.

Common tasks are wrapped in a Makefile — run make help:

make test            # core unit tests (JVM)
make build           # phone + Wear debug APKs
make run             # install + launch blep on a connected phone
make demo            # install + launch in demo mode (scripted data, no BLE)
make sim             # closed-loop tracking simulation + scenario table
make emulator-setup  # one-time: install emulator + create the AVD
make emulator        # boot it (UI only — emulators have no Bluetooth)
make web             # website dev server
make ci              # what CI runs on Linux (tests + Android build)

Core unit tests — no Android SDK needed:

cd app
./gradlew :core:jvmTest -Pblep.android=false

-Pblep.android=false skips the Android targets so the pure logic builds and tests anywhere. CI runs the full matrix.

Android (phone + Wear) — needs the Android SDK (ANDROID_HOME or app/local.properties):

cd app
./gradlew :androidApp:assembleDebug   # phone APK
./gradlew :wearApp:assembleDebug      # Wear OS APK

Min SDK 26 (phone) / 30 (Wear OS 3). BLE permissions are requested at runtime.

iOS / watchOS — needs macOS + Xcode + XcodeGen:

cd app/iosApp   && xcodegen generate && open iosApp.xcodeproj      # iPhone
cd app/watchApp && xcodegen generate && open watchApp.xcodeproj    # Apple Watch

A pre-build phase compiles the shared Kotlin framework via Gradle; the Info.plists declare NSBluetoothAlwaysUsageDescription.

Website:

cd web
npm install
npm run dev        # local dev server
npm run build      # static output in web/dist
npm run gen:icons  # regenerate PWA icons from logo.svg (only if it changes)
npm run gen:shots  # regenerate the landing-page gallery from the store screenshots

Deployed to GitHub Pages by .github/workflows/deploy.yml on push to main. The Pages source and blep.fyi custom domain are set in repo settings, so no CNAME file is committed.

 

📱 Platform support

Platform Status Notes
Android phone Compose Multiplatform
iOS (iPhone) Compose Multiplatform in a SwiftUI shell
Wear OS Wear Compose, standalone
Apple Watch SwiftUI + shared BlepCore
Zepp OS 🟡 Find-only mini-app (app/zeppApp). No general third-party BLE central/scan API, so the anti-tracking scan isn't possible.

 

♥ Donations

The website #donate section (the target of the app's Help & donate button) supports Stripe, Open Collective, PayPal and GitHub Sponsors, each with a payment record or receipt. The provider links live in web/index.html and web/donate.html.

 

🧪 Testing & CI

:core ships JVM unit tests for the RSSI filter, trend detector, device table, the full tracking state machine (calibration → … → completion, plus re-aim and stale-eviction edge cases), and the whole spatial layer — dead reckoning, particle-filter localisation (target found to within a few metres, follows a moving target, 3-D floor detection), path-loss calibration, step counting, haptic cadence and turn-by-turn guidance, all on synthetic walks — plus the safety layer (tracker-kind recognition, address-rotation correlation, cross-session history). CI additionally compiles the Android, iOS and watchOS Kotlin targets and builds the SwiftUI iOS + watchOS apps (XcodeGen + xcodebuild) so the Swift glue is verified too.

 

📄 License

MIT © Jan Guth

About

Find your lost Bluetooth things — a minimal cross-platform BLE pointer (iOS · Android · Wear OS · watchOS) that guides you to a device by signal strength. Kotlin Multiplatform + Compose.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors