mirror of
https://gitlab.com/eql/lqml.git
synced 2025-12-15 14:51:14 -08:00
example 'wear-os-gps': replace 'CircularGauge' with custom one
This commit is contained in:
parent
d89054267f
commit
2d5eb6968e
13 changed files with 284 additions and 89 deletions
|
|
@ -7,4 +7,5 @@
|
|||
(:file "lisp/distance")
|
||||
(:file "lisp/speed")
|
||||
(:file "lisp/qt")
|
||||
(:file "lisp/gauge")
|
||||
(:file "lisp/main")))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
88
examples/wear-os-gps/lisp/gauge.lisp
Normal file
88
examples/wear-os-gps/lisp/gauge.lisp
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
43
examples/wear-os-gps/platforms/ios/Info.plist
Normal file
43
examples/wear-os-gps/platforms/ios/Info.plist
Normal 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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import "." as Ext
|
||||
|
||||
Rectangle {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
--
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue