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.

S
Description
BLE proximity events for a kiosk screen, swayidle style
Readme MIT
52 KiB
Languages
Rust 85.2%
Shell 14.8%