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>
319 lines
10 KiB
QML
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
|
|
}
|
|
}
|