Files
Button/README.md
T
GrigoandClaude Opus 5.5 f07e153f09 Boot splash and periodic announcement banner
Splash: resources/splash/splash.gif built into the binary, played once by
SplashOverlay over a black cover present from the first frame (no UI
flash), then fades into the UI; hides icons loading. GIF, so no
QtMultimedia/gstreamer. Toggle: splash.enabled.

Banner (InfoBanner): metro-style band that fades/slides in with a hold
pictogram; text that does not fit runs as a seamless ticker for two
rounds. Never takes touches; hidden during splash and settings. Config
section banner (enabled, text, intervalSec, showSec, position,
accentColor), editable in the web UI and on-device settings; shown once
after saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:58:16 +00:00

409 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ButtonTask v2
Configurable touch-screen button panel for embedded Linux (Qt5 + QML), with a
companion Flask web configurator. Both share a single JSON config file.
## Features
- Dynamic list of buttons (1..N), each fires HTTP GET/POST on tap
- Per-button trigger mode: press-and-hold (DelayButton-style, animated progress
ring, configurable hold duration) or plain click — set in both editors
- Layout: grid (configurable column count) or list
- Configurable background: animated gradient, solid color or image
- Configurable feedback ring: color and width of the OK / error glow around buttons
- In-app settings dialog (tabbed pages): CRUD buttons, drag-and-drop reorder,
layout/background/feedback/password, Ethernet configuration and update info
- Web configurator (Flask blueprints + tabbed UI + SortableJS): same operations
remotely, plus icon upload, Ethernet configuration and OTA updates
- Main-screen tabs: buttons panel / BLE sensors table (MAC, battery, reading, RSSI)
- BLE GATT collector (`collector/`, bless + BlueZ): accepts sensor reports, 4-slot
registry, advertising mirror; status shared via `/opt/buttontask/run/`
- Ethernet configuration via NetworkManager (`nmcli`) from the web UI
- OTA software update with automatic rollback on a failed launch (health-check)
- Hot reload: app picks up config changes via `QFileSystemWatcher`
- Boot splash (`resources/splash/splash.gif`, built into the binary, played once
by `qml/SplashOverlay.qml`; covers icons loading). GIF, not mp4: no QtMultimedia
- Periodic announcement banner (`qml/InfoBanner.qml`, metro-style fade/slide,
ticker scroll for long text, never takes touches); text, period, duration,
position and colour in the web UI («Внешний вид») and on-device settings
## Layout
```
ButtonTask/
CMakeLists.txt Qt5 build
main.cpp
src/ C++ sources (ConfigManager, ButtonsModel, ButtonController,
SettingsContainer, SystemInfo, SensorsBridge)
collector/ BLE GATT collector (bless/BlueZ peripheral, N=4 slots)
qml/ Main.qml, SensorsPage.qml, BackgroundLayer.qml, ButtonDelegate.qml, SettingsDialog.qml
components/ BackHeader.qml, LabeledSlider.qml
settings/ MainPage / ButtonsPage / LayoutPage / BackgroundPage /
FeedbackPage / NetworkInfoPage / UpdatePage / ButtonEditorDialog
resources/ qml.qrc, icons.qrc
config/config.json default config (shipped next to the binary)
webconfig/ Flask service (modular):
app.py application factory + /healthz
config_store.py load/save/defaults (in sync with C++ ensureDefaults)
auth.py login/logout/session
api.py buttons/layout/background/feedback/settings/icons
network.py Ethernet via nmcli
updater.py OTA upload + status
paths.py install layout helpers
templates/ static/ *.service, requirements.txt
scripts/ bt-netconfig, bt-update, bt-service, buttontask.sudoers,
make-package.sh
```
The Qt app reads the config from (in order of priority):
1. `--config <path>` command-line argument
2. `BUTTONTASK_CONFIG` environment variable
3. `<applicationDir>/config/config.json` (default)
The Flask service reads the same paths.
## Building (embedded Linux, Qt5)
```sh
sudo apt install qtbase5-dev qtdeclarative5-dev \
qml-module-qtquick-controls2 qml-module-qtquick-layouts \
qml-module-qtgraphicaleffects qml-module-qtqml-models2 \
cmake build-essential
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
sudo make install
```
Binary lands in `/usr/local/bin/ButtonTask`. To run on a framebuffer/EGL device:
```sh
QT_QPA_PLATFORM=eglfs ButtonTask --config /opt/buttontask/config/config.json
```
## Web configurator
```sh
cd webconfig
pip3 install -r requirements.txt
BUTTONTASK_CONFIG=../config/config.json python3 app.py
```
Open `http://<device-ip>:8080`. Login: `admin` + the password from `settings.password`.
**Web port (`settings.webPort`):** use **1024 or higher** (default `8080`). On Linux,
ports **1–1023** are *privileged*: the web service runs as the unprivileged
`buttontask` user and cannot bind to them unless the systemd unit grants
`CAP_NET_BIND_SERVICE` (see `webconfig/buttontask-web.service`). Setting e.g. port
80 without that capability causes `Permission denied` and a restart loop.
## Deployment with systemd (versioned layout)
The device uses an A/B-style versioned layout so OTA updates can roll back:
```
/opt/buttontask/
versions/<ver>/ ButtonTask + webconfig/ (one dir per deployed version)
current -> versions/<ver> (active version symlink)
config/ icons/ shared, never replaced by an update
run/ qt.alive heartbeat, update-status.json
scripts/ bt-netconfig, bt-update, bt-service (root-owned)
```
First install:
```sh
sudo useradd -r -s /bin/false buttontask
sudo mkdir -p /opt/buttontask/{versions/2.0,config,icons,run,scripts}
sudo cp build/ButtonTask /opt/buttontask/versions/2.0/
sudo cp -r webconfig /opt/buttontask/versions/2.0/
sudo ln -sfn /opt/buttontask/versions/2.0 /opt/buttontask/current
sudo cp config/config.json /opt/buttontask/config/
# privileged helpers (root-owned) + sudoers whitelist
sudo install -m 0755 webconfig/scripts/bt-netconfig webconfig/scripts/bt-update \
webconfig/scripts/bt-service webconfig/scripts/bt-set-master \
webconfig/scripts/bt-settime webconfig/scripts/bt-reboot \
webconfig/scripts/bt-access webconfig/scripts/bt-brightness \
/opt/buttontask/scripts/
sudo /opt/buttontask/scripts/bt-set-master 'your-emergency-password'
sudo install -m 0440 webconfig/scripts/buttontask.sudoers /etc/sudoers.d/buttontask
sudo chmod 0440 /etc/sudoers.d/buttontask
sudo visudo -cf /etc/sudoers.d/buttontask
sudo chown -R buttontask:buttontask /opt/buttontask
sudo chown root:root /opt/buttontask/scripts/bt-* # scripts must be root-owned
# Qt eglfs needs a writable runtime dir for the unprivileged user
sudo mkdir -p /tmp/runtime-buttontask
sudo chown buttontask:buttontask /tmp/runtime-buttontask
sudo chmod 0700 /tmp/runtime-buttontask
# eglfs grabs KMS/DRM directly; disable the desktop display manager
sudo systemctl disable display-manager
sudo systemctl stop display-manager
sudo cp webconfig/buttontask.service /etc/systemd/system/
sudo cp webconfig/buttontask-web.service /etc/systemd/system/
sudo cp webconfig/buttontask-collector.service /etc/systemd/system/
# BLE collector needs BlueZ + bless (see collector/requirements.txt / wheels/)
sudo usermod -aG bluetooth buttontask
# auto-restart bluetoothd after crashes (stock unit has no Restart=)
sudo install -D -m0644 webconfig/bluetooth.service.d/override.conf \
/etc/systemd/system/bluetooth.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl enable --now bluetooth buttontask buttontask-web buttontask-collector
# verify sudoers (should succeed without a password prompt)
sudo -u buttontask sudo -n /opt/buttontask/scripts/bt-netconfig eth0 dhcp
```
### Touchscreen (device-specific override)
The base `buttontask.service` does not hardcode a touch device path because the
`eventN` number varies between boards. Copy the example override and set the
correct event for your touchscreen:
```sh
# find the touchscreen event number
grep -l -E 'ABS_X|ABS_MT_POSITION' /sys/class/input/event*/device/capabilities/abs 2>/dev/null \
| sed 's|.*/input/event||;s|/device.*||'
sudo mkdir -p /etc/systemd/system/buttontask.service.d
sudo cp /opt/buttontask/current/webconfig/buttontask.service.d/override.conf.example \
/etc/systemd/system/buttontask.service.d/override.conf
# edit override.conf — replace event1 with the number found above
sudo systemctl daemon-reload && sudo systemctl restart buttontask
```
### Migrating an already-deployed device
If the device was installed from an older guide, apply these fixes:
```sh
# 1. sudoers (fixes "sudo: a password is required" in web network config)
sudo install -m 0440 /opt/buttontask/current/webconfig/scripts/buttontask.sudoers \
/etc/sudoers.d/buttontask
sudo chmod 0440 /etc/sudoers.d/buttontask
sudo visudo -cf /etc/sudoers.d/buttontask
# 2. runtime dir
sudo mkdir -p /tmp/runtime-buttontask
sudo chown buttontask:buttontask /tmp/runtime-buttontask
sudo chmod 0700 /tmp/runtime-buttontask
# 3. display manager
sudo systemctl disable --now display-manager
# 4. updated unit + touch override (see above)
sudo cp /opt/buttontask/current/webconfig/buttontask.service /etc/systemd/system/
sudo systemctl daemon-reload
# 5. icons — until OTA with the path-resolution fix, set absolute iconsDir:
# "iconsDir": "/opt/buttontask/icons" in /opt/buttontask/config/config.json
sudo systemctl restart buttontask-web buttontask
```
### Troubleshooting
**`/usr/bin/env: 'bash\r': No such file or directory`**
Shell scripts were saved with Windows CRLF line endings. On the device, fix in place:
```sh
sudo sed -i 's/\r$//' /opt/buttontask/scripts/bt-*
```
Or re-copy the scripts from a repo checkout with LF endings (see `.gitattributes`).
**`sudo: a password is required` (web → Сеть)**
- `sudo visudo -cf /etc/sudoers.d/buttontask` must pass without errors
- web service must run as `buttontask`: `systemctl show -p User buttontask-web`
- helper scripts must be root-owned: `ls -la /opt/buttontask/scripts/bt-*`
- test manually: `sudo -u buttontask sudo -n /opt/buttontask/scripts/bt-netconfig eth0 dhcp`
**`QML Image: Cannot open: file:///./icons/...`**
- Icons are stored in `/opt/buttontask/icons/` (shared, outside the version dir)
- Set `"iconsDir": "/opt/buttontask/icons"` in config, or deploy a build with
the C++ path-resolution fix (relative `./icons` is resolved against
`BUTTONTASK_ROOT`, same as the web configurator)
**Touch not working**
- Check the override at `/etc/systemd/system/buttontask.service.d/override.conf`
- Re-detect the event number (see Touchscreen section above)
- Ensure the `buttontask` user is in supplementary groups `video render input`
(set in `buttontask.service`)
## Network configuration (Ethernet)
Open the web UI → tab **Сеть**. It reads the live state via `nmcli` and can
apply DHCP or a static IP/prefix/gateway/DNS. The change is performed by the
root helper `bt-netconfig` (whitelisted in sudoers); the unprivileged web
service never touches the network directly. The same settings can also be
applied from the device itself via the in-app **Сеть** page (it calls the same
`bt-netconfig` helper through sudo, so the Qt UI must run as the `buttontask`
user covered by the sudoers rule).
## OTA software update
Build a package and upload it from the web UI → tab **Обновление**:
```sh
./webconfig/scripts/make-package.sh 2.1 build dist
# -> dist/buttontask-2.1.tar.gz (+ sha256 to paste for integrity)
```
On upload, `bt-update` (detached, root) extracts the package into
`versions/<ver>`, syncs `webconfig/scripts/bt-*` and sudoers into
`/opt/buttontask/scripts/` and `/etc/sudoers.d/`, flips the `current` symlink,
restarts both services, then runs a health-check: the web `/healthz` must answer, the Qt UI heartbeat
(`run/qt.alive`) must be fresh, and the `buttontask` unit must be active within
`BUTTONTASK_HEALTH_TIMEOUT` (default 30s). If any check fails it automatically
switches `current` back to the previous version and restarts. Progress is shown
live and is also readable in the app's **Обновление** page.
OTA also changes system files as root (helpers, sudoers, systemd units and
config) and those are **not** rolled back. What it touches, the "old
`bt-update` runs the OTA" pitfall and how to add a new system change:
[docs/ota.md](docs/ota.md).
## Display / GUI watchdog
Targets "backlight on, no picture until power re-plug" (seen on PoE sites:
a supply dip can reset the MIPI-DSI panel while the RK3566 keeps running).
Three layers:
| Layer | What hangs | Detection | Action |
|---|---|---|---|
| Hardware watchdog (`system.conf.d/buttontask-watchdog.conf`) | kernel / systemd | `/dev/watchdog` not pinged for 20 s | board reset |
| `buttontask.service` + `SdWatchdog` | Qt GUI / render thread / GPU | no frame swapped for 30 s (`WATCHDOG=1` sent per real frame) | systemd kills + restarts the UI |
| `buttontask-displaywatch` (`bt-displaywatch`, root) | panel / DRM pipeline | kernel DRM/DSI timeouts; eth link down→up (PoE dip hint); UI restart storm; UI stuck stopping | panel re-init (stop UI → `fb0/blank` 4→0 → start UI); repeated → reboot (max 3/day) |
**Manual recovery on a dark screen:** hold **one finger for 10 s in the
bottom-left corner** (72×72 px; touch usually still works on a dark panel).
The corner is shielded by an empty MouseArea, so a button under it never
fires (and does not react to touches inside the corner). The panels in use are
single-touch (1 slot), hence no multi-finger gesture. Same from a
shell: `sudo /opt/buttontask/scripts/bt-displaywatch recover`.
**Incident log:** `/var/log/buttontask/display-incidents.log`
(`bt-displaywatch status`), also in the journal of `buttontask-displaywatch`.
At every boot it records whether the previous boot ended cleanly or abruptly
(power cut / hang), plus the last display/link kernel events of that boot.
Tunables (env in a unit drop-in): `DISPLAYWATCH_LINK_RECOVER=0` disables the
re-init on link flap; `DISPLAYWATCH_RECOVER_GAP`, `..._RECOVER_MAX`,
`..._RESTART_MAX`, `..._REBOOT_MAX` — see the script header.
Delivered by OTA: `bt-update` installs the units and the watchdog config
(`sync_system`); on devices whose OTA ran an older `bt-update`, the web service
runs `bt-update sync-system` at start. A changed `buttontask.service` applies
to the UI from its next restart (or reboot).
## LCD brightness
Web UI → **Система** → «Яркость экрана», or the on-device settings menu.
Uses `/sys/class/backlight/` via the privileged helper `bt-brightness`
(percent 0–100, stored in `settings.brightness`, restored when web/Qt start).
OTA packages sync `bt-*` helpers and sudoers via `bt-update`. On devices that
still run an older `bt-update` (before helper sync), run once after the first
OTA that includes this feature:
```sh
sudo install -m0755 /opt/buttontask/current/webconfig/scripts/bt-* \
/opt/buttontask/scripts/
sudo install -m0440 /opt/buttontask/current/webconfig/scripts/buttontask.sudoers \
/etc/sudoers.d/buttontask
sudo visudo -cf /etc/sudoers.d/buttontask
sudo -u buttontask sudo -n /opt/buttontask/scripts/bt-brightness status
```
## JSON config schema
```jsonc
{
"version": 2,
"layout": {
"mode": "grid", // "grid" | "list"
"columns": 2, // grid only
"spacing": 10,
"showLabels": true
},
"background": {
"type": "gradient", // "gradient" | "solid" | "image"
"color1": "#00007b",
"color2": "#7b007b",
"animated": true,
"imagePath": "" // filename inside iconsDir
},
"feedback": {
"okColor": "#00cc44", // ring/glow color on success
"errorColor": "#cc2200", // ring/glow color on error
"okWidth": 6, // ring width (px) on success
"errorWidth": 6, // ring width (px) on error
"glowRadius": 20 // glow radius (0 disables the glow)
},
"banner": { // periodic announcement over the buttons
"enabled": false,
"text": "Для вызова клининга удерживайте кнопку",
"intervalSec": 180, // 30..3600, period between shows
"showSec": 10, // 3..60, longer if the text has to scroll
"position": "bottom", // "bottom" | "top"
"accentColor": "#e4002b" // stripe + hold-ring colour
},
"splash": { "enabled": true }, // boot splash, read at app start
"settings": {
"darkMode": true,
"password": "admin", // shared with web auth
"kioskMode": false,
"iconsDir": "/opt/buttontask/icons", // dir for user icons & background images
"webPort": 8080 // use >= 1024 (1–1023 need root / CAP_NET_BIND_SERVICE)
},
"buttons": [
{
"id": "uuid",
"label": "Кнопка",
"iconPath": "cleaner1.png", // relative to iconsDir, or absolute
"action": {
"type": "http_get", // "http_get" | "http_post"
"url": "http://server/btn",
"headers": {}, // e.g. {"Authorization": "Bearer <token>"}
"body": "",
"timeoutMs": 7000, // optional request timeout (default 7000)
"acceptAnyResponse": false // optional: true = any HTTP reply is success
},
"feedback": {
"successText": "Отправлено",
"errorText": "Ошибка",
"fadeMs": 5000
},
"trigger": {
"mode": "hold", // "hold" (press-and-hold) | "click"
"holdMs": 800 // hold duration in ms before it fires
}
}
]
}
```
## Notes
- Both Qt and Flask write the config atomically (`QSaveFile` / `os.replace`).
- Qt watches the file via `QFileSystemWatcher` and reloads automatically when
the web configurator saves changes.
- Button success criteria: a request is **green** on a 1xx/2xx/3xx HTTP reply
(redirects are followed) and **red** on 4xx/5xx or a transport failure
(timeout / connection refused / DNS). Set `action.acceptAnyResponse: true` to
treat *any* received HTTP reply as success (only timeouts/refused fail), and
`action.timeoutMs` to tune the timeout. Add an API key/token via
`action.headers` if the target endpoint requires one.
- Web configurator endpoints are all session-protected (`@login_required` after
password login); only `/healthz` is public (it exposes just `ok` + version,
used by the OTA rollback health-check).
- Default credentials are `admin` / `admin` — change on first run.
- v1 (Android/RS485/voice) was removed; only the icon set was kept.