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):
--config <path>command-line argumentBUTTONTASK_CONFIGenvironment variable<applicationDir>/config/config.json(default)
The Flask service reads the same paths.
Building (embedded Linux, Qt5)
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:
QT_QPA_PLATFORM=eglfs ButtonTask --config /opt/buttontask/config/config.json
Web configurator
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:
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 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:
# 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:
# 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:
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/buttontaskmust 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./iconsis resolved againstBUTTONTASK_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
buttontaskuser is in supplementary groupsvideo render input(set inbuttontask.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 Обновление:
./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
{
"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
},
"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
QFileSystemWatcherand 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: trueto treat any received HTTP reply as success (only timeouts/refused fail), andaction.timeoutMsto tune the timeout. Add an API key/token viaaction.headersif the target endpoint requires one. - Web configurator endpoints are all session-protected (
@login_requiredafter password login); only/healthzis public (it exposes justok+ 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.