A swayidle-shaped daemon: `present <cmd>` runs when a Bluetooth LE device comes within an RSSI threshold, `absent <secs> <cmd>` once nobody has for that long. Near means 3 of the last 5 advertisements at or above -58 dBm by default; --calibrate prints the raw stream so the threshold is picked at the installed screen, not guessed. Nothing is stored: a few seconds of samples in memory, counted. Packaged for the [uhhm] registry from the checkout itself, built natively per arch on tag by Gitea Actions, with the version checked across Cargo.toml, PKGBUILD and the tag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
commit
8fe75daad6
@@ -0,0 +1,23 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux:latest
|
||||
steps:
|
||||
- name: Prepare container
|
||||
run: pacman -Syu --noconfirm --needed base-devel git rust dbus
|
||||
- name: Checkout
|
||||
uses: builtin:checkout
|
||||
- name: Format, lint, build
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo clippy --locked --release -- -D warnings
|
||||
cargo build --locked --release
|
||||
- name: PKGBUILD and Cargo.toml agree
|
||||
run: scripts/check-version
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Release
|
||||
|
||||
# A v* tag builds the Arch package natively per arch and uploads it to the
|
||||
# [uhhm] registry (namespace bl, as pacman.conf points there). REGISTRY_TOKEN
|
||||
# is a bl write:package token added as a repo secret; org repos do not inherit
|
||||
# user-level secrets.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
PACKAGER: uhhm CI <bendik@lynghaug.me>
|
||||
# Arch Linux ARM's makepkg.conf (klokka) differs from Arch's: pin where
|
||||
# packages land and their format, so publish-registry finds them.
|
||||
PKGDEST: .
|
||||
PKGEXT: .pkg.tar.zst
|
||||
|
||||
jobs:
|
||||
# runner-1 (Docker on ergo): Arch container on the ubuntu-latest label.
|
||||
x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux:latest
|
||||
steps:
|
||||
- name: Prepare container
|
||||
run: pacman -Syu --noconfirm --needed base-devel git rust dbus
|
||||
- name: Checkout
|
||||
uses: builtin:checkout
|
||||
- name: Check tag, PKGBUILD and Cargo.toml agree
|
||||
run: scripts/check-version "${GITHUB_REF_NAME#v}"
|
||||
- name: Build
|
||||
run: |
|
||||
useradd -m builder
|
||||
chown -R builder: .
|
||||
sudo -u builder --preserve-env=PACKAGER,PKGDEST,PKGEXT makepkg -f --noconfirm
|
||||
- name: Publish to the Arch package registry
|
||||
run: scripts/publish-registry .
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_URL: ${{ gitea.server_url }}/api/packages/bl/arch/uhhm
|
||||
|
||||
# klokka: bare Arch Linux ARM host. Nothing is installed with pacman there;
|
||||
# cargo comes from the rustup toolchain the action keeps in the runner's
|
||||
# home, hence -d (makepkg would otherwise look for a cargo *package*).
|
||||
# libdbus is on the host already (bluez pulls it in).
|
||||
aarch64:
|
||||
runs-on: aarch64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: builtin:checkout
|
||||
- name: Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Check tag, PKGBUILD and Cargo.toml agree
|
||||
run: scripts/check-version "${GITHUB_REF_NAME#v}"
|
||||
- name: Build
|
||||
run: makepkg -f -d --noconfirm
|
||||
- name: Publish to the Arch package registry
|
||||
run: scripts/publish-registry .
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_URL: ${{ gitea.server_url }}/api/packages/bl/arch/uhhm
|
||||
@@ -0,0 +1,5 @@
|
||||
/target
|
||||
*.pkg.tar.zst
|
||||
/pkg
|
||||
/src/*/
|
||||
!/src/*.rs
|
||||
Generated
+850
@@ -0,0 +1,850 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "bluer"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901"
|
||||
dependencies = [
|
||||
"custom_debug",
|
||||
"dbus",
|
||||
"dbus-crossroads",
|
||||
"dbus-tokio",
|
||||
"displaydoc",
|
||||
"futures",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"macaddr",
|
||||
"nix",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da7d1ad9567b3e11e877f1d7a0fa0360f04162f94965fc4448fbed41a65298e"
|
||||
dependencies = [
|
||||
"custom_debug_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug_derive"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a707ceda8652f6c7624f2be725652e9524c815bf3b9d55a0b2320be2303f9c11"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-crossroads"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-tokio"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "macaddr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "presence"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bluer",
|
||||
"clap",
|
||||
"futures",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "presence"
|
||||
version = "0.1.0"
|
||||
description = "BLE proximity events for a kiosk screen, swayidle style"
|
||||
license = "MIT"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bluer = { version = "0.17", features = ["bluetoothd"] }
|
||||
clap = { version = "4.6.7", features = ["derive"] }
|
||||
futures = "0.3.34"
|
||||
tokio = { version = "1.53.2", features = ["rt-multi-thread", "macros", "signal", "process", "time", "sync"] }
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Bendik Aagaard Lynghaug
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Maintainer: bl <bendik@lynghaug.me>
|
||||
# Built from the checkout it sits in: .gitea/workflows/release.yml runs makepkg
|
||||
# at the repo root on a v* tag, natively per arch, and uploads to the [uhhm]
|
||||
# registry. pkgver must match Cargo.toml and the tag; CI checks all three.
|
||||
#
|
||||
# source=() and $startdir on purpose: makepkg's own $srcdir is ./src, which is
|
||||
# where the Rust sources live, so copying into it would nest src/src. Cargo
|
||||
# builds in place into ./target (gitignored) instead.
|
||||
|
||||
pkgname=presence
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="BLE proximity events for a kiosk screen, swayidle style"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://project.uhhm.no/uhhm/presence"
|
||||
license=('MIT')
|
||||
depends=('bluez' 'dbus' 'gcc-libs' 'glibc')
|
||||
makedepends=('cargo')
|
||||
source=()
|
||||
|
||||
build() {
|
||||
cd "$startdir"
|
||||
CARGO_TARGET_DIR="$startdir/target" cargo build --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$startdir"
|
||||
install -Dm755 target/release/presence "$pkgdir/usr/bin/presence"
|
||||
install -Dm644 README.md "$pkgdir/usr/share/doc/presence/README.md"
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/presence/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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. The `REGISTRY_TOKEN` repo secret is a bl `write:package` token; org
|
||||
repos do not inherit it.
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# check-version [tag-version]: PKGBUILD pkgver, Cargo.toml version and, when
|
||||
# given, the tag must all be the same string. Version lives in Cargo.toml;
|
||||
# PKGBUILD mirrors it so makepkg needs no VCS pkgver().
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
cargo=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
|
||||
pkg=$(sed -n 's/^pkgver=//p' PKGBUILD)
|
||||
[ "$cargo" = "$pkg" ] || { echo "Cargo.toml $cargo != PKGBUILD pkgver $pkg" >&2; exit 1; }
|
||||
if [ $# -gt 0 ]; then
|
||||
[ "$1" = "$cargo" ] || { echo "tag $1 != version $cargo" >&2; exit 1; }
|
||||
fi
|
||||
echo "version $cargo"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# publish-registry <dir>: upload every built package in <dir> (debug packages
|
||||
# skipped) to the [uhhm] Gitea Arch registry. HTTP 409 means that
|
||||
# pkgver-pkgrel is already published, which is fine on re-runs.
|
||||
set -euo pipefail
|
||||
: "${REGISTRY_TOKEN:?}" "${REGISTRY_URL:?}"
|
||||
shopt -s nullglob
|
||||
pkgs=("$1"/*.pkg.tar.{zst,xz,gz})
|
||||
[ ${#pkgs[@]} -gt 0 ] || { echo "no packages in $1" >&2; exit 1; }
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
case "$pkg" in *-debug-*) continue ;; esac
|
||||
code=$(curl -sS -o /tmp/registry-resp -w '%{http_code}' \
|
||||
--user "bl:$REGISTRY_TOKEN" --upload-file "$pkg" "$REGISTRY_URL")
|
||||
case "$code" in
|
||||
2??) echo "uploaded $(basename "$pkg")" ;;
|
||||
409) echo "already published: $(basename "$pkg")" ;;
|
||||
*) echo "::error::$(basename "$pkg"): HTTP $code"; cat /tmp/registry-resp; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
//! presence: BLE proximity events for a kiosk, in the spirit of swayidle.
|
||||
//!
|
||||
//! Every Bluetooth LE 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`. Someone is present when any device is
|
||||
//! near. `present` commands run when that becomes true; `absent <secs>`
|
||||
//! commands run once nobody has been near for that many seconds.
|
||||
//!
|
||||
//! RSSI is a poor ruler: the same phone reads 15 dB apart in a hand and in a
|
||||
//! back pocket. Run with `--calibrate` at the installed screen before trusting
|
||||
//! any threshold.
|
||||
|
||||
use bluer::{Adapter, AdapterEvent, Address, DiscoveryFilter, DiscoveryTransport, Session};
|
||||
use clap::Parser;
|
||||
use futures::StreamExt;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::process::ExitCode;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
version,
|
||||
about = "BLE proximity events for a kiosk screen",
|
||||
after_help = "EVENTS (repeatable, in any order):\n \
|
||||
present <command> run when a device comes within range\n \
|
||||
absent <seconds> <command> run once nobody has been in range for <seconds>\n\n\
|
||||
Example (sway config):\n \
|
||||
exec presence --rssi -58 \\\n \
|
||||
present 'swaymsg \"output * power on\"' \\\n \
|
||||
absent 600 'swaymsg \"output * power off\"'"
|
||||
)]
|
||||
struct Args {
|
||||
/// RSSI at or above which a sample counts as near, in dBm
|
||||
#[arg(long, default_value_t = -58, allow_hyphen_values = true)]
|
||||
rssi: i16,
|
||||
/// Samples kept per device
|
||||
#[arg(long, default_value_t = 5)]
|
||||
window: usize,
|
||||
/// Samples at or above --rssi (out of --window) that make a device near
|
||||
#[arg(long, default_value_t = 3)]
|
||||
hits: usize,
|
||||
/// Forget samples older than this many seconds
|
||||
#[arg(long, default_value_t = 15)]
|
||||
stale: u64,
|
||||
/// Bluetooth adapter to use (default: the first one)
|
||||
#[arg(long)]
|
||||
adapter: Option<String>,
|
||||
/// Addresses to ignore, e.g. staff phones or the kiosk's own peripherals
|
||||
#[arg(long = "ignore", value_name = "ADDR")]
|
||||
ignore: Vec<Address>,
|
||||
/// Print every advertisement (time, address, rssi, near?, name) and run nothing
|
||||
#[arg(long)]
|
||||
calibrate: bool,
|
||||
/// Wait for each command to finish before handling further events
|
||||
#[arg(short = 'w', long)]
|
||||
wait: bool,
|
||||
/// Event definitions, swayidle style (see EVENTS below)
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
events: Vec<String>,
|
||||
}
|
||||
|
||||
struct Absent {
|
||||
after: Duration,
|
||||
command: String,
|
||||
fired: bool,
|
||||
}
|
||||
|
||||
struct Events {
|
||||
present: Vec<String>,
|
||||
absent: Vec<Absent>,
|
||||
}
|
||||
|
||||
fn parse_events(tokens: &[String]) -> Result<Events, String> {
|
||||
let mut ev = Events {
|
||||
present: Vec::new(),
|
||||
absent: Vec::new(),
|
||||
};
|
||||
let mut i = 0;
|
||||
while i < tokens.len() {
|
||||
match tokens[i].as_str() {
|
||||
"present" => {
|
||||
let cmd = tokens.get(i + 1).ok_or("present: missing command")?;
|
||||
ev.present.push(cmd.clone());
|
||||
i += 2;
|
||||
}
|
||||
"absent" => {
|
||||
let secs = tokens.get(i + 1).ok_or("absent: missing seconds")?;
|
||||
let secs: u64 = secs
|
||||
.parse()
|
||||
.map_err(|_| format!("absent: bad seconds {secs:?}"))?;
|
||||
let cmd = tokens.get(i + 2).ok_or("absent: missing command")?;
|
||||
ev.absent.push(Absent {
|
||||
after: Duration::from_secs(secs),
|
||||
command: cmd.clone(),
|
||||
fired: true,
|
||||
});
|
||||
i += 3;
|
||||
}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"unknown event {other:?} (expected present or absent)"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ev)
|
||||
}
|
||||
|
||||
fn run(command: &str, wait: bool) {
|
||||
let mut child = Command::new("sh");
|
||||
child.arg("-c").arg(command);
|
||||
let command = command.to_string();
|
||||
let fut = async move {
|
||||
match child.status().await {
|
||||
Ok(s) if s.success() => {}
|
||||
Ok(s) => eprintln!("presence: command exited with {s}: {command}"),
|
||||
Err(e) => eprintln!("presence: failed to run {command:?}: {e}"),
|
||||
}
|
||||
};
|
||||
if wait {
|
||||
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut));
|
||||
} else {
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
}
|
||||
|
||||
struct Samples {
|
||||
per_device: HashMap<Address, VecDeque<(Instant, i16)>>,
|
||||
window: usize,
|
||||
hits: usize,
|
||||
threshold: i16,
|
||||
stale: Duration,
|
||||
}
|
||||
|
||||
impl Samples {
|
||||
fn push(&mut self, addr: Address, rssi: i16, now: Instant) -> bool {
|
||||
let q = self.per_device.entry(addr).or_default();
|
||||
q.push_back((now, rssi));
|
||||
while q.len() > self.window {
|
||||
q.pop_front();
|
||||
}
|
||||
self.is_near(addr, now)
|
||||
}
|
||||
|
||||
fn is_near(&self, addr: Address, now: Instant) -> bool {
|
||||
self.per_device
|
||||
.get(&addr)
|
||||
.map(|q| {
|
||||
q.iter()
|
||||
.filter(|(t, r)| now.duration_since(*t) <= self.stale && *r >= self.threshold)
|
||||
.count()
|
||||
>= self.hits
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn purge(&mut self, now: Instant) {
|
||||
let stale = self.stale;
|
||||
self.per_device.retain(|_, q| {
|
||||
while q
|
||||
.front()
|
||||
.is_some_and(|(t, _)| now.duration_since(*t) > stale)
|
||||
{
|
||||
q.pop_front();
|
||||
}
|
||||
!q.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
fn anyone_near(&self, now: Instant) -> bool {
|
||||
self.per_device.keys().any(|a| self.is_near(*a, now))
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_adapter(name: Option<&str>) -> bluer::Result<Adapter> {
|
||||
let session = Session::new().await?;
|
||||
let adapter = match name {
|
||||
Some(n) => session.adapter(n)?,
|
||||
None => session.default_adapter().await?,
|
||||
};
|
||||
adapter.set_powered(true).await?;
|
||||
adapter
|
||||
.set_discovery_filter(DiscoveryFilter {
|
||||
transport: DiscoveryTransport::Le,
|
||||
duplicate_data: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
Ok(adapter)
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
let mut events = match parse_events(&args.events) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!("presence: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
if !args.calibrate && events.present.is_empty() && events.absent.is_empty() {
|
||||
eprintln!("presence: no events given and not --calibrate; nothing to do");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
if args.hits == 0 || args.hits > args.window {
|
||||
eprintln!("presence: --hits must be between 1 and --window");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let ignore: HashSet<Address> = args.ignore.iter().copied().collect();
|
||||
|
||||
let adapter = match open_adapter(args.adapter.as_deref()).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
eprintln!("presence: bluetooth: {e} (is bluetoothd running?)");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let mut stream = match adapter.discover_devices_with_changes().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("presence: discovery: {e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
eprintln!(
|
||||
"presence: scanning on {} (near = {} of {} samples >= {} dBm, samples expire after {}s)",
|
||||
adapter.name(),
|
||||
args.hits,
|
||||
args.window,
|
||||
args.rssi,
|
||||
args.stale
|
||||
);
|
||||
|
||||
let mut samples = Samples {
|
||||
per_device: HashMap::new(),
|
||||
window: args.window,
|
||||
hits: args.hits,
|
||||
threshold: args.rssi,
|
||||
stale: Duration::from_secs(args.stale),
|
||||
};
|
||||
let started = Instant::now();
|
||||
let mut present = false;
|
||||
let mut last_near = Instant::now();
|
||||
let mut tick = tokio::time::interval(Duration::from_secs(1));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => break,
|
||||
ev = stream.next() => {
|
||||
let Some(AdapterEvent::DeviceAdded(addr)) = ev else { if ev.is_none() { eprintln!("presence: discovery stream ended"); break; } continue; };
|
||||
if ignore.contains(&addr) { continue; }
|
||||
let Ok(device) = adapter.device(addr) else { continue };
|
||||
let Ok(Some(rssi)) = device.rssi().await else { continue };
|
||||
let now = Instant::now();
|
||||
let near = samples.push(addr, rssi, now);
|
||||
if args.calibrate {
|
||||
let name = device.name().await.ok().flatten().unwrap_or_default();
|
||||
println!("{:9.1} {} {:4} {} {}", started.elapsed().as_secs_f64(), addr, rssi, if near { "NEAR" } else { " " }, name);
|
||||
}
|
||||
}
|
||||
_ = tick.tick() => {
|
||||
let now = Instant::now();
|
||||
samples.purge(now);
|
||||
let near = samples.anyone_near(now);
|
||||
if near {
|
||||
last_near = now;
|
||||
// Fire `present` only after at least one absence has been announced,
|
||||
// so a phone flickering around the threshold does not re-run it.
|
||||
if !present && (events.absent.is_empty() || events.absent.iter().any(|a| a.fired)) {
|
||||
present = true;
|
||||
if !args.calibrate {
|
||||
for cmd in &events.present { run(cmd, args.wait); }
|
||||
}
|
||||
for a in events.absent.iter_mut() { a.fired = false; }
|
||||
eprintln!("presence: present");
|
||||
} else if !present {
|
||||
present = true;
|
||||
for a in events.absent.iter_mut() { a.fired = false; }
|
||||
}
|
||||
} else {
|
||||
let away = now.duration_since(last_near);
|
||||
for a in events.absent.iter_mut() {
|
||||
if !a.fired && away >= a.after {
|
||||
a.fired = true;
|
||||
eprintln!("presence: absent for {}s", a.after.as_secs());
|
||||
if !args.calibrate { run(&a.command, args.wait); }
|
||||
}
|
||||
}
|
||||
if present && events.absent.iter().all(|a| a.fired) { present = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Reference in New Issue
Block a user