Files
presence/README.md
T
Bendik Aagaard LynghaugandClaude Fable 5.1 8fe75daad6
CI / check (push) Successful in 1m34s
presence: BLE proximity events for a kiosk screen
A swayidle-shaped daemon: `present <cmd>` runs when a Bluetooth LE device
comes within an RSSI threshold, `absent <secs> <cmd>` once nobody has for
that long. Near means 3 of the last 5 advertisements at or above -58 dBm by
default; --calibrate prints the raw stream so the threshold is picked at the
installed screen, not guessed. Nothing is stored: a few seconds of samples in
memory, counted.

Packaged for the [uhhm] registry from the checkout itself, built natively per
arch on tag by Gitea Actions, with the version checked across Cargo.toml,
PKGBUILD and the tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:23:39 +02:00

3.9 KiB

presence

BLE proximity events for a kiosk screen, in the spirit of swayidle: you give it commands to run when someone comes near and when nobody has been near for a while. It listens to Bluetooth LE advertisements through BlueZ and treats their signal strength as a rough distance.

exec presence --rssi -58 \
    present 'swaymsg "output * power on"' \
    absent 600 'swaymsg "output * power off"'

How it decides

Every advertisement BlueZ hears is one RSSI sample for that device. A device is near when at least --hits of its last --window samples are at or above --rssi (defaults: 3 of 5 at -58 dBm). Samples older than --stale seconds are forgotten, so a phone that stops advertising stops counting.

Someone is present while any device is near. present commands run when that becomes true. Each absent <seconds> <command> runs once, after nobody has been near for that many seconds. present fires again only after at least one absent has fired, so a phone flickering around the threshold does not run it over and over. With no absent events, present fires on every arrival.

What it cannot do

RSSI is a poor ruler. The same phone reads 15 dB apart in a hand and in a back pocket, bodies in between swing it by 10 dB, and phone models differ in transmit power. A phone held out at 4 m and one pocketed at 1.5 m can read the same. Not every phone advertises: iPhones do constantly, many Androids only while their screen is on. Departure is only visible as silence, hence the timeout.

Treat it as "someone with a phone is around", with a threshold you tune on site. For a real 2 m boundary with millisecond response use a 24 GHz mmWave module (HLK-LD2410) instead; this tool exists to try the cheap thing first.

Addresses are randomized by modern phones and are never written anywhere by presence. It keeps a few seconds of samples in memory and counts. If you log --calibrate output, you are the one storing identifiers; do not keep it.

Calibrating

Run it at the installed screen, with the display mounted where it will hang, because the wall and the panel change the readings:

presence --calibrate

It prints every advertisement as seconds address rssi NEAR name and runs nothing. Stand where a visitor stands with your phone in a pocket, then at 4 m, then walk past. Pick --rssi from what you see; the default -58 is a starting point, not a measurement. --ignore <addr> drops the staff phones and the kiosk's own peripherals once you know their addresses.

Options

--rssi <dBm>       sample counts as near at or above this (default -58)
--window <n>       samples kept per device (default 5)
--hits <n>         near samples out of the window that make a device near (default 3)
--stale <secs>     forget samples older than this (default 15)
--adapter <hciN>   which adapter (default: the first)
--ignore <addr>    repeatable; addresses to skip
--calibrate        print advertisements, run nothing
-w, --wait         wait for each command to finish, like swayidle -w

Commands run through sh -c as the user who started presence, which in a sway config is the session user, so swaymsg finds the right socket.

Requirements

bluetoothd must be running (systemctl enable --now bluetooth). BlueZ's default D-Bus policy lets any user start discovery, so no root is needed. On a Raspberry Pi 3 the BLE controller shares the antenna with wifi; scanning costs wifi throughput, which is one more reason to wire the kiosk.

Install

From the [uhhm] Arch registry on x86_64 and aarch64:

sudo pacman -S presence

Or build it: cargo build --release, or makepkg at the repo root.

Release

Bump version in Cargo.toml and pkgver in PKGBUILD to the same string, commit, tag vX.Y.Z. CI refuses a tag that disagrees with either. Gitea Actions then builds the package natively on both arches and uploads it to the registry. The REGISTRY_TOKEN repo secret is a bl write:package token; org repos do not inherit it.