Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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, authenticated with the uhhm org's REGISTRY_TOKEN action secret,
which every repo in the org inherits.