/* 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.9 import QtMultimedia 5.8 import QtQuick.Window 2.2 import QtQuick.Controls 2.2 import "." import "./components" import "./styles" //import QtQuick.Layouts 1.11 import com.itsblue.speedclimbingstopwatch 1.0 Window { visible: true width: 540 height: 960 title: qsTr("Speedclimbing stw") property date currentTime: new Date() property int millis: 0 onBeforeRendering: { StyleSettings.refreshTheme() } Page { id:root anchors.fill: parent property double startTime: 0 property double stoppedTime: 0 property double currTime property double buzzer_offset: buzzerConn.offset property double last_button_pressed: buzzerConn.lastTriggered property var last_run : { 'stop_type': "", 'time': 0, 'react_time': 0 }; //array that contains all connections an their atatus property var connections: { 'buzzer': buzzerConn.status, 'startpad': startpadConn.status, 'baseStation': baseConn.status } //set default state to IDLE state: "IDLE" Rectangle { id: backgroundRect anchors.fill: parent color: StyleSettings.backgroundColor } Item { id: connections BuzzerConn { id: buzzerConn ipAdress: "192.168.4.10" property var status: {'status': buzzerConn.state, 'progress': buzzerConn.progress} onLastTriggeredChanged: { timer_1.handleToppad() } } Timer { id: buzzerRefreshTimer running: buzzerConn.state === "connected" interval: root.state === "RUNNING" ? 1:1000 repeat: false onTriggered: { buzzerConn.refresh() this.start() } } StartpadConn { id: startpadConn ipAdress: "192.168.4.11" property var status: {'status': startpadConn.state, 'progress': startpadConn.progress} property string color: root.state === "RUNNING" ? "SET_LED_RUNNING":"SET_LED_STARTING" onColorChanged: { appendCommand(color) } onLastTriggeredChanged: { timer_1.handleStartpad() } } Timer { id: startpadRefreshTimer running: startpadConn.state === "connected" interval: root.state === "RUNNING" || root.state === "STARTING" ? 1:1000 repeat: false onTriggered: { startpadConn.refresh() this.start() } } BaseStationConn { id: baseConn ipAdress: "localhost"//"raspberrypi.local" property var status: {'status': baseConn.state, 'progress': baseConn.progress, 'connections': baseConn.connections} function getTime(type){ var time = parseInt(sendCommand("GET_CURRTIME")) if(type === "readable"){ return(time / 1000).toFixed(3) } else if(type === "raw"){ return(time) } } } Timer { id: baseRefreshTimer running: baseConn.state === "connected" repeat: false interval: 1 onTriggered: { switch(root.state){ case "RUNNING": var baseReactTime = parseInt(baseConn.sendCommand("GET_REACTTIME")) console.log(baseReactTime) if(baseReactTime !== 0){ root.last_run.react_time = baseReactTime } timer_1.text = baseConn.getTime("readable") + " sec" if(baseConn.sendCommand("GET_TIMER_STATE") === "STOPPED"){ root.stop("manual") } break case "STARTING": var baseReactTime = parseInt(baseConn.sendCommand("GET_REACTTIME")) if(baseReactTime !== 0){ root.last_run.react_time = baseReactTime } var baseNextAction = baseConn.sendCommand("GET_NEXT_ACTION") if(baseNextAction === "at_marks"){ timer_1.text = qsTr("at your\nmarks") var baseNextActionDelay = parseFloat(baseConn.sendCommand("GET_NEXT_ACTION_DELAY_PROG")) prog.progress = baseNextActionDelay * 100 } else if(baseNextAction === "ready"){ timer_1.text = qsTr("ready") var baseNextActionDelay = parseFloat(baseConn.sendCommand("GET_NEXT_ACTION_DELAY_PROG")) prog.progress = baseNextActionDelay * 100 } else if(baseNextAction === "start"){ timer_1.text = "0.000 sec" } var baseState = baseConn.sendCommand("GET_TIMER_STATE") console.log(baseState) if(baseState === "RUNNING"){ timer_1.start(1) root.state = "RUNNING"; } else if(baseState === "STOPPED"){ var baseReactTime = parseInt(baseConn.sendCommand("GET_REACTTIME")) if(baseReactTime<0){ root.stop("false") } else{ root.stop("cancel") } root.state = "STOPPED" } break } start() } } } Timer { id: next_actionTimer property string action property double started_at running: false repeat: false onRunningChanged: { if(!running){ started_at = 0 if(action == "NONE"){ timer_1.text = "0.000 sec" } return } if(action === "at_marks"){ started_at = new Date().getTime() timer_1.text = "at your\nmarks" } else if(action === "ready"){ started_at = new Date().getTime() timer_1.text = "ready" } } onTriggered: { if(action === "at_marks"){ at_marksSound.play() } else if(action === "ready"){ action = "NONE" readySound.play() } } } Item { id: sounds SoundEffect { id: at_marksSound source: "qrc:/sounds/at_marks_1.wav" onPlayingChanged: { if(!playing && root.state==="STARTING"){ if(_cppAppSettings.loadSetting("ready_en") === "true"){ next_actionTimer.action = "ready" next_actionTimer.interval = _cppAppSettings.loadSetting("ready_delay")>0 ? _cppAppSettings.loadSetting("ready_delay"):1 next_actionTimer.start() } else{ startSound.play() } } } } SoundEffect { id: readySound source: "qrc:/sounds/ready_1.wav" onPlayingChanged: { if(!playing && root.state==="STARTING"){ startSound.play() } } } SoundEffect { //start sound id: startSound source: "qrc:/sounds/OFFICAL_IFSC_STARTIGNAL.wav" onPlayingChanged: { if(!playing && root.state==="STARTING"){ root.state = "RUNNING" } else if(playing) { console.log("start sound started") timer_1.start(3100) } } } SoundEffect { //false-start sound id: falseSound source: "qrc:/sounds/false.wav" } } /*------------------------ Timer text an upper line ------------------------*/ Item { id: time_container anchors { top: parent.top left: parent.left right: root.landscape() ? startButt.left:parent.right bottom: root.landscape() ? parent.bottom:startButt.top bottomMargin: root.landscape() ? undefined:parent.height * 0.1 rightMargin: root.landscape() ? parent.width * 0.05:0 } Rectangle { anchors.fill: parent color: StyleSettings.menuColor } SpeedTimer { id: timer_1 anchors.centerIn: parent elide: "ElideRight" color: StyleSettings.textColor toppadConn: buzzerConn baseConn: baseConn startpadConn: startpadConn text: "0.000 sec" onStopped: { root.state = "STOPPED" } onStartCanceled: { root.state = "STOPPED" next_actionTimer.stop() at_marksSound.stop() readySound.stop() startSound.stop() if(falseStart && baseConn.state !== "connected"){ falseSound.play() } } } Label { id: react_time property int rtime: root.last_run.react_time text: qsTr("reaction time (ms): ") + Math.round(rtime) opacity: (root.state === "RUNNING" || root.sate === "STARTING" || root.state === "STOPPED") && rtime !== 0 ? 1:0 color: StyleSettings.textColor anchors { horizontalCenter: parent.horizontalCenter top: timer_1.bottom topMargin: parent.height * 0.1 } Timer { running: root.state === "RUNNING" || root.sate === "STARTING" || root.state === "STOPPED" repeat: true interval: 1 onTriggered: { react_time.rtime = root.last_run.react_time } } } } ConnectionIcon { id: buzzerLogo source: "qrc:/graphics/icons/buzzer_black.png" status: root.connections["buzzer"].status anchors { top: parent.top topMargin: 10 left: parent.left leftMargin: 10 } height: root.landscape()? root.height*0.1:root.width*0.1 } ConnectionIcon { id: startpadLogo source: "qrc:/graphics/icons/startpad_black.png" status: root.connections["startpad"].status anchors { top: parent.top topMargin: 10 left: parent.left leftMargin: 5 + buzzerLogo.width } height: root.landscape()? root.height*0.1:root.width*0.1 } Rectangle { id: upper_line width: root.landscape() ? 1:parent.width height: root.landscape() ? parent.height:1 color: StyleSettings.lineColor anchors.left: root.landscape() ? time_container.right:parent.left anchors.top: root.landscape() ? parent.top:time_container.bottom anchors.bottom: root.landscape() ? parent.bottom:undefined } /*---------------------- Start / Stop / Reset button ----------------------*/ Button { id : startButt text: qsTr("start") property int size: root.landscape() ? parent.width * 0.5:parent.height * 0.5 anchors { bottom: parent.bottom bottomMargin: root.height * 0.5 - height * 0.5 right: parent.right rightMargin: root.width * 0.5 - width * 0.5 } contentItem: Text { //make text disappear } height: root.landscape() ? size > parent.height * 0.9 ? parent.height * 0.9:size : size width: root.landscape() ? size : size > parent.width * 0.9 ? parent.width * 0.9:size background: Rectangle { color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor border.color: StyleSettings.buttonBorderColor border.width: 1 radius: width / 2 Label { id: startButt_text text: startButt.text anchors.centerIn: parent font.pixelSize: parent.height * 0.16 font.family: "Helvetica" color: enabled ? StyleSettings.textColor:StyleSettings.disabledTextColor } } Behavior on text { //animate a text change enabled: true FadeAnimation { target: startButt_text } } onClicked: { switch(root.state) { case "IDLE": root.start() break case "RUNNING": root.stop("manual") break case "STOPPED": root.reset() break } } } ProgressCircle { id: prog anchors.fill: startButt opacity: baseConn.state !== "connected" ? next_actionTimer.started_at > 0 ? 1:0 :progress > 0 ? 1:0 lineWidth: 5 property int progress: 0 arcBegin: 0 arcEnd: baseConn.state !== "connected" ? 360 * (( next_actionTimer.interval - ( new Date().getTime() - next_actionTimer.started_at ) ) / next_actionTimer.interval) :(360/100) * progress colorCircle: "grey" animationDuration: baseConn.state === "connected" ? 150:0 Timer { id: prog_refresh running: parent.opacity === 1 && baseConn.state !== "connected" interval: 1 repeat: true onTriggered: { prog.arcEnd = 360 * (( next_actionTimer.interval - ( new Date().getTime() - next_actionTimer.started_at ) ) / next_actionTimer.interval) } } } /*---------------------- Cancel button ----------------------*/ RoundButton { id: cancelButt text: qsTr("cancel") anchors { right: startButt.right bottom: startButt.bottom } contentItem: Text { //make text disappear } height: startButt.height * 0.3 scale: 0 width: height enabled: root.state === "STARTING" onClicked: { root.stop("cancel") } Behavior on scale { PropertyAnimation { duration: 200 } } background: Rectangle { color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor border.color: StyleSettings.buttonBorderColor border.width: 1 radius: width / 2 Label { id: cancelButt_text text: cancelButt.text anchors.centerIn: parent font.pixelSize: parent.height * 0.16 font.family: "Helvetica" color: StyleSettings.textColor } } } /*------ Popups ------*/ SettingsDialog{ id: settingsDialog connections: root.connections onConnectRequested: { switch(type){ case "buzzer": buzzerConn.connect() break case "startpad": startpadConn.connect() break case "baseStation": baseConn.connectToHost() break } } } // ProfilesDialog { // id: profilesDialog // } /*------------------- lower line and menu -------------------*/ Rectangle { width: root.landscape() ? 1:parent.width height: root.landscape() ? parent.height:1 color: StyleSettings.lineColor anchors.right: root.landscape() ? menu_container.left:parent.right anchors.bottom: root.landscape() ? parent.bottom:menu_container.top anchors.top: root.landscape() ? parent.top:undefined } Item { id: menu_container anchors { bottom: parent.bottom right: parent.right left: root.landscape() ? startButt.right:parent.left top: root.landscape() ? parent.top:startButt.bottom topMargin: root.landscape() ? undefined:parent.height * 0.1 leftMargin: root.landscape() ? parent.width * 0.05:0 } Rectangle { anchors.fill: parent color: StyleSettings.menuColor } RoundButton { id: settingsButt anchors { //center verticalCenter: root.landscape() ? undefined:parent.verticalCenter horizontalCenter: root.landscape() ? parent.horizontalCenter:undefined //set anchors left: root.landscape() ? undefined:parent.left top: root.landscape() ? parent.top:undefined //align in landscape mode //for two buttons: topMargin: root.landscape() ? (parent.height - (height * 2)) / 3:undefined topMargin: root.landscape() ? (parent.height * 0.5 - (height * 0.5)):undefined //align in portrait mode //for two buttons: leftMargin: root.landscape() ? undefined:(parent.width - width * 2) / 3 leftMargin: root.landscape() ? undefined:(parent.width * 0.5 - width * 0.5) } height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 width: height onClicked: { settingsDialog.open() } background: Rectangle { color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor border.color: StyleSettings.buttonBorderColor border.width: 1 radius: width / 2 Image { id: settungsButt_Image source: StyleSettings.settIcon anchors.centerIn: parent height: parent.height * 0.7 width: parent.width * 0.7 mipmap: true } } } /* RoundButton { id: profilesButt anchors { verticalCenter: root.landscape() ? undefined:parent.verticalCenter horizontalCenter: root.landscape() ? parent.horizontalCenter:undefined left: root.landscape() ? undefined:settingsButt.right top: root.landscape() ? settingsButt.bottom:undefined topMargin: root.landscape() ? (parent.height - (height * 2)) / 3:undefined leftMargin: root.landscape() ? undefined:(parent.width - width * 2) / 3 } height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 width: height onPressedChanged: { if(pressed){ background.color = "lightgrey" } else { background.color = "white" } } onClicked: { profilesDialog.open() } background: Rectangle { color: "white" border.color: "grey" border.width: 1 radius: width / 2 Image { id: profilesButt_Image source: "qrc:/graphics/icons/user.png" anchors.centerIn: parent height: parent.height * 0.5 width: parent.width * 0.5 mipmap: true } } } */ } /*---------------------- Timer states ----------------------*/ states: [ State { name: "IDLE" //state for the start page PropertyChanges { target: timer_1; pixelSize: root.landscape() ? parent.width * 0.1:parent.height * 0.3; scale: 1 } PropertyChanges { target: time_container; anchors.bottomMargin: root.landscape() ? undefined:parent.height * 0.1; anchors.rightMargin: root.landscape() ? parent.height * 0.05:0 } PropertyChanges { target: startButt; enabled: true; text: qsTr("start"); size: root.landscape() ? parent.width * 0.5:parent.height * 0.5 anchors.bottomMargin: parent.height * 0.5 - startButt.height * 0.5 anchors.rightMargin: parent.width * 0.5 - startButt.width * 0.5 } }, State { name: "STARTING" //state for the start sequence PropertyChanges { target: startButt; enabled: false; text: qsTr("starting..."); anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode) anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) } PropertyChanges { target: timer_1; pixelSize: root.landscape() ? parent.width * 0.2:parent.height * 0.3; scale: 1 } PropertyChanges { target: cancelButt; scale: 1} PropertyChanges { target: menu_container; } }, State { name: "RUNNING" //state when the timer is running PropertyChanges { target: timer_1; pixelSize: root.landscape() ? parent.width * 0.2:parent.height * 0.3; scale: 1 } PropertyChanges { target: startButt; enabled: true; text: "stop" anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode) anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) } }, State { name: "STOPPED" //state when the meassuring is over PropertyChanges { target: timer_1; pixelSize: root.landscape() ? parent.width * 0.15:parent.height * 0.1; scale: 1 } PropertyChanges { target: startButt; enabled: true; text: qsTr("reset"); size: root.landscape() ? parent.height * 0.35:parent.height * 0.2; anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.2 - startButt.height * 0.5 anchors.rightMargin: root.landscape() ? parent.height * 0.2 - startButt.height * 0.5:parent.width * 0.5 - startButt.width * 0.5 } PropertyChanges { target: time_container; anchors.rightMargin: root.landscape() ? 0-startButt.width/2:undefined anchors.bottomMargin: root.landscape() ? undefined:0-startButt.height/2 } } ] /*---------------------- Timer animations ----------------------*/ transitions: [ Transition { NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "STOPPED" NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "IDLE" NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "RUNNING" //disable transitions for the RUNNING state } ] /*---------------------- Timer functions ----------------------*/ function landscape(){ return(root.height < root.width) } /*----Functions to control the stopwatch----*/ function start(){ if(baseConn.state === "connected"){ var ret = baseConn.sendCommand("CMD_START_TIMER") if(ret === "OK"){ root.state = "STARTING" timer_1.setStarting() return } } root.state = "STARTING" timer_1.setStarting() if(_cppAppSettings.loadSetting("at_marks_en") === "true"){ next_actionTimer.action = "at_marks" next_actionTimer.interval = _cppAppSettings.loadSetting("at_marks_delay")>0 ? _cppAppSettings.loadSetting("at_marks_delay"):1 next_actionTimer.start() return } if(_cppAppSettings.loadSetting("ready_en") === "true"){ next_actionTimer.action = "ready" next_actionTimer.interval = _cppAppSettings.loadSetting("ready_delay")>0 ? _cppAppSettings.loadSetting("ready_delay"):1 next_actionTimer.start() return } startSound.play() } function stop(type){ if(type === "manual" || type === "cancel"){ if(baseConn.state === "connected"){ baseConn.sendCommand("CMD_STOP_TIMER") } } root.state = "STOPPED" timer_1.stop(type) } function reset(){ if(baseConn.state === "connected"){ baseConn.sendCommand("CMD_RESET_TIMER") } timer_1.reset() root.state = "IDLE" } } }