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.

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