blep / blog
GitHub

Blog ยท 11 June 2026

Teaching a virtual user to find fake keys

blep guides you to a lost Bluetooth device by signal strength โ€” warmer, colder, turn left, walk. The awkward part: how do you test that? You can't unit-test a hallway. Here's how blep regression-tests the whole experience on a plain JVM, without a single real Bluetooth packet.

You can't unit-test a hallway #

RSSI โ€” the signal-strength number every Bluetooth scan gives you โ€” is noisy, multipath-riddled, and shaped by whatever's between you and the device, including your own body. blep's guidance is a feedback loop on top of that mess: filter the signal, detect the trend, decide a phase (calibrate โ†’ sweep โ†’ walk โ†’ pinpoint), say something useful on screen.

Tune one threshold and you've changed the behaviour of the whole loop. Testing that by pacing an actual corridor with actual keys takes twenty minutes per attempt, isn't reproducible, and definitely doesn't run in CI. For a long time the choice seemed to be: ship tuning changes on vibes, or don't ship them.

But blep's core is deliberately pure Kotlin with zero platform dependencies โ€” the same engine compiles for Android, iOS, Wear OS and watchOS. Nothing in the tracking loop knows what a Bluetooth stack is; it just consumes (rssi, heading, steps) samples. And anything that consumes plain numbers can be fed simulated numbers.

A pocket universe in ~80 lines #

The simulator's World is a tiny physics model that produces one thing: the integer dBm a phone would read at your current position and heading. It stacks the effects that matter in real life:

  • โ€ขLog-distance path loss โ€” the textbook txAt1m โˆ’ 10ยทnยทlogโ‚โ‚€(d) falloff, with a tunable environment exponent.
  • โ€ขBody shielding โ€” a cosine lobe that attenuates up to ~10 dB when the target is behind you. This is the effect blep's whole technique rides on, so the simulation must model it.
  • โ€ขWalls โ€” line segments that knock ~16 dB off any reading whose line-of-sight crosses them.
  • โ€ขForest canopy โ€” position-dependent fading (interfering sine fields), the "why is it cold here but warm two steps away" effect.
  • โ€ขSlopes and floors โ€” the world is 3-D, so "one floor up" is a real, structurally unsolvable scenario (you can't walk through a ceiling).
  • โ€ขA moving target โ€” the device itself can drift, like a tag on a wandering dog.
  • โ€ขDeterministic noise โ€” interfering sines instead of a random generator, so every run of a scenario is exactly reproducible. A flaky physics test helps nobody.

The user is a regex #

Here's the part I like most. The virtual user doesn't peek at the engine's internal state โ€” that would test what the engine thinks, not what it says. Instead it reads the actual on-screen guidance strings, the same ones a human sees, and obeys them literally: "turn 30ยฐ left" โ†’ turn 30ยฐ left. "keep going" โ†’ walk a step. "hold at your chest" โ†’ stand still.

That closes the loop end-to-end: world โ†’ signal model โ†’ RSSI filter โ†’ phase machine โ†’ spatial tracker โ†’ copywriting โ†’ simulated legs โ†’ new position โ†’ world. If a phase transition fires too early, if the turn hint says left when it means right, if the pinpoint copy never triggers โ€” the virtual user gets lost, and the test sees it. The guidance is only correct if a literal-minded reader can follow it to the target.

Thirteen ways to lose your keys #

make sim runs the suite โ€” thirteen scenarios from trivial to hostile โ€” and prints a table. This is real output from the current build:

โ”€โ”€ tracking simulation (timeout 180s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
scenario           straight   closest   found     walked   path-eff   vol     result
ahead 5 m            5.0 m     0.2 m       12s     8.8 m     0.6ร—    2.2   โ˜… clean
behind 8 m           8.0 m     0.2 m       14s    39.3 m     0.8ร—    2.1   โ˜… clean
to the side 6 m      6.0 m     0.0 m       14s     6.0 m     0.8ร—    1.3   โ˜… clean
diagonal 14 m       14.1 m     0.3 m       38s    47.4 m     2.3ร—    1.7   โ˜… clean
far 20 m            20.0 m     0.2 m       22s    42.7 m     0.9ร—    2.0   โ˜… clean
extra-far 35 m      35.0 m     0.1 m       32s    48.2 m     0.9ร—    2.1   โ˜… clean
through a wall       9.0 m     0.0 m       15s    28.9 m     0.8ร—    2.2   โ˜… clean
up a slope          12.1 m     0.0 m       17s    17.3 m     0.8ร—    1.9   โ˜… clean
forest 12 m         12.0 m     0.0 m       18s    56.0 m     0.9ร—    2.6   โ˜… clean
noisy room           8.0 m     0.2 m       14s    17.5 m     0.8ร—    3.6   โ˜… clean
randomized          13.0 m     0.2 m       26s    34.5 m     1.5ร—    2.0   โ˜… clean
one floor up         5.0 m     3.0 m   timeout    64.4 m    12.9ร—    2.6   โœ— fail
moving device        8.0 m     0.5 m       27s    21.0 m     2.4ร—    1.4   โ˜… clean
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  clean 12/13  ยท  reached 12/13  ยท  avg path-eff 1.1ร—  โ”€โ”€

path-eff is metres walked relative to a sensible search path โ€” it measures wandering, not just success. "one floor up" fails by design: the target is 3 m above your head and there are no stairs in the world; what's being checked is that blep gets you to the right spot under it (3.0 m closest approach โ€” directly below) instead of pacing in circles.

The suite is a CI gate, not a demo: the build fails if the clean count drops below 12/13, or if the trivial straight-ahead case is ever less than clean. A tuning tweak that helps the forest but breaks the hallway gets caught before it ships.

Chaos, robustness, and A/B physics #

Once the loop is closed and fast (~seconds for the whole suite), it becomes a tool, not just a test:

  • โ€ขmake chaos random-searches every tuning knob โ€” filter constants, phase thresholds, particle-filter parameters โ€” scoring each candidate against the suite. The current defaults came out of these runs, not out of guesses.
  • โ€ขmake robustness guards against the opposite failure: overfitting the thirteen named scenarios. It generates held-out random worlds and checks the tuning generalises to layouts it never saw.
  • โ€ขmake sim-gps is A/B physics: the same drift-prone outdoor walks with GPS fusion off vs. on, reporting mean closest-approach. It's how "does folding GPS into the local frame actually help?" became a number instead of a debate.
  • โ€ขmake sim-safety points the same machinery at the anti-tracking side: simulated commutes where one device genuinely follows you and dozens just share your routine, checking the detector flags the right one.

The whole trick is purity #

None of this needed a test framework, an emulator, or a single permission dialog. It needed one architectural decision, made early: the tracking engine is a pure Kotlin Multiplatform module that has never heard of Android or CoreBluetooth. Sensors and radios live behind expect/actual boundaries; the logic consumes numbers.

The payoff compounds. The same purity that makes the simulator possible is why one engine ships on four platforms, why the anti-tracking detector has scenario tables instead of anecdotes, and why a contributor can verify a tuning change from a laptop with no hardware at all:

git clone https://github.com/fentas/blep.fyi
cd blep.fyi
make sim

The keys are fake. The walls are fake. The user is a regex. The confidence is real.