352 lines
14 KiB
Markdown
352 lines
14 KiB
Markdown
# 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
|
||
- Button kinds: **press** (HTTP call) and **voice** (record PCM/WAV → classify-audio)
|
||
- Voice gestures: hold-to-speak or click start/stop; lit result cancels via cancel URL
|
||
- 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/Wi‑Fi configuration and update info
|
||
- Web configurator (Flask blueprints + tabbed UI + SortableJS): same operations
|
||
remotely, plus icon upload, Ethernet/Wi‑Fi configuration and OTA updates
|
||
- Network configuration via NetworkManager (`nmcli`) — Ethernet and Wi‑Fi
|
||
- OTA software update with automatic rollback on a failed launch (health-check)
|
||
- Hot reload: app picks up config changes via `QFileSystemWatcher`
|
||
- Demo server (`demo-server/`): Dockerized classify-audio + cancel + press log,
|
||
OpenRouter STT/classify via optional SOCKS5
|
||
|
||
## Layout
|
||
|
||
```
|
||
ButtonTask/
|
||
CMakeLists.txt Qt5 build
|
||
main.cpp
|
||
src/ C++ sources (ConfigManager, ButtonsModel, ButtonController,
|
||
SettingsContainer, SystemInfo)
|
||
qml/ Main.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 \
|
||
qtmultimedia5-dev libqt5multimedia5-plugins \
|
||
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 systemctl daemon-reload
|
||
sudo systemctl enable --now buttontask buttontask-web
|
||
|
||
# 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.
|
||
|
||
## 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)
|
||
},
|
||
"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.
|