diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3b5685d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shared-libraries"] + path = shared-libraries + url = https://git.itsblue.de/ScStw/shared-libraries/ diff --git a/ScStwMonitor.pro b/ScStwMonitor.pro new file mode 100644 index 0000000..0d46679 --- /dev/null +++ b/ScStwMonitor.pro @@ -0,0 +1,14 @@ +TEMPLATE = subdirs +CONFIG += ordered console + +SUBDIRS += \ + ScStwLibraries \ + ScStwMonitorSrc + +ScStwMonitorSrc.depends = Qt-Secret +ScStwLibraries.file = shared-libraries/ScStwLibraries/ScStwLibraries.pro + +contains(QMAKE_CXX, .*raspbian.*arm.*):{ + GLOBAL_TARGET_PATH = "/home/pi/ScStwMonitor" + cache(GLOBAL_TARGET_PATH, set) +} diff --git a/ScStwMonitor.pro.user b/ScStwMonitor.pro.user new file mode 100644 index 0000000..71c8ca4 --- /dev/null +++ b/ScStwMonitor.pro.user @@ -0,0 +1,1000 @@ + + + + + + EnvironmentId + {8b2b329f-2b96-47e0-8e3b-213b44b4afec} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + true + Builtin.Questionable + + true + Builtin.DefaultTidyAndClazy + 4 + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Android for armeabi-v7a (Clang Qt 5.12.6 for Android ARMv7) + Android for armeabi-v7a (Clang Qt 5.12.6 for Android ARMv7) + {9a47ee8f-1702-4ae9-96f9-7c13c7a1f9b3} + 0 + 0 + 0 + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Debug + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-29 + + true + QmakeProjectManager.AndroidBuildApkStep + false + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 2 + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Release + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-29 + + true + QmakeProjectManager.AndroidBuildApkStep + false + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 2 + + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Profile + /home/dorian/Qt/builds/build-ScStwMonitor-Android_for_armeabi_v7a_Clang_Qt_5_12_6_for_Android_ARMv7-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-29 + + true + QmakeProjectManager.AndroidBuildApkStep + false + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + + true + Qt4ProjectManager.AndroidDeployQtStep + false + + 1 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + Qt4ProjectManager.AndroidDeployConfiguration2 + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + + + 0 + + ScStwMonitorSrc + Qt4ProjectManager.AndroidRunConfiguration:/home/dorian/Documents/git/ScStw/monitor/ScStwMonitorSrc/ScStwMonitorSrc.pro + /home/dorian/Documents/git/ScStw/monitor/ScStwMonitorSrc/ScStwMonitorSrc.pro + + false + + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.Target.1 + + Desktop Qt 5.12.6 GCC 64bit + Desktop Qt 5.12.6 GCC 64bit + qt.qt5.5126.gcc_64_kit + 0 + 0 + 0 + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Debug + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 2 + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Release + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 2 + + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Profile + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + Qt4ProjectManager.Qt4RunConfiguration:/home/dorian/Documents/git/ScStw/monitor/ScStwMonitorSrc/ScStwMonitorSrc.pro + /home/dorian/Documents/git/ScStw/monitor/ScStwMonitorSrc/ScStwMonitorSrc.pro + + false + + false + true + true + false + false + true + + /home/dorian/Qt/builds/build-ScStwMonitor-Desktop_Qt_5_12_6_GCC_64bit-Debug/ScStwMonitorSrc + + 1 + + + + ProjectExplorer.Project.Target.2 + + Raspberry Pi + Raspberry Pi + {78391e56-a762-420f-834b-044152e6b610} + 0 + 0 + 0 + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Debug + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 2 + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Release + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 2 + + + true + 0 + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Profile + /home/dorian/Qt/builds/build-ScStwMonitor-Raspberry_Pi-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + + true + RemoteLinux.CheckForFreeDiskSpaceStep + + + + + / + 5242880 + + + + + true + RemoteLinux.KillAppStep + + + + + + + + + true + RemoteLinux.DirectUploadStep + + + + + false + true + + + + 3 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + DeployToGenericLinux + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + ProjectExplorer.CustomExecutableRunConfiguration + + + false + + false + true + false + false + true + + + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 1 + + RemoteLinux.CustomRunConfig + + + + 1 + + false + + false + true + false + false + true + false + + + :0 + + 2 + + + + ProjectExplorer.Project.TargetCount + 3 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/ScStwMonitorSrc/.gitignore b/ScStwMonitorSrc/.gitignore new file mode 100644 index 0000000..39f809d --- /dev/null +++ b/ScStwMonitorSrc/.gitignore @@ -0,0 +1,2 @@ +*.pro.user +*.pro.user* diff --git a/ScStwMonitorSrc/Banner.png b/ScStwMonitorSrc/Banner.png new file mode 100644 index 0000000..4361a3e Binary files /dev/null and b/ScStwMonitorSrc/Banner.png differ diff --git a/ScStwMonitorSrc/FadeAnimation.qml b/ScStwMonitorSrc/FadeAnimation.qml new file mode 100644 index 0000000..df750b1 --- /dev/null +++ b/ScStwMonitorSrc/FadeAnimation.qml @@ -0,0 +1,72 @@ +/* + Speed Climbing Stopwatch - Simple Stopwatch for Climbers + Copyright (C) 2018 Itsblue Development - Dorian Zeder + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, version 3 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import QtQuick 2.0 + +SequentialAnimation { + id: root + property QtObject target + property int fadeDuration: 150 + property int fadeDuration_in: fadeDuration + property int fadeDuration_out: fadeDuration + + property alias outValueScale: outAnimationScale.to + property alias inValueScale: inAnimationScale.to + + property alias outValueOpacity: outAnimationOpacity.to + property alias inValueOpacity: inAnimationOpacity.to + + property string easingType: "Quad" + ParallelAnimation { + NumberAnimation { // in the default case, fade scale to 0 + id: outAnimationScale + target: root.target + property: "scale" + duration: root.fadeDuration_in + to: 0.9 + easing.type: Easing["In"+root.easingType] + } + NumberAnimation { // in the default case, fade scale to 0 + id: outAnimationOpacity + target: root.target + property: "opacity" + duration: root.fadeDuration_in + to: 0 + easing.type: Easing["In"+root.easingType] + } + } + PropertyAction { } // actually change the property targeted by the Behavior between the 2 other animations + ParallelAnimation { + NumberAnimation { // in the default case, fade scale back to 1 + id: inAnimationScale + target: root.target + property: "scale" + duration: root.fadeDuration_out + to: 1 + easing.type: Easing["Out"+root.easingType] + } + NumberAnimation { // in the default case, fade scale to 0 + id: inAnimationOpacity + target: root.target + property: "opacity" + duration: root.fadeDuration_in + to: 1 + easing.type: Easing["In"+root.easingType] + } + } + +} diff --git a/ScStwMonitorSrc/FancyBusyIndicator.qml b/ScStwMonitorSrc/FancyBusyIndicator.qml new file mode 100644 index 0000000..0d15d80 --- /dev/null +++ b/ScStwMonitorSrc/FancyBusyIndicator.qml @@ -0,0 +1,61 @@ +import QtQuick 2.1 +import QtQuick.Controls 2.2 + +BusyIndicator { + id: control + + property double speed: 1 + property color lineColor: "#21be2b" + + width: 100 + height: 100 + + contentItem: Canvas { + id: spinnerCanvas + anchors.fill: parent + + property double progress: 0 + + function drawSpinner(ctx, width, height, progress){ + var margins = width * 0.01 + var lineWidth = width * 0.1 + + ctx.clearRect(0,0,width,height) + + ctx.beginPath(); + ctx.arc(width * 0.5 + margins, height * 0.5 + margins, height*0.5 - margins*2 - lineWidth , 0, 2*Math.PI); + + ctx.strokeStyle = "#dedede"; + ctx.lineWidth = lineWidth + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(width * 0.5 + margins, height * 0.5 + margins, height*0.5 - margins*2 - lineWidth, 2*Math.PI * progress, 2*Math.PI * progress + 0.5*Math.PI); + + ctx.strokeStyle = "#48db09"; + ctx.stroke(); + } + + + Timer { + interval: Math.floor(20 * 1/control.speed) + running: control.opacity > 0 && control.visible && control.running + repeat: true + + onTriggered: { + spinnerCanvas.progress += 0.0027*6 + if(spinnerCanvas.progress >= 1){ + spinnerCanvas.progress = 0 + } + + spinnerCanvas.requestPaint() + } + } + + onPaint: { + var ctx = getContext("2d"); + spinnerCanvas.drawSpinner(ctx, spinnerCanvas.height, spinnerCanvas.width, spinnerCanvas.progress) + } + + } +} diff --git a/ScStwMonitorSrc/ScStwMonitor.desktop b/ScStwMonitorSrc/ScStwMonitor.desktop new file mode 100644 index 0000000..acbbd09 --- /dev/null +++ b/ScStwMonitorSrc/ScStwMonitor.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Type=Application +Name=ScStwMonitor +Exec=/opt/ScStwMonitor/bin/ScStwMonitor diff --git a/ScStwMonitorSrc/ScStwMonitorSrc.pro b/ScStwMonitorSrc/ScStwMonitorSrc.pro new file mode 100755 index 0000000..489a945 --- /dev/null +++ b/ScStwMonitorSrc/ScStwMonitorSrc.pro @@ -0,0 +1,74 @@ +QT += quick widgets multimedia + +CONFIG += c++11 +CONFIG -= app_bundle + +TARGET = ScStwMonitor +VERSION = 1.1 + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp \ + baseconn.cpp \ + sources/scstwmonitorbackend.cpp + +RESOURCES += \ + qml.qrc \ + shared.qrc + + +# include submodules +include($$PWD/../shared-libraries/ScStwLibraries/ScStwLibraries.pri) +#include($$PWD/../shared-libraries/ScStwStyling/ScStwStyling.pri) + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH = + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +# target path for raspi +else: unix:!android: target.path = /home/pi/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +HEADERS += \ + baseconn.h \ + headers/scstwmonitorbackend.h + +DISTFILES += \ + android/AndroidManifest.xml \ + android/build.gradle \ + android/gradle/wrapper/gradle-wrapper.jar \ + android/gradle/wrapper/gradle-wrapper.properties \ + android/gradlew \ + android/gradlew.bat \ + android/res/values/libs.xml \ + android/src/MainActivity.java + +android { + ANDROID_PACKAGE_SOURCE_DIR = \ + $$PWD/android +} + +ios { + QMAKE_ASSET_CATALOGS += icon/Assets.xcassets + xcode_product_bundle_identifier_setting.value = "de.itsblue.ScStwMonitor" + + OBJECTIVE_SOURCES += \ + sleepprevent.mm + OBJECTIVE_HEADERS += \ + sleepprevent.h +} + diff --git a/ScStwMonitorSrc/SpeedHold.png b/ScStwMonitorSrc/SpeedHold.png new file mode 100644 index 0000000..87738b4 Binary files /dev/null and b/ScStwMonitorSrc/SpeedHold.png differ diff --git a/ScStwMonitorSrc/TimerColumn.qml b/ScStwMonitorSrc/TimerColumn.qml new file mode 100644 index 0000000..5bfc078 --- /dev/null +++ b/ScStwMonitorSrc/TimerColumn.qml @@ -0,0 +1,146 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.0 +import de.itsblue.ScStw 2.0 +import de.itsblue.ScStwMonitor 2.0 + +Column { + id: timerCol + + opacity: backend.scStwClient.state === ScStwClient.CONNECTED ? 1:0 + + spacing: 0 + + Repeater { + id: timerRep + + property var clearedTimers: removeDisabledTimers(backend.race.timers) + + function removeDisabledTimers(timers) { + var ret = [] + for(var i = 0; i < timers.length; i++) { + if(timers[i]["state"] !== ScStwTimer.DISABLED) + ret.push(timers[i]) + } + return ret + } + + model: clearedTimers.length + + delegate: Item { + id: timerDel + + width: parent.width + height: timerCol.height / timerRep.model + + Label { + id: laneNameLa + + anchors { + left: parent.left + } + + leftPadding: parent.width * 0.03 + + width: parent.width * 0.15 + height: parent.height * 0.5 + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + + text: ""//index === 0 ? "A":"B" + + color: "#2a5266" + + font.pixelSize: height + font.family: timerFont.name + + Rectangle { + anchors.fill: parent + color: "red" + opacity: 0 + } + } + + Label { + id: timerTextLa + + anchors.centerIn: parent + anchors.horizontalCenterOffset: laneNameLa.text !== "" ? parent.width * 0.06:0 + anchors.verticalCenterOffset: -(parent.height * 0.04 * reactTimeLa.opacity) + + width: parent.width * 0.8 + height: parent.height * 0.8 + + elide: "ElideRight" + color: ([ScStwTimer.WON].indexOf(timerRep.clearedTimers[index]["state"]) >= 0 ? "#6bd43b" : + [ScStwTimer.FAILED,ScStwTimer.LOST].indexOf(timerRep.clearedTimers[index]["state"]) >= 0 ? "#e03b2f": + "black") + + text: timerRep.clearedTimers[index]["text"] + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + font.pixelSize: height + font.family: timerFont.name + minimumPixelSize: 1 + } + + Label { + id: reactTimeLa + + property int rtime: timerRep.clearedTimers[index]["reactionTime"] + + anchors { + centerIn: parent + verticalCenterOffset: timerTextLa.contentHeight * 0.4 + reactTimeLa.contentHeight * 0.4 + timerTextLa.anchors.verticalCenterOffset + horizontalCenterOffset: parent.width * 0.06 + } + + width: parent.width * 0.6 + height: parent.height * 0.15 + + scale: enabled ? 1:0.9 + opacity: enabled ? 1:0 + + enabled: timerRep.clearedTimers[index]["state"] >= ScStwTimer.STARTING && rtime !== 0 + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + text: "reaction time (ms): " + Math.round(rtime) + + color: "black"//appTheme.style.textColor + + font.pixelSize: timerTextLa.font.pixelSize * 0.5 + font.family: timerFont.name + minimumPixelSize: 1 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + Behavior on scale { + NumberAnimation { + duration: 200 + } + } + + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } +} diff --git a/ScStwMonitorSrc/VolumeHigh.png b/ScStwMonitorSrc/VolumeHigh.png new file mode 100644 index 0000000..0ed4293 Binary files /dev/null and b/ScStwMonitorSrc/VolumeHigh.png differ diff --git a/ScStwMonitorSrc/VolumeLow.png b/ScStwMonitorSrc/VolumeLow.png new file mode 100644 index 0000000..038f967 Binary files /dev/null and b/ScStwMonitorSrc/VolumeLow.png differ diff --git a/ScStwMonitorSrc/android/AndroidManifest.xml b/ScStwMonitorSrc/android/AndroidManifest.xml new file mode 100644 index 0000000..ad3c7d2 --- /dev/null +++ b/ScStwMonitorSrc/android/AndroidManifest.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ScStwMonitorSrc/android/build.gradle b/ScStwMonitorSrc/android/build.gradle new file mode 100644 index 0000000..989d079 --- /dev/null +++ b/ScStwMonitorSrc/android/build.gradle @@ -0,0 +1,57 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0' + } +} + +repositories { + google() + jcenter() +} + +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qt5AndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion.toInteger() + + buildToolsVersion androidBuildToolsVersion + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qt5AndroidDir + '/res', 'res'] + resources.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + lintOptions { + abortOnError false + } +} diff --git a/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.jar b/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.properties b/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bf3de21 --- /dev/null +++ b/ScStwMonitorSrc/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ScStwMonitorSrc/android/gradlew b/ScStwMonitorSrc/android/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/ScStwMonitorSrc/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/ScStwMonitorSrc/android/gradlew.bat b/ScStwMonitorSrc/android/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/ScStwMonitorSrc/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ScStwMonitorSrc/android/res/drawable-hdpi/icon.png b/ScStwMonitorSrc/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..a26d006 Binary files /dev/null and b/ScStwMonitorSrc/android/res/drawable-hdpi/icon.png differ diff --git a/ScStwMonitorSrc/android/res/drawable-ldpi/icon.png b/ScStwMonitorSrc/android/res/drawable-ldpi/icon.png new file mode 100644 index 0000000..a26d006 Binary files /dev/null and b/ScStwMonitorSrc/android/res/drawable-ldpi/icon.png differ diff --git a/ScStwMonitorSrc/android/res/drawable-mdpi/icon.png b/ScStwMonitorSrc/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..a26d006 Binary files /dev/null and b/ScStwMonitorSrc/android/res/drawable-mdpi/icon.png differ diff --git a/ScStwMonitorSrc/android/res/values/libs.xml b/ScStwMonitorSrc/android/res/values/libs.xml new file mode 100644 index 0000000..4009a77 --- /dev/null +++ b/ScStwMonitorSrc/android/res/values/libs.xml @@ -0,0 +1,25 @@ + + + + https://download.qt.io/ministro/android/qt5/qt-5.9 + + + + + + + + + + + + + + + + + + + + diff --git a/ScStwMonitorSrc/android/src/MainActivity.java b/ScStwMonitorSrc/android/src/MainActivity.java new file mode 100644 index 0000000..2984682 --- /dev/null +++ b/ScStwMonitorSrc/android/src/MainActivity.java @@ -0,0 +1,9 @@ +package de.itsblue.SpeedClimbingStopwatchMonitor; +public class MainActivity extends org.qtproject.qt5.android.bindings.QtActivity { + @Override + public void onCreate(android.os.Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + //this.getWindow().setVolumeControlStream(android.view.AudioManager.STREAM_MUSIC); + } +} diff --git a/ScStwMonitorSrc/baseconn.cpp b/ScStwMonitorSrc/baseconn.cpp new file mode 100755 index 0000000..332e83d --- /dev/null +++ b/ScStwMonitorSrc/baseconn.cpp @@ -0,0 +1,504 @@ +#include "baseconn.h" + +BaseConn::BaseConn(QObject *parent) : QObject(parent) +{ + socket = new QTcpSocket(this); + + this->timeoutTimer = new QTimer(this); + this->timeoutTimer->setSingleShot(true); + + this->state = "disconnected"; + + connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)), + this, SLOT(gotError(QAbstractSocket::SocketError))); + + connect(this->socket, &QAbstractSocket::stateChanged, this, &BaseConn::socketStateChanged); + connect(this, &BaseConn::gotUpdate, this, &BaseConn::handleUpdate); + + this->nextConnectionId = 1; + + // init refresh timers + this->autoConnectRetryTimer = new QTimer(this); + this->autoConnectRetryTimer->setInterval(1000); + this->autoConnectRetryTimer->setSingleShot(true); + connect(this->autoConnectRetryTimer, &QTimer::timeout, this, &BaseConn::doConnectionAttempt); + this->autoConnectRetryTimer->start(); + + this->timerTextRefreshTimer = new QTimer(this); + this->timerTextRefreshTimer->setInterval(1); + this->timerTextRefreshTimer->setSingleShot(true); + connect(this->timerTextRefreshTimer, &QTimer::timeout, this, &BaseConn::refreshTimerTextList); + this->timerTextRefreshTimer->start(); +} + +void BaseConn::connectToHost() { + qDebug() << "+--- connecting"; + setState("connecting"); + + connect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); + + //connect + this->socket->connectToHost(this->ip, this->port); + + timeoutTimer->start(3000); +} + +void BaseConn::connectionTimeout() { + this->socket->abort(); + disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); +} + +bool BaseConn::init() { + disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); + this->timeoutTimer->stop(); + + connect(this->socket, &QTcpSocket::readyRead, this, &BaseConn::readyRead); + + this->setState("connected"); + + // init remote session + QJsonArray updateSubs = {"onTimersChanged", "onRaceStateChanged", "onNextStartActionChanged"}; + QJsonObject sessionParams = {{"updateSubs", updateSubs}, {"init", true}}; + + if(this->sendCommand(1, sessionParams)["status"] != 200) { + return false; + } + + return true; +} + +void BaseConn::deInit() { +} + +void BaseConn::closeConnection() +{ + qDebug() << "+--- closing connection"; + switch (socket->state()) + { + case 0: + socket->disconnectFromHost(); + break; + case 2: + socket->abort(); + break; + default: + socket->abort(); + } + + setState("disconnected"); +} + +void BaseConn::gotError(QAbstractSocket::SocketError err) +{ + //qDebug() << "got error"; + QString strError = "unknown"; + switch (err) + { + case 0: + strError = "Connection was refused"; + break; + case 1: + strError = "Remote host closed the connection"; + this->closeConnection(); + break; + case 2: + strError = "Host address was not found"; + break; + case 5: + strError = "Connection timed out"; + break; + default: + strError = "Unknown error"; + } + + emit gotError(strError); +} + +// ------------------------------------- +// --- socket communication handling --- +// ------------------------------------- + +void BaseConn::socketStateChanged(QAbstractSocket::SocketState socketState) { + switch (socketState) { + case QAbstractSocket::UnconnectedState: + { + this->setState("disconnected"); + break; + } + case QAbstractSocket::ConnectedState: + { + if(this->init()) { + this->setState("connected"); + } + else { + this->closeConnection(); + } + + break; + } + default: + { + //qDebug() << "+ --- UNKNOWN SOCKET STATE: " << socketState; + break; + } + } +} + +QVariantMap BaseConn::sendCommand(int header, QJsonValue data){ + if(this->state != "connected"){ + return {{"status", 910}, {"data", "not connected"}}; + } + + // generate id and witing requests entry + int thisId = nextConnectionId; + //qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId; + nextConnectionId ++; + + QEventLoop *loop = new QEventLoop(this); + QTimer *timer = new QTimer(this); + QJsonObject reply; + + this->waitingRequests.append({thisId, loop, reply}); + + QJsonObject requestObj; + requestObj.insert("id", thisId); + requestObj.insert("header", header); + requestObj.insert("data", data); + + QString jsonRequest = QJsonDocument(requestObj).toJson(); + + timer->setSingleShot(true); + // quit the loop when the timer times out + loop->connect(timer, SIGNAL(timeout()), loop, SLOT(quit())); + // quit the loop when the connection was established + // loop.connect(this, &BaseConn::gotReply, &loop, &QEventLoop::quit); + // start the timer before starting to connect + timer->start(3000); + + //write data + socket->write(jsonRequest.toLatin1()); + + //wait for an answer to finish (programm gets stuck in here) + loop->exec(); + + bool replyFound = false; + + // find reply and delete the request from waiting list + for(int i = 0; iwaitingRequests.length(); i++){ + if(this->waitingRequests[i].id == thisId){ + // request was found + replyFound = true; + // delete event loop + if(this->waitingRequests[i].loop != nullptr) { + delete this->waitingRequests[i].loop; + } + // store reply + reply = this->waitingRequests[i].reply; + // remove reply from waiting list + this->waitingRequests.removeAt(i); + } + } + + if(!replyFound) { + // some internal error occured + return {{"status", 900}, {"data", ""}}; + } + + if(timer->remainingTime() == -1){ + //the time has been triggered -> timeout + return {{"status", 911}, {"data", ""}}; + } + + delete timer; + + return {{"status", reply.value("header").toInt()}, {"data", reply.value("data").toVariant()}}; + +} + +void BaseConn::readyRead() { + + //qDebug() << "ready to ready " << socket->bytesAvailable() << " bytes" ; + QString reply = socket->readAll(); + + //qWarning() << "socket read: " << reply; + + processSocketMessage(reply); +} + +void BaseConn::processSocketMessage(QString message){ + QString startKey = ""; + QString endKey = ""; + + //qWarning() << "... processing message now ... : " << message; + + if(message == ""){ + return; + } + + if((message.startsWith(startKey) && message.endsWith(endKey)) && (message.count(startKey) == 1 && message.count(endKey) == 1)){ + // non-split message ( e.g.: 123456789 + } + else if(!message.contains(endKey) && (!this->readBuffer.isEmpty() || message.startsWith(startKey))){ + // begin of a split message ( e.g.: 123 ) + // or middle of a split message ( e.g.: 456 ) + //qWarning() << "this is a begin or middle of split a message"; + this->readBuffer += message; + return; + } + else if(!message.contains(startKey) && message.endsWith(endKey)) { + // end of a split message ( e.g.: 789 ) + + if(!this->readBuffer.isEmpty()){ + message = readBuffer + message; + readBuffer.clear(); + } + } + else if((message.count(startKey) > 1 || message.count(endKey) > 1) || (message.contains(endKey) && !message.endsWith(endKey) && message.contains(startKey) && !message.startsWith(startKey))) { + // multiple messages in one packet ( e.g.: 123456789987654321 ) + // or multiple message fragments in one message ( e.g.: 56789987654321 or 5678998765 ) + //qDebug() << "detected multiple messages"; + + int startOfSecondMessage = message.lastIndexOf(startKey); + // process first part of message + QString firstMessage = message.left(startOfSecondMessage); + this->processSocketMessage(firstMessage); + // process second part of message + QString secondMessage = message.right(message.length() - startOfSecondMessage); + this->processSocketMessage(secondMessage); + + return; + } + else { + // invalid message + return; + } + + //qWarning() << "... done processing, message: " << message; + this->socketReplyRecieved(message); +} + +void BaseConn::socketReplyRecieved(QString reply) { + reply.replace("", ""); + reply.replace("", ""); + + int id = 0; + + QJsonDocument jsonReply = QJsonDocument::fromJson(reply.toUtf8()); + QJsonObject replyObj = jsonReply.object(); + + //qDebug() << "got: " << reply; + + if(!replyObj.isEmpty()){ + id = replyObj.value("id").toInt(); + + if(id == -1) { + // this message is an update!! + emit this->gotUpdate(replyObj.toVariantMap()); + return; + } + + for(int i = 0; i < this->waitingRequests.length(); i++){ + if(this->waitingRequests[i].id == id){ + this->waitingRequests[i].reply = replyObj; + if(this->waitingRequests[i].loop != nullptr){ + this->waitingRequests[i].loop->quit(); + } + return; + } + } + } + + latestReadReply = reply; + emit gotUnexpectedReply(reply); +} + +// ------------------------ +// --- helper functions --- +// ------------------------ + +void BaseConn::doConnectionAttempt() +{ + if(this->state == "disconnected") { + qDebug() << "+--- trying to connect"; + this->connectToHost(); + } + + this->autoConnectRetryTimer->start(); +} + +void BaseConn::setState(QString newState){ + if(this->state != newState) { + qDebug() << "+--- BaseConn state changed: " << newState; + this->state = newState; + emit stateChanged(); + if(this->state == "disconnected") { + this->deInit(); + } + } +} + +int BaseConn::writeRemoteSetting(QString key, QString value) { + QJsonArray requestData; + requestData.append(key); + requestData.append(value); + return this->sendCommand(3000, requestData)["status"].toInt(); +} + +QString BaseConn::readRemoteSetting(QString key) +{ + QVariantMap reply = this->sendCommand(3001, key); + if(reply["status"] != 200){ + return "false"; + } + return reply["data"].toString(); +} + +// ------------------ +// - for timer sync - +// ------------------ + +void BaseConn::handleUpdate(QVariantMap data) { + int header = data["header"].toInt(); + switch (header) { + case 9000: + { + // the remote race state changed + this->remoteRaceState = data["data"].toInt(); + this->raceStateChanged(); + break; + } + case 9001: + { + // the remote timers have changed + this->refreshRemoteTimers(data["data"].toList()); + break; + } + case 9003: + { + // the next start action has changed + this->nextStartActionTotalDelay = data["data"].toMap()["nextActionDelay"].toDouble(); + this->nextStartActionDelayStartedAt = this->date->currentMSecsSinceEpoch() - (this->nextStartActionTotalDelay * data["data"].toMap()["nextActionDelayProg"].toDouble()); + this->nextStartAction = NextStartAction( data["data"].toMap()["nextAction"].toInt() ); + + emit this->nextStartActionChanged(); + } + + } +} + +void BaseConn::refreshRemoteTimers(QVariantList timers) { + QVariantList remoteTimers; + + for (int i = 0; i < timers.length(); i++) { + QVariantMap thisTimer = timers[i].toMap(); + if(thisTimer["state"].toInt() != DISABLED ) { + thisTimer.insert("startTime", this->date->currentMSecsSinceEpoch() - thisTimer["currTime"].toDouble()); + remoteTimers.append(thisTimer); + } + } + + this->remoteTimers = remoteTimers; +} + +void BaseConn::refreshTimerTextList() { + QVariantList tmpTimerTextList; + + for (int i = 0; i < this->remoteTimers.toList().length(); i++) { + + QString newText; + + switch (this->remoteTimers.toList()[i].toMap()["state"].toInt()) { + case IDLE: + newText = "00.000"; + break; + case STARTING: + newText = "00.000"; + break; + case WAITING: + newText = "False Start"; + break; + case RUNNING: { + double currTime = this->date->currentMSecsSinceEpoch() - this->remoteTimers.toList()[i].toMap()["startTime"].toDouble(); + QString currTimeString = (currTime < 10000 ? "0":"") + QString::number( currTime / 1000.0, 'f', 3 );//QString::number( (currTime) / 1000.0, 'f', 1 ); + newText = currTimeString; + break; + } + case WON: { + double currTime = this->remoteTimers.toList()[i].toMap()["currTime"].toDouble(); + newText = (currTime < 10000 ? "0":"") + QString::number( currTime / 1000.0, 'f', 3 ); + break; + } + case LOST: { + double currTime = this->remoteTimers.toList()[i].toMap()["currTime"].toDouble(); + newText = (currTime < 10000 ? "0":"") + QString::number( currTime / 1000.0, 'f', 3 ); + break; + } + case FAILED: + newText = "False Start"; + break; + case CANCELLED: + newText = "Cancelled"; + break; + case DISABLED: + newText = "---"; + break; + } + + QVariantMap timerMap = {{"text", newText}, {"reactTime", this->remoteTimers.toList()[i].toMap()["reactTime"].toInt()}, {"state", this->remoteTimers.toList()[i].toMap()["state"].toInt()}}; + tmpTimerTextList.append(timerMap); + } + + if(tmpTimerTextList != this->timerTextList) { + this->timerTextList = tmpTimerTextList; + emit this->timerTextChanged(); + } + + // calculate next start action delay progress + double nextStartActionRemainingDelay = this->nextStartActionTotalDelay - ( this->date->currentMSecsSinceEpoch() - this->nextStartActionDelayStartedAt ); + if(nextStartActionRemainingDelay > 0){ + this->nextStartActionDelayProgress = nextStartActionRemainingDelay / this->nextStartActionTotalDelay; + emit this->nextStartActionDelayProgressChanged(); + } + else { + this->nextStartActionDelayProgress = 0; + emit this->nextStartActionDelayProgressChanged(); + } + + this->timerTextRefreshTimer->start(); +} + +// ----------- +// - for qml - +// ----------- + +void BaseConn::setIP(const QString &ipAdress){ + this->ip = ipAdress; +} + +QString BaseConn::getIP() const +{ + return(this->ip); +} + +QString BaseConn::getState() const +{ + return(this->state); +} + +QVariant BaseConn::getTimerTextList() +{ + return this->timerTextList; +} + +int BaseConn::getRaceState() +{ + return this->remoteRaceState; +} + +double BaseConn::getNextStartActionDelayProgress() { + return this->nextStartActionDelayProgress; +} + +int BaseConn::getNextStartAction() { + return this->nextStartAction; +} diff --git a/ScStwMonitorSrc/baseconn.h b/ScStwMonitorSrc/baseconn.h new file mode 100755 index 0000000..d7bf76f --- /dev/null +++ b/ScStwMonitorSrc/baseconn.h @@ -0,0 +1,153 @@ +#ifndef BASECONN_H +#define BASECONN_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class BaseConn : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString ipAddress READ getIP WRITE setIP) + Q_PROPERTY(QString state READ getState NOTIFY stateChanged) + Q_PROPERTY(QVariant timers READ getTimerTextList NOTIFY timerTextChanged) + Q_PROPERTY(int raceState READ getRaceState NOTIFY raceStateChanged) + + Q_PROPERTY(double nextStartActionDelayProgress READ getNextStartActionDelayProgress NOTIFY nextStartActionDelayProgressChanged) + Q_PROPERTY(int nextStartAction READ getNextStartAction NOTIFY nextStartActionChanged) + +public: + explicit BaseConn(QObject *parent = nullptr); + + // values for the socket connection + QString ip; + ushort port = 3563; + int errors; + int errors_until_disconnect = 4; + + // the current state + QString state; + // can be: + // - 'disconnected' + // - 'connecting' + // - 'connected' + + QString latestReadReply; + + //---general status values---// + + // stuff for storing the timers + enum timerState { IDLE, STARTING, WAITING, RUNNING, WON, LOST, FAILED, CANCELLED, DISABLED }; + + QVariant remoteTimers; + QVariant timerTextList; + int remoteRaceState; + + // for next start action + enum NextStartAction { AtYourMarks, Ready, Start, None }; + NextStartAction nextStartAction; + double nextStartActionDelayProgress; + // only used in remote mode: + double nextStartActionDelayStartedAt; + double nextStartActionTotalDelay; + +private: + QDateTime *date; + //to get the current time + + QTcpSocket *socket; + //socket for communication with the extention + + QTimer *autoConnectRetryTimer; // timer to frequently trigger a connection attempt to the base station + QTimer *timeoutTimer; // timer to trigger connection timeout + QTimer *timerTextRefreshTimer; // timer to refresh the text of the timers on the frontend + + QString readBuffer; + + int nextConnectionId; + + struct waitingRequest { + int id; + QEventLoop * loop; + QJsonObject reply; + }; + + QList waitingRequests; + +signals: + void stateChanged(); + //is emitted, when the connection state changes + + void gotUnexpectedReply(QString reply); + + void gotUpdate(QVariantMap data); + + void gotError(QString error); + + // for qml + void timerTextChanged(); + void raceStateChanged(); + + void nextStartActionChanged(); + void nextStartActionDelayProgressChanged(); + +public slots: + + Q_INVOKABLE void connectToHost(); + //function to connect to the base station + + void connectionTimeout(); + + Q_INVOKABLE bool init(); + Q_INVOKABLE void deInit(); + + Q_INVOKABLE void closeConnection(); + + void gotError(QAbstractSocket::SocketError err); + + // --- socket communication handling --- + + Q_INVOKABLE QVariantMap sendCommand(int header, QJsonValue data = ""); + + // helper functions + void doConnectionAttempt(); + void setState(QString newState); + int writeRemoteSetting(QString key, QString value); + QString readRemoteSetting(QString key); + + // for timer sync + void handleUpdate(QVariantMap data); + void refreshRemoteTimers(QVariantList timers); + void refreshTimerTextList(); + + // for qml + QString getIP() const; + void setIP(const QString &ipAdress); + + QString getState() const; + + QVariant getTimerTextList(); + int getRaceState(); + + Q_INVOKABLE double getNextStartActionDelayProgress(); + Q_INVOKABLE int getNextStartAction(); + +private slots: + void readyRead(); + + void processSocketMessage(QString message); + + void socketReplyRecieved(QString reply); + + void socketStateChanged(QAbstractSocket::SocketState socketState); +}; + +#endif // BASECONN_H diff --git a/ScStwMonitorSrc/fonts/Arvo-Bold.ttf b/ScStwMonitorSrc/fonts/Arvo-Bold.ttf new file mode 100644 index 0000000..38341b1 Binary files /dev/null and b/ScStwMonitorSrc/fonts/Arvo-Bold.ttf differ diff --git a/ScStwMonitorSrc/fonts/Arvo-BoldItalic.ttf b/ScStwMonitorSrc/fonts/Arvo-BoldItalic.ttf new file mode 100644 index 0000000..b87118b Binary files /dev/null and b/ScStwMonitorSrc/fonts/Arvo-BoldItalic.ttf differ diff --git a/ScStwMonitorSrc/fonts/Arvo-Regular.ttf b/ScStwMonitorSrc/fonts/Arvo-Regular.ttf new file mode 100644 index 0000000..d8d0ec8 Binary files /dev/null and b/ScStwMonitorSrc/fonts/Arvo-Regular.ttf differ diff --git a/ScStwMonitorSrc/fonts/Arvo-RegularItalic.ttf b/ScStwMonitorSrc/fonts/Arvo-RegularItalic.ttf new file mode 100644 index 0000000..1a19337 Binary files /dev/null and b/ScStwMonitorSrc/fonts/Arvo-RegularItalic.ttf differ diff --git a/ScStwMonitorSrc/fonts/PTMono-Regular.ttf b/ScStwMonitorSrc/fonts/PTMono-Regular.ttf new file mode 100644 index 0000000..13a8004 Binary files /dev/null and b/ScStwMonitorSrc/fonts/PTMono-Regular.ttf differ diff --git a/ScStwMonitorSrc/headers/scstwmonitorbackend.h b/ScStwMonitorSrc/headers/scstwmonitorbackend.h new file mode 100644 index 0000000..80255d0 --- /dev/null +++ b/ScStwMonitorSrc/headers/scstwmonitorbackend.h @@ -0,0 +1,37 @@ +#ifndef SCSTWMONITORBACKEND_H +#define SCSTWMONITORBACKEND_H + +#include +#include +#include + +class ScStwMonitorBackend : public QObject +{ + Q_OBJECT + + Q_PROPERTY(ScStwRace* race READ getRace NOTIFY raceChanged) + Q_PROPERTY(ScStwClient *scStwClient READ getScStwClient NOTIFY scStwClientChanged) +public: + explicit ScStwMonitorBackend(QObject *parent = nullptr); + +private: + ScStwClient * scStwClient; + QTimer *autoConnectRetryTimer; // timer to frequently trigger a connection attempt to the base station + QTimer * timerTextRefreshTimer; + ScStwRemoteMonitorRace * remoteRace; + +public slots: + // functions for qml + Q_INVOKABLE ScStwRace *getRace(); + Q_INVOKABLE ScStwClient *getScStwClient(); + +private slots: + void refreshTimerText(); + void doConnectionAttempt(); + +signals: + void raceChanged(); + void scStwClientChanged(); +}; + +#endif // SCSTWMONITORBACKEND_H diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/100.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..972f35a Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/1024.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..9d13b94 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/114.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..39a25aa Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/120.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..014d1ec Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/128.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000..72cfa33 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/144.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..7562f0f Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/152.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..41643ef Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/16.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000..a2719ae Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/167.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..8f24c2e Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/172.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000..032c5f4 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/180.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..3fa5afc Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/196.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000..63feac7 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/20.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..b9b4d4d Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/216.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000..2a017a8 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/256.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000..4ae8a84 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/29.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..7e2784b Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/32.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000..92f455c Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/40.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..d4b653d Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/48.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000..b0624af Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/50.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..fb1bf7b Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/512.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000..56915ac Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/55.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000..7a59a85 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/57.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..9a04433 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/58.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..428f192 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/60.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..b9533b0 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/64.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000..feb066c Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/72.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..192afce Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/76.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..7cc238e Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/80.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..742dd26 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/87.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..6e55f23 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/88.png b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000..e42d3e8 Binary files /dev/null and b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/Contents.json b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e138c0b --- /dev/null +++ b/ScStwMonitorSrc/icon/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/ScStwMonitorSrc/icon/Assets.xcassets/Contents.json b/ScStwMonitorSrc/icon/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/ScStwMonitorSrc/icon/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ScStwMonitorSrc/icon/favicon.png b/ScStwMonitorSrc/icon/favicon.png new file mode 100644 index 0000000..a26d006 Binary files /dev/null and b/ScStwMonitorSrc/icon/favicon.png differ diff --git a/ScStwMonitorSrc/icon/favicon.xcf b/ScStwMonitorSrc/icon/favicon.xcf new file mode 100644 index 0000000..53636a6 Binary files /dev/null and b/ScStwMonitorSrc/icon/favicon.xcf differ diff --git a/ScStwMonitorSrc/icon/faviconNoOutline.png b/ScStwMonitorSrc/icon/faviconNoOutline.png new file mode 100644 index 0000000..e75c63a Binary files /dev/null and b/ScStwMonitorSrc/icon/faviconNoOutline.png differ diff --git a/ScStwMonitorSrc/icon/icons8-monitor-96.png b/ScStwMonitorSrc/icon/icons8-monitor-96.png new file mode 100644 index 0000000..e6c9434 Binary files /dev/null and b/ScStwMonitorSrc/icon/icons8-monitor-96.png differ diff --git a/ScStwMonitorSrc/main.cpp b/ScStwMonitorSrc/main.cpp new file mode 100755 index 0000000..ab55a3e --- /dev/null +++ b/ScStwMonitorSrc/main.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "headers/scstwmonitorbackend.h" +#if defined(Q_OS_IOS) +#include "sleepprevent.h" +#endif + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + QGuiApplication app(argc, argv); + + app.setOrganizationName("itsblue"); + app.setOrganizationDomain("itsblue.de"); + + qmlRegisterType("de.itsblue.ScStwMonitor", 2, 0, "ScStwMonitorBackend"); + // setup speed backend and App themes + qmlRegisterType("de.itsblue.ScStw", 2, 0, "ScStwRace"); + qmlRegisterType("de.itsblue.ScStw", 2, 0, "ScStwTimer"); + qmlRegisterType("de.itsblue.ScStw", 2, 0, "ScStw"); + qmlRegisterType("de.itsblue.ScStw", 2, 0, "ScStwClient"); + //qmlRegisterUncreatableType("de.itsblue.ScStw", 2, 0, "ScStwAppTheme", "The ScStwAppTheme has to be managed by a ScStwAppTheme manager and is therefore not creatable"); + //qmlRegisterType("de.itsblue.ScStw", 2, 0, "ScStwAppThemeManager"); + + + QQmlApplicationEngine engine; + + QSize size = app.screens().first()->size(); + engine.rootContext()->setContextProperty("XscreenHeight", size.height()); + engine.rootContext()->setContextProperty("XscreenWidth", size.width()); + + if(argc > 1 && QString(argv[1]) == "--noControls") + engine.rootContext()->setContextProperty("showControls", false); + else + engine.rootContext()->setContextProperty("showControls", true); + + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + #if defined(Q_OS_IOS) + SleepPrevent sp; + sp.setTimerDisabled(); + #endif + + // move the cursor into an invisible position + QScreen *screen = QGuiApplication::primaryScreen(); + QRect rect = screen->geometry(); + QCursor::setPos(rect.width(),rect.height()); + + return app.exec(); +} diff --git a/ScStwMonitorSrc/main.qml b/ScStwMonitorSrc/main.qml new file mode 100755 index 0000000..14b0ebf --- /dev/null +++ b/ScStwMonitorSrc/main.qml @@ -0,0 +1,417 @@ +import QtQuick 2.9 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import Qt.labs.settings 1.0 +import QtQuick.Layouts 1.0 + +import de.itsblue.ScStw 2.0 +import de.itsblue.ScStwMonitor 2.0 + +Window { + id: window + visible: true + width: XscreenWidth / 2 + height: XscreenHeight / 2 + title: qsTr("ScStwMonitor") + + //visibility: Window.FullScreen + + Page { + id: app + anchors.fill: parent + + function landscape() { + return app.width > app.height + } + + ScStwMonitorBackend { + id: backend + scStwClient.ipAddress: appSettings.baseStationIp + } + + Settings { + id: appSettings + property string baseStationIp: "192.168.4.1" + } + + Shortcut { + sequences: ["Ctrl+Q", StandardKey.Back] + onActivated: Qt.quit() + } + + Shortcut { + sequences: ["F11", "Esc"] + onActivated: { + if(window.visibility === Window.FullScreen) { + window.visibility = Window.Windowed + } + else { + window.visibility = Window.FullScreen + } + } + } + + FontLoader { + id: timerFont + source:"qrc:///fonts/PTMono-Regular.ttf" + } + + Loader { + id: mainComponentLoader + + anchors.fill: parent + + sourceComponent: backend.scStwClient.state === ScStwClient.CONNECTED ? displayComp:loadingComp + } + + Component { + id: displayComp + + Item { + id: displayItm + + anchors.fill: parent + + Image { + id: bannerImg + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: app.landscape() ? app.height * 0.01:app.width * 0.1 + } + + height: app.landscape() ? app.height * 0.25:app.height * 0.2 + + visible: showControls + + fillMode: Image.PreserveAspectFit + mipmap: true + + source: "qrc:/Banner.png" + } + + TimerColumn { + anchors.fill: parent + + opacity: !showControls || [ScStwRace.IDLE,ScStwRace.STARTING].indexOf(backend.race.state) < 0 ? 1:0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + Item { + id: controlsItm + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + visible: showControls + + Label { + id: clickHintLabel + + property string implicitText:[ + "tap anywhere\nto start", + "NEXT_START_ACTION", + "please wait...", + "running\ntap anywhere to stop", + "tap anywhere to reset" + ][backend.race.state] + + anchors.fill: parent + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + fontSizeMode: Text.Fit + + font.pixelSize: height * 0.3 + + color: backend.race.state === ScStwRace.STARTING ? "#e0b928":"grey" + + text: implicitText === "NEXT_START_ACTION" ? ["", "at your \nmarks", "ready", "starting..."][backend.race.nextStartActionDetails[ScStwRace.NextStartAction]+1]:implicitText + + Behavior on text { + FadeAnimation { + target: clickHintLabel + fadeDuration: 150 + } + } + } + + ProgressBar { + id: nextActiondelayProgressBar + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + height: app.landscape() ? app.height * 0.1:app.width * 0.1 + + opacity: backend.race.nextStartActionDetails[ScStwRace.NextStartAction] < 3 && backend.race.state === ScStwRace.STARTING ? 1:0 + + value: backend.race.nextStartActionDetails[ScStwRace.NextStartActionDelayProgress] + + + background: Rectangle { + implicitWidth: 200 + implicitHeight: parent.height + color: "lightgrey" + } + + contentItem: Item { + implicitWidth: 200 + implicitHeight: parent.height + + Rectangle { + width: nextActiondelayProgressBar.visualPosition * parent.width + height: parent.height + color: "grey" + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + MouseArea { + + parent: app + + anchors.fill: parent + + visible: controlsItm.visible + + enabled: visible + + onClicked: { + switch (backend.race.state) { + case ScStwRace.IDLE: + // IDLE + backend.race.start() + break; + case ScStwRace.STARTING: + // STARTING + backend.race.cancel() + break; + case ScStwRace.WAITING: + // WAITING + break; + case ScStwRace.RUNNING: + // RUNNING + backend.race.stop() + break; + case ScStwRace.STOPPED: + // STOPPED + backend.race.reset() + break; + + } + } + + RowLayout { + id: volumeSliderRow + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + margins: 5 + } + + height: app.landscape() ? app.height * 0.1:app.width * 0.12 + spacing: 0 + + opacity: backend.race.state === ScStwRace.IDLE ? 1:0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + Image { + Layout.preferredHeight: parent.height * 0.5 + Layout.preferredWidth: height * 0.7 + Layout.alignment: Layout.Center + + mipmap: true + + fillMode: Image.PreserveAspectFit + source: "qrc:/VolumeLow.png" + } + + Slider { + id: volumeSlider + + Layout.fillHeight: true + Layout.fillWidth: true + + implicitWidth: 200 + + value: parseFloat(backend.scStwClient.readRemoteSetting(ScStw.SoundVolumeSetting)) + + onPressedChanged: { + if(!pressed){ + volumeSlider.enabled = false + backend.scStwClient.writeRemoteSetting(ScStw.SoundVolumeSetting, value) + volumeSlider.enabled = true + } + } + + leftPadding: width * (app.landscape() ? 0.02:0.05) + rightPadding: leftPadding + + background: Rectangle { + x: volumeSlider.leftPadding + y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: volumeSliderRow.height * 0.1 + width: volumeSlider.availableWidth + height: implicitHeight + radius: 2 + color: "#bdbebf" + + Rectangle { + width: volumeSlider.visualPosition * parent.width + height: parent.height + color: "grey" + radius: 2 + } + } + + handle: Item { + x: volumeSlider.leftPadding - width * 0.15 + volumeSlider.visualPosition * (volumeSlider.availableWidth - width * 0.55) + y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2 + implicitWidth: volumeSliderRow.height + implicitHeight: implicitWidth + + Image { + anchors.fill: parent + + fillMode: Image.PreserveAspectFit + + source: "qrc:/SpeedHold.png" + } + } + } + + Image { + Layout.preferredHeight: parent.height * 0.5 + Layout.preferredWidth: height + Layout.alignment: Layout.Center + + mipmap: true + fillMode: Image.PreserveAspectFit + source: "qrc:/VolumeHigh.png" + } + + } + } + + states: [ + State { + when: [ScStwRace.IDLE,ScStwRace.STARTING].indexOf(backend.race.state) >= 0 + name: "big" + PropertyChanges { + target: clickHintLabel + + anchors.margins: app.landscape() ? app.height * 0.2:app.width * 0.1 + + width: parent.width * 0.7 + height: parent.height * 0.7 + } + + PropertyChanges { + target: controlsItm + + height: app.height + } + }, + State { + when: [ScStwRace.IDLE,ScStwRace.STARTING].indexOf(backend.race.state) < 0 + name: "small" + + PropertyChanges { + target: clickHintLabel + + anchors.margins: app.landscape() ? app.height * 0.01:app.width * 0.1 + } + + PropertyChanges { + target: controlsItm + + height: app.landscape() ? app.height * 0.2:app.height * 0.4 + } + } + ] + + transitions: [ + Transition { + from: "*" + to: "*" + + PauseAnimation { + duration: 150 + } + } + ] + + } + + } + } + + Component { + id: loadingComp + + Item { + id: loadingItm + + anchors.fill: parent + + FancyBusyIndicator { + id: loadingInd + anchors.centerIn: parent + + width: app.landscape() ? parent.height * 0.2 : parent.width *0.2 + height: width + } + + TextField { + id: ipAddrInputTf + + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + + opacity: backend.scStwClient.state === ScStwClient.CONNECTED ? 0:1 + + text: appSettings.baseStationIp + + onEditingFinished: { + appSettings.baseStationIp = text + backend.scStwClient.ipAddress = text + } + } + } + + } + } +} diff --git a/ScStwMonitorSrc/qml.qrc b/ScStwMonitorSrc/qml.qrc new file mode 100755 index 0000000..e80de3b --- /dev/null +++ b/ScStwMonitorSrc/qml.qrc @@ -0,0 +1,8 @@ + + + main.qml + FancyBusyIndicator.qml + TimerColumn.qml + FadeAnimation.qml + + diff --git a/ScStwMonitorSrc/shared.qrc b/ScStwMonitorSrc/shared.qrc new file mode 100644 index 0000000..fb10f14 --- /dev/null +++ b/ScStwMonitorSrc/shared.qrc @@ -0,0 +1,13 @@ + + + fonts/Arvo-Bold.ttf + fonts/Arvo-BoldItalic.ttf + fonts/Arvo-Regular.ttf + fonts/Arvo-RegularItalic.ttf + fonts/PTMono-Regular.ttf + Banner.png + SpeedHold.png + VolumeHigh.png + VolumeLow.png + + diff --git a/ScStwMonitorSrc/sleepprevent.h b/ScStwMonitorSrc/sleepprevent.h new file mode 100644 index 0000000..1934484 --- /dev/null +++ b/ScStwMonitorSrc/sleepprevent.h @@ -0,0 +1,5 @@ +class SleepPrevent +{ +public: + void setTimerDisabled(); +}; diff --git a/ScStwMonitorSrc/sleepprevent.mm b/ScStwMonitorSrc/sleepprevent.mm new file mode 100644 index 0000000..3c16e48 --- /dev/null +++ b/ScStwMonitorSrc/sleepprevent.mm @@ -0,0 +1,7 @@ +#import +#import +#include "sleepprevent.h" + +void SleepPrevent::setTimerDisabled() { + [[UIApplication sharedApplication] setIdleTimerDisabled: YES]; +} diff --git a/ScStwMonitorSrc/sources/scstwmonitorbackend.cpp b/ScStwMonitorSrc/sources/scstwmonitorbackend.cpp new file mode 100644 index 0000000..973b9d7 --- /dev/null +++ b/ScStwMonitorSrc/sources/scstwmonitorbackend.cpp @@ -0,0 +1,54 @@ +#include "../headers/scstwmonitorbackend.h" + +ScStwMonitorBackend::ScStwMonitorBackend(QObject *parent) : QObject(parent) +{ + this->scStwClient = new ScStwClient(); + this->remoteRace = new ScStwRemoteMonitorRace(this->scStwClient, this); + + // init refresh timers + this->autoConnectRetryTimer = new QTimer(this); + this->autoConnectRetryTimer->setInterval(1000); + this->autoConnectRetryTimer->setSingleShot(true); + connect(this->autoConnectRetryTimer, &QTimer::timeout, this, &ScStwMonitorBackend::doConnectionAttempt); + this->autoConnectRetryTimer->start(); + + this->timerTextRefreshTimer = new QTimer(this); + this->timerTextRefreshTimer->setInterval(1); + this->timerTextRefreshTimer->setSingleShot(true); + this->timerTextRefreshTimer->connect(this->timerTextRefreshTimer, &QTimer::timeout, this, &ScStwMonitorBackend::refreshTimerText); + this->refreshTimerText(); + +} + +void ScStwMonitorBackend::refreshTimerText() { + + // --- refresh timer text --- + if(this->getRace()->getState() == ScStwRace::RUNNING) { + emit this->getRace()->timersChanged(); + } + + // --- refresh next start action delay progress --- + if(this->getRace()->getState() == ScStwRace::STARTING) { + emit this->getRace()->nextStartActionDetailsChanged(); + } + + this->timerTextRefreshTimer->start(); +} + +void ScStwMonitorBackend::doConnectionAttempt() +{ + if(this->scStwClient->getState() == ScStwClient::DISCONNECTED) { + qDebug() << "+--- trying to connect"; + this->scStwClient->connectToHost(); + } + + this->autoConnectRetryTimer->start(); +} + +ScStwRace* ScStwMonitorBackend::getRace() { + return this->remoteRace; +} + +ScStwClient* ScStwMonitorBackend::getScStwClient() { + return this->scStwClient; +} diff --git a/shared-libraries b/shared-libraries new file mode 160000 index 0000000..bd52819 --- /dev/null +++ b/shared-libraries @@ -0,0 +1 @@ +Subproject commit bd52819a96394d9a59078cf7382453ede1c7b30d