example 'wear-os-gps': replace 'CircularGauge' with custom one

This commit is contained in:
pls.153 2022-07-24 23:46:52 +02:00
parent d89054267f
commit 2d5eb6968e
13 changed files with 284 additions and 89 deletions

View file

@ -7,4 +7,5 @@
(:file "lisp/distance")
(:file "lisp/speed")
(:file "lisp/qt")
(:file "lisp/gauge")
(:file "lisp/main")))

View file

@ -1,8 +1,7 @@
CONFIG += 32bit
LISP_FILES = $$files(lisp/*) app.asd make.lisp
android {
CONFIG += 32bit # for WearOS
32bit {
ECL = $$(ECL_ANDROID_32)
} else {
@ -10,6 +9,9 @@ android {
}
lisp.commands = $$ECL/../ecl-android-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:ios {
lisp.commands = $$(ECL_IOS)/../ecl-ios-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:unix {
lisp.commands = /usr/local/bin/ecl -shell $$PWD/make.lisp
} else:win32 {
@ -47,7 +49,6 @@ win32 {
android {
QT += androidextras
#DEFINES += INI_ASDF
DEFINES -= DESKTOP_APP
DEFINES += QT_EXTENSION
INCLUDEPATH = $$ECL/include
@ -66,6 +67,16 @@ android {
}
}
ios {
DEFINES -= DESKTOP_APP
INCLUDEPATH = $$(ECL_IOS)/include
LIBS = -L$$(ECL_IOS)/lib -lecl
LIBS += -leclatomic -leclffi -leclgc -leclgmp
LIBS += -L../../../platforms/ios/lib
QMAKE_INFO_PLIST = platforms/ios/Info.plist
}
32bit {
LIBS += -llqml32 -llisp32
} else {

View file

@ -0,0 +1,88 @@
(defpackage :gauge
(:use :cl :qml)
(:export
#:*gauge*
#:paint
#:number-pos
#:ini))
(in-package :gauge)
(defvar *gauge* "gauge")
(defvar *canvas* "gauge_canvas")
(defvar *numbers-loader* "gauge_numbers_loader")
(defun set-style (color width)
(qjs |setStyle| *canvas*
color width))
(defun draw-line (x1 y1 x2 y2)
(qjs |drawLine| *canvas*
x1 y1 x2 y2))
(defun rotate (angle)
(qjs |rotate| *canvas*
angle))
(defun arc (x y r start end)
(qjs |arc| *canvas*
x y r start end))
(defmacro with-path (&body body)
`(progn
(qjs |beginPath| *canvas*)
,@body
(qjs |stroke| *canvas*)))
(defmacro with-save (&body body)
`(progn
(qjs |save| *canvas*)
,@body
(qjs |restore| *canvas*)))
(defun to-rad (deg)
(/ (* deg pi) 180))
(defun paint () ; called from QML
(let ((r (q< |r| *gauge*))
(f (q< |f| *gauge*)))
;; limit zone
(set-style "red" (* 12 f))
(with-path ()
(arc 0 0 (- r (* 5 f))
(to-rad (- 360
(* 180 (- 1 (q< |limit| *gauge*)))))
(to-rad 360)))
;; thin ticks
(set-style "white" (* 2 f))
(with-save ()
(with-path ()
(rotate (to-rad 90))
(dotimes (i 60)
(draw-line 0 (- r (* 5 f)) 0 r)
(rotate (to-rad 3)))))
;; main ticks
(set-style "white" (* 4 f))
(with-path ()
(rotate (to-rad 90))
(dotimes (i 11)
(draw-line 0 (- r (* 12 f)) 0 r)
(rotate (to-rad 18))))))
(defun number-pos (xy index) ; called from QML
(let ((x? (string= "x" xy))
(r (q< |r| *gauge*))
(f (q< |f| *gauge*)))
(funcall (if x? '+ '-)
r
(* (- r (* 34 f))
(funcall (if x? 'sin 'cos)
(to-rad (+ 270 (* index 18))))))))
(defun ini ()
;; needs to be delayed because of recursive dependency:
;; - QML is loaded before Lisp
;; - 'x' and 'y' positions are calculated in Lisp
(q> |active| *numbers-loader* t))
(qlater 'ini)

View file

@ -1,3 +1,16 @@
;;; A 'Kalman' filter is a much more sophisticated filter than a simple
;;; 'low-pass' filter.
;;;
;;; It takes a theoretically well founded approach, which means: it makes an
;;; 'educated guess' of the next position to be expected, and 'corrects' it
;;; with actual GPS data, considering the accuracy and the speed of current raw
;;; GPS data.
;;;
;;; For most use cases this is already sufficiently precise.
;;;
;;; If you need more precision, you would need to integrate e.g. accelerometer
;;; data into the Kalman filter.
(defpackage :kalman
(:use :cl)
(:nicknames :kal)

View file

@ -52,7 +52,7 @@
(kal:filter lat lon accuracy speed)
(update-distance)
(update-speed)
(q> |value| ui:*speed* (speed*))
(q> |value| gauge:*gauge* (speed*))
(q> |text| ui:*distance* (str (round* (distance))))
(q> |text| ui:*accuracy* (str (round* accuracy 1)))
(when kal:*lat*
@ -70,7 +70,7 @@
(qt:keep-screen-on qt:*cpp* on))
(defun set-max-speed () ; called from QML
(q> |maximumValue| ui:*speed*
(q> |maximumValue| gauge:*gauge*
(q< |value| ui:*max-speed*)))
(qlater 'run)

View file

@ -4,8 +4,7 @@
#:*accuracy*
#:*distance*
#:*max-speed*
#:*position-source*
#:*speed*))
#:*position-source*))
(in-package :ui)
@ -13,4 +12,3 @@
(defparameter *distance* "distance")
(defparameter *max-speed* "max_speed")
(defparameter *position-source* "position_source")
(defparameter *speed* "speed")

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${QMAKE_SHORT_VERSION}</string>
<key>CFBundleSignature</key>
<string>${QMAKE_PKGINFO_TYPEINFO}</string>
<key>CFBundleVersion</key>
<string>${QMAKE_FULL_VERSION}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>${IPHONEOS_DEPLOYMENT_TARGET}</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSLocationAlwaysUsageDescription</key>
<string>To display speed and distance.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>To display speed and distance.</string>
</dict>
</plist>

View file

@ -1,92 +1,124 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Controls.Styles 1.4
import QtQuick.Extras 1.4
Rectangle {
anchors.fill: parent
id: gauge
objectName: "gauge"
anchors.centerIn: parent
width: Math.min(parent.height, parent.width)
height: width
color: "black"
CircularGauge {
id: speed
objectName: "speed"
anchors.fill: parent
maximumValue: 10
stepSize: 0.05
// main properties
property double value: 0
property double maximumValue: 10
property double limit: 8/10
Behavior on value {
NumberAnimation {
duration: 1000
easing.type: Easing.InOutCubic
}
property double r: canvas.width / 2 // main radius
property double f: width / 400 // size factor
onLimitChanged: canvas.requestPaint()
// ticks
Canvas {
id: canvas
z: 1
objectName: "gauge_canvas"
anchors.fill: parent
property var ctx
function rotate(angle) { ctx.rotate(angle) }
function beginPath() { ctx.beginPath() }
function arc(x, y, r, start, end) { ctx.arc(x, y, r, start, end) }
function save() { ctx.save() }
function restore() { ctx.restore() }
function stroke() { ctx.stroke() }
function setStyle(color, width) {
ctx.strokeStyle = color
ctx.lineWidth = width
}
style: CircularGaugeStyle {
id: style
labelStepSize: parent.maximumValue / 10
tickmarkStepSize: labelStepSize
minorTickmarkCount: 5
minimumValueAngle: -90
maximumValueAngle: 90
function drawLine(x1, y1, x2, y2) {
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}
property int limit: parent.maximumValue * 8/10
property string baseColor: "white"
property string limitColor: "orange"
onPaint: {
ctx = getContext("2d")
ctx.reset()
ctx.translate(gauge.r, gauge.r)
function toRad(deg) { return deg * (Math.PI / 180) }
Lisp.call("gauge:paint")
}
background: Canvas {
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.beginPath()
ctx.strokeStyle = limitColor
ctx.lineWidth = outerRadius * 0.02
ctx.arc(outerRadius, outerRadius, outerRadius - ctx.lineWidth / 2,
toRad(valueToAngle(limit) - 90), toRad(valueToAngle(speed.maximumValue) - 90))
ctx.stroke()
}
}
onWidthChanged: numbersLoader.reload()
onHeightChanged: numbersLoader.reload()
}
tickmark: Rectangle {
implicitWidth: outerRadius * 0.02
antialiasing: true
implicitHeight: outerRadius * 0.06
color: styleData.value >= limit ? limitColor : baseColor
}
// numbers (needs delayed loading on startup, hence 'Component' and 'Loader')
minorTickmark: Rectangle {
visible: styleData.value < limit
implicitWidth: outerRadius * 0.01
antialiasing: true
implicitHeight: outerRadius * 0.03
color: baseColor
}
Component {
id: numbers
tickmarkLabel: Text {
font.pixelSize: Math.max(6, outerRadius * 0.15)
Repeater {
model: 11
Text {
x: Lisp.call("gauge:number-pos", "x", index) - paintedWidth / 2
y: Lisp.call("gauge:number-pos", "y", index) - paintedHeight / 2
color: "white"
text: Math.round(gauge.maximumValue / 10 * index)
font.pixelSize: 28 * gauge.f
font.bold: true
text: styleData.value
color: styleData.value >= limit ? limitColor : baseColor
antialiasing: true
}
needle: Rectangle {
y: outerRadius * 0.15
implicitWidth: outerRadius * 0.07
implicitHeight: outerRadius * 0.85
radius: implicitWidth / 2
antialiasing: true
color: baseColor
}
foreground: Rectangle {
width: outerRadius * 0.15
height: width
radius: width / 2
color: baseColor
anchors.centerIn: parent
}
}
}
}
Loader {
id: numbersLoader
objectName: "gauge_numbers_loader"
active: false
sourceComponent: numbers
function reload() {
if (active) {
active = false
active = true
}
}
}
// needle
Item {
x: gauge.r
y: x
rotation: 180 / gauge.maximumValue
* Math.max(0, Math.min(gauge.value, gauge.maximumValue))
+ 90
Behavior on rotation {
NumberAnimation { duration: 1000; easing.type: Easing.InOutCubic }
}
Rectangle {
x: -width / 2
y: x
width: 28 * gauge.f
height: width
radius: width / 2
color: "white"
}
Rectangle {
x: -width / 2
y: -30 * gauge.f
width: 14 * gauge.f
height: gauge.r - 28 * gauge.f
radius: width / 2
color: "white"
}
}
}

View file

@ -1,5 +1,4 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import "." as Ext
Rectangle {

View file

@ -24,7 +24,8 @@ Rectangle {
delegate: Text {
text: modelData
font.pixelSize: 22
font.family: "Courier"
font.pixelSize: 24
font.bold: true
opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
Component.onCompleted: { maxSpeed.width = paintedWidth }

View file

@ -5,6 +5,9 @@ Note
This will only work with Qt 5.15 (customized AndroidManifest.xml file) and
requires WearOS 2 or later.
Running on an android/iOS phone will also work, please see the generic
[readme-build.md](../app-template/readme-build.md).
It is assumed that you already enabled the developer settings on your watch.
--

View file

@ -17,9 +17,9 @@ of e.g. a canoe session (only meant for constant altitude values). You probably
need to adapt the maximum speed value (km/h) to your personal needs, see
settings (swipe up).
An important feature is keeping the display always on (implemented with the Qt
JNI interface). But this also consumes more battery, so you can switch it off
in the settings.
An important feature (android only) is keeping the display always on
(implemented with the Qt JNI interface). But this also consumes more battery,
so you can switch it off in the settings.
The data is automatically logged, and can be accessed with e.g.
**Device File Explorer** from Android Studio (see Help / Find Action...).
@ -62,3 +62,10 @@ Closing the window quits the app. If you try to kill it with `ctrl-c`, you need
an additional `ctrl-d` to exit from ECL. To quit from Slime, do `(qq)` which is
short for `(qquit)`.
Note
----
Even though this example is made for a WearOS watch, it can also be run on any
android/iOS phone (preferably in landscape orientation).