# 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 - 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 ` command-line argument 2. `BUTTONTASK_CONFIG` environment variable 3. `/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://: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// ButtonTask + webconfig/ (one dir per deployed version) current -> versions/ (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 \ /opt/buttontask/scripts/ sudo /opt/buttontask/scripts/bt-set-master 'your-emergency-password' sudo tee /etc/sudoers.d/buttontask > /dev/null << 'EOF' buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-netconfig buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-update buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-service EOF 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 tee /etc/sudoers.d/buttontask > /dev/null << 'EOF' buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-netconfig buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-update buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-service EOF 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/`, 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": "/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 "} "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.