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.
Config file and user service
The same words, one per line, with the rest of the line as the command and no
quoting rules. The package ships /usr/share/doc/presence/config.example:
rssi -58
present swaymsg "output * power on"
absent 600 swaymsg "output * power off"
presence --config ~/.config/presence/config reads it; command-line options
override the file, events from both are used. A systemd --user unit is
installed that runs exactly that, so as the kiosk user:
mkdir -p ~/.config/presence && cp /usr/share/doc/presence/config.example ~/.config/presence/config
systemctl --user enable --now presence
It restarts on failure and only starts when the config exists. Starting it
from the sway config with exec presence ... works just as well; pick one.
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
cargo release patch --execute # or minor / major
cargo-release bumps Cargo.toml, mirrors the version into PKGBUILD with
pkgrel=1 (see release.toml), commits, tags vX.Y.Z and pushes. CI refuses
a tag that disagrees with either file, so edit neither by hand. 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.