Files
Button/qml/Main.qml
T
GrigoandClaude Opus 5.5 d71fa812da Display/GUI watchdog for the "backlight on, no picture" failure
On PoE sites the MIPI-DSI panel can lose its init on a supply dip while the
RK3566 keeps running; the UI may also hang or exit. Three layers:

- hardware watchdog: systemd RuntimeWatchdogSec=20s (system.conf.d)
- GUI: SdWatchdog pings systemd per swapped frame (WATCHDOG_USEC=30s, enabled
  by the binary so older versions run under the same unit) and writes
  run/qt.frame; buttontask.service gets Restart=always, NotifyAccess=main,
  TimeoutAbortSec=10s
- bt-displaywatch (root, buttontask-displaywatch.service): kernel DRM/DSI
  timeouts, eth link down->up (PoE dip hint), stale UI frames, restart storms
  -> panel re-init (stop UI, fb0 blank 4->0, start UI), repeated -> reboot
  (max 3/day, never during OTA); incident log in /var/log/buttontask/

Manual recovery: hold one finger 10 s in the bottom-left corner (panels are
single-touch). Delivered by OTA via bt-update sync_system; the web service
runs `bt-update sync-system` at start for devices updated by an older
bt-update.

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

319 lines
10 KiB
QML

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
import QtQuick.VirtualKeyboard 2.4
Window {
id: root
visible: true
width: 480
height: 480
title: "ButtonTask"
color: "black"
visibility: Window.FullScreen
property bool darkMode: true
function hideSensorsIfDisabled() {
if (!settingsContainer.collectorEnabled && mainTabs.currentIndex > 0)
mainTabs.currentIndex = 0
}
BackgroundLayer {
id: bg
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 6
TabBar {
id: mainTabs
Layout.fillWidth: true
Layout.preferredHeight: 36
background: Rectangle {
color: "#161628"
radius: 4
}
TabButton {
text: "Кнопки"
width: implicitWidth
contentItem: Text {
text: parent.text
color: parent.checked ? "#ffffff" : "#aaaaaa"
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? "#2a2a40" : "transparent"
radius: 4
}
}
TabButton {
id: sensorsTabBtn
text: "Датчики"
visible: settingsContainer.collectorEnabled
width: visible ? implicitWidth : 0
contentItem: Text {
text: parent.text
color: parent.checked ? "#ffffff" : "#aaaaaa"
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? "#2a2a40" : "transparent"
radius: 4
}
}
}
StackLayout {
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: mainTabs.currentIndex
// --- Buttons ---
Item {
id: content
Loader {
id: viewLoader
anchors.fill: parent
sourceComponent: settingsContainer.layoutMode === "list" ? listComp : gridComp
}
Component {
id: gridComp
Item {
id: gridRoot
anchors.fill: parent
readonly property int gridColumns: Math.max(1, settingsContainer.layoutColumns)
readonly property real cellSide: Math.max(64, width / gridColumns)
readonly property int gridRows: buttonsModel.count === 0
? 0
: Math.ceil(buttonsModel.count / gridColumns)
readonly property real gridContentHeight: gridRows * cellSide
readonly property bool gridFits: gridContentHeight > 0
&& gridContentHeight <= height
GridView {
id: gridView
width: gridRoot.width
height: gridRoot.gridFits ? gridRoot.gridContentHeight : gridRoot.height
y: gridRoot.gridFits ? (gridRoot.height - gridRoot.gridContentHeight) / 2 : 0
cellWidth: gridRoot.cellSide
cellHeight: gridRoot.cellSide
model: buttonsModel
clip: true
interactive: !gridRoot.gridFits
boundsBehavior: Flickable.StopAtBounds
delegate: ButtonDelegate {
width: gridView.cellWidth - settingsContainer.layoutSpacing
height: gridView.cellHeight - settingsContainer.layoutSpacing
showLabel: settingsContainer.showLabels
}
}
}
}
Component {
id: listComp
SwipeView {
id: swipeView
anchors.fill: parent
clip: true
interactive: buttonsModel.count > 1
Repeater {
model: buttonsModel
Item {
width: swipeView.width
height: swipeView.height
ButtonDelegate {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.8
height: width
showLabel: settingsContainer.showLabels
}
}
}
}
}
Text {
anchors.centerIn: parent
visible: buttonsModel.count === 0
text: "Нет кнопок.\nОткройте настройки и добавьте кнопку."
color: root.darkMode ? "#cccccc" : "#222222"
font.pixelSize: 18
horizontalAlignment: Text.AlignHCenter
}
}
// --- Sensors ---
SensorsPage {
id: sensorsPage
}
}
}
// Hidden hotspot: double-tap the top-right corner to open settings.
MouseArea {
id: settingsHotspot
anchors.right: parent.right
anchors.top: parent.top
width: 72
height: 72
z: 20
onDoubleClicked: passwordDialog.open()
}
// Display recovery corner: holding it 10 s re-inits the panel (detected in
// C++ RecoveryGesture). This area only shields buttons underneath; a
// button reaching into the corner does not react to touches there.
MouseArea {
id: recoveryHotspot
anchors.left: parent.left
anchors.bottom: parent.bottom
width: 72
height: 72
z: 20
}
Connections {
target: settingsContainer
onChanged: root.hideSensorsIfDisabled()
}
Component.onCompleted: hideSensorsIfDisabled()
readonly property bool secureDialogOpen: passwordDialog.opened || settingsDialog.opened
onSecureDialogOpenChanged: uiIdle.active = secureDialogOpen
Connections {
target: uiIdle
onIdle: {
if (passwordDialog.opened)
passwordDialog.close()
if (settingsDialog.opened)
settingsDialog.close()
}
}
Dialog {
id: passwordDialog
modal: true
title: "Пароль"
anchors.centerIn: parent
width: Math.min(420, parent.width * 0.8)
closePolicy: Popup.CloseOnEscape
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: ColumnLayout {
spacing: 8
Item {
Layout.fillWidth: true
Layout.preferredHeight: 40
TextField {
id: pwdField
anchors.fill: parent
placeholderText: "Введите пароль"
echoMode: TextInput.Password
selectByMouse: false
onAccepted: passwordDialog.accept()
onTextChanged: {
passwordDialog.showLast = length > 0
if (length === 0)
maskTimer.stop()
else
maskTimer.restart()
if (cursorPosition !== length)
cursorPosition = length
}
onCursorPositionChanged: {
if (cursorPosition !== length)
cursorPosition = length
}
}
Rectangle {
visible: passwordDialog.showLast && pwdField.length > 0
color: pwdField.palette.base
readonly property real cell: Math.max(
lastChar.implicitWidth,
(pwdField.cursorRectangle.x - pwdField.leftPadding) / pwdField.length)
x: pwdField.cursorRectangle.x - cell
y: pwdField.cursorRectangle.y
width: cell
height: pwdField.cursorRectangle.height
Text {
id: lastChar
anchors.centerIn: parent
text: pwdField.text.charAt(pwdField.text.length - 1)
font: pwdField.font
color: pwdField.palette.text
}
}
}
Text {
id: wrongPwd
Layout.fillWidth: true
visible: false
color: "red"
text: "Неверный пароль"
}
}
property bool showLast: false
Timer {
id: maskTimer
interval: 1000
onTriggered: passwordDialog.showLast = false
}
onOpened: {
pwdField.text = ""
showLast = false
wrongPwd.visible = false
pwdField.forceActiveFocus()
}
onAccepted: {
if (btnController.checkPassword(pwdField.text)) {
wrongPwd.visible = false
pwdField.text = ""
settingsDialog.open()
} else {
wrongPwd.visible = true
Qt.callLater(passwordDialog.open)
}
}
}
SettingsDialog {
id: settingsDialog
}
InputPanel {
id: inputPanel
parent: Overlay.overlay
z: 999
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: active
}
}