diff --git a/ENNOID-BMS-Tool.pro b/ENNOID-BMS-Tool.pro index c40953e..11e49b2 100644 --- a/ENNOID-BMS-Tool.pro +++ b/ENNOID-BMS-Tool.pro @@ -173,6 +173,8 @@ TRANSLATIONS = translations/cubo_en.ts \ translations/cubo_ru.ts \ translations/cubo_it.ts +RC_ICONS = qml/Icons/cubo-icon.ico + DISTFILES += \ android/AndroidManifest.xml \ android/gradle/wrapper/gradle-wrapper.jar \ diff --git a/qml/Controls/ChartView.qml b/qml/Controls/ChartView.qml index 8bdcb1a..b358979 100644 --- a/qml/Controls/ChartView.qml +++ b/qml/Controls/ChartView.qml @@ -1,6 +1,8 @@ import QtQuick 2.12 import QtCharts 2.3 +import QtQuick.Shapes 1.12 +import Controls 1.0 as Controls import Utils 1.0 ChartView { @@ -20,6 +22,7 @@ ChartView { property int defaultXMax: 10 property int defaultYMax: 1 property bool autoScaling: true + property int selectedSeriesIndex: 0 ValueAxis { id: valueAxisX @@ -45,10 +48,47 @@ ChartView { titleVisible: false } + Shape { + id: currentPointLine + implicitWidth: 1 + implicitHeight: chart.plotArea.height + x: chart.plotArea.x + y: chart.plotArea.y + + ShapePath { + strokeColor: Palette.borderColor + strokeStyle: ShapePath.SolidLine + startX: 0 + startY: 0 + PathLine { x: 0; y: currentPointLine.height } + } + } + + Rectangle { + id: currentPointInfo + + width: currentPointInfoLabel.width + height: currentPointInfoLabel.height + color: Palette.backgroundColor + border.width: 1 + border.color: Palette.borderColor + opacity: 0.9 + radius: 6 + visible: false + + Controls.SubtitleLabel { + id: currentPointInfoLabel + horizontalAlignment: Text.AlignHCenter + anchors.centerIn: parent + padding: 10 + } + } + MouseArea { - id: chartMouseAreaA + id: chartMouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton + hoverEnabled: true property real lastMouseX: 0 property real lastMouseY: 0 @@ -59,27 +99,28 @@ ChartView { if ((mouse.buttons & Qt.LeftButton) == Qt.LeftButton) { if (mouseX - lastMouseX > scrollThreshold) { chart.scrollLeft(mouseX - lastMouseX) - lastMouseX = mouseX - chart.autoScaling = false } else if (mouseX - lastMouseX < scrollThreshold) { chart.scrollRight(lastMouseX - mouseX) - lastMouseX = mouseX - chart.autoScaling = false } + lastMouseX = mouseX + chart.autoScaling = false } + + updatePointInfo() } onMouseYChanged: { + if ((mouse.buttons & Qt.LeftButton) == Qt.LeftButton) { if (mouseY - lastMouseY > scrollThreshold) { chart.scrollUp(mouseY - lastMouseY) - lastMouseY = mouseY - chart.autoScaling = false } else if (mouseY - lastMouseY < scrollThreshold) { chart.scrollDown(lastMouseY - mouseY) - lastMouseY = mouseY - chart.autoScaling = false } + lastMouseY = mouseY + chart.autoScaling = false } + + updatePointInfo() } onPressed: { if (mouse.button === Qt.LeftButton) { @@ -88,13 +129,71 @@ ChartView { } } onWheel: { - if (wheel.angleDelta.y > 0) { - chart.zoomIn() - chart.autoScaling = false - } else { - chart.zoomOut() - chart.autoScaling = false + var scaleFactor = 0.75 + var area = chart.plotArea + chart.autoScaling = false + + var yAxisPosition = mouseX <= chart.plotArea.x + if (yAxisPosition) { + if (wheel.angleDelta.y > 0) { + chart.zoomIn(Qt.rect(area.x, area.y + Math.round(area.height * (1 - scaleFactor) / 2), area.width, Math.round(area.height * scaleFactor))) + } else { + chart.zoomIn(Qt.rect(area.x, area.y - Math.round((area.height / scaleFactor) * (1 - scaleFactor) / 2), area.width, Math.round(area.height / scaleFactor))) + } + return } + + var xAxisPosition = mouseY >= (chart.plotArea.y + chart.plotArea.height) + if (xAxisPosition) { + if (wheel.angleDelta.y > 0) { + chart.zoomIn(Qt.rect(area.x + area.width * (1 - scaleFactor) / 2, area.y, Math.round(area.width * scaleFactor), area.height)) + } else { + chart.zoomIn(Qt.rect(area.x - (area.width / scaleFactor) * (1 - scaleFactor) / 2, area.y, Math.round(area.width / scaleFactor), area.height)) + } + return + } + + if (wheel.angleDelta.y > 0) { + let zoomedInArea = Qt.rect(area.x + Math.round((mouseX - area.x) * (1 - scaleFactor)), + area.y + Math.round((mouseY - area.y) * (1 - scaleFactor)), + Math.round(area.width * scaleFactor), + Math.round(area.height * scaleFactor)) + chart.zoomIn(zoomedInArea) + } else { + let zoomedOutArea = Qt.rect(area.x - Math.floor((mouseX - area.x) * (1 - scaleFactor)), + area.y - Math.floor((mouseY - area.y) * (1 - scaleFactor)), + Math.round(area.width * (1 + (1 - scaleFactor))), + Math.round(area.height * (1 + (1 - scaleFactor)))) + chart.zoomIn(zoomedOutArea) + } + + updatePointInfo() + } + + function updatePointInfo() { + var currentPointVisibility = mouseX >= chart.plotArea.x && mouseX <= chart.plotArea.x + chart.plotArea.width && + mouseY >= chart.plotArea.y && mouseY <= chart.plotArea.y + chart.plotArea.height + + currentPointInfo.visible = currentPointVisibility + currentPointInfo.x = mouseX + 5 + currentPointInfo.y = mouseY - currentPointInfo.height - 5 + + var currentX = Math.ceil(mapToValue(Qt.point(mouseX, mouseY), chart.series(selectedSeriesIndex)).x) + var selectedPoint = Qt.point(0, 0) + + if (currentX < chart.series(selectedSeriesIndex).count && currentX > 0) { + selectedPoint = chart.series(selectedSeriesIndex).at(currentX) + currentPointInfoLabel.text = selectedPoint.x + ", " +selectedPoint.y + } else { + currentPointInfo.visible = false + } + + currentPointLine.visible = currentPointVisibility + currentPointLine.x = mouseX } } + + onSeriesAdded: if (count === 1) { + series.pointAdded.connect(function (index){ chartMouseArea.updatePointInfo() }) + } } diff --git a/qml/Controls/ImageButton.qml b/qml/Controls/ImageButton.qml new file mode 100644 index 0000000..c4f1a67 --- /dev/null +++ b/qml/Controls/ImageButton.qml @@ -0,0 +1,27 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import Utils 1.0 + +Button { + id: control + + icon.width: 24 + icon.height: 24 + + contentItem: Image { + source: control.icon.source + sourceSize.width: control.icon.width + sourceSize.height: control.icon.height + fillMode: Image.PreserveAspectFit + } + + background: Rectangle { + implicitWidth: 44 + implicitHeight: 44 + opacity: enabled ? 1 : 0.3 + color: control.pressed ? Palette.pressedButtonColor : + control.hovered ? Palette.hoveredButtonColor : Palette.buttonColor + radius: 5 + } +} diff --git a/qml/Controls/TextField.qml b/qml/Controls/TextField.qml index e5af946..c61f75a 100644 --- a/qml/Controls/TextField.qml +++ b/qml/Controls/TextField.qml @@ -9,6 +9,8 @@ TextField { color: Palette.textColor selectByMouse: true selectionColor: Palette.selectedTextBackgroundColor + leftPadding: 16 + rightPadding: 16 background: Rectangle { color: enabled ? Palette.backgroundColor : Palette.hoveredBackgroundColor diff --git a/qml/Controls/qmldir b/qml/Controls/qmldir index 426b3c6..29ecb8f 100644 --- a/qml/Controls/qmldir +++ b/qml/Controls/qmldir @@ -24,3 +24,4 @@ BusyIndicator 1.0 BusyIndicator.qml MenuItemDelegate 1.0 MenuItemDelegate.qml ScrollIndicator 1.0 ScrollIndicator.qml OutlineImageButton 1.0 OutlineImageButton.qml +ImageButton 1.0 ImageButton.qml diff --git a/qml/Icons/cubo-icon.ico b/qml/Icons/cubo-icon.ico new file mode 100644 index 0000000..74750a7 Binary files /dev/null and b/qml/Icons/cubo-icon.ico differ diff --git a/qml/MainWindow.qml b/qml/MainWindow.qml index 74387e9..a2b34d9 100644 --- a/qml/MainWindow.qml +++ b/qml/MainWindow.qml @@ -9,7 +9,7 @@ import Utils 1.0 ApplicationWindow { id: window - title: qsTr("Cubo Verde BMS tool") + title: qsTr("Cubo Verde BMS") width: 1366 height: 768 visible: true diff --git a/qml/Screens/AkbMonitorScreen.qml b/qml/Screens/AkbMonitorScreen.qml index 099f275..182c908 100644 --- a/qml/Screens/AkbMonitorScreen.qml +++ b/qml/Screens/AkbMonitorScreen.qml @@ -6,6 +6,8 @@ import Cubo 1.0 import Utils 1.0 Item { + id: root + ColumnLayout { spacing: 15 anchors.fill: parent @@ -298,6 +300,8 @@ Item { Connections { target: BmsInterface.commands() + enabled: root.visible + onValuesReceived: { batteryChargeLevelLabel.text = values.soC batteryVoltageLabel.text = MathHelper.roundDouble(values.packVoltage) @@ -334,7 +338,7 @@ Item { Timer { id: refreshValuesTimer - interval: 5000 + interval: 1000 onTriggered: getValues() } diff --git a/qml/Screens/BmsSettingsScreen.qml b/qml/Screens/BmsSettingsScreen.qml index bc16222..96454d9 100644 --- a/qml/Screens/BmsSettingsScreen.qml +++ b/qml/Screens/BmsSettingsScreen.qml @@ -15,13 +15,15 @@ RowLayout { signal needWait(bool active, string text) - spacing: contentColumnSpacing + spacing: contentColumnSpacing - 10 Flickable { id: settingsFlickable clip: true + contentWidth: width - rightMargin - leftMargin contentHeight: configLayout.height boundsBehavior: Flickable.StopAtBounds + rightMargin: 10 ColumnLayout { id: configLayout diff --git a/qml/Screens/CellMonitorScreen.qml b/qml/Screens/CellMonitorScreen.qml index 48ce212..a9fc42e 100644 --- a/qml/Screens/CellMonitorScreen.qml +++ b/qml/Screens/CellMonitorScreen.qml @@ -8,6 +8,8 @@ import Cubo 1.0 import Utils 1.0 Item { + id: root + Component { id: cellListHeader @@ -22,6 +24,7 @@ Item { Layout.fillWidth: true Controls.SubtitleLabel { + id: indexTitleLabel text: qsTr("#") color: Palette.tableHeaderTextColor anchors.centerIn: parent @@ -35,6 +38,7 @@ Item { Layout.fillWidth: true Controls.SubtitleLabel { + id: voltageTitleLabel text: qsTr("Voltage") color: Palette.tableHeaderTextColor anchors.centerIn: parent @@ -48,6 +52,7 @@ Item { Layout.fillWidth: true Controls.SubtitleLabel { + id: balancingTitleLabel text: qsTr("Balancing") color: Palette.tableHeaderTextColor anchors.centerIn: parent @@ -64,15 +69,16 @@ Item { RowLayout { spacing: 10 width: ListView.view.width - height: 38 + height: 36 Item { - Layout.preferredWidth: parent.width / 6 - 20 + Layout.preferredWidth: parent.width / 6 - 24 } Controls.SubtitleLabel { id: indexLabel color: Palette.tableHeaderTextColor + horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: 25 Layout.alignment: Qt.AlignCenter } @@ -96,7 +102,7 @@ Item { } Item { - Layout.preferredWidth: parent.width / 6 - 20 + Layout.preferredWidth: parent.width / 6 - 24 } Component.onCompleted: { @@ -157,12 +163,14 @@ Item { Connections { target: BmsInterface.commands() + enabled: root.visible + onCellsReceived: { firstCellGroup.model = [] var firstModel = [] for (var i = 0; i < Math.min(cellCount, 16); ++i) { - firstModel.push({"voltage": MathHelper.roundDouble(cellVoltageArray[i]), "balancing": cellVoltageArray[i] < 0}) + firstModel.push({"voltage": Math.abs(MathHelper.roundDouble(cellVoltageArray[i], 3)), "balancing": cellVoltageArray[i] < 0}) } firstCellGroup.model = firstModel @@ -171,7 +179,7 @@ Item { var secondModel = [] for (var j = 16; j < Math.min(cellCount, 32); ++j) { - secondModel.push({"voltage": MathHelper.roundDouble(cellVoltageArray[j]), "balancing": cellVoltageArray[i] < 0}) + secondModel.push({"voltage": Math.abs(MathHelper.roundDouble(cellVoltageArray[j], 3)), "balancing": cellVoltageArray[i] < 0}) } secondCellGroup.model = secondModel } @@ -187,7 +195,7 @@ Item { Timer { id: refreshValuesTimer - interval: 5000 + interval: 1000 onTriggered: getValues() } diff --git a/qml/Screens/VisualizationScreen.qml b/qml/Screens/VisualizationScreen.qml index 4bc7748..4e01bb2 100644 --- a/qml/Screens/VisualizationScreen.qml +++ b/qml/Screens/VisualizationScreen.qml @@ -8,9 +8,10 @@ import Cubo 1.0 import Utils 1.0 ColumnLayout { + id: root spacing: 20 - property int currentTime: 0 + property real currentTime: 0 property var voltageData: [] property var currentData: [] @@ -86,6 +87,7 @@ ColumnLayout { property Controls.ChartView chartItem: undefined property color seriesColor: "black" property bool horizontal: true + property bool selected: false Controls.CheckBox { id: checkSeries @@ -114,9 +116,17 @@ ColumnLayout { Rectangle { color: seriesColor radius: width / 2 - Layout.preferredWidth: 18 - Layout.preferredHeight: 18 + border.width: selected ? 2 : 0 + border.color: "black" + Layout.preferredWidth: selected ? 21 : 18 + Layout.preferredHeight: selected ? 21 : 18 Layout.rightMargin: 20 + Layout.alignment: Qt.AlignCenter + + MouseArea { + anchors.fill: parent + onClicked: if (!selected) selected = true + } } Component.onCompleted: { @@ -124,6 +134,10 @@ ColumnLayout { horizontal = !ListView.view seriesColor = chartItem.series(modelData).color checkSeries.text = Qt.binding(function(){ return chartItem.series(modelData).name }) + + selectedChanged.connect(function (){ if (selected) chartItem.selectedSeriesIndex = index }) + chartItem.selectedSeriesIndexChanged.connect(function (){ if (typeof(selected) != "undefined") selected = chartItem.selectedSeriesIndex === index }) + selected = chartItem.selectedSeriesIndex === index } } } @@ -289,44 +303,6 @@ ColumnLayout { RowLayout { spacing: 20 - Controls.Button { - text: timer.running ? qsTr("Pause data collection") : qsTr("Resume data collection") - onClicked: { - if (timer.running) - timer.stop() - else - timer.start() - } - } - - Controls.Button { - text: qsTr("Clear data") - onClicked: { - currentTime = 0 - - clearData(voltageData, voltageLoader) - clearData(currentData, currentLoader) - clearData(batteryTemperatureData, batteryTemperatureLoader) - clearData(bmsTemperatureData, bmsTemperatureLoader) - clearData(cellVoltageData, cellVoltageLoader) - clearData(cellListData, cellListLoader) - - resetZoomButton.clicked() - } - - function clearData(dataObject, chartObject) { - for (var i = 0; i < dataObject.length; ++i) { - dataObject[i].splice(0, dataObject[i].length) - var series = chartObject.item.chart.series(i) - series.removePoints(0, series.count) - } - } - } - - Item { - Layout.fillWidth: true - } - Controls.Button { id: resetZoomButton text: qsTr("Reset zoom") @@ -382,6 +358,73 @@ ColumnLayout { return min } } + + Controls.Button { + property string pauseText: qsTr("Pause data collection") + property string resumeText: qsTr("Resume data collection") + text: pauseText + onClicked: { + if (timer.running) { + text = resumeText + timer.stop() + } else { + text = pauseText + timer.start() + } + } + } + + Controls.Button { + text: qsTr("Clear data") + onClicked: { + currentTime = 0 + + clearData(voltageData, voltageLoader) + clearData(currentData, currentLoader) + clearData(batteryTemperatureData, batteryTemperatureLoader) + clearData(bmsTemperatureData, bmsTemperatureLoader) + clearData(cellVoltageData, cellVoltageLoader) + clearData(cellListData, cellListLoader) + + resetZoomButton.clicked() + } + + function clearData(dataObject, chartObject) { + for (var i = 0; i < dataObject.length; ++i) { + dataObject[i].splice(0, dataObject[i].length) + var series = chartObject.item.chart.series(i) + series.removePoints(0, series.count) + } + } + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + spacing: 10 + + Controls.SubtitleLabel { + text: qsTr("Interval, s") + } + + Controls.TextField { + id: intervalField + validator: DoubleValidator { bottom: 0.1; top: 100; decimals: 1; locale: "en-US" } + text: "1" + onEditingFinished: timer.interval = parseFloat(text) * 1000 + Layout.preferredHeight: 44 + } + + Controls.ImageButton { + icon.source: "qrc:/Icons/check-indicator.svg" + icon.width: 32 + icon.height: 32 + Layout.preferredWidth: 42 + Layout.preferredHeight: 42 + } + } } Connections { @@ -395,6 +438,7 @@ ColumnLayout { Connections { target: BmsInterface.commands() + enabled: root.visible onValuesReceived: { addValueToChart(values.packVoltage, voltageData, voltageLoader, 0, 0.05) @@ -428,7 +472,7 @@ ColumnLayout { addValueToChart(cellVoltageArray[j], cellListData, cellListLoader, j, 0.05) } - currentTime += 1 + currentTime = Math.round((timer.interval / 1000 + currentTime + Number.EPSILON) * 10) / 10 } } diff --git a/qml/Utils/MathHelper.qml b/qml/Utils/MathHelper.qml index 777cc05..e291f71 100644 --- a/qml/Utils/MathHelper.qml +++ b/qml/Utils/MathHelper.qml @@ -2,7 +2,8 @@ pragma Singleton import QtQuick 2.12 QtObject { - function roundDouble(value) { - return Math.round((value + Number.EPSILON) * 100) / 100 + function roundDouble(value, decimals = 2) { + var factor = Math.pow(10, decimals) + return Math.round((value + Number.EPSILON) * factor) / factor } } diff --git a/qml/qml_items.qrc b/qml/qml_items.qrc index 304e3b4..ec37fbc 100644 --- a/qml/qml_items.qrc +++ b/qml/qml_items.qrc @@ -40,5 +40,6 @@ Controls/MenuItemDelegate.qml Controls/ScrollIndicator.qml Controls/OutlineImageButton.qml + Controls/ImageButton.qml