# 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 ` 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 ` drops the staff phones and the kiosk's own peripherals once you know their addresses. ## Options ``` --rssi sample counts as near at or above this (default -58) --window samples kept per device (default 5) --hits near samples out of the window that make a device near (default 3) --stale forget samples older than this (default 15) --adapter which adapter (default: the first) --ignore 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.