Files
Button/README.md
T
2026-06-17 04:55:15 +03:00

218 lines
8.4 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
- 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
- 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`
## 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 \
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`.
## 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 /opt/buttontask/scripts/
sudo install -m 0440 webconfig/scripts/buttontask.sudoers /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
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
```
## 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>`, 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.
## 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": "./icons", // dir for user icons & background images
"webPort": 8080
},
"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
}
}
]
}
```
## 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.