import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 Dialog { id: dlg modal: true parent: Overlay.overlay title: "Калибровка" anchors.centerIn: parent width: Math.min(460, parent ? parent.width * 0.92 : 460) closePolicy: Popup.CloseOnEscape standardButtons: Dialog.NoButton property string step: "pick" property string mac: "" property string sensorName: "" property int minMm: 0 property int capturedMm: 0 property double baseline: 0 property string waitError: "" property var live: [] readonly property color fg: "#ffffff" readonly property color muted: "#aaaaaa" background: Rectangle { color: "#161628" radius: 4 } onOpened: begin() onClosed: waitTimeout.stop() function begin() { waitTimeout.stop() waitError = "" minMm = 0 capturedMm = 0 refreshLive() if (live.length === 0) { mac = "" sensorName = "" step = "empty" } else if (live.length === 1) { selectSensor(live[0]) step = "minInstruct" } else { mac = "" sensorName = "" step = "pick" } } function refreshLive() { var out = [] var slots = sensorsBridge.sensorSlots for (var i = 0; i < slots.length; ++i) { if (!slots[i].empty && slots[i].mac) out.push(slots[i]) } live = out } function selectSensor(s) { mac = String(s.mac).toUpperCase() sensorName = s.name && String(s.name).length ? s.name : mac } function slotOf(addr) { var slots = sensorsBridge.sensorSlots var want = String(addr).toUpperCase() for (var i = 0; i < slots.length; ++i) { if (!slots[i].empty && String(slots[i].mac).toUpperCase() === want) return slots[i] } return null } function startWait(which) { var s = slotOf(mac) var seen = s && s.lastSeen !== undefined ? Number(s.lastSeen) : 0 baseline = isNaN(seen) ? 0 : seen waitError = "" step = which === "max" ? "maxWait" : "minWait" waitTimeout.restart() } function checkWait() { if (step !== "minWait" && step !== "maxWait") return var s = slotOf(mac) if (!s) return var seen = Number(s.lastSeen) var dist = Number(s.distRaw) if (isNaN(seen) || isNaN(dist)) return if (seen > baseline && dist >= 0) { waitTimeout.stop() capturedMm = dist step = step === "minWait" ? "minConfirm" : "maxConfirm" } } function instructionText() { if (step === "empty") return "Нет подключённых датчиков." if (step === "pick") return "Выберите датчик для калибровки." if (step === "minInstruct") return "Поставьте минимум.\nНажмите «Снять» и дождитесь нового показания датчика." if (step === "minWait") return "Ожидание минимума.\nНовый отчёт может прийти через несколько минут." if (step === "minConfirm") return "Минимум: " + capturedMm + " мм.\nЭто значение будет 0%." if (step === "maxInstruct") return "Поставьте максимум.\nНажмите «Снять» и дождитесь нового показания датчика." if (step === "maxWait") return "Ожидание максимума.\nНовый отчёт может прийти через несколько минут." if (step === "maxConfirm") return "Максимум: " + capturedMm + " мм.\nЭто значение будет 100%.\nМинимум: " + minMm + " мм — 0%." return "" } Connections { target: sensorsBridge onChanged: { if (dlg.step === "pick" || dlg.step === "empty") dlg.refreshLive() dlg.checkWait() } } Timer { id: waitTimeout interval: 300000 onTriggered: { dlg.waitError = "Нет нового показания" dlg.step = dlg.step === "maxWait" ? "maxInstruct" : "minInstruct" } } contentItem: ColumnLayout { spacing: 10 Text { Layout.fillWidth: true visible: dlg.sensorName.length > 0 && dlg.step !== "pick" text: dlg.sensorName color: dlg.muted font.pixelSize: 12 elide: Text.ElideRight } Text { Layout.fillWidth: true text: (dlg.step, dlg.capturedMm, dlg.minMm, dlg.instructionText()) color: dlg.fg font.pixelSize: 15 wrapMode: Text.Wrap } Text { Layout.fillWidth: true visible: dlg.waitError.length > 0 text: dlg.waitError color: "#ff6666" font.pixelSize: 13 wrapMode: Text.Wrap } Column { Layout.fillWidth: true visible: dlg.step === "pick" spacing: 6 Repeater { model: dlg.live delegate: Button { width: parent.width implicitHeight: 36 text: (modelData.name && String(modelData.name).length ? modelData.name : "—") + " " + modelData.mac onClicked: { dlg.selectSensor(modelData) dlg.waitError = "" dlg.step = "minInstruct" } background: Rectangle { radius: 3 color: parent.down ? "#3a3a58" : "#2a2a40" border.color: "#555570" } contentItem: Text { text: parent.text color: dlg.fg horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 13 elide: Text.ElideRight } } } } RowLayout { Layout.fillWidth: true spacing: 6 Button { readonly property bool shown: dlg.step === "minInstruct" || dlg.step === "maxInstruct" visible: shown text: "Снять" Layout.fillWidth: shown Layout.preferredWidth: shown ? 80 : 0 Layout.maximumWidth: shown ? 10000 : 0 Layout.preferredHeight: 36 onClicked: dlg.startWait(dlg.step === "maxInstruct" ? "max" : "min") background: Rectangle { radius: 3 color: parent.down ? "#3a3a58" : "#2a2a40" border.color: "#555570" } contentItem: Text { text: parent.text color: dlg.fg horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 13 } } Button { readonly property bool shown: dlg.step === "minConfirm" || dlg.step === "maxConfirm" visible: shown text: "Подтвердить" Layout.fillWidth: shown Layout.preferredWidth: shown ? 80 : 0 Layout.maximumWidth: shown ? 10000 : 0 Layout.preferredHeight: 36 onClicked: { if (dlg.step === "minConfirm") { dlg.minMm = dlg.capturedMm dlg.waitError = "" dlg.step = "maxInstruct" } else if (!sensorsBridge.saveCalibration(dlg.mac, dlg.minMm, dlg.capturedMm)) { dlg.waitError = "Не удалось сохранить" } else { dlg.close() } } background: Rectangle { radius: 3 color: parent.down ? "#3a3a58" : "#2a2a40" border.color: "#555570" } contentItem: Text { text: parent.text color: dlg.fg horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 13 } } Button { readonly property bool shown: dlg.step === "minConfirm" || dlg.step === "maxConfirm" visible: shown text: "Снять заново" Layout.fillWidth: shown Layout.preferredWidth: shown ? 80 : 0 Layout.maximumWidth: shown ? 10000 : 0 Layout.preferredHeight: 36 onClicked: { dlg.waitError = "" dlg.step = dlg.step === "maxConfirm" ? "maxInstruct" : "minInstruct" } background: Rectangle { radius: 3 color: parent.down ? "#3a3a58" : "#2a2a40" border.color: "#555570" } contentItem: Text { text: parent.text color: dlg.fg horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 13 } } Button { text: "Отмена" Layout.fillWidth: true Layout.preferredHeight: 36 onClicked: dlg.close() background: Rectangle { radius: 3 color: parent.down ? "#3a3a58" : "#2a2a40" border.color: "#555570" } contentItem: Text { text: parent.text color: dlg.fg horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 13 } } } } }