diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f899ce1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+
+CMakeLists.txt.user
+Tksi310Monitor.pro.user
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..cf1bbd5
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,92 @@
+cmake_minimum_required(VERSION 3.14)
+
+project(TksiMonitor VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Quick SerialPort LinguistTools)
+find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Quick SerialPort LinguistTools)
+
+set(TS_FILES translations/TksiMonitor_ru_RU.ts)
+
+set(PROJECT_SOURCES
+ sources/main.cpp
+ sources/DataController.h
+ sources/DataController.cpp
+ resources/resources.qrc
+ qml/qml.qrc
+ ${TS_FILES}
+)
+
+include_directories(sources)
+
+list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml)
+list(REMOVE_DUPLICATES QML_IMPORT_PATH)
+
+set(QML_IMPORT_PATH ${QML_IMPORT_PATH}
+ CACHE STRING "Qt Creator qml import paths"
+ FORCE
+)
+
+if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
+ qt_add_executable(TksiMonitor
+ MANUAL_FINALIZATION
+ ${PROJECT_SOURCES}
+ )
+# Define target properties for Android with Qt 6 as:
+# set_property(TARGET TksiMonitor APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
+# ${CMAKE_CURRENT_SOURCE_DIR}/android)
+# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
+
+ qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
+else()
+ if(ANDROID)
+ add_library(TksiMonitor SHARED
+ ${PROJECT_SOURCES}
+ )
+# Define properties for Android with Qt 5 after find_package() calls as:
+# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
+ else()
+ add_executable(TksiMonitor
+ ${PROJECT_SOURCES}
+ )
+ endif()
+
+ qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
+endif()
+
+target_link_libraries(TksiMonitor
+ PRIVATE
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Quick
+ Qt${QT_VERSION_MAJOR}::SerialPort
+)
+
+# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
+# If you are developing for iOS or macOS you should consider setting an
+# explicit, fixed bundle identifier manually though.
+if(${QT_VERSION} VERSION_LESS 6.1.0)
+ set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.TksiMonitor)
+endif()
+set_target_properties(TksiMonitor PROPERTIES
+ ${BUNDLE_ID_OPTION}
+ MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
+ MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+ MACOSX_BUNDLE TRUE
+ WIN32_EXECUTABLE TRUE
+)
+
+include(GNUInstallDirs)
+install(TARGETS TksiMonitor
+ BUNDLE DESTINATION .
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+
+if(QT_VERSION_MAJOR EQUAL 6)
+ qt_import_qml_plugins(TksiMonitor)
+ qt_finalize_executable(TksiMonitor)
+endif()
diff --git a/Tksi310Monitor.pro b/Tksi310Monitor.pro
new file mode 100644
index 0000000..3de30dd
--- /dev/null
+++ b/Tksi310Monitor.pro
@@ -0,0 +1,27 @@
+QT += quick quickcontrols2 serialport
+
+# You can make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+HEADERS += sources/DataController.h
+
+SOURCES += \
+ sources/main.cpp \
+ sources/DataController.cpp
+
+RESOURCES += qml/qml.qrc \
+ resources/resources.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH = qml
+
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
+
+# Rules for deployment.
+linux {
+ QMAKE_POST_LINK += "cqtdeployer -bin $$OUT_PWD/$$TARGET -qmlDir $$PWD/qml force-clear zip deploySystem -extraLibs krb5,k5crypto,keyutils,md4c,udev,double-conversion"
+ QMAKE_POST_LINK += " ; cd $$PWD ; zip -r $$OUT_PWD/DistributionKit/Tksi310Monitor.zip settings.ini"
+ QMAKE_POST_LINK += " ; cp $$OUT_PWD/DistributionKit/Tksi310Monitor.zip $$clean_path($$PWD/../Tksi310MonitorLinux.zip)"
+}
diff --git a/deploy.txt b/deploy.txt
new file mode 100644
index 0000000..7a96443
--- /dev/null
+++ b/deploy.txt
@@ -0,0 +1,3 @@
+# invoke this inside build directory
+# works for Ubuntu 22.04 and 20.04 with system qt5
+cqtdeployer -bin ./TksiMonitor -qmlDir ../TksiMonitor/qml force-clear deploySystem -extraLibs krb5,k5crypto,keyutils,md4c,udev,double-conversion
diff --git a/qml/Controls/DescriptionTextField.qml b/qml/Controls/DescriptionTextField.qml
new file mode 100644
index 0000000..31275ef
--- /dev/null
+++ b/qml/Controls/DescriptionTextField.qml
@@ -0,0 +1,28 @@
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+
+import Controls 1.0 as CustomControls
+import Palette 1.0
+
+ColumnLayout {
+ id: root
+ spacing: 8
+
+ property string descriptionText
+ property string text
+ property string indicatorText
+ property bool readonly: false
+
+ CustomControls.Label {
+ text: root.descriptionText
+ Layout.leftMargin: 24
+ Layout.fillWidth: true
+ }
+
+ CustomControls.TextField {
+ text: root.text
+ indicatorText: root.indicatorText
+ readOnly: root.readonly
+ Layout.fillWidth: true
+ }
+}
diff --git a/qml/Controls/GroupBox.qml b/qml/Controls/GroupBox.qml
new file mode 100644
index 0000000..3fddaef
--- /dev/null
+++ b/qml/Controls/GroupBox.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as Controls
+import QtQuick.Layouts 1.12
+
+import Controls 1.0 as CustomControls
+import Palette 1.0
+
+Controls.GroupBox {
+ id: control
+
+ leftPadding: 40
+ rightPadding: 40
+ topPadding: 56 + label.height + 48
+ bottomPadding: 56
+
+ property string subTitle: ""
+
+ background: Rectangle {
+ color: Palette.controlBackgroundColor
+ radius: 32
+ }
+
+ label: RowLayout {
+ width: parent.width - control.leftPadding - control.rightPadding
+ x: control.leftPadding
+ y: 56
+ spacing: 10
+
+ Text {
+ id: label
+ width: control.availableWidth
+
+ text: control.title
+ color: Palette.titleTextColor
+ elide: Text.ElideRight
+
+ font.family: "Roboto"
+ font.pixelSize: 40
+ font.weight: Font.Bold
+
+ Layout.fillWidth: true
+ }
+
+ CustomControls.IndicatorTextField {
+ text: control.subTitle
+ visible: control.subTitle.length > 0
+ font.pixelSize: 23
+ font.weight: Font.Normal
+ backgroundRadius: 16
+ readOnly: true
+ }
+ }
+}
diff --git a/qml/Controls/IndicatorTextField.qml b/qml/Controls/IndicatorTextField.qml
new file mode 100644
index 0000000..ed6b5f4
--- /dev/null
+++ b/qml/Controls/IndicatorTextField.qml
@@ -0,0 +1,29 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as Controls
+
+import Palette 1.0
+
+Controls.TextField {
+ id: control
+
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ topPadding: 10
+ bottomPadding: 10
+ leftPadding: 24
+ rightPadding: 24
+
+ font.family: "Roboto"
+ font.pixelSize: 40
+ font.weight: Font.Bold
+
+ color: Palette.labelTextColor
+ property color backgroundColor: Palette.indicatorBackgroundColor
+ property int backgroundRadius: 24
+
+ background: Rectangle {
+ color: backgroundColor
+ radius: backgroundRadius
+ }
+}
diff --git a/qml/Controls/Label.qml b/qml/Controls/Label.qml
new file mode 100644
index 0000000..11962b8
--- /dev/null
+++ b/qml/Controls/Label.qml
@@ -0,0 +1,7 @@
+import QtQuick 2.12
+
+Text {
+ font.family: "Roboto"
+ font.pixelSize: 24
+ font.weight: Font.Normal
+}
diff --git a/qml/Controls/Switch.qml b/qml/Controls/Switch.qml
new file mode 100644
index 0000000..74eb63f
--- /dev/null
+++ b/qml/Controls/Switch.qml
@@ -0,0 +1,73 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as Controls
+import QtQuick.Layouts 1.12
+
+import Palette 1.0
+
+Controls.Switch {
+ id: control
+
+ implicitHeight: 128
+ padding: 12
+
+ property string firstStateDescription
+ property string secondStateDescription
+
+ indicator: Item {
+ anchors.fill: control
+ anchors.margins: control.padding
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Controls.Label {
+ id: firstLabel
+
+ text: control.firstStateDescription
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ font.family: "Roboto"
+ font.pixelSize: 72
+ font.weight: Font.Bold
+
+ color: control.checked ? Palette.labelTextColor : Palette.alternativeLabelTextColor
+ Layout.preferredWidth: (parent.width -parent.spacing) / 2
+ }
+
+ Controls.Label {
+ id: secondLabel
+
+ text: control.secondStateDescription
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ font.family: "Roboto"
+ font.pixelSize: 72
+ font.weight: Font.Bold
+
+ color: control.checked ? Palette.alternativeLabelTextColor : Palette.labelTextColor
+ Layout.preferredWidth: (parent.width -parent.spacing) / 2
+ }
+ }
+
+ Rectangle {
+ color: Palette.switchActiveBackgroundColor
+ radius: 24
+
+ width: control.checked ? secondLabel.width : firstLabel.width
+ height: parent.height
+ x: control.checked ? secondLabel.x : firstLabel.x
+ z: -1
+ }
+ }
+
+ contentItem: Item {
+ }
+
+ background: Rectangle {
+ color: Palette.switchBackgroundColor
+ radius: 32
+ }
+}
diff --git a/qml/Controls/TextField.qml b/qml/Controls/TextField.qml
new file mode 100644
index 0000000..ae99f2d
--- /dev/null
+++ b/qml/Controls/TextField.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as Controls
+
+import Palette 1.0
+
+Controls.TextField {
+ id: control
+
+ implicitHeight: 108
+
+ horizontalAlignment: Text.AlignRight
+ verticalAlignment: Text.AlignVCenter
+
+ topPadding: 12
+ bottomPadding: 12
+ leftPadding: 24
+ rightPadding: 24 + Math.ceil(indicatorLabel.width) + (indicatorLabel.visible ? 12 : 0)
+
+ font.family: "Roboto"
+ font.pixelSize: 72
+ font.weight: Font.Bold
+
+ color: Palette.labelTextColor
+
+ property string indicatorText
+
+ background: Rectangle {
+ radius: 32
+ color: Palette.controlBackgroundColor
+ border.width: 1
+ border.color: Palette.borderColor
+
+ width: control.width
+ height: control.height
+
+ Controls.Label {
+ id: indicatorLabel
+ visible: control.indicatorText.length > 0
+ text: control.indicatorText
+
+ anchors.right: parent.right
+ anchors.rightMargin: 24
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 20
+
+ font.family: "Roboto"
+ font.pixelSize: 40
+ font.weight: Font.Normal
+
+ color: Palette.descriptionTextColor
+ }
+ }
+}
diff --git a/qml/Controls/TristateSwitch.qml b/qml/Controls/TristateSwitch.qml
new file mode 100644
index 0000000..20dc933
--- /dev/null
+++ b/qml/Controls/TristateSwitch.qml
@@ -0,0 +1,76 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12 as Controls
+import QtQuick.Layouts 1.12
+
+import Palette 1.0
+
+Controls.CheckBox {
+ id: control
+
+ implicitHeight: 128
+ padding: 12
+ tristate: true
+ checkState: Qt.Unchecked
+
+ property string firstStateDescription
+ property string secondStateDescription
+
+ indicator: Item {
+ anchors.fill: control
+ anchors.margins: control.padding
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Controls.Label {
+ id: firstLabel
+
+ text: control.firstStateDescription
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ font.family: "Roboto"
+ font.pixelSize: 72
+ font.weight: Font.Bold
+
+ color: control.checkState === Qt.PartiallyChecked ? Palette.alternativeLabelTextColor : Palette.labelTextColor
+ Layout.preferredWidth: (parent.width -parent.spacing) / 2
+ }
+
+ Controls.Label {
+ id: secondLabel
+
+ text: control.secondStateDescription
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ font.family: "Roboto"
+ font.pixelSize: 72
+ font.weight: Font.Bold
+
+ color: control.checkState === Qt.Checked ? Palette.alternativeLabelTextColor : Palette.labelTextColor
+ Layout.preferredWidth: (parent.width -parent.spacing) / 2
+ }
+ }
+
+ Rectangle {
+ color: Palette.switchActiveBackgroundColor
+ radius: 24
+ visible: control.checkState !== Qt.Unchecked
+
+ width: control.checkState === Qt.Checked ? secondLabel.width : firstLabel.width
+ height: parent.height
+ x: control.checkState === Qt.Checked ? secondLabel.x : firstLabel.x
+ z: -1
+ }
+ }
+
+ contentItem: Item {
+ }
+
+ background: Rectangle {
+ color: Palette.switchBackgroundColor
+ radius: 32
+ }
+}
diff --git a/qml/Controls/qmldir b/qml/Controls/qmldir
new file mode 100644
index 0000000..5006f75
--- /dev/null
+++ b/qml/Controls/qmldir
@@ -0,0 +1,8 @@
+module Controls
+TextField 1.0 TextField.qml
+IndicatorTextField 1.0 IndicatorTextField.qml
+DescriptionTextField 1.0 DescriptionTextField.qml
+Label 1.0 Label.qml
+GroupBox 1.0 GroupBox.qml
+Switch 1.0 Switch.qml
+TristateSwitch 1.0 TristateSwitch.qml
diff --git a/qml/Palette/Palette.qml b/qml/Palette/Palette.qml
new file mode 100644
index 0000000..59b7357
--- /dev/null
+++ b/qml/Palette/Palette.qml
@@ -0,0 +1,23 @@
+pragma Singleton
+import QtQuick 2.12
+
+QtObject {
+ property color titleTextColor: "#232323"
+ property color labelTextColor: "#232323"
+ property color alternativeLabelTextColor: "#FFFFFF"
+ property color descriptionTextColor: "#757575"
+
+ property color backgroundColor: "#EEE"
+ property color controlBackgroundColor: "#FCFCFC"
+ property color indicatorBackgroundColor: "#F0F4FF"
+
+ property color switchBackgroundColor: "#F0F4FF"
+ property color switchActiveBackgroundColor: "#3069FE"
+
+ property color borderColor: "#D0D0D0"
+
+ property color informationColor: "#4C68ED"
+ property color invalidColor: "#A31C00"
+ property color goodColor: "#009352"
+ property color neutralColor: "#DFE0EB"
+}
diff --git a/qml/Palette/qmldir b/qml/Palette/qmldir
new file mode 100644
index 0000000..1d90b4f
--- /dev/null
+++ b/qml/Palette/qmldir
@@ -0,0 +1,2 @@
+module Palette
+singleton Palette 1.0 Palette.qml
\ No newline at end of file
diff --git a/qml/main.qml b/qml/main.qml
new file mode 100644
index 0000000..9a93014
--- /dev/null
+++ b/qml/main.qml
@@ -0,0 +1,369 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+
+import Controls 1.0 as CustomControls
+import Palette 1.0
+
+import Tksi 1.0
+
+ApplicationWindow {
+ id: root
+ width: 1920
+ height: 1080
+ visible: true
+ visibility: Qt.platform.os === "linux" ? "FullScreen" : "Windowed"
+ flags: Qt.platform.os === "linux" ? Qt.FramelessWindowHint | Qt.Window : Qt.Window
+ title: "ТКСИ 310 монитор"
+
+ ColumnLayout {
+ anchors.leftMargin: 56
+ anchors.rightMargin: 56
+ anchors.topMargin: 80
+ anchors.bottomMargin: 58
+ anchors.fill: parent
+
+ spacing: 72
+
+ RowLayout {
+ spacing: 96
+
+ ColumnLayout {
+ spacing: 16
+
+ CustomControls.Label {
+ text: "Статус модулей"
+ }
+
+ RowLayout {
+ spacing: 16
+
+ CustomControls.IndicatorTextField {
+ text: DataController.firstStatus
+ horizontalAlignment: Text.AlignHCenter
+ readOnly: true
+ Layout.preferredWidth: 160
+ }
+
+ CustomControls.IndicatorTextField {
+ text: DataController.secondStatus
+ horizontalAlignment: Text.AlignHCenter
+ readOnly: true
+ Layout.preferredWidth: 160
+ }
+
+ CustomControls.IndicatorTextField {
+ visible: DataController.firstStatus === "0x07" && DataController.secondStatus === "0x07"
+ text: "Готов к работе"
+ backgroundColor: "#C4FFCA"
+ backgroundRadius: 16
+ font.pixelSize: 24
+ font.weight: Font.Normal
+ leftPadding: 16
+ rightPadding: 16
+ readOnly: true
+ Layout.preferredWidth: 200
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ Layout.fillHeight: true
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ // visible: modeSwitch.checkState === Qt.PartiallyChecked
+ descriptionText: "Вход ПСН"
+ indicatorText: "В"
+ text: DataController.psnInputVoltage
+ readonly: true
+ Layout.alignment: Qt.AlignTop
+ Layout.fillWidth: false
+ Layout.preferredWidth: 320
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Сеть"
+ indicatorText: "В"
+ text: DataController.mainVoltage
+ readonly: true
+ Layout.alignment: Qt.AlignTop
+ Layout.fillWidth: false
+ Layout.preferredWidth: 320
+ }
+
+ ColumnLayout {
+ spacing: 16
+
+ CustomControls.Label {
+ id: dateLabel
+ text: "Статус модулей"
+ Layout.alignment: Qt.AlignCenter
+ }
+
+ CustomControls.IndicatorTextField {
+ id: timeField
+ readOnly: true
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: 220
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ Timer {
+ interval: 1000
+ repeat: true
+ running: true
+ triggeredOnStart: true
+
+ onTriggered: {
+ let date = new Date()
+ dateLabel.text = Qt.formatDate(date, "dd.MM.yyyy")
+ timeField.text = Qt.formatTime(date, "hh:mm:ss")
+ }
+ }
+
+ Layout.fillHeight: true
+ }
+
+ Layout.fillHeight: false
+ Layout.fillWidth: true
+ }
+
+ CustomControls.TristateSwitch {
+ id: modeSwitch
+// enabled: false // uncomment for non clickable
+ firstStateDescription: "АИН 1,2,3"
+ secondStateDescription: "АИН 4,5,6"
+ checkState: DataController.ainMode === 2 ? Qt.Unchecked : DataController.ainMode === 1 ? Qt.Checked : Qt.PartiallyChecked
+ Layout.fillHeight: false
+ Layout.fillWidth: true
+ }
+
+ RowLayout {
+ RowLayout {
+ spacing: 56
+ visible: modeSwitch.checkState === Qt.PartiallyChecked
+
+ CustomControls.GroupBox {
+ title: "АИН 1"
+ subTitle: (DataController.ainReservedMask & 0x1) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain1Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain1Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ CustomControls.GroupBox {
+ title: "АИН 2"
+ subTitle: (DataController.ainReservedMask & 0x2) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain2Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain2Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ CustomControls.GroupBox {
+ title: "АИН 3"
+ subTitle: (DataController.ainReservedMask & 0x4) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain3Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain3Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ RowLayout {
+ spacing: 56
+ visible: modeSwitch.checkState === Qt.Checked
+
+ CustomControls.GroupBox {
+ title: "АИН 4"
+ subTitle: (DataController.ainReservedMask & 0x8) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain4Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain4Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ CustomControls.GroupBox {
+ title: "АИН 5"
+ subTitle: (DataController.ainReservedMask & 0x10) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain5Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain5Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ CustomControls.GroupBox {
+ title: "АИН 6"
+ subTitle: (DataController.ainReservedMask & 0x20) != 0 ? "Резервирование" : ""
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 32
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Напряжение"
+ indicatorText: "B"
+ text: DataController.ain6Voltage
+ readonly: true
+ Layout.fillWidth: true
+ }
+
+ CustomControls.DescriptionTextField {
+ descriptionText: "Ток"
+ indicatorText: "А"
+ text: DataController.ain6Current
+ readonly: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Layout.preferredWidth: (parent.width - 2 * parent.spacing) / 3
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Item {
+ visible: modeSwitch.checkState === Qt.Unchecked
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+
+ MouseArea {
+ visible: Qt.platform.os === "linux"
+ anchors.fill: parent
+ enabled: false
+ cursorShape: Qt.BlankCursor
+ }
+
+ background: Rectangle {
+ color: Palette.backgroundColor
+ }
+}
diff --git a/qml/qml.qrc b/qml/qml.qrc
new file mode 100644
index 0000000..92f8050
--- /dev/null
+++ b/qml/qml.qrc
@@ -0,0 +1,15 @@
+
+
+ main.qml
+ Controls/TextField.qml
+ Controls/qmldir
+ Palette/Palette.qml
+ Palette/qmldir
+ Controls/GroupBox.qml
+ Controls/Label.qml
+ Controls/IndicatorTextField.qml
+ Controls/Switch.qml
+ Controls/DescriptionTextField.qml
+ Controls/TristateSwitch.qml
+
+
diff --git a/resources/fonts/Roboto/LICENSE.txt b/resources/fonts/Roboto/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/resources/fonts/Roboto/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/resources/fonts/Roboto/Roboto-Black.ttf b/resources/fonts/Roboto/Roboto-Black.ttf
new file mode 100644
index 0000000..0112e7d
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Black.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-BlackItalic.ttf b/resources/fonts/Roboto/Roboto-BlackItalic.ttf
new file mode 100644
index 0000000..b2c6aca
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-BlackItalic.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Bold.ttf b/resources/fonts/Roboto/Roboto-Bold.ttf
new file mode 100644
index 0000000..43da14d
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Bold.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-BoldItalic.ttf b/resources/fonts/Roboto/Roboto-BoldItalic.ttf
new file mode 100644
index 0000000..bcfdab4
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-BoldItalic.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Italic.ttf b/resources/fonts/Roboto/Roboto-Italic.ttf
new file mode 100644
index 0000000..1b5eaa3
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Italic.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Light.ttf b/resources/fonts/Roboto/Roboto-Light.ttf
new file mode 100644
index 0000000..e7307e7
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Light.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-LightItalic.ttf b/resources/fonts/Roboto/Roboto-LightItalic.ttf
new file mode 100644
index 0000000..2d277af
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-LightItalic.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Medium.ttf b/resources/fonts/Roboto/Roboto-Medium.ttf
new file mode 100644
index 0000000..ac0f908
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Medium.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-MediumItalic.ttf b/resources/fonts/Roboto/Roboto-MediumItalic.ttf
new file mode 100644
index 0000000..fc36a47
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-MediumItalic.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Regular.ttf b/resources/fonts/Roboto/Roboto-Regular.ttf
new file mode 100644
index 0000000..ddf4bfa
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Regular.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-Thin.ttf b/resources/fonts/Roboto/Roboto-Thin.ttf
new file mode 100644
index 0000000..2e0dee6
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-Thin.ttf differ
diff --git a/resources/fonts/Roboto/Roboto-ThinItalic.ttf b/resources/fonts/Roboto/Roboto-ThinItalic.ttf
new file mode 100644
index 0000000..084f9c0
Binary files /dev/null and b/resources/fonts/Roboto/Roboto-ThinItalic.ttf differ
diff --git a/resources/resources.qrc b/resources/resources.qrc
new file mode 100644
index 0000000..df94610
--- /dev/null
+++ b/resources/resources.qrc
@@ -0,0 +1,16 @@
+
+
+ fonts/Roboto/Roboto-Black.ttf
+ fonts/Roboto/Roboto-BlackItalic.ttf
+ fonts/Roboto/Roboto-Bold.ttf
+ fonts/Roboto/Roboto-BoldItalic.ttf
+ fonts/Roboto/Roboto-Italic.ttf
+ fonts/Roboto/Roboto-Light.ttf
+ fonts/Roboto/Roboto-LightItalic.ttf
+ fonts/Roboto/Roboto-Medium.ttf
+ fonts/Roboto/Roboto-MediumItalic.ttf
+ fonts/Roboto/Roboto-Regular.ttf
+ fonts/Roboto/Roboto-Thin.ttf
+ fonts/Roboto/Roboto-ThinItalic.ttf
+
+
diff --git a/settings.ini b/settings.ini
new file mode 100644
index 0000000..529aaf4
--- /dev/null
+++ b/settings.ini
@@ -0,0 +1,7 @@
+[General]
+name=/dev/ttyS0
+;baudRate=9600
+;dataBits=8
+;parity=0
+;stopBits=1
+;flowControl=0
diff --git a/sources/DataController.cpp b/sources/DataController.cpp
new file mode 100644
index 0000000..a7e1cdf
--- /dev/null
+++ b/sources/DataController.cpp
@@ -0,0 +1,290 @@
+#include "DataController.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+DataController::DataController(QObject* parent)
+ : QObject{parent}
+ , serialPort_(new QSerialPort)
+{
+ connect(serialPort_.data(), &QSerialPort::readyRead, this, &DataController::serialReadyRead);
+ connect(serialPort_.data(), &QSerialPort::errorOccurred, this, &DataController::serialError);
+
+ // for correct qApp->exit();
+ QTimer::singleShot(0, this, [this]{ openSerialPort(); });
+}
+
+DataController::~DataController()
+{
+}
+
+QObject* DataController::qmlInstance(QQmlEngine* /*engine*/, QJSEngine* /*scriptEngine*/)
+{
+ return new DataController;
+}
+
+void DataController::serialReadyRead()
+{
+ const auto data = serialPort_->readAll();
+ qDebug() << "Serial data received (HEX):" << QDateTime::currentDateTime() << data.toHex();
+
+ serialData_ += data;
+ parseSerialData();
+}
+
+void DataController::serialError()
+{
+ const auto error = serialPort_->error();
+ if (error == QSerialPort::NoError)
+ {
+ return;
+ }
+
+ qCritical() << "A serial port error has occurred. Port:" << serialPort_->portName() << "Error:" << serialPort_->errorString();
+ qApp->exit(1);
+}
+
+QVariantMap DataController::readSettings()
+{
+ QSettings settings("settings.ini", QSettings::IniFormat);
+
+ const auto keys = settings.allKeys();
+ if (keys.isEmpty())
+ {
+ qCritical() << "File settings.ini is empty. Please put the file next to the executable file.";
+ qApp->exit(1);
+ }
+
+
+ QVariantMap settingsMap;
+ for (const auto& key: keys)
+ {
+ settingsMap[key] = settings.value(key);
+ }
+
+ return settingsMap;
+}
+
+void DataController::openSerialPort()
+{
+ const auto settings = readSettings();
+ if (settings.isEmpty())
+ {
+ return;
+ }
+
+ const auto portName = settings["name"].toString();
+
+ // for testing
+ if (portName == "fake")
+ {
+ mockSerialData();
+ parseSerialData();
+ return;
+ }
+
+ serialPort_->setPortName(portName);
+
+ if (settings.contains("baudRate"))
+ {
+ serialPort_->setBaudRate(settings["baudRate"].toInt());
+ }
+
+ if (settings.contains("dataBits"))
+ {
+ serialPort_->setDataBits((QSerialPort::DataBits)settings["dataBits"].toInt());
+ }
+
+ if (settings.contains("parity"))
+ {
+ serialPort_->setParity((QSerialPort::Parity)settings["parity"].toInt());
+ }
+
+ if (settings.contains("stopBits"))
+ {
+ serialPort_->setStopBits((QSerialPort::StopBits)settings["stopBits"].toInt());
+ }
+
+ if (settings.contains("flowControl"))
+ {
+ serialPort_->setFlowControl((QSerialPort::FlowControl)settings["flowControl"].toInt());
+ }
+
+ if (!serialPort_->open(QIODevice::ReadOnly))
+ {
+ qCritical() << "Can't open serial port. Port:" << serialPort_->portName() << "Error:" << serialPort_->errorString();
+ qApp->exit(1);
+ }
+}
+
+void DataController::parseSerialData()
+{
+ const char* startBytes = "\xD0\x0D";
+ const auto startPosition = serialData_.indexOf(startBytes);
+ if (startPosition == -1)
+ {
+ serialData_.clear();
+ return;
+ }
+
+ if (startPosition > 0)
+ {
+ serialData_ = serialData_.mid(startPosition);
+ }
+
+ if (serialData_.size() < 36)
+ {
+ return;
+ }
+
+ auto extractInt = [](const QByteArray& array, int start, int length) -> uint32_t
+ {
+ uint32_t result = 0;
+ if (length > 4)
+ {
+ return result;
+ }
+
+ // reversed byte order
+ for (int i = 0; i < length; ++i)
+ {
+ memcpy(reinterpret_cast(&result) + i, array.data() + start + length - i - 1, 1);
+ }
+
+ // same byte order
+// memcpy(&result, array.data() + start, length);
+ return result;
+ };
+
+ auto statusToString = [](uint32_t status) -> QString
+ {
+ auto hexStatus = "0x" + QString("%1").arg(status, 2, 16, QLatin1Char('0')).toUpper();
+ if (hexStatus.compare("0xFF", Qt::CaseInsensitive) == 0)
+ {
+ hexStatus = " — ";
+ }
+
+ return hexStatus;
+ };
+
+ auto shortIntToString = [](uint16_t parameter) -> QString
+ {
+ return parameter == std::numeric_limits::max() ? " — " : QString::number(parameter);
+ };
+
+ auto shortDoubleToString = [](uint16_t parameter) -> QString
+ {
+ return parameter == std::numeric_limits::max() ? " — " : QString::number(parameter / 10.0, 'f', 1);
+ };
+
+ const auto package = serialData_.mid(0, 36);
+ serialData_ = serialData_.mid(36);
+
+ quint16 calculatedChecksum = 0;
+ for (int i = 0; i < 34; ++i)
+ {
+ calculatedChecksum += static_cast(package.at(i));
+ }
+
+ const auto checksum = extractInt(package, 34, 2);
+ if (calculatedChecksum != checksum)
+ {
+ qWarning() << "incorrect checksum";
+ return;
+ }
+
+ setProperty("firstStatus", statusToString(extractInt(package, 2, 1)));
+ setProperty("secondStatus", statusToString(extractInt(package, 3, 1)));
+
+ setProperty("psnInputVoltage", shortIntToString(extractInt(package, 4, 2)));
+ setProperty("mainVoltage", shortIntToString(extractInt(package, 6, 2)));
+
+ setProperty("ainMode", extractInt(package, 8, 1));
+ setProperty("ainReservedMask", extractInt(package, 9, 1));
+
+ setProperty("ain1Voltage", shortIntToString(extractInt(package, 10, 2)));
+ setProperty("ain1Current", shortDoubleToString(extractInt(package, 12, 2)));
+
+ setProperty("ain2Voltage", shortIntToString(extractInt(package, 14, 2)));
+ setProperty("ain2Current", shortDoubleToString(extractInt(package, 16, 2)));
+
+ setProperty("ain3Voltage", shortIntToString(extractInt(package, 18, 2)));
+ setProperty("ain3Current", shortDoubleToString(extractInt(package, 20, 2)));
+
+ setProperty("ain4Voltage", shortIntToString(extractInt(package, 22, 2)));
+ setProperty("ain4Current", shortDoubleToString(extractInt(package, 24, 2)));
+
+ setProperty("ain5Voltage", shortIntToString(extractInt(package, 26, 2)));
+ setProperty("ain5Current", shortDoubleToString(extractInt(package, 28, 2)));
+
+ setProperty("ain6Voltage", shortIntToString(extractInt(package, 30, 2)));
+ setProperty("ain6Current", shortDoubleToString(extractInt(package, 32, 2)));
+}
+
+void DataController::mockSerialData()
+{
+ serialData_.clear();
+
+ serialData_.append((char)0xD0);
+ serialData_.append((char)0x0D);
+
+ char status1 = 0x0;
+ char status2 = 0x0;
+ serialData_.append(status1);
+ serialData_.append(status2);
+
+ uint16_t inputVoltage = qToBigEndian((uint16_t)3010);
+ uint16_t mainVoltage = qToBigEndian((uint16_t)384);
+ serialData_.append(reinterpret_cast(&inputVoltage), 2);
+ serialData_.append(reinterpret_cast(&mainVoltage), 2);
+
+ char mode = 0;
+ char reservedMask = 0x12;
+ serialData_.append(mode);
+ serialData_.append(reservedMask);
+
+ uint16_t voltage1 = qToBigEndian((uint16_t)380);
+ uint16_t current1 = qToBigEndian((uint16_t)535);
+ serialData_.append(reinterpret_cast(&voltage1), 2);
+ serialData_.append(reinterpret_cast(¤t1), 2);
+
+ uint16_t voltage2 = qToBigEndian((uint16_t)381);
+ uint16_t current2 = qToBigEndian((uint16_t)527);
+ serialData_.append(reinterpret_cast(&voltage2), 2);
+ serialData_.append(reinterpret_cast(¤t2), 2);
+
+ uint16_t voltage3 = qToBigEndian((uint16_t)383);
+ uint16_t current3 = qToBigEndian((uint16_t)729);
+ serialData_.append(reinterpret_cast(&voltage3), 2);
+ serialData_.append(reinterpret_cast(¤t3), 2);
+
+ uint16_t voltage4 = qToBigEndian((uint16_t)381);
+ uint16_t current4 = qToBigEndian((uint16_t)536);
+ serialData_.append(reinterpret_cast(&voltage4), 2);
+ serialData_.append(reinterpret_cast(¤t4), 2);
+
+ uint16_t voltage5 = qToBigEndian((uint16_t)382);
+ uint16_t current5 = qToBigEndian((uint16_t)528);
+ serialData_.append(reinterpret_cast(&voltage5), 2);
+ serialData_.append(reinterpret_cast(¤t5), 2);
+
+ uint16_t voltage6 = qToBigEndian((uint16_t)383);
+ uint16_t current6 = qToBigEndian((uint16_t)730);
+ serialData_.append(reinterpret_cast(&voltage6), 2);
+ serialData_.append(reinterpret_cast(¤t6), 2);
+
+ quint16 checksum = 0;
+ for (int i = 0; i < serialData_.size(); ++i)
+ {
+ checksum += static_cast(serialData_.at(i));
+ }
+
+ checksum = qToBigEndian(checksum);
+ serialData_.append(reinterpret_cast(&checksum), 2);
+}
diff --git a/sources/DataController.h b/sources/DataController.h
new file mode 100644
index 0000000..002c9ab
--- /dev/null
+++ b/sources/DataController.h
@@ -0,0 +1,108 @@
+#ifndef DATACONTROLLER_H
+#define DATACONTROLLER_H
+
+#include
+
+class QQmlEngine;
+class QJSEngine;
+class QSerialPort;
+
+class DataController : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString firstStatus MEMBER firstStatus_ NOTIFY firstStatusChanged)
+ Q_PROPERTY(QString secondStatus MEMBER secondStatus_ NOTIFY secondStatusChanged)
+
+ Q_PROPERTY(QString psnInputVoltage MEMBER psnInputVoltage_ NOTIFY psnInputVoltageChanged)
+ Q_PROPERTY(QString mainVoltage MEMBER mainVoltage_ NOTIFY mainVoltageChanged)
+
+ Q_PROPERTY(int ainMode MEMBER ainMode_ NOTIFY ainModeChanged)
+ Q_PROPERTY(int ainReservedMask MEMBER ainReservedMask_ NOTIFY ainReservedMaskChanged)
+
+ Q_PROPERTY(QString ain1Voltage MEMBER ain1Voltage_ NOTIFY ain1VoltageChanged)
+ Q_PROPERTY(QString ain1Current MEMBER ain1Current_ NOTIFY ain1CurrentChanged)
+
+ Q_PROPERTY(QString ain2Voltage MEMBER ain2Voltage_ NOTIFY ain2VoltageChanged)
+ Q_PROPERTY(QString ain2Current MEMBER ain2Current_ NOTIFY ain2CurrentChanged)
+
+ Q_PROPERTY(QString ain3Voltage MEMBER ain3Voltage_ NOTIFY ain3VoltageChanged)
+ Q_PROPERTY(QString ain3Current MEMBER ain3Current_ NOTIFY ain3CurrentChanged)
+
+ Q_PROPERTY(QString ain4Voltage MEMBER ain4Voltage_ NOTIFY ain4VoltageChanged)
+ Q_PROPERTY(QString ain4Current MEMBER ain4Current_ NOTIFY ain4CurrentChanged)
+
+ Q_PROPERTY(QString ain5Voltage MEMBER ain5Voltage_ NOTIFY ain5VoltageChanged)
+ Q_PROPERTY(QString ain5Current MEMBER ain5Current_ NOTIFY ain5CurrentChanged)
+
+ Q_PROPERTY(QString ain6Voltage MEMBER ain6Voltage_ NOTIFY ain6VoltageChanged)
+ Q_PROPERTY(QString ain6Current MEMBER ain6Current_ NOTIFY ain6CurrentChanged)
+
+public:
+ explicit DataController(QObject* parent = nullptr);
+ ~DataController();
+
+ static QObject* qmlInstance(QQmlEngine* engine, QJSEngine* scriptEngine);
+
+signals:
+ void firstStatusChanged();
+ void secondStatusChanged();
+
+ void psnInputVoltageChanged();
+ void mainVoltageChanged();
+
+ void ainModeChanged();
+ void ainReservedMaskChanged();
+
+ void ain1VoltageChanged();
+ void ain1CurrentChanged();
+ void ain2VoltageChanged();
+ void ain2CurrentChanged();
+ void ain3VoltageChanged();
+ void ain3CurrentChanged();
+
+ void ain4VoltageChanged();
+ void ain4CurrentChanged();
+ void ain5VoltageChanged();
+ void ain5CurrentChanged();
+ void ain6VoltageChanged();
+ void ain6CurrentChanged();
+
+private slots:
+ void serialReadyRead();
+ void serialError();
+
+private:
+ QVariantMap readSettings();
+ void openSerialPort();
+ void parseSerialData();
+ void mockSerialData();
+
+ QString firstStatus_ = " — ";
+ QString secondStatus_ = " — ";
+
+ QString psnInputVoltage_;
+ QString mainVoltage_;
+
+ int ainMode_ = 0;
+ int ainReservedMask_ = 0;
+
+ QString ain1Voltage_;
+ QString ain1Current_;
+ QString ain2Voltage_;
+ QString ain2Current_;
+ QString ain3Voltage_;
+ QString ain3Current_;
+
+ QString ain4Voltage_;
+ QString ain4Current_;
+ QString ain5Voltage_;
+ QString ain5Current_;
+ QString ain6Voltage_;
+ QString ain6Current_;
+
+ QByteArray serialData_;
+ QScopedPointer serialPort_;
+};
+
+#endif // DATACONTROLLER_H
diff --git a/sources/main.cpp b/sources/main.cpp
new file mode 100644
index 0000000..090bdbf
--- /dev/null
+++ b/sources/main.cpp
@@ -0,0 +1,72 @@
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "DataController.h"
+
+int main(int argc, char *argv[])
+{
+ //#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ // QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ //#endif
+
+ QGuiApplication app(argc, argv);
+
+ QCoreApplication::setOrganizationName("Tksi");
+ QCoreApplication::setOrganizationDomain("Tksi.rus");
+ QCoreApplication::setApplicationName("Tksi monitor");
+
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Black.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-BlackItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Bold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-BoldItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Italic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Light.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-LightItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Medium.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-MediumItalic.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Regular.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-Thin.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Roboto/Roboto-ThinItalic.ttf");
+
+ QTranslator translator;
+ const auto uiLanguages = QLocale::system().uiLanguages();
+ for (const auto& locale : uiLanguages)
+ {
+ const auto baseName = "TksiMonitor_" + QLocale(locale).name();
+ if (translator.load(":/i18n/" + baseName))
+ {
+ app.installTranslator(&translator);
+ break;
+ }
+ }
+
+ QQmlApplicationEngine engine;
+
+ qmlRegisterSingletonType("Tksi", 1, 0, "DataController", &DataController::qmlInstance);
+
+ auto isDebug = false;
+#ifdef QT_DEBUG
+ isDebug = true;
+#endif
+ engine.rootContext()->setContextProperty("isDebug", isDebug);
+
+ const QUrl url(QStringLiteral("qrc:/main.qml"));
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app,
+ [url](QObject *obj, const QUrl &objUrl)
+ {
+ if (!obj && url == objUrl)
+ {
+ QCoreApplication::exit(-1);
+ }
+ }, Qt::QueuedConnection);
+
+ engine.addImportPath(QStringLiteral("qrc:/"));
+ engine.load(url);
+
+ return app.exec();
+}
diff --git a/translations/TksiMonitor_ru_RU.ts b/translations/TksiMonitor_ru_RU.ts
new file mode 100644
index 0000000..743f57f
--- /dev/null
+++ b/translations/TksiMonitor_ru_RU.ts
@@ -0,0 +1,3 @@
+
+
+